Testing Library
03 / 03

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/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 own version of these notes — editable, searchable, and organised by your stack.

Start free