Unhandled error gets swallowed by async event handler in test with react and mocha.js - reactjs

I have a curious behaviour in my tests when trying to test an async event handler that awaits a promise. I am using Mocha for my test in combination with global-jsdom, #testing-library/react and #testing-library/user-event to render my component and click a button.
If I now have an error inside the awaited function, this error gets swallowed somewhere and Mocha exits with a passed test.
I build a minimal example to show what I mean:
AsyncFail.js:
import React from 'react';
const AsyncFail = () => {
const asyncHandleClick = async () => {
console.log('test')
await callAsync; //not defined, should throw error
}
return <div onClick={asyncHandleClick} data-testid='button'></div>
};
export default AsyncFail;
import React from 'react';
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import AsyncFail from 'asyncFailTest';
describe('asyncFail', () => {
let user;
beforeEach( () => {
user = userEvent.setup();
});
afterEach( () => {
})
context('test async failure', () => {
it('should throw ReferenceError', async () => {
render(
<AsyncFail/>
);
await user.click(screen.getByTestId('button'));
});
});
});
The button click only logs 'test' to the console
I was expecting the error in the best case to cause the test to fail. But I would already be happy if the error would be logged to console.error(), because than I could at least as a workaround spy on this call to check for errors.
I am aware that I should wrap the call with a try/catch-block, but I also want to cover the case if that is forgotten by chance.
I know that an error gets thrown because I can attach a listener to process for that:
In test file:
function onUncaught(err){
console.error(err);
}
beforeEach(() => {
process.on('unhandledRejection', onUncaught);
user = userEvent.setup();
});
This logs:
ReferenceError: callAsync is not defined
What is happening here? Am I doing something wrong?

Related

Dispatch a Custom Event and test if it was correctly triggered (React TypeScript, Jest)

I am trying to validate a custom event listener that lies inside a react function useEffect hook as shown below:
export interface specialEvent extends Event {
detail?: string
}
function Example() {
React.useEffect(()=>{
document.addEventListener('specialEvent', handleChange)
return () => {
document.removeEventListener('specialEvent',handleChange)
}
})
const handleChange = (event:SpecialEvent) => {
...
}
}
I want to trigger this custom event listener and test it in jest:
it('should trigger "specialEvent" event Listener Properly', async () => {
const specialEvent = new CustomEvent('specialEvent')
const handleChange = jest.fn()
render(<Example />)
await waitFor(() => {
window.document.dispatchEvent(specialEvent)
expect(window.document.dispatchEvent).toHaveBeenNthCalledWith(1, 'specialEvent')
expect(specialEvent).toHaveBeenCalledTimes(1)
})
})
This code gives me the following error:
expect(received).toHaveBeenNthCalledWith(n, ...expected)
Matcher error: received value must be a mock or spy function
Received has type: function
Received has value: [Function dispatchEvent]
As suggested in one of the answers, I tried this:
//Assert Statements
const specialEvent = new CustomEvent('specialEvent');
const handleSelect = jest.fn();
act(() => {
render(<Example />)
});
await waitFor(() => {
window.document.dispatchEvent(specialEvent)
expect(handleSelect).toHaveBeenCalledTimes(1)
});
But this time it says expected call to be 1 but recieved 0.
Can someone help me resolving this?
When testing, code that causes React state updates should be wrapped into act(...). If the handleChange does not cause the React state to update, you don't need to use act.
Besides, it's better not to test the implementation detail, for your case, the test implementation detail statements are:
expect(window.document.dispatchEvent).toHaveBeenNthCalledWith(1, 'specialEvent')
expect(specialEvent).toHaveBeenCalledTimes(1)
Every small change to the implementation detail will cause the test case need to be modified. We should test the UI from the perspective of the user, who doesn't care about the implementation details of the UI, only about rendering the UI correctly.
What you should test is: what happens to the output of the component when the custom event is fired and the state is changed in the event handler.
E.g.
index.tsx:
import React, { useState } from 'react';
export interface SpecialEvent extends Event {
detail?: string;
}
export function Example() {
const [changed, setChanged] = useState(false);
React.useEffect(() => {
document.addEventListener('specialEvent', handleChange);
return () => {
document.removeEventListener('specialEvent', handleChange);
};
});
const handleChange = (event: SpecialEvent) => {
console.log(event);
setChanged((pre) => !pre);
};
return <div>{changed ? 'a' : 'b'}</div>;
}
index.test.tsx:
import { render, screen, act } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import React from 'react';
import { Example } from './';
describe('70400540', () => {
test('should pass', () => {
const specialEvent = new CustomEvent('specialEvent');
render(<Example />);
expect(screen.getByText('b')).toBeInTheDocument();
act(() => {
window.document.dispatchEvent(specialEvent);
});
expect(screen.getByText('a')).toBeInTheDocument();
});
});
window.document.dispatchEvent(specialEvent) will cause the React state to change, so we wrap it into act(...).
Test result:
PASS examples/70400540/index.test.tsx (11.259 s)
70400540
✓ should pass (59 ms)
console.log
CustomEvent { isTrusted: [Getter] }
at Document.handleChange (examples/70400540/index.tsx:16:13)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 12.655 s
package versions:
"#testing-library/react": "^11.2.2",
"react": "^16.14.0",
"jest": "^26.6.3",
As the error message says, the toHaveBeenNthCalledWith matcher requires a mock or spy to be passed to expect.
However, you probably do not need to make any assertion about window.document.dispatchEvent being called because you know you are calling it on the line above in your test.
For more information, check the docs on toHaveBeenNthCalledWith here: https://jestjs.io/docs/expect#tohavebeennthcalledwithnthcall-arg1-arg2-

