ReactJS - RTL await - reactjs

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

Related

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

RTL now wrapped in act() warning - how to wait for multiple state updates

I have a page that renders a heading and a form (using react-hook-form). There's also a async api call populating the form.
I have a test that just tests if the heading is present.
it('renders correct heading', async () => {
renderWithContext(<CreateBusinessPage />);
const heading = await screen.findByRole('heading', {
name: 'Tell us more about your business',
});
expect(heading).toBeInTheDocument();
});
However, I end up with two not-wrapped-in-act warnings.
I've tried
renderWithContext(<CreateBusinessPage />);
const heading = await screen.findByRole('heading', {
name: 'Tell us more about your business',
});
await waitFor(() => {
expect(heading).toBeInTheDocument();
});
});
but the warnings persist.
Only when I do
renderWithContext(<CreateBusinessPage />);
const heading = await screen.findByRole('heading', {
name: 'Tell us more about your business',
});
await waitFor(async () => {
await expect(heading).toBeInTheDocument();
});
});
However this seems wrong, expect isn't async...??
The stack trace ends the react-hook-form controller, but I don't care about the form. I'm testing the heading.
So my question is: What are best practices to get pass state updates on a page, that are not relevant for the test?
Doing
await act(async() => {
await undefined
});
right after render also does the trick, but it seems like a hack.
I've read all I could find on this, but I still don't know how to handle situations like these. Any help is highly appreciated.
Thanks!

How to trigger setState on a mocked hook

I have a component that uses a hook that fetches data from the server, and I have mocked that hook to return my testing data.
Now if the mutate function (returned by the hook) is called, the normal implementation fetches the data again and causes a re-render (I'm using swr, here the mutate reference).
How to I trigger a re-render / setState on a mocked hook?
What I want to test: simply, if the user creates an item the item list should be re-fetched and displayed.
Code to illustrate the issue:
const existing = [...]
const newlyCreated = [...];
useData.mockReturnValue({ data: [existing] });
const { getByRole, findByText } = render(<MyComponent />);
const form = getByRole("form");
const createButton = within(form).getByText("Create");
useData.mockReturnValue({ data: [existing, newlyCreated] });
// createButton.click();
// Somehow trigger re-render???
for (const { name } of [existing, newlyCreated]) await findByText(name);
You don't need to trigger a re-render in your test.
The issue is: the button on your UI mutates the data, but since you're mocking useData that mutation isn't happening.
You can simply add mutate() to your mock and assign it a mock function.
You don't need to unit test the inner working of SWR's own mutate() - that's already covered by their own project.
const existing = ["Mercury", "Venus", "Earth", "Mars"];
const newItem = "Jupiter";
test("Create button should call mutate() with correct value", async () => {
const mutate = jest.fn();
jest.spyOn(useData, "default").mockImplementation(() => ({
data: existing,
mutate
}));
let result;
act(() => {
result = render(<Form />);
});
await waitFor(() => {
existing.forEach((item) => {
expect(result.getByText(item)).toBeInTheDocument();
});
});
const input = result.container.querySelector("input");
fireEvent.change(input, { target: { value: newItem } });
const createButton = result.getByText("Create");
createButton.click();
expect(mutate).toBeCalledWith([...existing, newItem], false);
});
Example on CodeSandbox

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

async fetch triggered three times

I use this in my react app to fetch data from my backend server:
React.useEffect(() => {
const fetchWidgets = async () => {
const response = await fetch("http://localhost:1202/data");
const responseData = await response.json();
setData(responseData);
console.log(responseData);
};
fetchWidgets();
});
It fetching data works fine, but the function seems to be triggered three times for some reason.
responseData is logged three times.
React.useEffect runs every time after component renders, unless you tell it not by defining a dependency array as its second argument; since you are setting a state inside its body which causes the comonent to re-render, you will see it happens multiple times. to fix the problem you may pass an empty array [] it will only run once after first render and acts like componentDidMount in class components. or add some dependency to run only if the dependencies change;
React.useEffect(() => {
const fetchWidgets = async () => {
const response = await fetch("http://localhost:1202/data");
const responseData = await response.json();
setData(responseData);
console.log(responseData);
};
fetchWidgets();
},[]);
Use Empty Brackets for the second parameter of useEffect.
React.useEffect(() => {
const fetchWidgets = async () => {
const response = await fetch("http://localhost:1202/data");
const responseData = await response.json();
setData(responseData);
console.log(responseData);
};
fetchWidgets();
},[]);
That will ensure the useEffect only runs once.

Resources