All topics
Testing · Learning hub

Testing Library notes for developers

Master Testing Library with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Testing notes
Testing Library

Core Concepts & Query API

Testing Library: Core Concepts & Query API @testing-library is a family of packages for testing UI components. The philosophy: test the way users use your softw

Testing Library: Core Concepts & Query API

@testing-library is a family of packages for testing UI components. The philosophy: test the way users use your software — through accessible roles, visible text, and labels — not implementation details.

The Three Guiding Principles

  • Test what users see: query by role, label, and text — not by CSS class or internal state

  • Avoid implementation details: don't test which state variables changed or which methods were called

  • Accessible by default: queries that work for users also work for assistive technologies

Query Variants

Every query strategy comes in three variants:

  getBy*        Throws if 0 or 2+ elements found.  Use when asserting element EXISTS.
  queryBy*      Returns null if not found.           Use when asserting element ABSENT.
  findBy*       Returns Promise, retries on DOM changes. Use for ASYNC elements.

  + plural versions for multiple elements:
  getAllBy*   queryAllBy*   findAllBy*

Query Strategies — Priority Order

1. ByRole          — most preferred; queries ARIA roles (reflects accessibility tree)
2. ByLabelText     — for form controls with <label>, aria-label, aria-labelledby
3. ByPlaceholderText — form controls; role queries are better when available
4. ByText          — for non-interactive elements (headings, paragraphs, divs)
5. ByDisplayValue  — currently selected value in input/textarea/select
6. ByAltText       — img, area elements with alt attribute
7. ByTitle         — elements with title attribute
8. ByTestId        — last resort; add data-testid to element manually

Rule of thumb: if a screen reader would find it by role or label, use that query.
              If not, consider improving your component's accessibility first.

ByRole — Complete Reference

// Common ARIA roles and the HTML elements that map to them:

// Interactive
screen.getByRole('button')          // <button>, <input type="button/submit/reset">
screen.getByRole('link')            // <a href="...">
screen.getByRole('textbox')         // <input type="text">, <textarea>
screen.getByRole('checkbox')        // <input type="checkbox">
screen.getByRole('radio')           // <input type="radio">
screen.getByRole('combobox')        // <select>, or custom combobox with aria-haspopup
screen.getByRole('listbox')         // <select multiple>
screen.getByRole('option')          // <option>
screen.getByRole('switch')          // toggle with role="switch"
screen.getByRole('slider')          // <input type="range">
screen.getByRole('spinbutton')      // <input type="number">
screen.getByRole('searchbox')       // <input type="search">

// Structure
screen.getByRole('heading', { level: 1 })   // <h1> through <h6>
screen.getByRole('list')            // <ul>, <ol>
screen.getByRole('listitem')        // <li>
screen.getByRole('table')           // <table>
screen.getByRole('row')             // <tr>
screen.getByRole('cell')            // <td>
screen.getByRole('columnheader')    // <th>
screen.getByRole('img')             // <img> with alt text

// Landmark
screen.getByRole('navigation')      // <nav>
screen.getByRole('main')            // <main>
screen.getByRole('banner')          // <header>
screen.getByRole('contentinfo')     // <footer>
screen.getByRole('form')            // <form>
screen.getByRole('dialog')          // role="dialog" or <dialog>
screen.getByRole('alert')           // role="alert" — live region for errors
screen.getByRole('status')          // role="status" — live region for status

// Options
screen.getByRole('button', {
  name: /submit/i,          // accessible name (visible text, aria-label, aria-labelledby)
  hidden: true,             // include elements with display:none or visibility:hidden
  selected: true,           // for options
  checked: true,            // for checkboxes/radios
  pressed: true,            // for toggle buttons (aria-pressed)
  expanded: true,           // for expandable elements (aria-expanded)
  level: 2,                 // for headings
})
Testing Library

user-event & jest-dom

Testing Library: user-event & jest-dom user-event v14 user-event simulates real browser interactions more accurately than fireEvent. It triggers all intermediat

Testing Library: user-event & jest-dom

user-event v14

user-event simulates real browser interactions more accurately than fireEvent. It triggers all intermediate events (pointerover, pointerenter, focus, click, input, change, blur) just like a real browser would.

import userEvent from '@testing-library/user-event'

// Setup — create an instance (tracks pointer/keyboard state across calls)
const user = userEvent.setup()

// Typing
await user.type(element, 'hello')         // fires keydown/keypress/input/keyup per char
await user.type(element, '{Enter}')       // special key
await user.type(element, 'hello{Tab}')    // text then Tab

// Clear and type (replace value)
await user.clear(element)
await user.type(element, 'new value')

// Click
await user.click(element)
await user.dblClick(element)
await user.tripleClick(element)           // select all text in input

// Right click / special clicks
await user.pointer({ keys: '[MouseRight]', target: element })

// Keyboard
await user.keyboard('hello')             // types without focus events
await user.keyboard('{Tab}')
await user.keyboard('{Shift>}A{/Shift}') // shift+A
await user.keyboard('{Control>}a{/Control}')  // ctrl+A (select all)

// Select
await user.selectOptions(select, 'value')         // single select
await user.selectOptions(multiSelect, ['a', 'b']) // multi-select
await user.deselectOptions(multiSelect, 'a')

// File upload
const file = new File(['file contents'], 'photo.jpg', { type: 'image/jpeg' })
await user.upload(fileInput, file)
await user.upload(fileInput, [file1, file2])  // multiple files

// Hover
await user.hover(element)
await user.unhover(element)

// Tab navigation
await user.tab()                // focus next focusable element
await user.tab({ shift: true }) // focus previous

fireEvent — when to use

import { fireEvent } from '@testing-library/react'

