I want to write a test for a utils method. In that method I get a html element by id and then change the color of the element. The problem is that element is only available after a button click. How can I mock the element?
UtilListItem.js
import variables from '../stylesheets/Variables.scss';
export function activeListItem(props){
let listItem = document.getElementById(props.id);
listItem.style.backgroundColor = variables.whiteGray;
return listItem;
}
UtilListeItem.test.js
it('check if the correct color is set for the acitve list item', () => {
let props = {id:'123'}
const listItem = activeListItem(props);
expect(listItem.style.backgroundColor).toBe('#ededed');
});
error
TypeError: Cannot read property 'style' of null
I'd suggest you to you jest.spyOn. It's a really handy way to spy on a function and/or attach some mock behaviour.
You can use it like this:
imoprt { activeListItem } from './utils';
let spy;
beforeAll(() => {
spy = jest.spyOn(document, 'getElementById');
});
describe('activeListItem', () => {
describe('with found element', () => {
let mockElement;
beforeAll(() => {
// here you create the element that the document.createElement will return
// it might be even without an id
mockElement = document.createElement(....);
spy.mockReturnValue(mockElement);
});
// and then you could expect it to have the background
it('should have the background applied', () => {
expect(mockElement.style.backgroundColor).toBe('#ededed');
});
});
describe('without found element', () => {
// and here you can create a scenario
// when document.createElement returns null
beforeAll(() => {
spy.mockReturnValue(null);
});
// and expect you function not to throw an error
it('should not throw an error', () => {
expect(() => activeListItem({id:'123'})).not.toThrow();
});
});
});
It's also a good idea to mock the .scss file, since it's a dependency of your utility file, so that when it's change it won't affect your unit test.
There are two options I can think of, you can opt either of them:
Put a check on listItem of function activeListItem
export function activeListItem(props) {
let listItem = document.getElementById(props.id);
if (listItem === null) {
return;
}
listItem.style.backgroundColor = variables.whiteGray;
return listItem;
}
Add dummy element in your test case
it('check if the correct color is set for the acitve list item', () => {
/** Create and add dummy div **/
let testId = "dummy-testId";
let newDiv = document.createElement("div");
newDiv.setAttribute("id", testId);
document.body.appendChild(newDiv);
let props = {id: testId}
const listItem = activeListItem(props);
expect(listItem.style.backgroundColor).toBe('#ededed');
});
This line have problem:
let listItem = document.getElementById(props.id);
Create element in the first place for mocking in jest. Be sure to wait for document and inject it.
What you doing is getting element when isn't ready to test / non exist in this context.
--- EDITED TO ADD EXAMPLE ---
What need to be added:
https://jestjs.io/docs/en/configuration#setupfiles-array
Others response to similar problem with example solution:
https://stackoverflow.com/a/41186342/5768332
A combination of the top two answers worked well for me:
...
beforeAll(() => {
const modalDiv = document.createElement("div");
modalDiv.setAttribute("id", "react-modal-container");
document.body.appendChild(modalDiv);
});
...
Related
I was doing some basic jest unit testing in attempt to learn it more.
I have this issue I do not know how to explain
This file has the child function add
// FileB.js
const add = (a, b) => {
return a + b;
}
module.exports = {
add,
};
This file has the parent function addTen
// FileA.js
const { add } = require('./FileB');
const addTen = num => {
return add(10, num);
}
module.exports = {
addTen,
};
this is my test file, where I am trying to either check <mockedFunction/spy>.mock.calls or do toHaveBeenCalledWith to see if the inner child method, add, is being passed in 10,10 when i call addTen(10);
This file is not used in any real env, its simply me trying to learn jest + unit testing more.
// randomTest.js
const { addTen } = require('../src/FileA');
const fileB = require('../src/FileB');
describe('temp', () => {
it('temp', () => {
const addSpy = jest.spyOn(fileB, 'add');
addTen(10);
console.log(addSpy.mock.calls);
expect(addSpy).toHaveBeenCalledWith(10,10)
});
});
Now for the issue, the test fails, saying add was never called, or nothing passed in. I logged the add function within FileB
However, if I modify FileA in this way, by importing entore module instead of destructuring, the test passes and I cna log out the functions and everything and see it works
This is what works
// FileA.js
const fileB = require('./FileB');
const addTen = num => {
return fileB.add(10, num);
}
module.exports = {
addTen,
};
Why does this slight change work?
I am new to front-end testing, and I am trying to get a good grasp on it.
In the process, inside one project, I am creating a test for a Vue component. I want to test that the behaviour of the code is correct (The component has code inside the mounted() hook that has to perform basically some checks and an API call).
I want to check that the code reaches one method. Previously to that, the code creates a click event listener to one element in the DOM.
My test emulates a click event (triggers it), but it cannot assert that the proper method has been called after the click event.
This is due to it not finding the element in the DOM to which it has to add the event listener. It seems that the code cannot find anything inside the document (using .getElementById()).
I wonder why, and how I would resolve this, since I have been stuck here for hours and I haven't found any solution that could work here, even when I have learned some interesting things in the process. I will leave a code example with the code structure I have built:
Inside the component:
<template>
// ...
<button id = "myButton">Add</button>
</template>
<script>
import { classInExternalScriptsFile } from "#/scripts/externalScriptsFile ";
let classIESF = new classInExternalScriptsFile();
export default {
methods: {
setup: function () {
classIESF.setupMethod();
},
},
mounted() {
this.setupMethod();
},
};
</script>
Inside the scriptsFile
export class classInExternalScriptsFile {
setupMethod() {
let myButton = document.getElementById("myButton") // <-- getElementById() returns a null here
if (typeof myButton !== "undefined" && myButton !== null) {
myButton.onclick = () => { // <-- The test code complains because it cannot enter here
// Some lines...
this.mySuperMethod()
}
}
}
mySuperMethod() {
// API call etc.
}
}
Inside the .spec.js test file:
// imports...
import { classInExternalScriptsFile } from "#/scripts/externalScriptsFile.js";
describe("description...", () => {
const mySuperMethodMock = vi
.spyOn(classInExternalScriptsFile.prototype, "mySuperMethod")
.mockImplementation(() => {});
test("That the button performs x when clicked", () => {
let wrapper = mount(myComponent, {
props: ...,
});
let myButton = wrapper.find('[test-id="my-button"]');
myButton.trigger("click");
expect(mySuperMethodMock).toHaveBeenCalled(); // <-- The test fails here
}
}
So I have this Display() function which fetches events from the Google Calendar via an API and store each event's name via element.summary into the events set. And then once the events set is populated, I iterate through the set via for (let item of events) and create a new <a> tag for each event/item in the set using the name as the text via <a>{item}</a> (for e.g. <a>call<a>, then I push each <a> tag into a new array called tabs and then finally return the tabs array. The events set contains three items and when I console.log, I see the correct items ("call", "kist", & "go") in the set. However, once I console.log the tabs array, it only contains one <a> tag whose value is null whereas it is supposed to contain three <a> tags since it iterates through the events set which has three items and is supposed to create an <a> tag for each. Also, I get the error that item is not defined for the line for (let item of events), somehow I cannot iterate through the events set. See console output here.
function Display() {
let events = new Set()
let tabs = []
ApiCalendar.listUpcomingEvents(10)
.then(({result}: any) => {
result.items.forEach(element => {
console.log(element.summary)
events.add(element.summary)
}
);
console.log(events)
for (let item of events)
console.log(item)
tabs.push(<a>{item}</a>)
console.log(tabs)
return tabs
});
}
This is the class that I made in the same file as the above function, which basically renders a 'Log In' button if user is not logged in to their calendar, or renders the array of <a> tags returned by the Display() function if user is already logged in. However, even though the Display() function above does return something (i.e. an array of <a> tags) and the render() function inside the class also returns a <div> element with the corresponding component inside the div, I get the error Uncaught Error: Display(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null. I am new to JavaScript and have no idea what I'm doing wrong. Any help is greatly appreciated and thank you in advance.
export default class LoginControl extends React.Component {
constructor(props) {
super(props);
this.state = {
sign: ApiCalendar.sign,
};
}
render() {
const isLoggedIn = this.state.sign;
let ele;
if (isLoggedIn) {
ele = <Display/>;
} else {
ele = <Button>'Sign In'</Button>;
}
return (
<div>
{ele}
</div>
);
}
}
Your Display function calls an async method and returns nothing. You will need to utilize state and effect inside Display to render returned data. But then, you will encounter errors if user navigates away from page before data is fetched.
Best solution for this problem would be to utilize redux and redux-thunk
Caution, untested code below
If you feel like you don't need redux, try this approach
async function fetchItems() {
const result = await ApiCalendar.listUpcomingEvents(10);
return result.result.items.map(({summary}) => summary);
}
function Display() {
const [items, saveItems] = useState([]);
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
(async () => {
const items = await fetchItems();
//Do not update state if component is unmounted
if (isMounted.current) {
saveItems(items);
}
})();
}, []);
return <>{items.map(item => <a key={item}>{item}</a>)}</>
}
If you want to render more than summary, you can do it like this
async function fetchItems() {
const result = await ApiCalendar.listUpcomingEvents(10);
return result.result.items.map(({summary, somethingElse}) => ({summary, somethingElse}));
//can be replaced with return [...result.result.items]; to get all props
}
function Display() {
//... Main logic of Display component is the same,
//so I wouldn't duplicate it here
return <>{items.map(item => <div key={item.summary}>{item.summary} {item.somethingElse}</div>)}</>
}
It seems that you are not returning anything on Display component.
You can't return a promise on a component so you need to make it inside useEffect using react hooks or component lifecycle - and no, you don't need redux just to achieve this.
function Display() {
let events = new Set()
let tabs = [];
const [items, setItems] = useState([]);
const getList = async () => {
const res = await ApiCalendar.listUpcomingEvents(10);
setItems(res.items);
}
useEffect(async () => {
getList();
}, []);
return items.map(item => <div>{item}</div>);
}
So i was trying to structure my code inside a class so it can be more organized, but iam struggling. I have the code:
class App {
constructor() {
// Get elements from DOM
const titleBox = document.getElementById('titleBox');
const navBox = document.getElementById('navBox');
const navLinks = document.querySelectorAll('.header__listLink');
const headerTitle = document.getElementById('headerTitle');
const headerSubtitle = document.getElementById('headerSubtitle');
const ulNav = document.getElementById('ulNav');
const ulNavLinks = document.querySelectorAll('.ulNavLink');
// for each nav link, add an event listener, expanding content area
navLinks.forEach((link) => {
link.addEventListener('click', this.clickedLinkState);
});
}
clickedLinkState(e) {
e.preventDefault();
titleBox.classList.remove("header__title-box");
titleBox.classList.add("header__title-box--linkClicked");
headerTitle.classList.remove("header__title");
headerTitle.classList.add("header__title--linkClicked");
headerSubtitle.classList.remove("header__subtitle");
headerSubtitle.classList.add("header__subtitle--linkClicked");
ulNav.classList.remove("header__listInline");
ulNav.classList.add("header__listInline--linkClicked");
navBox.classList.remove("header__nav-box");
navBox.classList.add("header__nav-box--linkClicked");
ulNavLinks.forEach((navLink) => {
navLink.classList.remove("header__listLink");
navLink.classList.add("header__listLink--linkClicked");
});
}
}
const app = new App();
And i got the error: "main.js:40 Uncaught ReferenceError: ulNavLinks is not defined
at HTMLLIElement.clickedLinkState (main.js:40)". the 'ulNavLinks' is a nodeList.
I was trying to define the elements using 'this.titleBox = ...', for exemple, but it got even worse, i could not access it from my clickedLinkState method. Outside the class it was working.
Why i cant access the 'ulNavLinks' inside my method? and why i cant access my propesties inside the method if i declare them 'this.titleBox', 'this.navBox'?
In JavaScript, as for now, instance properties can only being defined inside class methods using keyword this (here is the doc).
Also there is an experimental feature of supporting public/private fields, which you may use with some build steps due to poor browser support.
Make sure to use this:
class App {
constructor() {
// Get elements from DOM
this.titleBox = document.getElementById('titleBox');
this.navBox = document.getElementById('navBox');
this.navLinks = document.querySelectorAll('.header__listLink');
this.headerTitle = document.getElementById('headerTitle');
this.headerSubtitle = document.getElementById('headerSubtitle');
this.ulNav = document.getElementById('ulNav');
this.ulNavLinks = document.querySelectorAll('.ulNavLink');
// for each nav link, add an event listener, expanding content area
this.navLinks.forEach((link) => {
link.addEventListener('click', this.clickedLinkState.bind(this));
});
}
clickedLinkState(e) {
e.preventDefault();
this.titleBox.classList.remove("header__title-box");
this.titleBox.classList.add("header__title-box--linkClicked");
this.headerTitle.classList.remove("header__title");
this.headerTitle.classList.add("header__title--linkClicked");
this.headerSubtitle.classList.remove("header__subtitle");
this.headerSubtitle.classList.add("header__subtitle--linkClicked");
this.ulNav.classList.remove("header__listInline");
this.ulNav.classList.add("header__listInline--linkClicked");
this.navBox.classList.remove("header__nav-box");
this.navBox.classList.add("header__nav-box--linkClicked");
this.ulNavLinks.forEach((navLink) => {
navLink.classList.remove("header__listLink");
navLink.classList.add("header__listLink--linkClicked");
});
}
}
const app = new App();
I have a React component that renders a <Link/>.
render: function () {
var record = this.props.record;
return (
<Link to="record.detail" params={{id:record.id}}>
<div>ID: {record.id}</div>
<div>Name: {record.name}</div>
<div>Status: {record.status}</div>
</Link>
);
}
I can easily obtain the rendered <a/>, but I'm not sure how to test that the href was built properly.
function mockRecordListItem(record) {
return stubRouterContext(require('./RecordListItem.jsx'), {record: record});
}
it('should handle click', function () {
let record = {id: 2, name: 'test', status: 'completed'};
var RecordListItem = mockRecordListItem(record);
let item = TestUtils.renderIntoDocument(<RecordListItem/>);
let a = TestUtils.findRenderedDOMComponentWithTag(item, 'a');
expect(a);
// TODO: inspect href?
expect(/* something */).to.equal('/records/2');
});
Notes: The stubRouterContext is necessary in React-Router v0.13.3 to mock the <Link/> correctly.
Edit:
Thanks to Jordan for suggesting a.getDOMNode().getAttribute('href'). Unfortunately when I run the test, the result is null. I expect this has to do with the way stubRouterContext is mocking the <Link/>, but how to 'fix' is still TBD...
I use jest and enzyme for testing. For Link from Route I use Memory Router from their official documentation https://reacttraining.com/react-router/web/guides/testing
I needed to check href of the final constructed link. This is my suggestion:
MovieCard.js:
export function MovieCard(props) {
const { id, type } = props;
return (
<Link to={`/${type}/${id}`} className={css.card} />
)
};
MovieCard.test.js (I skip imports here):
const id= 111;
const type= "movie";
test("constructs link for router", () => {
const wrapper = mount(
<MemoryRouter>
<MovieCard type={type} id={id}/>
</MemoryRouter>
);
expect(wrapper.find('[href="/movie/111"]').length).toBe(1);
});
Ok. This simply took some digging into the stubRouterContext that I already had.
The third constructor argument, stubs, is what I needed to pass in, overriding the default makeHref function.
Working example:
function mockRecordListItem(record, stubs) {
return stubRouterContext(require('./RecordListItem.jsx'), {record: record}, stubs);
}
it('should handle click', function () {
let record = {id: 2, name: 'test', status: 'completed'};
let expectedRoute = '/records/2';
let RecordListItem = mockRecordListItem(record, {
makeHref: function () {
return expectedRoute;
}
});
let item = TestUtils.renderIntoDocument(<RecordListItem/>);
let a = TestUtils.findRenderedDOMComponentWithTag(item, 'a');
expect(a);
let href = a.getDOMNode().getAttribute('href');
expect(href).to.equal(expectedRoute);
});
It was right there in front of me the whole time.
You can use a.getDOMNode() to get the a component's DOM node and then use regular DOM node methods on it. In this case, getAttribute('href') will return the value of the href attribute:
let a = TestUtils.findRenderedDOMComponentWithTag(item, 'a');
let domNode = a.getDOMNode();
expect(domNode.getAttribute('href')).to.equal('/records/2');