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

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.

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

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

Flushing a Promise queue?

While writing tests using Jest, an asynchronous mocked Axios request was causing a setState() call after a component was mounted. This resulted in console output during the test run.
it('renders without crashing', () => {
const mockAxios = new MockAdapter(axios);
mockAxios.onGet(logsURL).reply(200, [...axios_logs_simple]);
const div = document.createElement('div');
ReactDOM.render(<Logs />, div);
ReactDOM.unmountComponentAtNode(div);
});
And in the <Logs> component:
componentDidMount() {
axios.get(apiURL).then(response => {
this.setState({data: response.data});
});
}
I found a blog post showing a solution, but I don't understand how it does anything useful:
it('renders without crashing', async () => { //<-- async added here
const mockAxios = new MockAdapter(axios);
mockAxios.onGet(logsURL).reply(200, [...axios_logs_simple]);
const div = document.createElement('div');
ReactDOM.render(<Logs />, div);
await flushPromises(); //<-- and this line here.
ReactDOM.unmountComponentAtNode(div);
});
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
As I understand it, it creates a new promise that resolves immediately. How can that guarantee other asynchronous code will resolve?
Now, it solved the issue. There are no more console messages during the test run. The test still passes (it's not a very good one). But this just seems like I just landed on the other side of a race condition, rather than preventing the race condition in the first place.
I will start with the reason why do you need this solution.
The reason is that you don't have the reference to the async task and you need your test assertion to run after the async task.
If you had it, you can just await on it, this will assure that your test assertion will run after the async task. (in your case it is ReactDOM.unmountComponentAtNode)
In your example the async task is inside componentDidMount which react calls, so you don't have the reference to the async task.
Usually a simple implementation of flushPromises will be:
const flushPromises = () => new Promise(resolve => setImmediate(resolve));
This implementation is based on the fact that async operations in javascript are based on task queue (javascript has several types of queues for async tasks).
Specifically, promises are queued in a micro-task queue. Each time an async operation (such as http request) finishes the async task dequeues and executes.
setImmediate is a method that gets a callback and stores it on the Immediates Queue and will be called in the next iteration of the event loop.
This Immediates Queue is checked after the Micro-task queue (which holds the promises).
Let's analyze the code, we are creating a new promise which is enqueued at the end of the Micro-task queue, it's resolve will be called on the next iteration of the event loop, that means it will be resolved after all the promises that are already enqueued.
Hope this explanation helps.
Pay attention that if inside of the async task you will enqueue a new promise, it will enter the queue at the end, that means that the promise that flushPromises return will not run after it.
Few posts / videos for more info:
https://blog.insiderattack.net/promises-next-ticks-and-immediates-nodejs-event-loop-part-3-9226cbe7a6aa
https://www.youtube.com/watch?v=8aGhZQkoFbQ
https://www.youtube.com/watch?v=cCOL7MC4Pl0

Jest: Wait for called .then of async method in componentDidMount

I am currently stuck on writing a test of my React-App.
I have an async call in my componentDidMount method and are updating the state after returning. However, I do not get this to work.
I have found several solutions and None seems to work as expected. Below is the nearest point I have come to.
App:
class App extends Component<{}, IState> {
state: IState = {
fetchedData: false
};
async componentDidMount() {
await thing.initialize();
this.test();
}
test = () => {
this.setState({ fetchedData: true })
};
render() {
return this.state.fetchedData ? (
<div>Hello</div>
) : (
<Spinner />
);
}
}
The test
it('Base test for app', async () => {
const spy = spyOn(thing, 'initialize').and.callThrough(); // just for debugging
const wrapper = await mount(<App />);
// await wrapper.instance().componentDidMount(); // With this it works, but componentDidMount is called twice.
wrapper.update();
expect(wrapper.find('Spinner').length).toBe(0);
});
Well, so...thing.initialize is called (it is an async method that fetches some stuff).
If I do explicitly call wrapper.instance().componentDidMount() then it will work, but componentDidMount will be called twice.
Here are my ideas that I have tried but None succeeded:
Spying on thing.initialize() -> I did not find out how I proceed with the test after the method has been called and finished.
Spying on App.test -> The same here
Working with promises instead of async await
At the beginning, I had an thing.initialize().then(this.test) in my componentDidMount
It can't be much, but can someone tell me which piece I am missing?
if this is integration test you better to follow awaiting approach that say Selenium use: that is, just wait until some element appears or timeout reached. How it should be coded depends on library you use(for Puppeter it should be waitForSelector).
Once it's about unit test then I suggest you different approach:
mock every single external dependencies with Promise you control(by your code it's hard to say if automatic mock will work or you need to compose mock factory but one of them or both will help)
initialize element(I mean just run shallow() or mount())
await till your mocks are resolved(with extra await, using setTimeout(... ,0) or flush-promises will work, check how microtasks/macrotasks works)
assert against element's render and check if your mocks has been called
And finally:
setting state directly
mocking/spying on internal methods
verifying against state
are all lead to unstable test since it's implementation details you should not worry about during unit-testing. And it's hard to work with them anyway.
So your test would look like:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('loads data and renders it', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.resolve(/*data you expect to return*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(1);
})
or you may test how component behaves on rejection:
import thing from '../thing';
import Spinner from '../../Spinner';
import flushPromises from 'flush-promises';
it('renders error message on loading failuer', async () => {
jest.mock('../thing'); // thing.implementation is already mocked with jest.fn()
thing.initialize.mockReturnValue(Promise.reject(/*some error data*/));
const wrapper = shallow(<App />);
expect(wrapper.find(Spinner)).toHaveLength(1);
await flushPromises();
expect(wrapper.find(Spinner)).toHaveLength(0);
expect(wrapper.find(SomeElementYouRenderWithData)).toHaveLength(0);
expect(wrapper.find(SomeErrorMessage)).toHaveLength(1);
})

How to mock client.readQuery from Apollo 2 in Jest

There's a component which I am using the cache that I created in apollo, stored inside the client.
At some point, as a callback from an input, I do the following call:
const originalName = client.readQuery<NamesCached>({ query: NamesCached })!.names
this returns directly an array of objects.
However, I do not know how to test this in my component. My first assumption was to mock exclusively the client.readQuery to return me an array from my __fixtures__. That didn't work.
Here is my best shot to mock it:
it('filters the name in the search', () => {
jest.doMock('#/ApolloClient', () => ({
readQuery: jest.fn()
}))
const client = require('#/ApolloClient')
client.readQuery.mockImplementation(()=> Promise.resolve(names()))
const { getByPlaceholderText, queryByText } = renderIndexNames()
const input = getByPlaceholderText('Search…')
fireEvent.change(input, { target: { value: 'Second Name' } })
expect(queryByText('Title for a name')).not.toBeInTheDocument()
})
For this test I am using react-testing-library. This is my best shot so far...
jest.doMock('#/ApolloClient', () => ({
readQuery: jest.fn()
}))
const client = require('#/ApolloClient')
client.readQuery.mockImplementation(()=> Promise.resolve(names()))
It is important that relies on a single IT, not on the top since there are other tests that are using the client and works properly.
With That I could had mocked in the test when invokin client.readQuery() but then in the component itself goes back to its own original state
My goal
Find a way that the client.readQuery can be mocked and return the data I am looking for. I thought in mocking, but any other solution that can work for a single or group of tests( without all of them) would be more than welcome.
I tried as well mocking on the top, but then the others are failing and i couldn't reproduce to go back to the real implementation

Resources