I don't know how to test key down event & prevent default. Test reached code but preventDefault has never been called: Received number of calls: 0
React Component - App.js
const onKeyDown = e => {
console.log("==== TEST REACHED HERE ====")
e.preventDefault(); // NEVER CALLED ???
};
useEffect(() => {
document.addEventListener("keydown", onKeyDown, false);
return () =>
document.removeEventListener("keydown", onKeyDown, false);
}, []);
Unit test
it("should prevent default action on key down", () => {
const { getByRole } = render(<App {...props} />);
const grid = getByRole("app");
const mockEvent = { preventDefault: jest.fn() };
fireEvent.keyDown(grid, mockEvent);
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
});
I found a more explicit way than accepted answer. You can use createEvent from 'react-testing-library' to manually create the event before firing it.
it("should prevent default action on key down", () => {
const { getByRole } = render(<App {...props} />);
const grid = getByRole("app");
const keyDownEvent = createEvent.keyDown(grid);
fireEvent(grid, keyDownEvent);
expect(keyDownEvent.defaultPrevented).toBe(true);
});
I think this method can be reused to test other things than defaultPrevented !
It seems that you can not mock preventDefault property of the event with react testing library.
Under the hook, fireEvent is calling dispatchEvent, so you can take advantage of the fact that calling event.preventDefault returns false if the event is cancelled:
it("should prevent default action on key down", () => {
const { getByRole } = render(<App {...props} />);
const grid = getByRole("app");
const isPrevented = fireEvent.keyDown(grid);
expect(isPrevented).toBe(false);
});
Related
I need to test the following component that consumes a custom hook of mine.
import { useMyHook } from 'hooks/useMyHook';
const MyComponent = () => {
const myHookObj = useMyHook();
const handler = () => {
myHookObj.myMethod(someValue)
}
return(
<button onClick={handler}>MyButton</button>
);
};
This is my test file:
jest.mock('hooks/useMyHook', () => {
return {
useMyHook: () => {
return {
myMethod: jest.fn(),
};
},
};
});
describe('<MyComponent />', () => {
it('calls the hook method when button is clicked', async () => {
render(<MyComponent {...props} />);
const button = screen.getByText('MyButton');
userEvent.click(button);
// Here I need to check that the `useMyHook.method`
// was called with some `value`
// How can I do this?
});
});
I need to check that the useMyHook.method was called with some value.
I also want to test it from multiple it cases and it might be called with different values on each test.
How can I do this?
This is how I was able to do it:
import { useMyHook } from 'hooks/useMyHook';
// Mock custom hook that it's used by the component
jest.mock('hooks/useMyHook', () => {
return {
useMyHook: jest.fn(),
};
});
// Mock the implementation of the `myMethod` method of the hook
// that is used by the Component
const myMethod = jest.fn();
(useMyHook as ReturnType<typeof jest.fn>).mockImplementation(() => {
return {
myMethod: myMethod,
};
});
// Reset mock state before each test
// Note: is needs to reset the mock call count
beforeEach(() => {
myMethod.mockReset();
});
Then, on the it clauses, I'm able to:
it (`does whatever`, async () => {
expect(myMethod).toHaveBeenCalledTimes(1);
expect(myMethod).toHaveBeenLastCalledWith(someValue);
});
Summary
I'm writing test code for my react app, but somehow, it always fails.
My app code is very simple, there is only one button, and if it's clicked, a function handleSubmit is fired.
What the handler does are
Fetching data from backend(This is async function)
Move to /complete page.
What I did
I mocked the function fetching data from API in test code
I mocked the useHistory in test code
Note
I realized that if the line that is fetching data from API is commented out, the test will pass.
Code
My main app code
import { useFetchDataFromAPI } from '#/usecase/useFetchDataFromAPI';
:
const { fetchDataFromAPI } = useFetchDataFromAPI();
:
const handleSubmit = async () => {
// If the line below is not commented out, test will fail
// const { id } = await fetchDataFromAPI();
history.push(`/complete`);
};
return (
<>
<button onClick={handleSubmit}>Button</button>
</>
My test code
:
jest.mock('#/usecase/useFetchDataFromAPI', () => ({
useFetchDataFromAPI: () => {
return { fetchDataFromAPI: jest.fn((): number => {
return 1;
})}
}
}));
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom') as any,
useHistory: () => ({
push: mockHistoryPush,
}),
}));
:
const renderApplicationWithRouterHistory = () => {
const history = createMemoryHistory();
const wrapper = render(
<Router history={history}>
<Application />
</Router>
);
return { ...wrapper, history };
};
:
describe('Test onClick handler', async () => {
test('Submit', () => {
const { getByText, getByRole } = renderApplication();
const elementSubmit = getByText('Button');
expect(elementSubmit).toBeInTheDocument();
fireEvent.click(elementSubmit);
expect(mockHistoryPush).toHaveBeenCalled();
});
});
Your event handler is called on button click, but because it is asynchronous, its result is not evaluated until after your test runs. In this particular case, you don't need the async behavior, so just use:
const handleSubmit = () => {
history.push(`/complete`)
}
testing-library provides a method waitFor for this if your handler did need to await something:
await waitFor(() => expect(mockHistoryPush).toHaveBeenCalled())
Though another simple way is to simply await a promise in your test so that the expectation is delayed by a tick:
fireEvent.click(elementSubmit);
await Promise.resolve();
expect(mockHistoryPush).toHaveBeenCalled();
in this case I am doing an example project which tries to show a phrase and the author of the phrase the first time the component is loaded and when a button is clicked to obtain a new phrase. The problem with testing is I am trying to simulate the way the user interacts with the application and having two tests asynchronously where each one has the act () function I get the following error:
console.error node_modules/react-dom/cjs/react-dom-test-utils.development.js:87
Warning: You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one.
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
console.error node_modules/react-dom/cjs/react-dom.development.js:88
Warning: An update to QuoteContainer inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in QuoteContainer (created by WrapperComponent)
in WrapperComponent
The test is passing but it generates this alert. I already looked for alternatives such as avoiding asynchronous testing but when doing it the test does not pass, it forces me to put the await in the act.
This example is simple but I would like to apply this same test in more complex applications to verify the correct operation of it. So it is probably necessary to have more asynchronous tests.
Here is the container component:
import React, {
useEffect,
useState,
} from "react";
import Quote from "./quote.component";
import { getQuote } from "../../repository/quote.repository";
const QuoteContainer = () => {
const [quote, setQuote] = useState("");
const [author, setAuthor] = useState("");
const [isLoading, setLoading] = useState(true);
useEffect(() => {
getQuoteData();
}, []);
const newQuoteHandler = () => {
getQuoteData();
};
const getQuoteData = async () => {
const {
quoteText,
quoteAuthor,
} = await getQuote();
setAuthor(quoteAuthor);
setQuote(quoteText);
setLoading(false);
};
return (
<Quote
isLoading={isLoading}
quote={quote}
author={author}
newQuoteHandler={newQuoteHandler}
/>
);
};
export default QuoteContainer;
And the test related to the container:
import React from "react";
import { server, rest } from "../../mocks/server";
import waitForExpect from "wait-for-expect";
import { mount } from "../../enzymeConfig";
import QuoteContainer from "./quote.container";
import { act } from "react-dom/test-utils";
import { QuoteText } from "../quote/quote.style";
import { SpinnerContainer } from "../withSpinner/withSpinner.style";
describe("testing quote api", () => {
it("should render spinner on start", () => {
const wrapper = mount(<QuoteContainer />);
expect(
wrapper.find(SpinnerContainer)
).toHaveLength(1);
});
it("should render actual component on load information", async (done) => {
expect.assertions(1);
const wrapper = mount(<QuoteContainer />);
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual(
"Know how to listen, and you will profit even from those who talk badly."
);
done();
});
});
});
it("should change quote and author when Quote Button click", async (done) => {
const wrapper = mount(<QuoteContainer />);
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual(
"Know how to listen, and you will profit even from those who talk badly."
);
done();
});
});
rest.get(
"apiURL",
(req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
quoteText: "this is an other frase",
quoteAuthor: "Plutarch ",
senderName: "",
senderLink: "",
})
)
);
wrapper
.find(CustomButton)
.at(0)
.simulate("click");
await act(async () => {
await waitForExpect(() => {
wrapper.update();
expect(
wrapper.find(QuoteText).text()
).toEqual("this is an other frase");
done();
});
});
});
});
Thanks from now
Despite reading the documentation of enzyme, and act, I could not find a response to my use case because the examples only show simple use cases.
I have a React component displaying a button. The onClick handler sets a loading boolean and calls an external API. I want to assert that the component shows the loading indicator when we click on the button.
Here is the component:
export default function MyButton(): ReactElement {
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<any>(null);
const onClick = async (): Promise<void> => {
setLoading(true);
const response = await fetch('/uri');
setData(await response.json());
setLoading(false);
};
if (loading) {
return <small>Loading...</small>;
}
return (
<div>
<button onClick={onClick}>Click Me!</button>
<div>
{data}
</div>
</div>
);
}
And here is the test:
test('should display Loading...', async () => {
window.fetch = () => Promise.resolve({
json: () => ({
item1: 'item1',
item2: 'item2',
}),
});
const component = mount(<MyButton />);
// Case 1 ✅ => validates the assertion BUT displays the following warning
component.find('button').simulate('click');
// Warning: An update to MyButton inside a test was not wrapped in act(...).
// When testing, code that causes React state updates should be wrapped into act(...):
// act(() => {
/* fire events that update state */
// });
/* assert on the output */
// This ensures that you're testing the behavior the user would see in the browser. Learn more at [URL to fb removed because SO does not accept it]
// Case 2 ❌ => fails the assertion AND displays the warning above
act(() => {
component.find('button').simulate('click');
});
// Case 3 ❌ => fails the assertion BUT does not display the warning
await act(async () => {
component.find('button').simulate('click');
});
expect(component.debug()).toContain('Loading...');
});
As you can see, if I get rid of the warning, my test is not satisfying anymore as it waits for the promise to resolve. How can we assert the intermediate state change while using act?
Thanks.
Just resolve promise manually:
const mockedData = {
json: () => ({
item1: 'item1',
item2: 'item2',
}),
};
let resolver;
window.fetch = () => new Promise((_resolver) => {
resolver = _resolver;
});
// ....
await act(async () => {
component.find('button').simulate('click');
});
expect(component.debug()).toContain('Loading...');
resolver(mockedData);
expect(component.debug()).not.toContain('Loading...');
PS but in sake of readability I'd rather have 2 separate tests: one with new Promise(); that never resolves and another with Promise.resolve(mockedData) that would be resolved automatically
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside);
}
/**
* Set the wrapper ref
*/
setWrapperRef(node) {
this.wrapperRef = node;
}
handleClickOutside(event) {
/* istanbul ignore next */
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
this.props.clickHandler();
}
}
How do i call the handleClickOutside function. How should I mimic the clickOutside here? Please help
it('lets click outside and close dropdown', () => {
const handleClickOutside = sinon.spy();
expect(handleClickOutside.called).to.be.true;
wrapper.unmount();
});
Assuming this is a HOC or render prop that renders other components as children (this.props.children) -- also the following is a Jest and Enzyme test, so it may be slightly different than what you're using.
components/__test__/ClickHandler.test.js
const clickHandler = jest.fn()
const initialProps = {
clickHandler,
children: <button className="example">Test</button>
...other props
};
const wrapper = shallow(<ClickHandler {...initialProps} />);
describe("Example", () => {
it('handles clicks inside of the component', () => { // this would test the event lisenter, the class method, and the clickHandler -- slightly overkill
const spy = jest.spyOn(wrapper.instance(), 'handleClickOutside');
wrapper.instance().forceUpdate();
wrapper.find('button').simulate('click');
expect(spy).toHaveBeenCalled();
expect(clickHandler).toHaveBeenCalledTimes(0);
spy.mockClear();
});
it('handles clicks outside of the component', () => { // this is a more straight forward test that assumes the event listener is already working
const event = { target: <div className="example">Outside Div</div> };
wrapper.instance().handleClickOutside(event);
expect(clickHandler).toHaveBeenCalled();
});
})