Wait for multiple elements to be removed React testing Library - reactjs

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

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

Testing react-lazyload in React testing library

I am unit testing a component that makes use of react-lazyload to lazyload stuff. While rendering in my unit test, I noticed that the Lazyload is not rendering the content and its placeholder is being shown. I tried using waitFor with async/await to wait for lazily loaded content to render in next tick cycle but it still fails and screen.debug() still shows the placeholder in the dom.
Is there any way I can test such a component? Here is what I tried:
render(<WrapperComponent />)
await waitFor(() => {
expect(screen.getByText('Lazily loaded text!')).toBeInTheDocument()
});
// source code
import LazyLoad from 'react-lazyload';
const WrapperComponent = () => {
return (
<div>
<LazyLoad placeholder="Loading..." >
<div>Lazily loaded text!</div>
</LazyLoad>
</div>
);
}
Lazily loaded content has a div with the text being expected above.
We can mock the LazyLoad component from react-lazyload library at the top of our test file so that the LazyLoad component behave just like a wrapper around our actual component that we need to test for.
jest.mock(
'react-lazyload',
() =>
function LazyLoad({ children }) {
return <>{children}</>
}
)
test('component wrapped with Lazyload', () => {
const {screen} = render(< WrapperComponent />)
expect(screen.getByText('Lazily loaded text!')).toBeInTheDocument()
})
Test does not actually trigger real data fetch, you have to mock it.
Probably the function that requests for data should be the one you mock.
You can utilize forceVisible:
import { forceVisible } from 'react-lazyload';
it('Testing a component using react-lazyload', async () => {
const screen = render(<WrapperComponent />)
forceVisible();
await waitFor(() => {
expect(screen.getByText('Lazily loaded text!')).toBeInTheDocument()
});
});

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

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

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