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

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()
})

Related

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

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?

React Jest & React Testing Library, Writing test for Video player (play and pause)

I am using <ReactPlayer/> as part of my project, it gives you <video /> in HTML
I want to write the test cases for this.
To write test case, without using autoPlay I play the video manually so I have to write a test case for play and pause event of <ReactPlayer/> or <video />
Is there any way to write test case for event of "Play"
Thanks in advance
I coincidentally was working through the a similar challenge with this library. For me, this kind of ended up being a pain in the ass to work out because I was trying to test custom code using their on events (i.e. onPlay or onStart).
Given the information you provided, you shouldn't need to test the player itself. It's a third party library and they have their own test coverage that handles the functionality of the player.
As for me, trying to navigate how it all worked from an implementation standpoint seemed like a bad idea, so I ended up mocking the whole player out. I was trying to test that my code was being called on an onStart event, so I made sure my mock would handle that from the props:
// mock.jsx
jest.mock('react-player/file', () => (p) => {
const props = { ...p }
delete props.onStart
return (<div {...props} playing={p.playing.toString()} onClick={p.onStart} />)
});
// component.jsx
import React from 'react';
import tracking from 'tracking';
import ReactPlayer from 'react-player/file';
const TheVideoPlayer = () => {
const trackTheVideoPlayer = () => {
tracking.trackEvent('foo', { bar: 'baz' })
}
return (
<ReactPlayer
className={styles.theVideoPlayer}
data-testid="the-video-player"
light={backgroundSrc}
url={theVideoUrl()}
onStart={trackTheVideoPlayer}
playing
/>
)
}
// test.jsx
import { fireEvent, render, screen } from 'app/utils/test-utils';
import React from 'react';
import TheVideoPlayer from './TheVideoPlayer';
import tracking from 'tracking';
const mockTracking = { trackEvent: jest.fn() };
jest.spyOn(tracking, 'track').mockReturnValue(mockTracking);
describe('the video player', () => {
const createComponent = () => render(<TheVideoPlayer />);
describe('on start', () => {
it('Fires tracking Event', async () => {
createComponent();
const mockTrackProperties = { bar: 'baz' }
fireEvent.click(screen.getByTestId('video-player'))
expect(trackEvent).toBeCalledWith('foo', mockTrackProperties);
});
});
Since js-dom doesn't have a onStart event handler, I ended up using onClick and simulating a click. I also had to type cast the "playing" boolean to a string because jsDOM was giving a console error (but tests were passing). This doesn't feel particularly good or clean, but it does ensure that my code is being tested in an "onStart" event, however that is implemented in the library.

React Testing Library's waitFor not working

I am using React Testing Library to unit test my ReactJS code. There are several async events in the UI, like fetching data and displaying a new page on click of button. The React code is somewhat like this:
// Inside ParentComponent.tsx
const [isChildVisible, setChildVisibility] = useState(false);
const showChild = () => setChildVisibility(true);
return(
<>
<button data-testid="show-child" onClick={showChild}>Show Child</button>
{isChildVisible && <ChildComponent {..childProps}/>}
</>
)
Where ChildComponent mounts, it fetches some data and then re-renders itself with the hydrated data. My unit test looks like:
jest.mock('../../../src/service'); // mock the fetch functions used by ChildComponent to fetch its data
describe('ParentComponent', () => {
test('renders ChildComponent on button click', async () => {
const screen = render(<ParentComponent />);
userEvent.click(screen.getByTestId('show-child'));
await (waitFor(() => screen.getByText('text rendered by child')));
});
});
When I run this test, I get the error "TestingLibraryElementError: Unable to find an element with the text: text rendered by child. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.".
I am not sure why it's happening, but one of the reason maybe that it's taking more than one second to hydrate and render the child component. Thus I want to change the default wait time for waitFor, but I can't find a way to do it from the docs (the default wait time is one second). So is it possible to change the default wait time?
EDIT: Increasing the wait time is still causing the same error. So the issue is something else.
I found the answer here: React Testing Library - using 'await wait()' after fireEvent
TLDR: "You can not use wait with getBy*. getBy is not async and will not wait." Better is to use findBy*. This is the async version of getBy.
It's specified within the documentation. waitFor Documentation
function waitFor<T>(
callback: () => T | Promise<T>,
options?: {
container?: HTMLElement
timeout?: number //This is 1000ms. Change timeout here.
interval?: number
onTimeout?: (error: Error) => Error
mutationObserverOptions?: MutationObserverInit
}
): Promise<T>
//For 3 seconds.
await (waitFor(() => screen.getByText('text rendered by child'),{timeout:3000}));
The default timeout is 1000ms which will keep you under Jest's default timeout of 5000ms.
I had an issue similar to this when I was setting up testing for a test application. The way I fixed this issue was to force re-render the component.
In this case your code would look something like:
import {render, screen} from "#testing-library/react";
describe('ParentComponent', () => {
test('renders ChildComponent on button click', async () => {
const {rerender} = render(<ParentComponent />);
userEvent.click(screen.getByTestId('show-child'));
rerender(<ParentComponent />)
await (waitFor(() => screen.getByText('text rendered by child')));
});
});
I hope this works for you. Also to be noted that you can use the screen export from the react testing library. It seems like there should be a way to do this automatically, but I haven't been able to find it.
Adding link to the rerender docs: https://testing-library.com/docs/react-testing-library/api/#rerender
For those who are using jest-expo preset which breaks this functionality you need to modify the jest-expo preset to include the code from testing-library/react-native
/* eslint-disable #typescript-eslint/no-var-requires */
const { mergeDeepRight } = require("ramda");
const jestExpoPreset = require("jest-expo/jest-preset");
const testingLibraryPreset = require("#testing-library/react-native/jest-preset");
/*
* Modify the existing jest preset to implement the fix of #testing-library/react-native to get the
* async waitFor working with modern timers.
*/
jestExpoPreset.setupFiles = [
testingLibraryPreset.setupFiles[0],
...jestExpoPreset.setupFiles,
testingLibraryPreset.setupFiles[testingLibraryPreset.setupFiles.length - 1],
];
module.exports = mergeDeepRight(jestExpoPreset, {
testResultsProcessor: "jest-sonar-reporter",
moduleFileExtensions: ["js", "jsx", "ts", "tsx", "yml"],
modulePathIgnorePatterns: ["<rootDir>/lib/"],
globals: {
"ts-jest": {
babelConfig: "./babel.config.js",
},
},
});

enzyme testing - can't find button

I have a fairly basic react component in a React app. I want to test that the "submitted" portion of the state changes from false to true when the form is submitted. Not particularly hard. But the enzyme test seems unable to find the button. Not sure if it has to do with the if/else statement.
Here is the component:
import React from 'react';
import { connect } from 'react-redux';
import { questionSubmit } from '../actions/users';
import { getCurrentUser, clearMessage } from '../actions/auth';
export class AnswerForm extends React.Component {
constructor(props) {
super(props);
this.state = {
submitted: false
}
}
handleFormSubmit(event) {
event.preventDefault();
this.setState({ submitted: true });
this.props.dispatch(questionSubmit(this.answerInput.value, this.props.currentUsername));
this.answerInput.value = '';
}
handleNextButton() {
this.setState({ submitted: false });
this.props.dispatch(getCurrentUser(this.props.currentUsername))
}
render() {
let nextButton;
let form;
let message = <p>{this.props.message}</p>
if (this.state.submitted) {
nextButton = <button className="button-next" onClick={() => this.handleNextButton()}>Next</button>;
}
else {
form =
<form onSubmit={e => this.handleFormSubmit(e)}>
<input className="input-answer" ref={input => this.answerInput = input}
placeholder="Your answer" />
<button id="button-answer" type="submit">Submit</button>
</form>;
}
return (
<div>
<p>{this.props.message}</p>
{form}
{nextButton}
</div>
)
}
}
export const mapStateToProps = (state, props) => {
return {
message: state.auth.message ? state.auth.message : null,
currentUsername: state.auth.currentUser ? state.auth.currentUser.username : null,
question: state.auth.currentUser ? state.auth.currentUser.question : null
}
}
export default connect(mapStateToProps)(AnswerForm);
Here is the test:
import React from 'react';
import {AnswerForm} from '../components/answer-form';
import {shallow, mount} from 'enzyme';
describe('<AnswerForm />', () => {
it('changes submitted state', () => {
const spy = jest.fn();
const wrapper = mount(<AnswerForm dispatch={spy}/> );
wrapper.instance();
expect(wrapper.state('submitted')).toEqual(false);
const button = wrapper.find('#button-answer');
button.simulate('click')
expect(wrapper.state('submitted')).toEqual(true);
});
});
I get this error when I try running this test:
expect(received).toEqual(expected)
Expected value to equal:
true
Received:
false
at Object.it (src/tests/answer-form.test.js:24:44)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
Any ideas? It's a pretty straight shot other than the if statement. Not sure what is going on here.
The issue here is that the intrinsic DOM event propagation that is expected to occur between a submit button and a form element is not being done by enzyme or React during simulation.
The event system in React is all synthetic in order to normalise browser quirks, they actually all get added to document (not the node you add the handler to) and fake events are bubbled through the components by React (I highly recommend watching this webinar from the React core team explaining in event system in depth)
This makes testing them a little unintuitive and sometimes problematic, because simulation does not trigger real DOM event propagation
In enzyme, events triggered on shallow renders are not real events at all and will not have associated DOM target. Even when using mount which does have a DOM fragment backing it, it still uses React's synthetic event system, so simulate still only tests synthetic events bubbling though your components, they do not propagate via real DOM, so simulating a click on a submit button does not in turn intrinsically trigger a submit DOM event on the form itself, as its the browser not React that is responsible for that. https://github.com/airbnb/enzyme/issues/308
So two ways to get around that in a test are...
1) Not ideal from a UI test perspective as bypasses button, but clean for a unit test, especially as it should work with shallow rendering to isolate the component.
describe('<AnswerForm />', () => {
const spy = jest.fn();
const wrapper = shallow(<AnswerForm dispatch={spy}/> );
it('should show form initially', () => {
expect(wrapper.find('form').length).toEqual(0);
})
describe('when the form is submitted', () => {
before(() => wrapper.find('form').simulate('submit')))
it('should have dispatched the answer', () => {
expect(spy).toHaveBeenCalled();
});
it('should not show the form', () => {
expect(wrapper.find('form').length).toEqual(0);
});
it('should show the "next" button', () => {
expect(wrapper.find('#button-next').length).toEqual(1);
});
});
});
2) Trigger a real click event on DOM button element itself, rather than simulating it on your component as if it were a Selenium functional test (so feels a little dirty here), which the browser will propagate into a form submit before React catches the submit event and takes over with synthetic events. Therefore this only works with mount
describe('<AnswerForm />', () => {
const spy = jest.fn();
const wrapper = mount(<AnswerForm dispatch={spy}/> );
it('should show form initially', () => {
expect(wrapper.find('form').length).toEqual(0);
})
describe('when form is submitted by clicking submit button', () => {
before(() => wrapper.find('#button-answer').getDOMNode().click())
it('should have dispatched the answer', () => {
expect(spy).toHaveBeenCalled();
});
it('should not show the form', () => {
expect(wrapper.find('form').length).toEqual(0);
});
it('should show the "next" button', () => {
expect(wrapper.find('#button-next').length).toEqual(1);
});
});
});
You'll also note I'm not testing state itself. It's generally bad practice to test state directly as its pure implementation detail (state change should eventually cause something more tangible to happen to the component that can instead be tested).
Here I have instead tested that your event causes the dispatch spy to have been called with correct args, and that the Next button is now shown instead of the form. That way it is more focused on outcomes and less brittle should you ever refactor the internals.
Be mindful that the component you are testing is not the AnswerForm class component, but rather the wrapped component created by passing AnswerForm to react-redux's connect higher order component.
If you use shallow rendering rather than full mounting, you can use the dive() function of the Enzyme API to get to your actual class component. Try this:
import React from 'react';
import {AnswerForm} from '../components/answer-form';
import {shallow, mount} from 'enzyme';
describe('<AnswerForm />', () => {
it('changes submitted state', () => {
const spy = jest.fn();
const wrapper = shallow(<AnswerForm dispatch={spy}/> );
expect(wrapper.dive().state('submitted')).toEqual(false);
const button = wrapper.dive().find('#button-answer');
button.simulate('click')
expect(wrapper.dive().state('submitted')).toEqual(true);
});
});
Another option is to test the non-wrapped component instance directly. To do this, you just need to change your export and import. In answer-form.js:
export class AnswerForm extends React.Component
...your code
export default connect(mapStateToProps)(AnswerForm);
This exports the non-wrapped component in addition to the wrapped component. Then your imports in answer-form.test.js:
import WrappedAnswerForm, { AnswerForm } from 'path/to/answer-form.js`;
This way, you can test AnswerForm functionality independently, assuming you don't need to test any Redux received props. Check out this GitHub issue for more guidance.

