Open christemple opened 3 years ago
Hi, I also faced this problem and got some small reproducible patterns, and some memorization solve them.
working sample: https://codesandbox.io/s/jest-dom-404-repro-mmb2s
It seems that the element~s~ of the first result are detached from the tree. Are you in the same situation?
This is amazing, thanks so much for your help @berlysia, I'm going to find some time today to find the culprit components and implement your suggestions :+1:
We've also run into this issue; it can be reliably reproduced in https://github.com/CMSgov/easi-app/tree/e05031401a79414a38244094642c05b0e5c58a19, the failing test is src/views/SystemList/index.test.tsx
-> System List View -> when there are requests -> displays a table. The repro isn't minimal, unfortunately.
It'd be useful if this could be solved within jest-dom/testing-library, so the expect(await [query])
pattern can be used in tests without changing the implementation of the component(s) being tested.
I have similar issues in many tests
because sporadically
getRootNode
returns the React node that got matched itself rather than theDocument
Hmmm, this seems to be the key of the issue. However, we do not control getRootNode
. It's from jsdom. I wonder if either jsdom or the way findByText
works has something to do with this. I do not see much that we can do from jest-dom's perspective.
I tried this for now: I forked the codesandbox example provided by @berlysia above (see https://github.com/testing-library/jest-dom/issues/404#issuecomment-952179213). Then I focused on one of the failing tests alone, and modified it in this way:
test.only('findBy', async () => {
const { debug } = setup() // modified setup to return render too
const element = await screen.findByText('foo')
debug()
console.log('root node', element.getRootNode())
console.log('parent', element.parentElement)
expect(element).toBeInTheDocument()
})
This is the output I get in the console:
Not sure what to make of it. The element appears placed correctly in the DOM tree, but for some reason its parentElement
is null
, and calling its getRootElement()
returns the element itself, instead of the document where it lives.
I'm honestly not sure right now how .toBeInTheDocument
's implementation is at fault here. Suggestions are welcome.
Any updates on this ticket? I can confirm I am still getting flaky tests on our Jenkins and we're not really sure what's going on. The waitFor
s don't seem to be helping either.
I am running into this as well. It's 100% reproducible. Flakey only in the sense that the first test works as expected, but subsequent tests all fail. Any of them that are run in isolation will pass.
If I have 2 identical tests that do this:
render(<TestFilter/>);
const available = await screen.findByRole('list', { name: /available/i });
expect(available).toBeVisible()
When run together, the first test will pass, and the second one will fail. When it fails, it's failing in toBeInTheDocument
when it tests element.ownerDocument === element.getRootNode(...)
. The call to getRootNode
returns the top-level element rather than the document. Strangely, if I look at the HTML inside element.ownerDocument.body, I see that the expected HTML has been rendered in there.
If I use waitFor
instead of await
, as was suggested at the top of this thread, then all tests pass.
Why is the element.ownerDocument === element.getRootNode(...)
check required rather than a more straighforward document.contains(element)
? And for that matter, why is this checkHtmlElement
call needed as well?
I'm considering implementing my own toBeInTheDocument
matcher for my codebase as a workaround to this issue, but I'd like to understand the implications of just doing a basic document.contains()
check.
:memo: My first reproducible code (in https://github.com/testing-library/jest-dom/issues/404#issuecomment-952179213 ) is now fixed with:
@testing-library/jest-dom
5.14.1 ~ 5.16.5@testing-library/react
13.0.0 ~ 13.4.0 (effective changes are between 12.1.5 and 13.0.0 ?):memo: https://github.com/CMSgov/easi-app/tree/e05031401a79414a38244094642c05b0e5c58a19 also works on my machine, with update deps described above.
Is anyone still experiencing this issue, with latest deps?
After updating our dependencies the problem we were experiencing disappeared.
Unfortunately I can't use react testing library v13 due to our project being frozen on v12. I have been suffering this exact issue and is sadly causing me to lose confidence in my tests. Like you, I get sporadic failures and my own digging found me at the same conclusion this thread has, with getRootNode
sometimes returning the Document
and sometimes returning the DOM element of the node being tested.
Hopefully not stating the obvious here or being naive, but seems to be if renders happen after I do my assertions.
I added this:
console.log('getting');
const content = await screen.findByText(/Test/i);
console.log(content.ownerDocument);
console.log(content.getRootNode({ composed: true }));
console.log('got');
And the 2 logs are next to each other, as I was wondering if a rerender would 'detach' the element I got from the document, but that makes no sense if these 2 logs output next to each other.
Running jest with --no-cache
changes the failure rate to mostly fail rather than mostly pass, not sure if that's a red herring, or..
Does anyone have an understanding as to how and why getRootNode
changes?
I did another research for previous comment. I've found that the detached element is easy to get and hard to notice.
Here is my latest research. https://codesandbox.io/s/peaceful-thunder-h7tfmx
This mini-app says two points:
And we already know that components declared in render cause component switching.
So the following patterns are suspicious:
function Inline() {
const Needle = () => <span>needle</span>;
return (
<div>
<Needle />
</div>
);
}
Real world example: Row renderer of table-like component
const SEED = [1, 2, 3];
const USER = 2;
export function App() {
const seed = SEED; // given table values
const user = useAsyncValue(USER); // something like `fetch("/user")`
const rowRenderer = useCallback(
function Row({ id }: { id: number }) {
return (
<span>
id: {id}
{id === 2 && ", needle"}
{id === user && ", me"}
</span>
);
},
[user]
);
return (
<div>{seed ? seed.map((x) => rowRenderer({ id: x })) : "(empty)"}</div>
);
// `x => <rowRenderer id={x} / >` will achieve same result
}
If you like, change the version on the Dependencies tab and check it out.
It would be nice to be able to extract the variables in the parent scope, reference them using props, and move the component to the top level.
function Needle() {
return <span>needle</span>;
}
function A() {
return <div><Needle /></span>
}
function B() {
return <div><Needle /></span>
}
function App() {
return <div>{Math.random() < 0.5 ? <A/> : <B/>}</div>
}
This pattern also cause detached elements. Real world example is not ready, but a SPA router could belong to this pattern.
This appears to be a much bigger issue in react 18 vs 17. I've got hundreds of tests failing due to toBeInTheDocument
after upgrading even though the element is clearly present. It seems like there should be a way for await findBy*
to wait more before it returns the element it finds, as it seems to find one before its fully placed in the dom, which I think is why waitFor
works 100% of the time. Considering the recommendation has always been to use await findBy*
over waitFor(() => getBy*)
its a bit of a bummer that its busted with new versions of react for fairly trivial use cases 😞
@lilasquared I'm also in the process of upgrading to React 18 and bumped into this exact same problem :(
@testing-library/jest-dom
version: 5.14.1node
version: 14.17.1yarn
version: 1.22.10dom-testing-library
version: 8.1.0@testing-library/react
version: 12.0.0Relevant code or config:
What you did:
Used expectation pattern [1] over [2]
What happened:
Sporadic failures
Received element is not visible (element is not in the document)
Problem description:
Essentially this check fails: https://github.com/testing-library/jest-dom/blob/main/src/to-be-in-the-document.js#L11 because sporadically
getRootNode
returns the React node that got matched itself rather than theDocument
.Which means that
findBy...
succeeds in returning a matched node, however when verifying if this node is in theDocument
with.toBeInTheDocument()
it fails.Suggested solution:
I don't know enough about the internals of when the root node changes from the React node to the Document node. I know for sure the root node eventually changes to
Document
, hence why thewaitFor
[2] pattern works.Reproducing is difficult because of it's sporadic nature, but I'm essentially creating this issue to find out if others are being affected by this, and also to learn a bit more about the internals that could be resulting in this issue.
In the meantime I'll be using the
waitFor
[2] expectation pattern from now, but I'm worried that I may forget about this issue and have sporadic failures again in the future since pattern [1] is the first that comes to mind.