Usage with test-matchers

For unit and component tests — assert accessibility inline in a Jest or Vitest suite, against whatever DOM the test already has, rather than driving a real or headless browser. If you're testing full rendered pages instead, see the browser-driven bindings.

@surea11y/test-matchers is one matcher, toHaveNoA11yViolations(), that plugs into either framework's expect.extend() unmodified — it has no framework-specific code of its own. It scans whatever jsdom-based environment your test already runs under; it doesn't depend on jsdom itself or set one up for you. surea11y's engine is synchronous, so this matcher is too — no await, no forgetting one and silently passing on a resolved promise.

$ npm install --save-dev @surea11y/test-matchers

Setup

Your test environment must already be jsdom-based — this matcher scans whatever document/window it provides. Register the matcher once, in whichever setup file your test runner already loads:

Jest

// jest.setup.js
expect.extend({
toHaveNoA11yViolations: require('@surea11y/test-matchers').toHaveNoA11yViolations
});
 
// jest.config.js
module.exports = {
testEnvironment: 'jsdom', // or the explicit jest-environment-jsdom package, depending on your Jest version
setupFilesAfterEnv: ['./jest.setup.js']
};

Vitest

// vitest.setup.js
import { expect } from 'vitest';
import { toHaveNoA11yViolations } from '@surea11y/test-matchers';
 
expect.extend({ toHaveNoA11yViolations });
 
// vitest.config.js
export default {
test: {
environment: 'jsdom', // Vitest doesn't bundle jsdom -- install it as a devDependency too
setupFiles: ['./vitest.setup.js']
}
};

The bundled .d.ts augments both Jest's global jest.Matchers interface and Vitest's @vitest/expect interfaces, with no @types/jest or vitest import required to ship it — whichever framework is actually installed, only the matching augmentation takes effect.

Usage

Works against a plain DOM node:

test('no accessibility violations', () => {
document.body.innerHTML = '<img src="logo.png">';
 
expect(document.body).toHaveNoA11yViolations(); // fails: missing alt
});

...or any testing-library container — React Testing Library's, Vue Test Utils' wrapper.element, Angular Testing Library's, Svelte Testing Library's — since all of them expose a rendered DOM element the same way:

const { render } = require('@testing-library/react');
 
test('MyComponent has no accessibility violations', () => {
const { container } = render(<MyComponent />);
 
expect(container).toHaveNoA11yViolations();
});

This is framework-agnostic on the test-runner side too — the snippets above are identical whether they run under Jest or Vitest, once the matcher is registered.

Asserting .not

test('a page with a violation fails the assertion', () => {
document.body.innerHTML = '<main><img src="logo.png"></main>';
 
expect(document.body).not.toHaveNoA11yViolations();
});

Reusing one scan across multiple assertions

If you're already computing a surea11y scan result yourself — to assert on specific rules across several it() blocks without re-scanning each time — pass the result object directly instead of a DOM node:

const { runDomRulesInPage } = require('@surea11y/core');
 
const result = runDomRulesInPage(null, '#main', {}, null);
expect(result).toHaveNoA11yViolations();

Filtering and configuring scans

The second argument accepts anything @surea11y/core accepts — see Engine options for the full surface. A few common cases:

// Only run rules relevant to WCAG 2.0 A
expect(container).toHaveNoA11yViolations({ tags: { include: 'wcag2a' } });
 
// Skip a rule you've decided not to enforce yet
expect(container).toHaveNoA11yViolations({ rules: { exclude: 'target-size-minimum' } });
 
// Ignore a third-party widget you don't control
expect(container).toHaveNoA11yViolations({ excludeSelectors: ['.intercom-launcher'] });

API

ParamTypeMeaning
receivedElement, Document, or scan resultWhat to check. A DOM Element/Document is scanned automatically; an object shaped like { checksResults: [...] } is asserted on directly, with no scan performed. Anything else throws a TypeError.
engineOptionsobject (optional)Passed straight through to @surea11y/core's scan — the same options object used everywhere else in the ecosystem (see Engine options). Ignored when received is already a scan result.

Returns the { pass, message } shape both frameworks' expect.extend() expect — you never call this directly; it's wired up once via expect.extend() as shown in Setup above.

What it checks and doesn't

Only fail outcomes gate the assertion — matching every other surea11y binding's convention that cantTell results are advisory, not failures. A vague link like <a href="/pricing">Click here</a> is cantTell, not fail, so it never breaks a passing test on its own — though when an assertion does fail for other reasons, any cantTell results are appended to the failure message as a separate note, so they stay visible. See Known limitations for what this engine won't automate at all.

Scoping to an element only reports what's inside it — a failure elsewhere on the page never fails an assertion scoped to one container. Under the hood the scanned element is temporarily tagged with a unique attribute and scanned by that selector, invisibly to your test.

A handful of rules check a whole-page fact, not anything in a specific subtree — page-title-present, html-lang-attr-present, and a few others check document.title/document.documentElement directly, since asking whether the page has a title makes no sense for an arbitrary component snippet. Passing an Element (a testing-library container, document.body) reports these notApplicable rather than fail, so a missing <title> never breaks a component-level test; passing document itself evaluates them for real. If you do scan document directly, set document.title/document.documentElement's lang once in your setup file rather than per test.

Failure messages

Lists every occurrence, not just every rule — one rule can flag several elements — with the rule ID, severity, a human-readable summary, the failing element's selector, and a fix hint where available:

expected no accessibility violations, but found 1:
1) img-alt-present (serious): Missing alt attribute on <img>.
at html > body > img
Add an alt attribute or use alt="" for decorative images.

Any cantTell results alongside the failures are appended as a separate note, so they stay visible without affecting pass/fail:

2 rule(s) need manual review (cantTell, not counted as failures): link-name-quality-manual, color-contrast-computable

Runnable, side-by-side examples for both frameworks: Jest and Vitest. For the complete engineOptions surface (locale, contrast modes, shadow DOM, custom rules, WCAG-version tag combinations), see ENGINE_OPTIONS.md — this package forwards whatever you pass rather than duplicating that reference.