How to wait to assert an element never appears in the document? - reactjs

I want to assert that an element never appears in my document. I know I can do this:
import '#testing-library/jest-dom/extend-expect'
it('does not contain element', async () => {
const { queryByText } = await render(<MyComponent />);
expect(queryByText('submit')).not.toBeInTheDocument();
});
But in my case I need to wait to ensure that the element isn't added after a delay. How can I achieve this?

There are two ways to do this, both involving react-testing-library's async helper function waitFor.
The first and simpler method is to wait until something else happens in your document before checking that the element doesn't exist:
import '#testing-library/jest-dom/extend-expect'
it('does not contain element', async () => {
const { getByText, queryByText } = await render(<MyComponent />);
await waitFor(() => expect(getByText('something_else')).toBeInTheDocument());
expect(queryByText('submit')).not.toBeInTheDocument();
});
You can use the same strategy with any valid Jest assertion:
import '#testing-library/jest-dom/extend-expect'
import myFunc from './myFunc'
it('does not contain element', async () => {
const { getByText, queryByText } = await render(<MyComponent />);
await waitFor(() => expect(myFunc).toBeCalled());
expect(queryByText('submit')).not.toBeInTheDocument();
});
If there isn't any good assertion you can use to wait for the right time to check an element does not exist, you can instead use waitFor to repeatedly check that an element does not exist over a period of time. If the element ever does exist before the assertion times out, the test will fail. Otherwise, the test will pass.
import '#testing-library/jest-dom/extend-expect'
it('does not contain element', async () => {
const { getByText } = await render(<MyComponent />);
await expect(async () => {
await waitFor(
() => expect(getByText('submit')).toBeInTheDocument();
);
}).rejects.toEqual(expect.anything());
});
You can adjust the amount of time waitFor will keep checking and how frequently it will check using the timeout and interval options. Do note, though, that since this test waits until waitFor times out for the test to pass, increasing the timeout option will directly increase the time this test takes to pass.
And here is the helper function I wrote to avoid having to repeat the boilerplate:
export async function expectNever(callable: () => unknown): Promise<void> {
await expect(() => waitFor(callable)).rejects.toEqual(expect.anything());
}
Which is then used like so:
it('does not contain element', async () => {
const { getByText } = await render(<MyComponent />);
await expectNever(() => {
expect(getByText('submit')).toBeInTheDocument();
});
});

We use plain JavaScript and the expectNever function from #Nathan throws an error:
Error: expect(received).rejects.toEqual()
Matcher error: received value must be a promise
I modified it to look and feel more like waitFor and this works:
const waitForNeverToHappen = async (callable) => {
await expect(waitFor(callable)).rejects.toEqual(expect.anything())
}
await waitForNeverToHappen(() => expect(screen.getByText('submit')).toBeInTheDocument())

Consider using waitForElementToBeRemoved documented here:
https://testing-library.com/docs/guide-disappearance/#waiting-for-disappearance

Related

React jest hook testing: waitFor internal async code

