How do you test for the non-existence of an element using jest and react-testing-library?
Asked Answered
F

14

421

I have a component library that I'm writing unit tests for using Jest and react-testing-library. Based on certain props or events I want to verify that certain elements aren't being rendered.

getByText, getByTestId, etc throw and error in react-testing-library if the element isn't found causing the test to fail before the expect function fires.

How do you test for something not existing in jest using react-testing-library?

Fadiman answered 12/10, 2018 at 15:59 Comment(1)
I mean the fact this question had this much popularity speaks about how intuitive the API is.Symphonic
F
730

From DOM Testing-library Docs - Appearance and Disappearance

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:

const submitButton = screen.queryByText('submit')
expect(submitButton).toBeNull() // it doesn't exist

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.

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

not.toBeInTheDocument

The 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.

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()
Fid answered 12/10, 2018 at 16:2 Comment(14)
My bad kentcdodds, thank you. I used getByTestId and got the same error. And, I didn't check the FAQ, sorry. Great library! Can you modify your answer to include the `.toBeNull();Fadiman
I believe the link above was meant to point to the react-testing-library docsResnatron
The new docs site was published a few days ago. I should have used a more permanent link. Thanks for the update @pbre!Fid
Another handy resource: testing-library.com/docs/react-testing-library/cheatsheetFadiman
and queryByText for those who want the equivalent to getByText that is null safeChantel
I really don't want to set a ton of test-id's just to test for the non-existence of certain elements at certain states, it kind of goes against the guidelines of testing-library doesn't it?Humperdinck
Agreed. You can use the query* variant of any of the queries.testing-library.com/docs/dom-testing-library/api-queriesFid
It might be worth noting that this works when the queryBy.. or getBy.. is de-structured off the render method. I was using the screen.get.. methods and it throws an error.Yip
@Sam, make sure you're on the latest version of Testing Library. The screen API is now recommended over destructuring.Fid
not.toBeInTheDocument is not in @react-native-community/testing-libraryHelvetic
toBeInTheDocument is part of jest-dom not RTLQuianaquibble
when should we use toThrow instead of not.toBeInTheDocumentValidity
But queryBy... is synchronous. What if I what to check that element does not appear some time after assertion? Basically a version of findBy... that does not throw if an element was not found.Submarginal
This link to the RTL doc on "waiting for disappearance" now seems to be out-of-date: testing-library.com/docs/guide-disappearance/…. I think the new best way to test for disappearance is waitForElementToBeRemoved testing-library.com/docs/dom-testing-library/api-async/… .Cotyledon
Y
98

Use queryBy / queryAllBy.

As you say, getBy* and getAllBy* throw an error if nothing is found.

However, the equivalent methods queryBy* and queryAllBy* instead return null or []:

queryBy

queryBy* queries return the first matching node for a query, and return null if no elements match. This is useful for asserting an element that is not present. This throws if more than one match is found (use queryAllBy instead).

queryAllBy queryAllBy* queries return an array of all matching nodes for a query, and return an empty array ([]) if no elements match.

https://testing-library.com/docs/dom-testing-library/api-queries#queryby

So for the specific two you mentioned, you'd instead use queryByText and queryByTestId, but these work for all queries, not just those two.

Yip answered 4/11, 2019 at 12:27 Comment(5)
This is way better than the accepted answer. Is this API newer?Bavaria
Thanks for the kind words! This is basically the same functionality as the accepted answer, so I don't think it's a newer API (but I could be wrong). The only real difference between this answer and the accepted one is that the accepted answer says that there's only method which does this (queryByTestId) when in fact there are two whole sets of methods, of which queryByTestId is one specific example.Yip
Thanks I'd much prefer this than setting test-idsHumperdinck
Thank you for that detailed explanation. It's a such a subtle difference that I didn't see it despite looking at their example here: github.com/testing-library/jest-dom#tobeinthedocument :face-palm:Infraction
I needed to know how to do this for getByTestId since that is what I want to key off of. I was not aware that they had a queryByTestId which would have taken me at least another query on DuckDuckGo and who knows how much additional time to find a proper solution. Mucho Graciaaahhh!Superordinate
W
62

