Patterns, Accessibility and Testing
Pick component patterns that keep APIs flexible (context-based compound components, headless hooks with prop getters and state reducers), build on native semantics for accessibility, and test the way users interact: role-based queries, awaited user-event calls, and assertions on what people actually perceive.
Key points
- 1
Query priority:
getByRole(withname) >getByLabelText> placeholder/text/display value > alt text/title >getByTestId. If a role query can't find something, assistive tech may not either. - 2
getBy*throws on zero or many matches,queryBy*returns null (use it only for absence), andfindBy*is async and retries (1000 ms default).*AllBy*variants return arrays. - 3
user-event 14 calls are async: create
const user = userEvent.setup()andawaitevery interaction. It enforces real constraints (pointer-events, maxLength, focus) thatfireEventskips. - 4
Keep
waitForcallbacks free of side effects, prove an element exists before asserting it disappears, and fix act() warnings by awaiting the final UI rather than silencing them. - 5
Compound components should share state through context, not
cloneElement, so consumers can wrap and reorder children. Apply HOCs once at module level and copy statics they hide. - 6
Accessibility basics in React: native elements (
<button>,<label htmlFor>),useIdfor associations, live regions that are mounted before their text changes, focus management for dialogs and route changes, and native<dialog>.showModal()orinertfor modals. - 7
Test with fresh per-test state (new QueryClient, providers via the
wrapperoption), mock the network at the boundary (MSW), and remember jsdom does not load your CSS or implementshowModal; use a real browser for those.
Common traps
A negative assertion with a typo in the query (
queryByText('Invalid emial')) passes forever;waitForElementToBeRemovedthrows if the element is missing at the start.aria-labelreplaces the visible text as the accessible name, sogetByRole('button', { name: 'Delete' })fails for<button aria-label="Remove item">Delete</button>and voice-control users can't say what they see.Under fake timers,
await user.type(...)hangs unless you passadvanceTimers(ordelay: null) touserEvent.setup().
Read the source
Test yourself on Patterns, Accessibility and Testing
Ten questions, with the answer and explanation after each one.