React Simulate back button behavior in react testing library and jest

Hey guys I've got a component that overrides the back button behavior by creating a popstate event, but I haven't found a way to test it's functionality. It should be as easy as creating a spy and checking if the window.confirm is being called, but it's not calling the function when I do window.history.back(), and I don't understand why.
Also if I pull the function outside of the component, it's being rendered as an anonymous function and the remove event listener isn't being called, which makes the popup event display on every page, and there's no way to remove it because it's an anonymous function. I'm able to test the function though and the logic is working just fine (;
How do we fix this? What should I do?
This function stops the initial back button navigation behavior, and creates a popup event to ask if the person wants to navigate to the home page, and if they click okay then they navigate, otherwise they stay on the page. We have this wrapped around a page after they submit a form to prevent them from submitting another form when they click the back button
Here's my component:
import React, {useEffect, ReactElement} from 'react';
import { navigate } from '#reach/router';
export interface BackButtonBehaviorProps {
children: ReactElement;
}
export const BackButtonBehavior = ({children}: BackButtonBehaviorProps) => {
const onBackButtonEvent = (e: { preventDefault: () => void; }) => {
e.preventDefault();
const backButtonIsConfirmed = window.confirm("Your claim has been submitted, would you like to exit before getting additional claim information?");
if (backButtonIsConfirmed) {
navigate('/');
} else {
window.history.pushState(window.history.state, "success page", window.location.pathname); // When you click back (this refreshes the current instance)
}
};
useEffect(() => {
window.history.pushState(window.history.state, "", window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent, true);
// As you're unmounting the component
return () => {
window.removeEventListener('popstate', onBackButtonEvent);
};
}, []);
return (children);
};
If I pull the function outside of the component and export it, it's rendered in the popstate event listeners as anonymous, and will not be deleted when I'm unmounting the component, and there's no way to fix that. Here are the tests that work when I exported it though:
import {cleanup, render} from '#testing-library/react';
import * as router from '#reach/router';
import { mockPersonalReturnObj } from '../Summary/testData';
describe('<App />', () => {
let navigateSpy: any;
let backButtonBehavior: any;
beforeEach(() => {
const mod = require('./BackButtonBehavior');
backButtonBehavior = mod.onBackButtonEvent;
navigateSpy = jest.spyOn(router, 'navigate');
});
afterEach(() => {
jest.resetAllMocks();
cleanup();
});
it('should display a popup when the user clicks the back button', async () => {
jest.spyOn(global, 'confirm' as any).mockReturnValueOnce(true);
backButtonBehavior({preventDefault: () => {}});
expect(global.confirm).toHaveBeenCalled();
});
it('should navigate to home page when you click ok on the window.confirm popup', async () => {
jest.spyOn(global, 'confirm' as any).mockReturnValueOnce(true);
backButtonBehavior({preventDefault: () => {}});
expect(global.confirm).toHaveBeenCalled();
expect(await navigateSpy).toHaveBeenCalledWith('/');
});
});
I haven't found a way to call the global confirm when I do a test and I literally just want to test if the window.confirm event is being called (there's a bunch of ways to check window.confirm through spies, none of them worked). I need a way to simulate the back button click event for this to be called, but I haven't found a way to do this. Here's a test example:
it('should navigate to home page when you click ok on the window.confirm popup', async () => {
jest.spyOn(global, 'confirm' as any).mockReturnValueOnce(true);
render(
<BackButtonBehavior>
<CurrentPage />
</BackButtonBehavior>
);
window.history.back();
expect(global.confirm).toHaveBeenCalled();
expect(await navigateSpy).toHaveBeenCalledWith('/', {});
});
How do we simulate clicking the back button in the browser for react tests?