Return value of a mocked function does not have `then` property

I have the following async call in one of my React components:
onSubmit = (data) => {
this.props.startAddPost(data)
.then(() => {
this.props.history.push('/');
});
};
The goal here is to redirect the user to the index page only once the post has been persisted in Redux (startAddPost is an async action generator that sends the data to an external API using axios and dispatches another action that will save the new post in Redux store; the whole thing is returned, so that I can chain a then call to it in the component itself). It works in the app just fine, but I'm having trouble testing it.
import React from 'react';
import { shallow } from 'enzyme';
import { AddPost } from '../../components/AddPost';
import posts from '../fixtures/posts';
let startAddPost, history, wrapper;
beforeEach(() => {
startAddPost = jest.fn();
history = { push: jest.fn() };
wrapper = shallow(<AddPost startAddPost={startAddPost} history={history} />);
});
test('handles the onSubmit call correctly', () => {
wrapper.find('PostForm').prop('onSubmit')(posts[0]);
expect(startAddPost).toHaveBeenLastCalledWith(posts[0]);
expect(history.push).toHaveBeenLastCalledWith('/');
});
So I obviously need this test to pass, but it fails with the following output:
● handles the onSubmit call correctly
TypeError: Cannot read property 'then' of undefined
at AddPost._this.onSubmit (src/components/AddPost.js:9:37)
at Object.<anonymous> (src/tests/components/AddPost.test.js:25:46)
at process._tickCallback (internal/process/next_tick.js:109:7)
So how can I fix this? I suspect this is a problem with the test itself because everything works well in the actual app. Thank you!
Your code is not testable in the first place. You pass in a callback to the action and execute it after saving the data to the database like so,
export function createPost(values, callback) {
const request = axios.post('http://localhost:8080/api/posts', values)
.then(() => callback());
return {
type: CREATE_POST,
payload: request
};
}
The callback should be responsible for the above redirection in this case. The client code which uses the action should be like this.
onSubmit(values) {
this.props.createPost(values, () => {
this.props.history.push('/');
});
}
This makes your action much more flexible and reusable too.
Then when you test it, you can pass a stub to the action, and verify whether it is called once. Writing a quality, testable code is an art though.
The problem with your code is that the startAddPost function is a mock function which does not return a Promise, but your actual this.props.startAddPost function does return a Promise.
That's why your code works but fails when you try to test it, leading to the cannot read property.... error.
To fix this make your mocked function return a Promise like so -
beforeEach(() => {
startAddPost = jest.fn().mockReturnValueOnce(Promise.resolve())
...
});
Read more about mockReturnValueOnce here.

Resources