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

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

Related

How to test the function after .then()?

I'm trying to write the unit test for a function.
Here is the code:
const show = () => {
SheetManager.hide('Screen1')
.then(() => {
SheetManager.show('Screen2', {
payload: {
preSheet: id,
},
});
});
};
The test code is like:
jest.spyOn(SheetManager, 'hide').mockImplementation(() => {});
jest.spyOn(SheetManager, 'show').mockImplementation(() => {});
await waitFor(() => expect(SheetManager.hide).toBeCalledTimes(1));
await waitFor(() => expect(SheetManager.hide).toBeCalledWith('Screen1'));
// await waitFor(() => expect(SheetManager.show).toBeCalledTimes(1));
// await waitFor(() => expect(SheetManager.show).toBeCalledWith('Screen2'));
The problem is the two commented line of test code of show function do not work. I think it's because the .then(), so the show function is called inside hide function.
So how should I change the code to make it work?
Look forward to your suggestion!

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

ReactJS - RTL await

I'm trying to use async method in RTL to get the elements which would appear after fetching data. In the docs they said findBy method is combination of getBy and waitFor. Is there any specific case when to use one of it?
My case is:
Show input elements after success fetching data
Fire event change of each input
Fire event click on submit button
Show alert after success
With this approach, my test always failed and return Received length: 0
render(<Component {...mockProps} />);
const inputs = await screen.findAllByRole('textbox');
expect(inputs).toHaveLength(5); //failed
But with this approach, my test get passed
render(<Component {...mockProps} />);
await waitFor(() => {
const inputs = screen.getAllByRole('textbox');
expect(inputs).toHaveLength(5); //passed
});
How to get passed test with the first approach?
Because i want to call async method again after the form submitted, should i do this?
await waitFor(async () => {
const inputs = screen.getAllByRole('textbox');
expect(inputs).toHaveLength(5);
const [input1, input2, ...etc] = inputs;
fireEvent.change(input1, {target: {value: 'first'}};
...etc
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
})
});

RTL await waitFor vs await findBy*

I'm not really understand how to use async method in RTL. In the docs they said findBy method is combination of getBy and waitFor. Is there any specific case when to use one of it?
My case is:
Show input elements after success fetching data
Fire event change of each input
Fire event click on submit button
With this approach, my test always failed and return Received length: 0
render(<Component {...mockProps} />);
const inputs = await screen.findAllByRole('textbox');
expect(inputs).toHaveLength(5); //failed
But with this approach, my test get passed
render(<Component {...mockProps} />);
await waitFor(() => {
const inputs = screen.getAllByRole('textbox');
expect(inputs).toHaveLength(5); //passed
});
How to get passed test with the first approach?
Because i want to call async method again after the inputs shows, should i do this?
await waitFor(async () => {
const inputs = screen.getAllByRole('textbox');
expect(inputs).toHaveLength(5);
const [input1, input2, ...etc] = inputs;
fireEvent.change(input1, {target: {value: 'first'}};
...etc
await waitFor(() => {
expect(onSubmit).toHaveBeenCalled();
})
});
You can have separate waitFor()s like this:
await waitFor(async () => {
const inputs = screen.getAllByRole('textbox');
expect(inputs).toHaveLength(5);
const [input1, input2, ...etc] = inputs;
fireEvent.change(input1, {target: {value: 'first'}};
...etc
});
await waitFor(() => {
expect(onSubmit).toHaveBeenCalled();
})
If you want to stick with the first approach, you can use a findBy along with and getBy so you wait for the element to exist before "getting" it:
render(<Component {...mockProps} />);
await screen.findAllByRole('textbox');
const inputs = screen.getAllByRole('textbox');
expect(inputs).toHaveLength(5);
const [input1, input2, ...etc] = inputs;
fireEvent.change(input1, {target: {value: 'first'}};
...etc
await waitFor(() => {
expect(onSubmit).toHaveBeenCalled();
})

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

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

Resources