Skip to content

docs: rework disappearance guide to provide more guidance #499

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 79 additions & 36 deletions docs/guide-disappearance.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,93 +3,136 @@ id: guide-disappearance
title: Appearance and Disappearance
---

Sometimes you need to test that an element is present and then disappears or
vice versa.
Sometimes you need to test that an element is present and then disappears or the
other way around.

## Waiting for appearance

If you need to wait for an element to appear, the [async wait
utilities][async-api] allow you to wait for an assertion to be satisfied before
proceeding. The wait utilities retry until the query passes or times out.
If you need to wait for an element to appear, [`findBy`][find-by] and the [async
wait utility][async-api] `waitFor` allow you to wait for an assertion to be
satisfied before proceeding. The wait utilities retry until the query passes or
times out.

```jsx
Wait for appearance and return the element using `findBy`:

```javascript
test('movie title appears', async () => {
// element is initially not present...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add an extra assertion to check that it's not present initially?

// element is initially not present...
expect(movieTitle).not.toBeInTheDocument()

Copy link
Author

@kimroen kimroen Jun 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but I feel that isn't entirely relevant and detracts a little from what we're trying to show in this example. Also, asserting that things don't exist is covered further down on the page.

If we did do that though, there should also be code here to do something that would make the title appear after asserting it isn't here, and I'm not sure it makes sense to get into that here.

We could add a comment saying as much after the assertion, like this:

test('movie title appears', async () => {
  expect(screen.queryByText('the lion king')).not.toBeInTheDocument()
  
  // pretend we do something here that should make the title appear

  const movieTitle = await screen.findByText('the lion king')
  expect(movieTitle).toBeInTheDocument()
})

But I think it might be better to alter the comment in the test to something that makes it clearer that this would be part of a longer test:

Suggested change
// element is initially not present...
// ...code setting up a page that will eventually contain a movie title

Do you think the last suggestion would help, or is there maybe something else we could do to alleviate the confusion or lack of clarity we are hoping to fix by adding this assertion?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the first suggestion is much clearer instead of changing the comment tbh

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the first suggestion is much clearer instead of changing the comment tbh

Fair! In your mind, what is it that is unclear that adding this assertion solves? Or is there something else that makes it so you want it to be added?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, you're right with not wanting to have things in here that you'll explain later, so maybe the changed comment is indeed a better option to go with here...

Maybe we can make a full example (like the second suggestion) later on the page? 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's value in keeping the first examples minimal, without having to decipher fake application code. Expanding on the comment and making clear this is a snippet of a larger test should be good.


const movieTitle = await screen.findByText('the lion king')
expect(movieTitle).toBeInTheDocument()
})
```

Or wait for appearance using `waitFor` and `getBy`:

```javascript
test('movie title appears', async () => {
// element is initially not present...

// wait for appearance
await waitFor(() => {
expect(getByText('the lion king')).toBeInTheDocument()
expect(screen.getByText('the lion king')).toBeInTheDocument()
})

// wait for appearance and return the element
const movie = await findByText('the lion king')
})
```

These do more or less the same thing (`findBy` uses `waitFor` under the hood),
but the `findBy` version results in simpler code and a better error message.
Comment on lines +39 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this is interesting.

I'm not sure I agree on the statement (I believe using waitFor is more explicit, and helps the test conveying meaning) but that's entirely personal. The question here is whether if docs should showcase two ways of doing the same thing 🤔

If we agree on findBy being simpler and packed with a better error message, then I'd say this is the way docs should suggest 👍

waitFor is useful for other stuff (such as waiting for XHR requests to happen, or other). It might appear somewhere else in docs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the last bit that needs some attention, then we can merge!

Comment on lines +39 to +40
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
These do more or less the same thing (`findBy` uses `waitFor` under the hood),
but the `findBy` version results in simpler code and a better error message.
These do more or less the same thing (`findBy` uses `waitFor` under the hood),
but the `findBy` version produces errors more like a `getBy` query,
instead of a general-purpose timeout error.


_Note: `toBeInTheDocument()` comes from [`@testing-library/jest-dom`][jest-dom]_

## Waiting for disappearance

The `waitForElementToBeRemoved` [async helper][async-api] function uses a
callback to query for the element on each DOM mutation and resolves to `true`
when the element is removed.
When you want to wait until an element has disappeared, you can use
`waitForElementToBeRemoved` or `waitFor`.

The `waitForElementToBeRemoved` [async wait utility][async-api] takes an element
as an argument and waits until that element has been removed from the document.

```javascript
test('movie title no longer present in document', async () => {
// element is present
const movie = screen.queryByText('the mummy')
await waitForElementToBeRemoved(movie)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see @kentcdodds commented over here that this should receive a callback, not the element directly: testing-library/react-testing-library#673 (comment)

I feel like I've seen that doing it this way works too though, so I'm not sure.

Copy link
Member

@afontcu afontcu Jun 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth expanding on this section later with a note about working with React, because sometimes the node gets destroyed and replaced, making these types of tests either pass or fail when they shouldn't


```jsx
test('movie title no longer present in DOM', async () => {
// element is removed
await waitForElementToBeRemoved(() => queryByText('the mummy'))
// element has been removed
expect(movie).not.toBeInTheDocument()
})
```

Using
`waitForElementToBeRemoved` works by using a
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the waitFor style should also be documented here, particularly for working with React, when the node reference itself is unstable but a retryable query inside a callback would work.

[`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
is more efficient than polling the DOM at regular intervals with `waitFor`.
internally, which means it responds to the element being removed in a very
efficient way.

You can also pass an array or a callback to `waitForElementToBeRemoved` -
[see the documentation for more options](dom-testing-library/api-async.md#waitforelementtoberemoved).

Another option is using the `waitFor` helper.

The `waitFor` [async helper][async-api] function retries until the wrapped function
stops throwing an error. This can be used to assert that an element disappears
from the page.
The `waitFor` [async wait utility][async-api] retries until the wrapped function
stops throwing an error. Because expectations throw errors when they fail,
putting one in the wrapped function can be used to wait for the element to be
removed.

```jsx
```javascript
test('movie title goes away', async () => {
// element is initially present...
// note use of queryBy instead of getBy to return null
// instead of throwing in the query itself
await waitFor(() => {
expect(queryByText('i, robot')).not.toBeInTheDocument()
expect(screen.queryByText('i, robot')).not.toBeInTheDocument()
})
})
```

Polling like this is not as efficient as observing for mutations using
`waitForElementToBeRemoved`, but sometimes it's the best option.
Copy link
Author

@kimroen kimroen Jun 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any good examples of when waitFor would work and waitForElementToBeRemoved wouldn't in this context? If not, then maybe it shouldn't have equal space on this page.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree 👍

If not, I wouldn't even mention a "negated" waitFor as an alternative to waitForElementToBeRemoved

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I've removed the waitFor option here ✨


## Asserting elements are not present

The standard `getBy` methods throw an error when they can't find an element, so
if you want to make an assertion that an element is _not_ present in the DOM,
you can use `queryBy` APIs instead:
As opposed to waiting for removal, this is for when you are at a point in your
test where you know an element shouldn't be present.
Comment on lines +73 to +74
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section can be tightened up. It's unclear what "this" refers to here. Also, we shouldn't assume jest-dom is installed and should mention the null check.

Suggested change
As opposed to waiting for removal, this is for when you are at a point in your
test where you know an element shouldn't be present.
If you want to assert that an element is not present _right now_,
rather than testing that it is present and will disappear, use
`queryBy` instead of `getBy` for the query, and then assert that
nothing was found.


You might reach for `getBy` to check that something is not present, but `getBy`
queries throw an error when they can't find an element, which means you don't
get the chance to use the result in an expectation. If you want to make an
assertion that an element is _not_ present in the document, you can use
`queryBy` queries instead:

```javascript
const submitButton = screen.queryByText('submit')
expect(submitButton).toBeNull() // it doesn't exist
expect(submitButton).not.toBeInTheDocument() // it doesn't exist in the document
```

The `queryAll` APIs version return an array of matching nodes. The length of the
array can be useful for assertions after elements are added or removed from the
DOM.
The `queryAll` query methods return an array of matching elements. The length of
the array can be useful for assertions after elements are added or removed from
the document.

```javascript
const submitButtons = screen.queryAllByText('submit')
expect(submitButtons).toHaveLength(2) // expect 2 elements
```

### `not.toBeInTheDocument`
_Note: `not.toBeInTheDocument()` comes from
[`@testing-library/jest-dom`][jest-dom]_

The [`jest-dom`](ecosystem-jest-dom.md) utility library provides the
## The `toBeInTheDocument()` matcher

The [`@testing-library/jest-dom`][jest-dom] utility library provides the
`.toBeInTheDocument()` matcher, which can be used to assert that an element is
in the body of the document, or not. This can be more meaningful than asserting
a query result is `null`.
a query result is `null`, and it also provides more helpful error messages when
your tests fail.

```javascript
import '@testing-library/jest-dom/extend-expect'
// use `queryBy` to avoid throwing an error with `getBy`
const submitButton = screen.queryByText('submit')
expect(submitButton).not.toBeInTheDocument()
```

Read about how to set this up [in the documentation](jest-dom).

[async-api]: dom-testing-library/api-async.md
[find-by]: dom-testing-library/api-queries.md#findby
[jest-dom]: ecosystem-jest-dom.md