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