How to wait for something NOT to happen in Testing Library?
Asked Answered
K

2

8

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?

Klotz answered 24/6, 2021 at 15:48 Comment(0)
K
14

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.

Klotz answered 9/7, 2021 at 14:0 Comment(2)
Thank you! I had a lot of trouble figuring this out. I extrapolated your text version to a helper: gist.github.com/sirbrillig/330cb59d4d6d788174fbb294edf09599Donal
If it's helpful to anyone, I updated my Gist linked above to include a Jest custom matcher.Donal
C
6

Taking the concepts put forth in Tamlyn's excellent self-answer, I've extrapolated this into a utility called verifyNeverOccurs that has the same signature as Testing Library's waitFor but that only fails if the callback assertion you send it ever passes:

import { waitFor, waitForOptions } from '@testing-library/react';

/**
 * Inverse of RTL's `waitFor`; used to verify that a thing does *not* occur. 
 * Useful for situations in which verifying that some effect did occur would
 * require using `await waitFor()` and you need to test that the effect does
 * not occur. Like `waitFor`, it must be `await`ed.
 * @param {function} negativeAssertionFn - a callback function that expects a thing you do _not_ expect will occur
 * @param {Object} options - options object with same shape as `waitFor`'s options argument (ultimately just passed through to `waitFor`)
 * @return {void}
 */

const verifyNeverOccurs = async (negativeAssertionFn: () => unknown, options?: waitForOptions) => {
  await expect(
    waitFor(negativeAssertionFn, options),
  ).rejects.toThrow();
};

export default verifyNeverOccurs;

(This is in TypeScript but you could always strip out the types if you wanted it in vanilla JS).

An example usage:

// fails if `element` ever contains the text "oh no"
verifyNeverOccurs(() => expect(element).toHaveTextContent('oh no'));
Clomb answered 24/11, 2022 at 5:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.