How to wait for something NOT to happen in Testing Library?

Viewed 1272

I want to assert on a condition which I know will not be true immediately, but might be true after an async action, in which the test should fail.

Say I'm testing this counter component:

function Counter() {
  const [value, setValue] = useState(1);
  function decrement() {
    if (value >= 0) { // <- off by one bug
      someAsyncAction().then(() => setValue(value - 1));
    }
  }
  return (
    <>
      Value is {value}
      <button onClick={decrement}>Decrement</button>
    </>
  );
}

I can write this test to check that the value should not go below zero:

const button = screen.getByRole("button", { name: "Decrement" });
expect(screen.getByText("Value is 1")).toBeInTheDocument();

userEvent.click(button);
expect(await screen.findByText("Value is 0")).toBeInTheDocument();

userEvent.click(button);
// !!! wrong !!!
expect(screen.getByText("Value is 0")).toBeInTheDocument();
expect(screen.queryByText("Value is -1")).not.toBeInTheDocument();
// !!! wrong !!!

But the last two assertions will always pass, even though the component has a bug which means it will asynchronously update to show "Value is -1".

What is the recommended way to deal with this kind of situation?

1 Answers

The best I've come up with is:

await expect(screen.findByText("Value is -1", {}, {timeout: 100})).rejects.toThrow();

This tries to find the text, waits for the timeout (reduced from the default 1 second to 100ms to speed up the tests) then rejects which is caught by expect. If the text is present then the findByText call will resolve so the expect will reject and the test will fail (don't forget to await the expect).

This pattern can be extended to other assertions using waitFor:

await expect(waitFor(() => {
  expect(screen.getByRole('input')).toHaveValue(-1);
}, { timeout: 100 })).rejects.toThrow();

While this works, it's a bit complicated, especially the waitFor form. I feel this can still be improved so please chip in if you have any suggestions.

Related