jest icon indicating copy to clipboard operation
jest copied to clipboard

[Bug]: `document` is `null` during microtasks at the end of a test

Open eps1lon opened this issue 2 years ago • 13 comments

Version

27.5.1

Steps to reproduce

  1. clone https://github.com/eps1lon/react-testing-library-error-repro/blob/jest-repro/yarn.lock
  2. checkout jest-repro branch
  3. yarn install
  4. yarn test

Expected behavior

Test fails due to timeout.

Actual behavior

document is null during a microtask scheduled from a ReactDOMTestUtils.act call.

Additional context

Continued from https://github.com/facebook/jest/issues/9056#issuecomment-1098048482 which is a reduced repro of https://github.com/testing-library/react-testing-library/issues/1027

Environment

System:
    OS: Linux 5.13 Ubuntu 20.04.4 LTS (Focal Fossa)
    CPU: (16) x64 Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz
  Binaries:
    Node: 14.18.3 - ~/.nvm/versions/node/v14.18.3/bin/node
    Yarn: 1.22.17 - ~/.nvm/versions/node/v14.18.3/bin/yarn
    npm: 6.14.15 - ~/.nvm/versions/node/v14.18.3/bin/npm
  npmPackages:
    jest: ^27.5.1 => 27.5.1

eps1lon avatar Apr 13 '22 14:04 eps1lon

This error is difficult for non-react savvy folks to understand. It would be great to fix the issue and add more error messaging to direct folks on what to do about it a bit more clearly.

thebrianbug avatar May 05 '22 20:05 thebrianbug

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 30 days.

github-actions[bot] avatar Jun 04 '22 20:06 github-actions[bot]

comment

eps1lon avatar Jun 04 '22 21:06 eps1lon

As discussed in https://github.com/testing-library/react-testing-library/issues/1027. This issue is experienced very often while running async tests on Github Actions CI. As a mitigation I add jest.runAllTimers() at the end of test to exhaust all micro/macro task queues, but it rarely works.

Also, I have tests which run successfully on local machine but fail only in Github Actions CI with:

      var evt = document.createEvent('Event');

edishu avatar Jun 21 '22 22:06 edishu

I have been able to consistently reproduce this error on my local machine. But to do that I have to do yarn test --runInBand while also putting my machine under a heavy workload (e.g. playing a few youtube videos on the highest resolution or screen share). Still no luck in finding the culprit. 😭

johachi avatar Jun 22 '22 05:06 johachi

I ended up patching react-dom in my node_modules locally and added some debugging code to identify the problematic test. You can see it at https://github.com/Zylphrex/react-testing-library-error-repro. It's a little gross but I hope it helps.

Zylphrex avatar Jun 22 '22 13:06 Zylphrex

I ended up patching react-dom in my node_modules locally and added some debugging code to identify the problematic test. You can see it at https://github.com/Zylphrex/react-testing-library-error-repro. It's a little gross but I hope it helps.

This worked excellently well! 🎉 Thank you so much @Zylphrex !

It did point me to the test and place I thought was probably the culprit. I still can't figure out why it is failing though. But I can solve the problem by adding a longer timeout for the test that is failing but it is not a solution I'm satisfied with.

johachi avatar Jun 23 '22 12:06 johachi

After trying random things to solve this issue, I discovered that increasing test timeout (jest.setTimeout, 30000) or test('title', () => {...}, 30000)) fixed it for me. Disclaimer - I don't fully understand how this helped and whether this is reliable. Would appreciate an explanation if this is the proper workaround 🙏

mjchang avatar Jun 23 '22 21:06 mjchang

@mjchang If you were facing the same problem, that should fix it.

The root cause of this problem when I initially opened up testing-library/react-testing-library#1027 was that the test timed out during one of the getBy* queries. Your choices here are to increase the timeouts or make your test run faster.

I believe what's happening here is because the test had timed out, jest started to do tear downs (I'm not sure exactly what). And at some point after, the getBy* tries to poll for results again and encounters this particular error which is not helpful in determining what the actual problem was.

Zylphrex avatar Jun 23 '22 22:06 Zylphrex

I experienced this one today. Error is obtuse and stack trace is unhelpful, especially within a large team.

Some days I fight with Jest, some days I let Jest win.

I just gave up and deleted the spec.

Problem solved ;)

drewlustro avatar Jul 05 '22 23:07 drewlustro

I believe I'm having this same issue, only the test that consistently triggers this error is empty. With or without code in the test case it gives me this error, but as soon as I comment out the entire test case, my other tests pass without an issue.

My Jest version is 24.9.0 Node version is 18.6 I've also tried using node version 16.15.0 which made no difference that I can see.

My test file:

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'
import userEvent from '@testing-library/user-event';
import axios from 'axios';

import Login from '../Login.js';