Hope this will be helpfull

this table shows why/when function errors

which functions are asynchronous

what is return statement for function

enter image description here

Weissmann answered 10/11, 2022 at 9:53 Comment(4)
This can help me, I use queryByTestId() and check with toBeNull()Dejecta
The queryBy matchers are what I always use for testing if something doesn't exist. For example, the assertion expect(queryByTestId('some-button')).notToBeInTheDocument() will pass if that button is not rendered.Snobbish
Arigato/Dankeschön/Thanks. Read more and probably image refGolfer
Please add the source link so that others can understand whether this is up-to-datePhrenic
C
38

getBy* throws an error when not finding an elements, so you can check for that

expect(() => getByText('your text')).toThrow('Unable to find an element');
Cymric answered 18/6, 2020 at 10:50 Comment(3)
This can be pretty error-prone. Error throws are used for debugging purposes and not to for verifcation.Cohbert
This worked in my case but, only after using arrow function. Can you please let me know why we need it? It does not work without it.Bohr
@RahulMahadik because expect(...) will call the anonymous function when it's ready to detect/catch a thrown error. If you omit the anonymous arrow function, you will immediately call a throwing function as the code executes, which immediately breaks out of execution.Inertia
M
32
const submitButton = screen.queryByText('submit')
expect(submitButton).toBeNull() // it doesn't exist

expect(submitButton).not.toBeNull() // it exist
Marmot answered 24/5, 2021 at 0:26 Comment(2)
This should be the accepted answer. Unlike getBy* and findBy* methods, queryBy* does not throw an error if the item is not found. It simply returns null.Ultra
I think it depends on your use case, if you need async side effects to settle then using a solution like findBy with expects to throw should be better I think?Haleigh
A
30

You have to use queryByTestId instead of getByTestId.

Here a code example where i want to test if the component with "car" id isn't existing.

 describe('And there is no car', () => {
  it('Should not display car mark', () => {
    const props = {
      ...defaultProps,
      base: null,
    }
    const { queryByTestId } = render(
      <IntlProvider locale="fr" messages={fr}>
        <CarContainer{...props} />
      </IntlProvider>,
    );
    expect(queryByTestId(/car/)).toBeNull();
  });
});
Addam answered 14/5, 2019 at 13:7 Comment(0)
E
25

Worked out for me (if you want to use getByTestId):

expect(() => getByTestId('time-label')).toThrow()
Electrotechnics answered 18/10, 2021 at 13:38 Comment(0)
E
10

Another solution: you could also use a try/catch block

expect.assertions(1)
try {
    // if the element is found, the following expect will fail the test
    expect(getByTestId('your-test-id')).not.toBeVisible();
} catch (error) {
    // otherwise, the expect will throw, and the following expect will pass the test
    expect(true).toBeTruthy();
}

EDIT:

I found a one-line alternative:

expect(screen.queryByTestId("your-test-id")).not.toBeInTheDocument();
Enwind answered 22/1, 2021 at 16:2 Comment(2)
This will work, by Jest will warn you of "Avoid calling expect conditionally" (jest/no-conditional-expect)Brunner
You can also use expect(findByTestId('your-test-id')).rejects.toThrow(); since the promise-based findByTestId() will reject when the element is not found upon awaiting. expect(promise).rejects.toThrow() will await the promise and pass the test when the promise rejects due to a thrown exception.Inertia
V
5

The default behavior of queryByRole is to find exactly one element. If not, it throws an error. So if you catch an error, this means the current query finds 0 element

expect(
   ()=>screen.getByRole('button')
).toThrow()

getByRole returns 'null', if it does not find anthing

 expect(screen.queryByRole('button')).toEqual((null))

