skip to content

In a frontend component test, what does an automated accessibility check such as jest-axe's axe(container) followed by expect(results).toHaveNoViolations() actually do, and what does a passing result prove?

level: juniorimportance: should knowfreq 40%

answer

  1. rules over rendered markup, nothing more
  2. engine returns buckets, not a verdict
  3. only one bucket fails the test
  4. green means no enabled rule fired

basics

~20 s

An automated accessibility check evaluates a fixed set of rules against the component's rendered DOM and fails the test if any rule is violated. Passing means no enabled rule fired — not that the component is accessible.

solid answer

~40 s

You render the component, hand the resulting DOM subtree to the accessibility engine, and it walks that markup evaluating machine-checkable rules — does every image have an `alt`, does every input have a label, is every `aria-*` value legal, does every button expose a name. The engine returns a results object with `violations`, `passes` and `incomplete` entries; the `toHaveNoViolations` matcher fails the test when `violations` is non-empty and prints the offending rule and node so the failure is actionable. The value is that it is nearly free: one extra assertion per component test catches the mistakes that are objectively decidable from markup. What it proves is narrow — no rule in the enabled set fired against this markup in this environment. That is a floor to build on, not a certificate of accessibility.

code

jsx · 11 lines
jsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import Button from './Button';

expect.extend(toHaveNoViolations);

test('button has no automatically detectable violations', async () => {
  const { container } = render(<Button>Save</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

go deeper

for a junior

Be able to describe the three steps out loud — render the component, run the check over the rendered container, assert there are no violations — and say plainly that a pass means no rule fired, not that the component is accessible.

for a middle

Explain the mechanics: the engine evaluates deterministic rules against DOM nodes, returns violations, passes, incomplete and inapplicable, and only violations fail the assertion. Be ready to say why the call must be awaited.

for a senior

Show judgment about scope and placement — check the rendered container rather than the document, put the check in a shared render helper, and treat it as a regression net for shared components rather than a compliance claim.

for a principal

Own the framing for the organisation: define what a green suite is allowed to claim, where this layer sits relative to linting, browser checks and manual review, and how you stop violation counts from becoming the accessibility metric.

## What the check is actually made of An automated accessibility check is a rule engine, not an oracle. Libraries such as jest-axe (and its Vitest-flavoured equivalent, vitest-axe) are thin wrappers around the axe-core engine: they give you an `axe()` function that takes a DOM node and returns results, plus a matcher, `toHaveNoViolations`, that turns those results into a test failure with a readable message. Each rule is a small deterministic program over the DOM. `image-alt` asks whether an `<img>` exposes alternative text. `label` asks whether a form control is associated with a label. `button-name` asks whether a button ends up with a non-empty accessible name. `aria-valid-attr-value` asks whether an ARIA attribute's value is one the specification allows. Every one of these questions can be answered by inspecting nodes and attributes, which is exactly why they can be automated — and it is also the boundary of what automation can do. ## The shape of the assertion The pattern is always the same three steps: render, run, assert. ```js const { container } = render(<Button>Save</Button>); const results = await axe(container); expect(results).toHaveNoViolations(); ``` Three details matter. First, `axe()` is asynchronous — the engine walks the tree and resolves a promise, so the call must be awaited or the assertion runs against a promise and silently passes. Second, you pass the container, i.e. the DOM subtree the component rendered, not a component instance and not a serialized string; the engine needs real nodes because rules read computed attributes and relationships. Third, the matcher has to be registered with the test framework before it can be used (`expect.extend(toHaveNoViolations)`), typically once in a shared setup file rather than in every test. ## Reading the result object The results carry four buckets, and only one of them fails your test. - `violations` — rules that ran and failed. `toHaveNoViolations` fails on these. - `passes` — rules that ran and succeeded. - `incomplete` — rules that ran but could not decide, usually because the environment lacked the information they needed. The matcher does **not** fail on these. - `inapplicable` — rules with nothing in the markup to check. The `incomplete` bucket is the one people are surprised by. A rule landing there is neither a pass nor a fail; it is the engine saying "a human has to look at this". A green test tells you nothing about anything sitting in that bucket. ## What a green run does and does not claim A green run supports exactly one claim: none of the rules that were enabled and could run found a defect in this markup. It does not claim the component is usable. It cannot tell you whether the alternative text is meaningful, whether the tab order matches the visual order, whether focus is visible, or whether a screen reader announces something a person can act on. Those require human judgment or a real browser, and treating a green suite as proof of accessibility is the single most common misuse of this tool. It is also scoped to *this markup in this environment*. A component test typically renders a fragment into a bare container in a simulated DOM, so page-level rules have nothing meaningful to evaluate and rules that depend on layout or painting cannot resolve real values at all. ## Where it earns its place Because the check costs one assertion, the sensible default is to fold it into the tests you were writing anyway — especially for shared, reusable components, where one missing label is multiplied across every screen that consumes them. It works best as a regression net: once a defect has been found and fixed, the rule stops it from coming back, forever, at no ongoing cost. What it should not become is the whole accessibility strategy. It is one layer among several — static linting of the markup as it is authored, rule checks over rendered DOM in component tests, a real-browser pass for anything involving colour and layout, and manual keyboard and screen-reader review of the flows that matter. Each catches things the others structurally cannot. ## Common mistakes Forgetting `await` is the classic, and it produces a test that can never fail. Asserting on `document.body` instead of the rendered container pulls in unrelated markup left by other tests. Running the check on a component that has not reached its final state — still loading, still empty — checks markup no user ever sees. And disabling a rule to make a red test green, without recording why, converts a real defect into a permanent blind spot.

  • What happens if you forget to await the axe call?
    `axe()` returns a promise, so `expect(promise).toHaveNoViolations()` asserts against an object that has no violations array. Depending on the matcher and framework you get either a confusing error or, worse, a passing test that never checked anything. Awaiting the call — or returning the promise from the test — is what makes the assertion real.
  • Should you pass the rendered container or the whole document to the check?
    The container, in a component test. Scoping to the subtree the component rendered keeps the failure attributable to this component and avoids inheriting markup from the harness or from other mounts. Passing the whole document also invites page-level rules to fire on a fragment that was never meant to be a page.
  • Where in the suite would you put this check so it does not become boilerplate?
    In a shared helper or a custom render wrapper, so the check is on by default and one place controls which rules run. Registering the matcher once in the framework's setup file, rather than per test file, keeps the pattern to a single line at the point of use and makes the rule configuration reviewable as one artefact.

saying these in an interview costs you the question

  • Green automated checks mean the component is accessible
  • Assumes the engine judges whether alt text is meaningful
  • Forgets the check is asynchronous and must be awaited
  • Thinks incomplete results fail the test like violations do
  • Believes one rule check replaces manual keyboard review

context