// fireEvent fires a single synthetic event (no pointer/focus cascade)
// Use when userEvent doesn't support a specific event, or for performance

fireEvent.click(element)
fireEvent.change(input, { target: { value: 'new value' } })
fireEvent.submit(form)
fireEvent.keyDown(element, { key: 'Escape', code: 'Escape' })
fireEvent.scroll(scrollContainer, { target: { scrollTop: 100 } })

// Custom events
fireEvent(element, new MouseEvent('click', { bubbles: true, cancelable: true }))

jest-dom Custom Matchers

// npm install --save-dev @testing-library/jest-dom
// import '@testing-library/jest-dom' in setup file

// Existence
expect(el).toBeInTheDocument()
expect(el).not.toBeInTheDocument()

// Visibility
expect(el).toBeVisible()                // visible (not hidden by CSS or ancestors)
expect(el).not.toBeVisible()

// State
expect(el).toBeDisabled()
expect(el).toBeEnabled()
expect(el).toBeChecked()               // checkbox/radio
expect(el).not.toBeChecked()
expect(el).toBeRequired()
expect(el).toBeValid()                 // form validation
expect(el).toBeInvalid()
expect(el).toHaveFocus()
expect(el).toBeEmptyDOMElement()       // no children

// Content
expect(el).toHaveTextContent('Hello')
expect(el).toHaveTextContent(/hello/i)
expect(el).toHaveTextContent('Hello', { normalizeWhitespace: true })

// Value
expect(el).toHaveValue('alice@example.com')
expect(el).toHaveValue(['option1', 'option2'])  // multi-select
expect(el).toHaveDisplayValue('Selected Option')

// Attributes & style
expect(el).toHaveAttribute('type', 'submit')
expect(el).toHaveAttribute('aria-expanded', 'false')
expect(el).toHaveClass('active')
expect(el).toHaveClass('btn', 'primary')        // multiple classes
expect(el).toHaveStyle({ display: 'none' })
expect(el).toHaveStyle('color: red; font-size: 14px')

// ARIA
expect(el).toHaveAccessibleName('Close dialog')
expect(el).toHaveAccessibleDescription('This action cannot be undone')
Testing Library

Framework Adapters & Accessibility Testing

Testing Library: Framework Adapters & Accessibility Framework Packages @testing-library/react — React (most popular) @testing-library/vue — Vue 3 (and Vue 2 via

Testing Library: Framework Adapters & Accessibility

Framework Packages

  • @testing-library/react — React (most popular)

  • @testing-library/vue — Vue 3 (and Vue 2 via @testing-library/vue2)

  • @testing-library/angular — Angular

  • @testing-library/svelte — Svelte

  • @testing-library/preact — Preact

  • @testing-library/dom — Core DOM utilities (no framework)

  • All share the same query API, userEvent, and jest-dom matchers

Vue Testing Library

import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import MyComponent from './MyComponent.vue'

test('button click updates count', async () => {
  const user = userEvent.setup()
  render(MyComponent, {
    props: { initialCount: 0 },
    global: {
      plugins: [router, pinia],    // Vue plugins
      stubs: { MyHeavyChild: true },
    },
  })

  await user.click(screen.getByRole('button', { name: /increment/i }))
  expect(screen.getByText('Count: 1')).toBeInTheDocument()
})

Angular Testing Library

import { render, screen } from '@testing-library/angular'
import { MyComponent } from './my.component'
import { MyService } from './my.service'

test('shows user name', async () => {
  await render(MyComponent, {
    declarations: [MyComponent],
    providers: [
      { provide: MyService, useValue: { getName: () => 'Alice' } }
    ],
  })

  expect(screen.getByText('Hello, Alice!')).toBeInTheDocument()
})

Accessibility Testing

// jest-axe — automated accessibility violations
// npm install --save-dev jest-axe
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)

test('Login form has no accessibility violations', async () => {
  const { container } = render(<LoginForm />)
  const results = await axe(container)
  expect(results).toHaveNoViolations()
})

// axe checks for:
// - Missing alt text on images
// - Form inputs without labels
// - Insufficient color contrast
// - Missing landmark regions
// - Incorrect heading hierarchy
// - Interactive elements that aren't focusable

// Manually test keyboard navigation
test('modal can be closed with Escape', async () => {
  const user = userEvent.setup()
  render(<Modal isOpen={true} onClose={jest.fn()} />)

  const modal = screen.getByRole('dialog')
  expect(modal).toBeInTheDocument()

  // Focus should be trapped inside modal
  await user.tab()
  expect(screen.getByRole('button', { name: /close/i })).toHaveFocus()

  await user.keyboard('{Escape}')
  expect(modal).not.toBeInTheDocument()
})

// Check ARIA attributes
expect(screen.getByRole('button', { name: /menu/i }))
  .toHaveAttribute('aria-expanded', 'false')

await user.click(screen.getByRole('button', { name: /menu/i }))

expect(screen.getByRole('button', { name: /menu/i }))
  .toHaveAttribute('aria-expanded', 'true')

DOM Testing Library (framework-agnostic)

import { getByRole, queryByText, findByText } from '@testing-library/dom'

// Query directly against a DOM node (no framework)
const container = document.getElementById('app')!

const button = getByRole(container, 'button', { name: /submit/i })
const error = queryByText(container, /error/i)
const loaded = await findByText(container, /welcome/i)

// within() — scope queries to a subtree
import { within } from '@testing-library/react'

const table = screen.getByRole('table')
const rows = within(table).getAllByRole('row')
expect(rows).toHaveLength(5)

const firstRow = rows[1]  // skip header
expect(within(firstRow).getByText('Alice')).toBeInTheDocument()
expect(within(firstRow).getByRole('button', { name: /edit/i })).toBeEnabled()

Keep your Testing Library knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever