Querying by Role and Text

RNTL's guide ranks queries by how closely they match what users perceive: first *ByRole with a name, which is what a screen reader announces; then *ByLabelText, *ByPlaceholderText and *ByDisplayValue for inputs; then *ByText; and last *ByTestId, which no user ever sees. Each comes as getBy (throws when nothing matches), queryBy (returns null) and findBy (waits up to a second), plus an All form of each.

The roles and labels Auditing and Translating gave the cart for TalkBack make it easy to test. The test mocks the session (Authentication) and fills the real store:

__tests__/CartTab.test.tsx (excerpt)JSX
jest.mock('../src/session', () => ({ useSession: () => ({ user: null }) }));
beforeEach(() => {
  useCart.getState().clear();
  useCart.getState().add(3);                        // Salt and Saffron
  useCart.getState().add(5);                        // The Clockmaker's Paradox
});
test('removing a book updates the heading and the total', async () => {
  const user = userEvent.setup();
  await render(<CartTab />);
  await user.press(screen.getByRole('button', { name: 'Remove Salt and Saffron' }));
  expect(screen.getByRole('heading', { name: '1 book in your cart' })).toBeOnTheScreen();
  expect(screen.queryByText('Salt and Saffron')).not.toBeOnTheScreen();
  expect(screen.getByText(/^Total/)).toHaveTextContent('Total $16.20');
  // ...
});

The file's first run took 50 seconds, mostly Babel 41,624 transforming the screen's imports; with the cache warm it took 8. Use queryBy only to assert absence, because getBy throws before expect can read its result. And when a query is awkward to write, fix the component rather than reaching for testID: two identical "Remove" buttons would have been as hard for this test to tell apart as they were for TalkBack.