Course outline

React / Coordinated application behavior / Lesson 12

Testing and accessible interactions

Verify observable component contracts, coordinate updates correctly, and test the full keyboard and error-recovery journey.

About 4 min readContent reviewed
On this page

Lesson goals

  • Assert behavior that users rely on
  • Test a small interactive contract
  • Cover accessibility as behavior

Assert behavior that users rely on

A component test should connect an input or interaction to an observable result under a justified expectation. Prefer assertions about names, values, messages, and state over private Hook call counts or incidental wrapper structure. Derive expected results from the requirement, not by copying the implementation’s formula into the test. A snapshot can document structure but does not prove that clicking, typing, focus, or recovery works. Begin with the smallest meaningful contract and add boundary cases such as empty input, rejected action, and repeated interaction. Keep network and time dependencies controlled so a failure points to the behavior rather than an unrelated service.

Coordinate committed work before assertions

React updates can be scheduled, so tests need the renderer’s supported coordination before inspecting the result. The official act helper groups test interactions and flushes relevant React work before assertions when used as documented. Modern testing libraries often wrap their interaction and render helpers, while a real browser test can wait for an observable condition. Do not scatter arbitrary sleeps until a flaky test turns green. Fake timers and mocked promises still require appropriate integration with the chosen runner. The test environment may need its documented act configuration; that is test setup, not a production global to add without understanding it.

Test a small interactive contract

The first example exposes a button whose accessible name communicates a fixed action and a visible output showing the count. A useful test locates “Add one,” activates it, and asserts that the output becomes one. It should also verify repeated clicks and any documented lower or upper bound if one exists. This example intentionally has no bound rather than inventing a requirement. Native button semantics provide keyboard activation, but the surrounding product still needs meaningful focus styling and order. An accessible-name query can catch a lost label that a CSS-selector-only test would miss.

jsx Component module — requires the course React host
import { useState } from "react";
export default function App() { const [count, setCount] = useState(0); return <main><h1>Test target</h1><button type="button" onClick={() => setCount(c => c + 1)}>Add one</button><output aria-label="Count">{count}</output></main>; }
Expected output
Initial Count: 0. After Add one: 1.

A test can locate the named button and assert the committed Count output after activation.

Make a minimal coordinated verification explicit

The second module exports a verify helper to demonstrate act without assuming an installed third-party test runner. It mounts an isolated counter, clicks inside act, reads the committed output, and always unmounts. The host must supply a separate empty container and the documented test environment configuration. This is a teaching helper, not a complete project test suite or a recommendation to replace user-level interaction tools with raw events everywhere. Real keyboard and typing behavior needs browser or appropriately modeled interaction tests. Never mount a second test root into a container already owned by the application.

jsx Component module — requires the course React host
import { act, useState } from "react";
import { createRoot } from "react-dom/client";
function Counter() { const [n, setN] = useState(0); return <><button type="button" onClick={() => setN(x => x + 1)}>Add</button><output>{n}</output></>; }
export async function verify(container) { const root = createRoot(container); try { await act(async () => root.render(<Counter />)); await act(async () => container.querySelector("button").click()); const result = container.querySelector("output").textContent; if (result !== "1") throw new Error("Expected one"); return result; } finally { await act(async () => root.unmount()); } }
export default function App() { return <main><h1>Coordinated test target</h1><Counter /></main>; }
Expected output
Calling verify with an isolated empty test container returns "1".

The helper needs a separate test container and act-enabled environment. It returns one only after the assertion and releases its root in finally.

Cover accessibility as behavior

Check labels, descriptions, keyboard activation, focus entry and exit, visible focus, and feedback after invalid input. A disclosure must expose its current state and reveal the correct content; a dialog requires a full focus and dismissal contract. Automated accessibility checks find some errors but cannot establish that the workflow is understandable or that every custom widget follows its pattern. Include supported assistive technology and browser combinations in the product’s evidence. React can render semantic HTML, but it does not make a custom interaction accessible automatically. Keep an explicit manual or browser-based review for the parts that a unit test cannot meaningfully prove.

Common errors and fixes

A test reads the old DOM immediately

Why it happens
It asserted before scheduled work committed.
How to fix it
Use supported coordination or await the observable result.
Prevent it next time
Avoid arbitrary sleep-based assertions.

A snapshot passes while keyboard use fails

Why it happens
Only initial markup was checked.
How to fix it
Exercise activation, focus, and recovery paths.
Prevent it next time
Match tests to the interaction contract.

Tests leak state into each other

Why it happens
Roots or external subscriptions were not released.
How to fix it
Unmount and clean fixtures on every completion path.
Prevent it next time
Run focused tests independently and together.

Check your understanding

1. Choose assertions for a submit workflow that can fail validation.

Show worked solution

Assert a useful labeled input, an invalid result with specific correction text, preserved safe input, successful correction, and a truthful completion state. Also inspect keyboard focus and message association. A passing submit callback count alone does not establish any of those user outcomes.

2. Why should test cleanup run after a failed assertion too?

Show worked solution

A failed assertion does not release mounted roots, timers, or listeners automatically. Use the runner’s cleanup hooks or finally boundaries so later tests receive independent fixtures. Otherwise one legitimate failure can trigger a cascade of misleading failures caused by leaked resources rather than new defects.

Key takeaways

  • Test observable promises and realistic interactions.
  • Coordinate React work before assertions.
  • Accessibility needs more than static markup checks.

Official references