dom-testing-library: infinite loop when DOM mutation happens in waitFor callback

Jest hangs on the following sample test:

const { waitFor } = require("@testing-library/dom");

describe("test", () => {
  it("test", async () => {
    let value = 1;

    setTimeout(() => {
      value = 2;
    });

    await waitFor(() => {
      // both these lines are important: it's necessary to do a mutation and to fail in the first iteration
      // It works normally, if we comment one of these lines
      document.body.setAttribute("data-something", 'whatever');
      expect(value).toEqual(2);
    });

    console.log("execution never comes here");
  });
});

Two conditions have to be met:

  • there should be a DOM mutation in waitFor callback
  • first execution of waitFor callback should fail

Expected result: waitFor should resolve the promise after a successful iteration regardless whether there were DOM mutations or not

Current result: waitFor calls the callback infinitely even if subsequent iterations don’t throw any exception.

Environment: @testing-library/dom@7.26.4 jest@26.6.2

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 1
  • Comments: 18 (14 by maintainers)

Commits related to this issue

Most upvoted comments

Because ATL invokes an Angular detection cycle within the waitFor callback, we might end up in an infinite loop because this leads to a mutation of the DOM in some cases (https://github.com/testing-library/angular-testing-library/issues/230).

Would it be an option to keep track of the execution time inside waitFor ourselves (when fake timers aren’t used) in this case. If the timeout limit is reached, we could then throw an error. This won’t be as precise as the current setTimeout, but it will provide a fallback to prevent infinite loops in cases when DOM mutations are happening inside the waitFor callback.

If we want to add this to DTL, I can create a PR for this and we can take it from there. I tried this implementation before creating this comment, and it doesn’t affect the current tests.

The very first step of waitFor with or without fake timers is to check the callback. (https://github.com/testing-library/dom-testing-library/blob/master/src/wait-for.js#L56 or https://github.com/testing-library/dom-testing-library/blob/master/src/wait-for.js#L96) waitFor doesn’t consider booleans, its either callback throws (try again) or doesn’t throw (success). (https://github.com/testing-library/dom-testing-library/blob/master/src/wait-for.js#L146) If you need to verify against boolean then your callback should use toBeTruthy() eg.

await waitFor(() => expect(condition).toBeTruthy())

I do not really understand how await waitFor(() => false); console.log('Foo') work. In my case waitFor waits for nothing. No matter if the result of the callback is true or false, the promise (waitFor) will always be resolved and Foo is logged. Have I understood something wrong here?

some env infos:

  • real browser with karma + jasmine.
  • angular extension of the testing-library. – waitFor is re-exported from testing-library. Therefore the issue should be reported here 👍

I agree that it’s better not to put interactions and we should mention it in the docs. But IMO waitFor needs to ensure that it will stop checking results after the provided timeout. In this case the timeout is being ignored for the moment. Moreover it blocks the whole Jest to finish and it’s hard to figure out where the problem is.

Maybe I will try to go deeper and prepare some solution for the problem later.

Maybe I should provide some details on how we came to this problem. There was the following code in our tests:

await wait(() => {
    const submitButton = getByTestId(...);
    expect(submitButton.disabled).toBeFalse();
    fireEvent.click(submitButton);
});

At the first glance it doesn’t do any DOM mutations. But there was a listener for click events that cause changes in the DOM. Since we were using Jest 24 with MutationObserver shim, the problem occurred only sometimes and led to failures in waits of other tests in the module. We were trying to catch it for few months because we were not able to stably reproduce it and most of the time it was failing in CI. After switching to Jest 26 with the latest JSDOM which supports MutationObserver it always fails in that block, so we were able to identity the problem and fix it with:

const submitButton = await waitFor(() => {
    const submitButton = getByTestId(...);
    expect(submitButton.disabled).toBeFalse();
    return submitButton;
});
fireEvent.click(submitButton);

However, it’s strange that such innocent code (although I agree it’s not great that it produces DOM mutations as a side effect) hangs the whole Jest.

Hi guys. I have tested before the release and it doesn’t work too. I wonder if waitFor is made for making mutation. Or just assert things. Because making the mutation outside of the waitFor works well. The following code works:

describe("test", () => {
    it("test", async () => {
        let value = 1;

        setTimeout(() => {
            value = 2;
        });

        setTimeout(() => {
            document.body.setAttribute("data-something", 'whatever');
        }, 500);

        await waitFor(() => {
            expect(value).toEqual(2);
        });

        console.log("execution never comes here");
    });
});

(The waitFor fails the first time and pass the second one) In my projects, I just used waitFor for waitings things to appeared (just expect), that I can’t do with find* queries. What do you think @kentcdodds ?