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

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

Related

Should you render components / select elements in each `test()/it()` block or globally?

In react-testing-library you have to render your react component before executing some tests on its elements.
For several tests on the same component, should you avoid
rendering the component multiple times? Or do you have to render it in each
test()/it() block?
Should you select elements of the component (e.g. button) in each test()/it() block, or should you lift the selection, and select only once?
Does it have any impact on the execution time of the tests?
Is one of the approaches a best practice/antipattern?
Why does the last example fail?
For the basic component I have the following testing approaches:
function MyComponent() {
return (
<>
<button disabled>test</button>
<button disabled>another button</button>
</>
);
}
e.g.
describe("MyComponent", () => {
it("renders", async () => {
const { getByRole } = render(<MyComponent />);
const button = getByRole("button", { name: /test/i });
expect(button).toBeInTheDocument();
});
it("is disabled", async () => {
// repetetive render and select, should be avoided or adopted?
const { getByRole } = render(<MyComponent />);
const button = getByRole("button", { name: /test/i });
expect(button).toBeDisabled();
});
});
vs.
describe("MyComponent", () => {
const { getByRole } = render(<MyComponent />);
const button = getByRole("button", { name: /test/i });
it("renders", async () => {
expect(button).toBeInTheDocument();
});
it("is disabled", async () => {
expect(button).toBeDisabled();
});
});
I would expect the second approach to have a faster execution time since the component has to be rendered only once, but I don't know how to measure it and if it is an anti-pattern?
While it seems to be more DRY, if I add another toBeInTheDocument check, it fails.
Why is this the case?
describe("MyComponent", () => {
const { getByRole } = render(<MyComponent />);
const button = screen.getByRole("button", { name: /test/i });
const button2 = screen.getByRole("button", { name: /another button/i });
it("renders", async () => {
expect(button).toBeInTheDocument(); //ok
});
it("is disabled", async () => {
expect(button).toBeDisabled(); // ok
});
it("renders second button", async () => {
expect(button2).toBeInTheDocument(); // fails: element could not be found in the document
});
});
So this approach seems to be more error-prone!?
Each test should be as atomic as possible, meaning that it should not be using anything that other tests are also using and should run with a fresh state. So relating that to your examples, the first one would be the correct pattern.
When you have a test suite that contains sharable state between unit tests e.g. objects or environment variables, the test suite is very prone to errors. The reason for that is; if one of the unit tests happens to mutate one of the shared objects; all of the other unit tests will also be affected by this, causing them to exhibit unwanted behaviour. This can result in test failures where the code is technically correct or even set up landmines for future developers where the addition of new tests which are correct would still result in failures, hence causing major headaches in figuring out why this is happening.
The only exception to this rule would be immutable primitive variables (e.g. string, number, boolean with the use of const keyword) as tests will not be able to mutate them and they are useful for storing reusable ids, text etc.
Ofcourse, repeating the setup of each unit test can make them really clunky, that's why jest offers the beforeEach, beforeAll, afterEach and afterAll functions to extract the repeating logic. However, this opens up the vulnerability of shared state, so do be careful and make sure that all state is refreshed before any tests are kicked off. Ref.
For the last question as to why your last unit test in the last example is failing - it appears that you are using getByRole to look for button text. You should be using getByText instead. getByRole is used with role attributes (e.g. <button role="test">test</button>) which you don't seem to be using.

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

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

React Testing Library - using 'await wait()' after fireEvent

I'm trying to use Testing Library to check for DOM Elements after a fireEvent.click. I know I need to wait after the fireEvent, but am not sure why simply using await doesn't work? Below is the same test written two ways -- the first one fails, the second passes. I don't understand why the first one fails...am very grateful for any insights!
p.s. -- I know wait is deprecated and waitFor is preferred, however due to some constraints I can not update the version at this time :(
FAILING TEST
// This test fails with the following error and warning:
// Error: Unable to find an element by: [data-test="name_wrapper"]
// Warning: An update to OnlinePaymentModule inside a test was not wrapped in act(...).
it('this is a failing test...why', async () => {
const { getByText, getByTestId } = render(<Modal {...props} />);
const button = getByText('open modal');
fireEvent.click(button);
const nameWrapper = await getByTestId('name_wrapper');
expect(
nameWrapper.getElementsByTagName('output')[0].textContent
).toBe('Jon Doe');
const numberWrapper = await getByTestId('number_wrapper');
expect(
numberWrapper.getElementsByTagName('output')[0].textContent
).toBe('123456');
});
PASSING TEST -- Why does this pass but first one fails?
// This test passes with no warnings
it('this is a passing test...why', async () => {
const { getByText, getByTestId } = render(<Modal {...props} />);
const button = getByText('open modal');
fireEvent.click(button);
await wait(() => {
const nameWrapper = getByTestId('name_wrapper');
expect(
nameWrapper.getElementsByTagName('output')[0].textContent
).toBe('Jon Doe');
const numberWrapper = getByTestId('number_wrapper');
expect(
numberWrapper.getElementsByTagName('output')[0].textContent
).toBe('123456');
})
});
5 months later I'm coming back to answer my question (I've learned a lot since posting this question lol)....
First of all, since it is 5 months later I want to underscore that it is better to use the userEvent library instead of fireEvent if possible.
I also would be remiss to not call out that there are a lot of antipatterns in the code ...You should only ever make one assertion in waitFor. You should avoid using getByTestId in favor of more accessible alternatives.
And finally the reason the first test was failing is that you can not use wait with getBy*. getBy is not async and will not wait. This would have been the better solution:
fireEvent.click(button);
const nameWrapper = await findByTestId('name_wrapper');
Then the test would have waited on the nameWrapper element to be available.
The second test passed because getBy is wrapped in RTL's async utility, wait (wait is now deprecated in favor of waitFor). That is essentially what findBy does under the hood -- findBy is the async version of getBy.
When I posted the question I didn't fully understand that await is a Javascript key word (and just syntactical sugar to make code wait on a promise to resolve). wait (now waitFor) is a utility from RTL that will make execution of the test wait until the callback does not throw an error.

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