findByRole runs asynchronously, so it returns a Promise. If it does not find an element, it rejects the promise. If you are using this, you need to run async callback

test("testing", async () => {
  let nonExist = false;
  try {
    await screen.findByRole("button");
  } catch (error) {
    nonExist = true;
  }
  expect(nonExist).toEqual(true);
});
    
Vomer answered 1/2, 2023 at 6:20 Comment(0)
L
3

You can use react-native-testing-library "getAllByType" and then check to see if the component is null. Has the advantage of not having to set TestID, also should work with third party components

 it('should contain Customer component', () => {
    const component = render(<Details/>);
    const customerComponent = component.getAllByType(Customer);
    expect(customerComponent).not.toBeNull();
  });
Label answered 9/10, 2019 at 15:10 Comment(1)
This kind of breaches the premise of not having implementation details (such as the component name) in the test.Bavaria
W
3
// check if modal can be open
const openModalBtn = await screen.findByTestId("open-modal-btn");
fireEvent.click(openModalBtn);

expect(
  await screen.findByTestId(`title-modal`)
).toBeInTheDocument();


// check if modal can be close
const closeModalBtn = await screen.findByTestId(
  "close-modal-btn"
);
fireEvent.click(closeModalBtn);

export const sleep = (ms: number) => {
  return new Promise((resolve) => setTimeout(resolve, ms));
};

export const pause = async (ms?: number) => {
  await act(async () => {
    await sleep(ms || 100);
  });
};


const Way1 = async ()=>{
  await pause();
  expect(screen.queryByTestId("title-modal")).toBeNull();
}
const Way2 = async ()=>{
  let error = false
  try{
    await screen.findByTestID("title-modal")
  } catch (e) {
    error = true
  }
  expect(error).toEqual(true)
}

when you use findByTestId function, the library will wait for the element to appear (for 2s by default, I don't remember exactly), you don't need to use pause() function but when you use queryByTestId function, it run once time and not wait for the element, So everytime you do something that lead to a states changes, so you need to manually wait for it

Wulfe answered 24/11, 2022 at 10:38 Comment(3)
This feels like a hack, it works for what I need (until I find a better solution). I have some components that fetch some data (through a hook), store that data in state without rendering anything, then when a button is clicked it sends that data somewhere else. I was having trouble ensuring that the mocked fetch would resolve before the fake button click, but adding this sleep worked for my tests.Anatol
To add to this solution, you may need to wrap the sleep in an act to avoid the Testing Library warning: await act(() => sleep(500));Anatol
@Anatol I updated the answer with further explanationWulfe
G
1

I recently wrote a method to check visibility of element for a jest cucumber project.

Hope it is useful.

public async checknotVisibility(page:Page,location:string) :Promise<void> 
{
    const element = await page.waitForSelector(location);
    expect(element).not.toBe(location); 
}
Gat answered 8/2, 2022 at 15:58 Comment(0)
A
0

If someone ends up here looking for the solution for React Native:

expect(screen.queryByText("some text")).not.toBeOnTheScreen();
Arsenide answered 11/12, 2023 at 19:44 Comment(0)
P
-1

don't want to bury the lead, so here's the right solution ✅

waitFor(() => queryByTestId(/car/) === null)

There are issues with all of the answers here so far...

don't use getByTestId, that'll have to wait 😴 for the timeout because it's expecting the element to eventually be there. Then it'll throw and you'll have to catch that, which is a less readable test. Finally you could have a RACE CONDITION 🚫 where getByTestId is evaluated before the element disappears and our test will flake.

Just using queryByTestId without waitFor is a problem if the page is changing at all and the element has not disappeared yet. RACE CONDITION 🚫

deleteCarButton.click();
expect(queryByTestId(/car/)).toBeNull(); //

if expect() gets evaluated before the click handler and render completes we'll have a bad time.

Pharisaic answered 29/9, 2022 at 17:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.