Description
Elements returned from asynchronous findBy
queries are not 100% compatible with waitForElementToBeRemoved
.
@testing-library/dom
version:7.29.2
- Testing Framework and version:
- fresh project generated with CRA 4.0.1
jest@26.6.0
- DOM Environment:
jest-environment-jsdom@26.6.2
jsdom@16.4.0
Relevant code or config:
Also available in codesandbox below.
import React, { useLayoutEffect, useState } from "react";
import { render, waitForElementToBeRemoved, screen } from "@testing-library/react";
function Component() {
const [visible, setVisible] = useState(false);
useLayoutEffect(() => {
const timer1 = setTimeout(() => setVisible(true), 10);
const timer2 = setTimeout(() => setVisible(false), 15);
return () => {
clearTimeout(timer1);
clearTimeout(timer2);
};
}, []);
return (
<div>
{visible && <span id="test-id">Content</span>}
</div>
);
}
for(const i of Array(200).fill(null).map((_, i) => i)) {
test(`unstable test ${i}`, async () => {
render(<Component />);
const content = await screen.findByText("Content");
await waitForElementToBeRemoved(content);
});
}
Above is only a minimal repro. My real use case includes querying "Loading" text which is visible while MSW is handling the request. It does not use setTimeout
at all. Something like:
// Click submit button
userEvent.click(screen.getByRole("button", { name: "Submit" }));
// Loader should appear
const loader = await screen.findByText("Loading");
// Loader should disappear once request resolves
await waitForElementToBeRemoved(loader);
What you did:
Queried element with asynchronous query findBy*
. I would expect element returned to be present in the DOM. If it wasn't the query should throw error.
Passed the queried element to waitForElementToBeRemoved
.
What happened:
Element returned from findByText
was not in DOM when waitForElementToBeRemoved
started handling it.
FAIL src/App.test.js
× unstable test (66 ms)
● unstable test
The element(s) given to waitForElementToBeRemoved are already removed. waitForElementToBeRemoved requires that the element(s) exist(s) before waiting for removal.
33 | render(<Component />);
34 | const content = await screen.findByText("Content");
> 35 | await waitForElementToBeRemoved(content);
| ^
36 | });
37 |
38 |
at initialCheck (node_modules/@testing-library/dom/dist/wait-for-element-to-be-removed.js:16:11)
at waitForElementToBeRemoved (node_modules/@testing-library/dom/dist/wait-for-element-to-be-removed.js:39:3)
at Object.<anonymous> (src/App.test.js:35:11)
Most of the time these issues come up really randomly. We might have successful builds for months on CI and then this error occurs. Triggering new build usually fixes this. 😄
Reproduction:
The codesandbox is still showing incorrect error message Cannot read property 'parentElement' of null
but this was fixed in #871.
https://codesandbox.io/s/dom-testing-libraryissues876-2-fe3fi?file=/src/React.test.js
I'm unable to reproduce this with DTL only though: https://codesandbox.io/s/dom-testing-libraryissues876-2-fe3fi?file=/src/Dom.test.js
Problem description:
I was hoping the initialCheck
done by waitForElementToBeRemoved
would have been the cause but it looks like the parentElement
is not even present before element is given to it.
const content = await screen.findByText("Content");
console.log(content.outerHTML); // <span id="test-id">Content</span>
console.log(content.parentElement); // null
await waitForElementToBeRemoved(content); // The element(s) given to waitForElementToBeRemoved are already removed. waitForElementToBeRemoveds that the element(s) exist(s) before waiting for removal.
Suggested solution:
- At minimum this should be documented at https://testing-library.com/docs/dom-testing-library/api-async/#waitforelementtoberemoved.
- I'm not sure whether
waitForElementToBeRemoved
should care if element is present in the DOM initially. - Mark elements returned from async queries and warn if passed to
waitForElementToBeRemoved
?element.RTL_TYPE = Symbol.for("FINDBY")
etc.
There is a workaround for this: Don't use waitForElementToBeRemoved
.
const content = await screen.findByText("Content");
await waitFor(() => expect(content).not.toBeInTheDocument());