How to test react-toastify with jest and react-testing-library

I have a screen with some form, and on submission, I send the request to back-end with axios. After successfully receiving the response, I show a toast with react-toastify. Pretty straight forward screen. However, when I try to test this behavior with an integration test using jest and react testing library, I can't seem to make the toast appear on DOM.
I have a utility renderer like that to render the component that I'm testing with toast container:
import {render} from "#testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";
export const renderWithToastify = (component) => (
render(
<div>
{component}
<ToastContainer/>
</div>
)
);
In the test itself, I fill the form with react-testing-library, pressing the submit button, and waiting for the toast to show up. I'm using mock service worker to mock the response. I confirmed that the response is returned OK, but for some reason, the toast refuses to show up. My current test is as follows:
expect(await screen.findByRole("alert")).toBeInTheDocument();
I'm looking for an element with role alert. But this seems to be not working.
Also, I tried doing something like this:
...
beforeAll(() => {
jest.useFakeTimers();
}
...
it("test", () => {
...
act(() =>
jest.runAllTimers();
)
expect(await screen.findByRole("alert")).toBeInTheDocument();
}
I'm kind of new to JS, and the problem is probably due to asynch nature of both axios and react-toastify, but I don't know how to test this behavior. I tried a lot of things, including mocking timers and running them, mocking timers and advancing them, not mocking them and waiting etc. I even tried to mock the call to toast, but I couldn't get it working properly. Plus this seems like an implementation detail, so I don't think I should be mocking that.
I think the problem is I show the toast after the axios promise is resolved, so timers gets confused somehow.
I tried to search many places, but failed to find an answer.
Thanks in advance.
Thank you #Estus Flask, but the problem was much much more stupid :) I had to render ToastContainer before my component, like this:
import {render} from "#testing-library/react";
import React from "react";
import {ToastContainer} from "react-toastify";
export const renderWithToastify = (component) => {
return (
render(
<div>
<ToastContainer/>
{component}
</div>
)
);
};
Then, the test was very simple, I just had to await on the title of the toast:
expect(await screen.findByText("alert text")).toBeInTheDocument();
The findByRole doesn't seem to work for some reason, but I'm too tired to dig deeper :)
I didn't have to use any fake timers or flush the promises. Apperently, RTL already does those when you use await and finBy* queries, only the order of rendering was wrong.
In order to use a mock when you don't have access to the DOM (like a Redux side effect) you can do:
import { toast } from 'react-toastify'
jest.mock('react-toastify', () => ({
toast: {
success: jest.fn(),
},
}))
expect(toast.success).toHaveBeenCalled()
What I would do is mock the method from react-toastify to spy on that method to see what is gets called it, but not the actual component appearing on screen:
// setupTests.js
jest.mock('react-toastify', () => {
const actual = jest.requireActual('react-toastify');
Object.assign(actual, {toast: jest.fn()});
return actual;
});
and then in the actual test:
// test.spec.js
import {toast} from 'react-toastify';
const toastCalls = []
const spy = toast.mockImplementation((...args) => {
toastCalls.push(args)
}
)
describe('...', () => {
it('should ...', () => {
// do something that calls the toast
...
// then
expect(toastCalls).toEqual(...)
}
}
)
Another recommendation would be to put this mockImplementation into a separate helper function which you can easily call for the tests you need it for. This is a bear bones approach:
function startMonitoring() {
const monitor = {toast: [], log: [], api: [], navigation: []};
toast.mockImplementation((...args) => {
monitor.toast.push(args);
});
log.mockImplementation((...args) => {
monitor.log.push(args);
});
api.mockImplementation((...args) => {
monitor.api.push(args);
});
navigation.mockImplementation((...args) => {
monitor.navigation.push(args);
});
return () => monitor;
}
it('should...', () => {
const getSpyCalls = startMonitoring();
// do something
expect(getSpyCalls()).toEqual({
toast: [...],
log: [...],
api: [...],
navigation: [...]
});
});
Here, the solution was use getByText:
await waitFor(() => {
expect(screen.getByText(/Logged!/i)).toBeTruthy()
})

When testing, code that causes React state updates should be wrapped into act

I have this test:
import {
render,
cleanup,
waitForElement
} from '#testing-library/react'
const TestApp = () => {
const { loading, data, error } = useFetch<Person>('https://example.com', { onMount: true });
return (
<>
{loading && <div data-testid="loading">loading...</div>}
{error && <div data-testid="error">{error.message}</div>}
{data &&
<div>
<div data-testid="person-name">{data.name}</div>
<div data-testid="person-age">{data.age}</div>
</div>
}
</>
);
};
describe("useFetch", () => {
const renderComponent = () => render(<TestApp/>);
it('should be initially loading', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('loading')).toBeDefined();
})
});
The test passes but I get the following warning:
Warning: An update to TestApp 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 TestApp
console.error
node_modules/react-dom/cjs/react-dom.development.js:506
Warning: An update to TestApp 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 TestApp
The key is to await act and then use async arrow function.
await act( async () => render(<TestApp/>));
Source:
https://stackoverflow.com/a/59839513/3850405
Try asserting inside 'await waitFor()' - for this your it() function should be async
it('should be initially loading', async () => {
const { getByTestId } = renderComponent();
await waitFor(() => {
expect(getByTestId('loading')).toBeDefined();
});
});
Keep calm and happy coding
I was getting the same issue which gets resolved by using async queries (findBy*) instead of getBy* or queryBy*.
expect(await screen.findByText(/textonscreen/i)).toBeInTheDocument();
Async query returns a Promise instead of element, which resolves when an element is found which matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of 1000ms. If you need to find more than one element, use findAllBy.
https://testing-library.com/docs/dom-testing-library/api-async/
But as you know it wont work properly if something is not on screen. So for queryBy* one might need to update test case accordingly
[Note: Here there is no user event just simple render so findBy will work otherwise we need to put user Event in act ]
Try using await inside act
import { act } from 'react-dom/test-utils';
await act(async () => {
wrapper = mount(Commponent);
wrapper.find('button').simulate('click');
});
test('handles server ok', async () => {
render(
<MemoryRouter>
<Login />
</MemoryRouter>
)
await waitFor(() => fireEvent.click(screen.getByRole('register')))
let domInfo
await waitFor(() => (domInfo = screen.getByRole('infoOk')))
// expect(domInfo).toHaveTextContent('登陆成功')
})
I solved the problem in this way,you can try it.
I don't see the stack of the act error, but I guess, it is triggered by the end of the loading when this causes to change the TestApp state to change and rerender after the test finished. So waiting for the loading to disappear at the end of the test should solve this issue.
describe("useFetch", () => {
const renderComponent = () => render(<TestApp/>);
it('should be initially loading', async () => {
const { getByTestId } = renderComponent();
expect(getByTestId('loading')).toBeDefined();
await waitForElementToBeRemoved(() => queryByTestId('loading'));
});
});
React app with react testing library:
I tried a lot of things, what worked for me was to wait for something after the fireevent so that nothing happens after the test is finished.
In my case it was a calendar that opened when the input field got focus. I fireed the focus event and checked that the resulting focus event occured and finished the test. I think maybe that the calendar opened after my test was finished but before the system was done, and that triggered the warning. Waiting for the calendar to show before finishing did the trick.
fireEvent.focus(inputElement);
await waitFor(async () => {
expect(await screen.findByText('December 2022')).not.toBeNull();
});
expect(onFocusJestFunction).toHaveBeenCalledTimes(1);
// End
Hopes this helps someone, I just spent half a day on this.
This is just a warning in react-testing-library (RTL). you do not have to use act in RTL because it is already using it behind the scenes. If you are not using RTL, you have to use act
import {act} from "react-dom/test-utils"
test('',{
act(()=>{
render(<TestApp/>)
})
})
You will see that warning when your component does data fetching. Because data fetching is async, when you render the component inside act(), behing the scene all the data fetching and state update will be completed first and then act() will finish. So you will be rendering the component, with the latest state update
Easiest way to get rid of this warning in RTL, you should run async query functions findBy*
test("test", async () => {
render(
<MemoryRouter>
<TestApp />
</MemoryRouter>
);
await screen.findByRole("button");
});