I have the following hook:
import { useEffect, useRef, useState } from "react";
function useAsyncExample() {
const isMountedRef = useRef(false);
const [hasFetchedGoogle, setHasFetchedGoogle] = useState(false);
useEffect(() => {
if (!isMountedRef.current) {
isMountedRef.current = true;
const asyncWrapper = async () => {
await fetch("https://google.com");
setHasFetchedGoogle(true);
};
asyncWrapper();
}
}, []);
return hasFetchedGoogle;
}
With the following jest test (using msw and react-hooks testing library):
import { act, renderHook } from "#testing-library/react-hooks";
import { rest } from "msw";
import mswServer from "mswServer";
import useAsyncExample from "./useAsyncExample";
jest.useFakeTimers();
describe("using async hook", () => {
beforeEach(() =>
mswServer.use(
rest.get("https://google.com/", (req, res, ctx) => {
return res(ctx.json({ success: ":)" }));
})
)
);
test("should should return true", async () => {
const { result, waitFor, waitForNextUpdate, waitForValueToChange } = renderHook(() => useAsyncExample());
// ... things I tried
});
});
And I am simply trying to wait for the setHasFetchedGoogle call.
I tried multiple things:
await waitForNextUpdate(); // failed: exceeded timeout of max 5000 ms
await waitForValueToChange(() => result.current[1]); // failed: exceeded timeout of max 5000 ms
await waitFor(() => result.current[1]) // failed: exceeded timeout of max 5000 ms
The closest I have come so far is the with the following:
const spy = jest.spyOn(global, "fetch");
// ...
await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
expect(spy).toHaveBeenLastCalledWith("https://google.com");
But even this ends right before the setHasFetchedGoogle call happens, since it only await the fetch.
Online I found plenty of examples for component, where you can waitFor an element or text to appear. But this is not possible with hooks, since I am not rendering any DOM elements.
How can I listen to internal async logic of my hook? I though the waitForNextUpdate has exactly that purpose, but it doesn't work for me.
Any help is appreciated!
Actually it turns out my example works as I wanted it to.
The problem is that in the real-world case I have, the custom hook is more complex and has other logic inside which uses setTimeouts. Therefore I had jest.useFakeTimers enabled.
Apparently jest.useFakeTimers doesn't work together with waitForNextUpdate.
See more info
I will now try to get my tests to work by enabling/disabling the fakeTimers when I need them
As you said in your answer, you are using jest.useFakeTimers, but you are incorrect to say it doesn't work with waitForNextUpdate because it does.
Here is an example. I've modified your request to google to simply be an asynchronous event by waiting for two seconds. Everything should be the same with an actual mocked request though.
const wait = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay))
function useAsyncExample() {
const isMountedRef = useRef(false);
const [hasFetchedGoogle, setHasFetchedGoogle] = useState(false);
useEffect(() => {
if (!isMountedRef.current) {
isMountedRef.current = true;
const asyncWrapper = async () => {
await wait(200);
setHasFetchedGoogle(true);
};
asyncWrapper();
}
}, []);
return hasFetchedGoogle;
}
// The test, which assumes a call to jest.useFakeTimers occurred in some beforeEach.
it('should should return true', async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsyncExample())
expect(result.current).toBe(false)
act(() => {
jest.advanceTimersByTime(200)
})
await waitForNextUpdate()
expect(result.current).toBe(true)
})

Testing-Library React don't trigger click after an async

I'm trying to write a test in Jest.
When the fireEvent.click comes after an async it it doesn't trigger the function.
This is the exact case where few lines of code are better than hundreds of words that describes the issue. In the snippet below, the second and third test are exactly the same (copy-and-pasted) except for the name of the test itself of course.
import {render, screen, waitFor, fireEvent} from '#testing-library/react';
import React from 'react';
const Component = ({asyncClick, syncClick}) => {
function onClick(){
console.log('clicked');
syncClick();
setTimeout(() => {
asyncClick();
}, 50);
}
return <div data-testid="toBeClick" onClick={onClick}>
</div>;
}
describe('a test', function(){
it('element exists', function(){
render(<Component/>);
let el = screen.getByTestId('toBeClick');
expect(el).toBeInTheDocument();
});
it('element Click', async function(){
let syncClick = jest.fn();
let asyncClick = jest.fn();
render(<Component asyncClick={asyncClick} syncClick={syncClick} />);
let el = screen.getByTestId('toBeClick');
fireEvent.click(el);
expect(syncClick).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(asyncClick).toHaveBeenCalledTimes(1);
});
});
it('element Click / 2', async function(){
let syncClick = jest.fn();
let asyncClick = jest.fn();
render(<Component asyncClick={asyncClick} syncClick={syncClick} />);
let el = screen.getByTestId('toBeClick');
fireEvent.click(el);
expect(syncClick).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(asyncClick).toHaveBeenCalledTimes(1);
});
});
})
The second test pass. The third test fails. Why?
As a side note, removing async/await and the waitFor from the second test, even the third test starts to pass.
-- UPDATE
Looks like the failure is due to an import in jest-setup.js:
require('ionic/js/ionic.js');
I'm importing Ionic v1 and it breaks the tests.
Wrap async actions that trigger something in act.
act(() => {
fireEvent.click()
});
Can try to use jest.useFakeTimers() and jest.runAllTimers().
In begin of test jest.useFakeTimers() and after click dispatched jest.runAllTimers()
If nothing works can try
await new Promise(resolve => setTimeout(resolve, 0));

React Testing Library: Why does the order of the tests matter?

Here's a link to codesanbox
Reordering the below tests would make find one after the other test to pass.
Another way to make it pass would be to make should be able to find 3 directly test to pass, for example, by making it find 2 instead of 3.
describe("Counter", () => {
test("should be able to find 3 directly", async () => {
render(<Counter />);
const three = await waitFor(() => screen.findByText(/3/i));
expect(three).toBeInTheDocument();
});
test("find one after the other", async () => {
render(<Counter />);
const one = await waitFor(() => screen.findByText(/1/i));
expect(one).toBeInTheDocument();
const two = await waitFor(() => screen.findByText(/2/i));
expect(two).toBeInTheDocument();
const three = await waitFor(() => screen.findByText(/3/i));
expect(three).toBeInTheDocument();
});
});
So the question is, why does the order of the tests matter? Why is the clean up not working?
It looks like waitFor has reach its timeout as waiting for the counter to 3 which means it is unrelated to ordering in this case I guess.
You would fix by increase the timeout to wait your counter as following:
test("should be able to find 3 directly", async () => {
render(<Counter />);
const three = await waitFor(() => screen.findByText(/3/i), {
timeout: 3e3, // 3s wait would find 3
});
expect(three).toBeInTheDocument();
});

