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.jsexpect.extend({toHaveNoA11yViolations: require('@surea11y/test-matchers').toHaveNoA11yViolations});// jest.config.jsmodule.exports = {testEnvironment: 'jsdom', // or the explicit jest-environment-jsdom package, depending on your Jest versionsetupFilesAfterEnv: ['./jest.setup.js']};
Vitest
// vitest.setup.jsimport { expect } from 'vitest';import { toHaveNoA11yViolations } from '@surea11y/test-matchers';expect.extend({ toHaveNoA11yViolations });// vitest.config.jsexport default {test: {environment: 'jsdom', // Vitest doesn't bundle jsdom -- install it as a devDependency toosetupFiles: ['./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 Aexpect(container).toHaveNoA11yViolations({ tags: { include: 'wcag2a' } });// Skip a rule you've decided not to enforce yetexpect(container).toHaveNoA11yViolations({ rules: { exclude: 'target-size-minimum' } });// Ignore a third-party widget you don't controlexpect(container).toHaveNoA11yViolations({ excludeSelectors: ['.intercom-launcher'] });
API
| Param | Type | Meaning |
|---|---|---|
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 > imgAdd 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.