Spying on React child component method

I am trying to test my React component with jest and enzyme. I have a form component that uses react-skylight component. I am triggering .show() function on form submit and just when response from server is successful.
My test is currently like this:
import MyForm from './MyForm';
import Popup from 'react-skylight';
describe('<MyForm />', () => {
it('should show popup on success', () => {
const popupShowSpy = jest.spyOn(Popup.prototype, 'show');
const myForm = mount(<MyForm />);
myForm.update();
myForm.find('form').simulate('submit');
expect(popupShowSpy).toHaveBeenCalled();
});
});
but I am getting an error when I run tests:
expect(jest.fn()).toHaveBeenCalled()
Expected mock function to have been called.
I found here discusion about similar problem, but for me it is not working.
Solution:
Problem was with axios module. It was updating the component, but the response taht was mocked was not resolved, so thanks to this post here, I've managed to write tests. And I wrapped the child components function call in parent component own function and spied on that parent function.
import MyForm from './MyForm';
import Popup from 'react-skylight';
describe('<MyForm />', () => {
it('should show popup on success', async() => {
const popupShowSpy = jest.spyOn(MyForm.prototype, 'showPopup');
const myForm = mount(<MyForm />);
const response = Promise.resolve({
data: {
message: 'Error: some error'
},
status: 400
});
axios.post = jest.fn(() => response);
myForm.find('form').simulate('submit');
await response;
myForm.update(); // <- child component is rendered correctly
expect(popupShowSpy).toHaveBeenCalled();
});
});
Solution:
Problem was with axios module. It was updating the component, but the response that was mocked was not resolved, so thanks to this post here, I've managed to write tests. And I wrapped the child components function call in parent component own function and spied on that parent function.
import MyForm from './MyForm';
import Popup from 'react-skylight';
describe('<MyForm />', () => {
it('should show popup on success', async() => {
const popupShowSpy = jest.spyOn(MyForm.prototype, 'showPopup');
const myForm = mount(<MyForm />);
const response = Promise.resolve({
data: {
message: 'Error: some error'
},
status: 400
});
axios.post = jest.fn(() => response);
myForm.find('form').simulate('submit');
await response;
myForm.update(); // <- child component is rendered correctly
expect(popupShowSpy).toHaveBeenCalled();
});
});

Resources