I want to spyOn method, return value, and continue running through the script

I am spying on a method, in this case: axios.post. I want to resolve it and continue running the script, because in the then block, it has to run another method. The current way I am solving it, is with a jest.spyOn(axios, 'post').mockImplementation(() => Promise.resolve( ));. This, as it says, a mock implementation, and will completely forego the actual code. What I want is a way to resolve this call (by returnValue or something) and continue the actual code. I remember Jasmin had something to the effect of that.
EDIT:
I seems that the axios promise.resolve is actually progressing it. The problem more lies in that I am trying to mock the helpers.showResponseMessage and Jest keeps insisting it does not get called (even when the console.log within the mock gets called). Any ideas?
Contact (Axios call I am trying to resolve properly)
const recommendationHandler = (event) => {
event.preventDefault();
axios.post('/contact.json', extractData(config.addMessage))
.then(() => {
showResponseMessage(`Din besked er sendt.`, initialState, false, setConfig, 5000);
})
.catch(() => {
showResponseMessage(`Der opstod en fejl ved sendning. Prøv igen senere.`, {}, true, setConfig, 10000);
});
}
Testfile
import React from 'react';
import { configure, shallow, mount, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import axios from '../../axios-instances/axios-firebase'
import * as helpers from '../../utility/Helpers/Helpers'
import Contact from './Contact';
configure({ adapter: new Adapter() });
describe('<Contact />', () => {
it.only('should POST data to server if necessary payload is included', async () => {
jest.spyOn(axios, 'post').mockImplementation(() => Promise.resolve( ));
const wrapper = mount(<Contact />);
jest.spyOn(helpers, 'showResponseMessage').mockImplementationOnce(_ => console.log('test'));
wrapper.find('form').simulate('submit', {
preventDefault: () => { }
});
expect(axios.post).toHaveBeenCalledTimes(1);
expect(helpers.showResponseMessage).toHaveBeenCalled();
expect(wrapper.find('#responseMessage')).toHaveLength(1);
});
});
Stackoverflow provided, as it always does.
Apparently you need to put in an await Promise.resolve() to split it into microtasks. I am not quite sure how it works, and if somebody could explain, it would be great.
Stackoverflow answer: How to test async function with spyOn?
describe('<Contact />', () => {
it.only('should POST data to server if necessary payload is included', async () => {
jest.spyOn(axios, 'post').mockImplementation(() => Promise.resolve( ));
const wrapper = mount(<Contact />);
jest.spyOn(helpers, 'showResponseMessage').mockImplementationOnce(_ => console.log('test'));
wrapper.find('form').simulate('submit', {
preventDefault: () => { }
});
expect(axios.post).toHaveBeenCalledTimes(1);
await Promise.resolve(); <-- THIS IS APPARENTLY A MICROTASK
expect(helpers.showResponseMessage).toHaveBeenCalled();
expect(wrapper.find('#responseMessage')).toHaveLength(1);
});
});

How to mock async call in React functional component using jest

I am testing a functional component that has a submit button that makes an async call to an api. The async call is located within a custom hook. As per standard testing practices, I have mocked the hook, so that my mock will be called instead of the actual async api:
someComponent.test.js
jest.mock("../../../CustomHooks/user", () => ({
useUser: () => ({
error: null,
loading: false,
forgotPassword: <SOMETHING HERE>
})
}));
I know that my forgotPassword function is called because when I change it to forgotPassword: "", I get an error in my test stating that forgotPassword is not a function.
A very simple representation of the function that is called when my submit button is clicked is this:
someComponent.js
import { useUser } from "../../../CustomHooks/user"
const SomeComponent = () => {
....state and other things etc....
const { error, loading, forgotPassword } = useUser()
const submit = async () => {
await forgotPassword(emailValue);
setState(prevState => {
return {
...prevState,
content: "code"
};
});
}
}
NOTE: My call to the async function await forgotPassword... is wrapped in a try/catch block in my code, but I have left this out for clarity.
In production, when the submit button is pressed, the async call occurs, and then the state should be switched, thus rendering some other components. My test looks to see if these components have been rendered (I am using react testing library for this).
The problem that I am having is that no matter what I place in the placeholder of the first code block, my test will always fail as the setState block is never reached. If I remove the await statement, then the setState block is hit and the component that I want to appear is there as the state has changed. However, obviously this will not work as intended outside of the test as the actual call is asynchronous. Here are some of the approaches that I have tried that do not work:
DOESN'T WORK
forgotPassword: () => {
return Promise.resolve({ data: {} });
}
DOESN'T WORK
forgotPassword: jest.fn(() => {
return Promise.resolve();
})
DOESN'T WORK
forgotPassword: jest.fn(email => {
return new Promise((resolve, reject) => {
if (email) {
resolve(email);
} else {
reject("Error");
}
});
}),
As I have said already, if I remove the await statement, then the state changes and the component appears, and hence the test passes. However, for obvious reasons, this is not what I want.
Extra Info
Here is a simplified version of my test:
it("changes state/content from email to code when submit clicked", () => {
const { getByTestId, getByText, debug } = render(<RENDER THE COMPONENT>);
const submitButton = getByTestId("fpwSubmitButton");
expect(submitButton).toBeInTheDocument();
const emailInput = getByTestId("fpwEmailInput");
fireEvent.change(emailInput, {
target: { value: "testemail#testemail.com" }
});
fireEvent.click(submitButton);
debug();
THE STATEMENTS BELOW ARE WHERE IT FAILS AS THE STATE DOESN'T CHANGE WHEN AWAIT IS PRESENT
const codeInput = getByTestId("CodeInput");
expect(codeInput).toBeInTheDocument();
});
To anyone who encounters this same problem, I found three ways that this can be solved (the preferred method is Option 3). All methods use a simple mock function that replaces the <SOMETHING HERE> of the first code block in my question. This can be replaced with () => {}:
jest.mock("../../../CustomHooks/user", () => ({
useUser: () => ({
error: null,
loading: false,
forgotPassword: () => {}
})
}));
Option 1
The first approach is to wrap your test code that relies on an async function in a setTimeout with a done callback:
it("changes state/content from email to code when submit clicked", done => {
const { getByTestId, debug } = render(<RENDER THE COMPONENT>);
const submitButton = getByTestId("fpwSubmitButton");
expect(submitButton).toBeInTheDocument();
const emailInput = getByTestId("fpwEmailInput");
fireEvent.change(emailInput, {
target: { value: "testemail#testemail.com" }
});
fireEvent.click(submitButton);
setTimeout(() => {
const codeInput = getByTestId("CodeInput");
expect(codeInput).toBeInTheDocument();
done();
});
debug();
});
Notice on the top line the done call back, as well as the test code wrapped in setTimeout at the bottom, and then invoking the callback within the setTimeout to tell jest that the test is done. If you don't call the done callback, the test will fail as it will timeout.
Option 2
The second approach is to use a function called flushPromises():
function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
}
it("changes state/content from email to code when submit clicked", async () => {
const { getByTestId, debug } = render(<RENDER THE COMPONENT>);
const submitButton = getByTestId("fpwSubmitButton");
expect(submitButton).toBeInTheDocument();
const emailInput = getByTestId("fpwEmailInput");
fireEvent.change(emailInput, {
target: { value: "testemail#testemail.com" }
});
fireEvent.click(submitButton);
await flushPromises();
const codeInput = getByTestId("CodeInput");
expect(codeInput).toBeInTheDocument();
debug();
});
Notice the flushPromises() function at the top, and then the call site towards the bottom.
Option 3 (Preferred Method)
The final method is to import wait from react-testing-library, set your test as asynchronous and then await wait() whenever you have async code:
...
import { render, fireEvent, cleanup, wait } from "#testing-library/react";
...
it("changes state/content from email to code when submit clicked", async () => {
const { getByTestId, debug } = render(<RENDER THE COMPONENT>);
const submitButton = getByTestId("fpwSubmitButton");
expect(submitButton).toBeInTheDocument();
const emailInput = getByTestId("fpwEmailInput");
fireEvent.change(emailInput, {
target: { value: "testemail#testemail.com" }
});
fireEvent.click(submitButton);
await wait()
const codeInput = getByTestId("CodeInput");
expect(codeInput).toBeInTheDocument();
debug();
});
All of these solutions work because they wait for the next event loop before executing the test code. Wait() is basically a wrapper around flushPromises() with the added benefit of including act(), which will help to silence test warnings.
try something like this
forgotPassword: jest.fn( async email => {
return await new Promise( ( resolve, reject ) => {
if ( email ) {
resolve( email );
} else {
reject( "Error" );
}
} );
} );
If it doesn't work let me know.

Resources