React Testing Library's waitFor not working - reactjs

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",
},
},
});

Related

Test functionality with timeouts in react-testing-library and jest

I'm trying to follow TDD, and I have a span that should appear on screen after 5 seconds. I haven't implemented the span at all, so the test should fail, but currently it passes the test expect(messageSpan).toBeInTheDocument.
Here are my two tests:
it("doesn't show cta message at first", () => {
render(<FAB />);
const messageSpan = screen.queryByText(
"Considering a career in nursing? Join our team!"
);
expect(messageSpan).toBeNull(); // passes, as it should
});
it("should show the cta message after 5 secs", () => {
render(<FAB />);
setTimeout(() => {
const messageSpan = screen.getByText( // also tried queryByText instead of get
"Considering a career in nursing? Join our team!"
);
expect(messageSpan).toBeInTheDocument(); // also passes, even though messageSpan should throw an error.
}, 5000);
});
Here's my FAB component, where you can see there's no message at all:
export default function FAB() {
return (
// using styled-components; there's no content in any of these.
<StyledFABContainer>
<StyledFABButton>
<BriefcaseIcon />
</StyledFABButton>
</StyledFABContainer>
);
}
To complicate things, I don't plan to have a set function I call for the setTimeout. I will simply set state after a set time of 5 secs. So don't think I can use the suggestions in the timer mocks section of jest docs: https://jestjs.io/docs/timer-mocks
My two questions are:
a) Why would this pass and not throw an error/null?
b) How can I properly test setTimeout functionality in RTL?
UPDATE: Have tried using various combinations of useFakeTimers, act, waitFor etc., but no luck. Here's my current test as it's written out, and throwing two errors - one saying I need to use act when changing state (which I am, but still) and one saying my messageSpan is null:
it("cta message to display after 5 secs", async () => {
jest.useFakeTimers();
const el = document.createElement("div");
act(() => {
ReactDOM.render(<FAB />, el);
});
jest.advanceTimersByTime(5000);
const messageSpan = screen.queryByText(
"Considering a career in nursing? Join our team!"
);
expect(messageSpan).toBeInTheDocument();
});
You can not use setTimeout like this in your tests. An obvious reason would be that you do not want to wait 5 seconds in your test to then continue. Imagine a component that would change after 10min. You cant wait that long but should use jests mocked timers API instead.
You can progress the nodejs timer so your component changes, then immediately make your assertion.
Other remarks about the tests you wrote:
If you are awaiting changes you should use await waitFor(() => ...) from testing library. It checks the expectations to pass every 50ms by default for a total of 5sec. If all expectations pass it continues. You should make asynchronous assertions there.
"expect(messageSpan).toBeNull()" should be "expect(messageSpan).not.toBeInTheDocument()". Its good to go with the accessibility way testing library provides.
If working with an asynchronous test I found a solution on this blog: https://onestepcode.com/testing-library-user-event-with-fake-timers/
The trick is to set the delay option on the userEvent to null.
const user = userEvent.setup({ delay: null });
Here is a full test case
test("Pressing the button hides the text (fake timers)", async () => {
const user = userEvent.setup({ delay: null });
jest.useFakeTimers();
render(<Demo />);
const button = screen.getByRole("button");
await user.click(button);
act(() => {
jest.runAllTimers();
});
const text = screen.queryByText("Hello World!");
expect(text).not.toBeInTheDocument();
jest.useRealTimers();
});

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.

Wait for multiple elements to be removed React testing Library

I have a react application where I'm using jest and react testing library for my unit tests. I have a unit test where I want to test that SomeComponent when loaded doesn't render any skeleton.
SomeComponent renders skeletons when data isn't loaded yet. Problem is that multiple skeletons are rendered and waitForElementToBeRemoved only takes one HTMLElement.
So I was wondering if there was a way to waitForMultipleElementsToBeRemoved?
test("SomeComponent when loaded doesn't render any skeleton", async () => {
render(
<SomeComponent />,
)
const skeletons = screen.getAllByTestId("skeleton");
await waitForElementToBeRemoved(skeletons);
expect(
screen.queryByTestId("skeleton")
).not.toBeInTheDocument();
});
Note:
This test works as intended when there is only one skeleton.
You can just:
await waitForElementToBeRemoved(() => screen.getAllByTestId("initLoader"));
or in your case:
await waitForElementToBeRemoved(() => skeletons);
I think you just missing the callback. Otherwise your code is fine!
You could do this if you only want to test for 1 skeleton to be rendered
test("only 1 skeleton", async () => {
const { getAllByTestId } = render(
<div data-testId="skeleton">spooky skeleton</div>
);
const skeletons = getAllByTestId("skeleton");
await waitFor(()=>{
expect(skeletons.length).toBe(1);
})
});
Here, I use getAllByTestId, but instead of checking for it to not be in the document, I just check if the length of the returned query is 1, meaning there is only one skeleton element being rendered.
I also wrap it in a waitFor as I'm assuming that the skeletons disappear over time. Note that the default timeout is only 1000ms, but this can be configured using the options parameter.
sandbox

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");
});

Resources