describe('Login component', () => {
    const mockSubmit = jest.fn();
    jest.mock('axios');

    beforeEach(() => {
        mockSubmit.mockClear();
        render(<Login handleLogin={mockSubmit} />);
        const onSubmit = new mockSubmit;
    });

    test('inputing username updates state', () => {

        fireEvent.change(getUsername(), {target: {value: 'Text'}});
        expect(screen.getByDisplayValue('Text')).toBeInTheDocument();
    });

    test('inputing password updates state', () => {

        fireEvent.change(getPassword(), {target: {value: 'password'}});
        expect(screen.getByDisplayValue('password')).toBeInTheDocument();
    });

    test('handleLogin is called when form submits', () => {

        enterFormData();
        fireEvent.submit(screen.getByTitle('login_form'));
        expect(mockSubmit).toHaveBeenCalledTimes(1);
    });

    test('handleLogin is called when submit button clicked', () => {

        enterFormData();
        fireEvent.click(screen.getByRole('button'));
        expect(mockSubmit).toHaveBeenCalledTimes(1);
        expect(mockSubmit).toBeCalled();
        expect(mockSubmit).toReturnWith();
    });

    // test('axios post request', () => {


    // });
});

function getUsername() {
    return screen.getByPlaceholderText(/username/);
}

function getPassword() {
    return screen.getByPlaceholderText(/password/);
}

function enterFormData() {
    fireEvent.change(getUsername(), {target: {value: 'Text'}});
    fireEvent.change(getPassword(), {target: {value: 'password'}});
}

And here is a gist with the stack trace and the component that I'm testing.

When the 5th and last test is commented out (like above) my tests run. But if I do not comment it out, I get the error. The test did have code in it but removing it did not make a difference.

I'm hoping someone could point me in the direction of a work around, as I'm completely stumped.

ramonaspence avatar Aug 01 '22 21:08 ramonaspence

@ramonaspence Your error looks slightly different to what I've seen but something you can try is to comment out the test that passed just before the failure (which is probably the second last test in the test suite). If this is the same problem/something similar, the previous test that passed may be leaving some asynchronous work that runs after it passes.

Zylphrex avatar Aug 02 '22 03:08 Zylphrex

Okay, thank you! I didn't consider that what I'm testing has a promise in it.

ramonaspence avatar Aug 02 '22 23:08 ramonaspence

It seems that the reason why this is rarely happening locally but often while running async tests on Github Actions CI is that those failing tests (for whatever various reasons) take up quite some time to complete and ultimately running into a timeout especially on smaller CPU machines / environments.

Of course one would expect those tests to fail with a timeout. What seems to happen instead though is that said tests are getting cleared and after clearing, document-object becomes unavailable. That is the error that we are getting. Totally confusing. That's the reason why increasing timeout for those tests avoids this issue.

HansKre avatar Aug 24 '22 11:08 HansKre

Is it possible to get a reproduction without React where this is an issue? I think the underlying problem isn't jest doing something weird (we cannot inspect promise states, so we don't know to wait. And even if we could - how long are we supposed to wait?), but rather React scheduling callbacks in a way that breaks stack traces leading to very confusing errors.

React throws an error, but the trace is useless - if React kept the frames from when the callback was scheduled I assume at least the error would be usable?

https://github.com/facebook/react/blob/d0f396651bd654a9c2058d9c779aab843651ca9a/packages/shared/invokeGuardedCallbackImpl.js#L80-L94


Of course, Jest can add a wait before nulling out document to allow microtasks to run, but that doesn't help for timeouts, only in the very specific case where only a promise is what we're waiting for in the exact moment a test times out

SimenB avatar Aug 25 '22 13:08 SimenB

I see I pretty much just repeated what @Zylphrex said in https://github.com/facebook/jest/issues/12670#issuecomment-1163133802. IMO React should do that by default (or, not that as it's quite hacky 😅 but something that keeps a usable stack).

Again, I agree the error from Jest is unfortunate in this case, but I'm not sure there's anything we can really do (while React definitely can do something)

SimenB avatar Aug 25 '22 13:08 SimenB

The general problem is that Jest is the only environment where document === null. Everywhere else it's either defined or typeof document === 'undefined' (or at least just undefined). We can fix it in React (it already includes Jest specific document checks). But we'll always play catch-up because implementors need to be aware that document can be null.

eps1lon avatar Aug 25 '22 14:08 eps1lon

I guess you can say the underlying issue is something like https://github.com/jsdom/jsdom/issues/955. We call window.close before document is set to null, but while timers gets cancelled it doesn't seem like microtasks does.

I doubt there's anything that we can do in Jest, unless you have any suggestions?

SimenB avatar Aug 26 '22 08:08 SimenB

is there any WA for this issue?

zxsure avatar Apr 26 '23 13:04 zxsure

As of #13972 (released in 29.5.0), we no longer set the document to null. So this shouldn't be an issue anymore.

If it is, please open up a new issue.

SimenB avatar Aug 13 '23 15:08 SimenB

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

github-actions[bot] avatar Sep 13 '23 00:09 github-actions[bot]