@accesslint/vitest
v0.3.2
Published
Accessibility assertions for Vitest — toBeAccessible() matcher powered by AccessLint
Maintainers
Readme
@accesslint/vitest
Accessibility assertions for Vitest. Adds a toBeAccessible() matcher powered by AccessLint that checks for WCAG 2.2 Level A and AA violations.
Installation
npm install --save-dev @accesslint/vitestvitest >= 2.0 is required as a peer dependency.
Setup
Add @accesslint/vitest as a setup file in your Vitest config:
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["@accesslint/vitest"],
},
});This automatically registers the toBeAccessible() matcher.
Manual registration
import { accesslintMatchers } from "@accesslint/vitest/matchers";
import { expect } from "vitest";
expect.extend(accesslintMatchers);Usage
Pass any DOM Element to expect() and call toBeAccessible():
const container = document.createElement("div");
container.innerHTML = '<img src="photo.jpg" alt="A photo">';
expect(container).toBeAccessible();Violations are scoped to descendants of the element you pass, so you can test components in isolation. When the element is not the document root, page-level rules (html-has-lang, document-title, landmarks, etc.) are skipped automatically — override with componentMode: false if you want them back.
Framework recipes
Works with any DOM produced in a jsdom or happy-dom environment.
React — @testing-library/react
import { render } from "@testing-library/react";
import { LoginForm } from "./LoginForm";
test("LoginForm is accessible", () => {
const { container } = render(<LoginForm />);
expect(container).toBeAccessible();
});Vue — @testing-library/vue
import { render } from "@testing-library/vue";
import LoginForm from "./LoginForm.vue";
test("LoginForm is accessible", () => {
const { container } = render(LoginForm);
expect(container).toBeAccessible();
});Svelte — @testing-library/svelte
import { render } from "@testing-library/svelte";
import LoginForm from "./LoginForm.svelte";
test("LoginForm is accessible", () => {
const { container } = render(LoginForm);
expect(container).toBeAccessible();
});Angular — @testing-library/angular
import { render } from "@testing-library/angular";
import { LoginFormComponent } from "./login-form.component";
test("LoginForm is accessible", async () => {
const { container } = await render(LoginFormComponent);
expect(container).toBeAccessible();
});The matcher accepts anything that's an Element — container, screen.getByRole('form'), a plain document.createElement(...), or document.body for a full-page audit.
Options
expect(container).toBeAccessible({
/** Rule IDs to disable for this assertion. */
disabledRules: ["distinguishable/color-contrast"],
/** Additional rules to run on top of the built-in set. */
additionalRules: [myCompiledDeclarativeRule],
/** Include AAA-level rules (excluded by default). */
includeAAA: true,
/** Skip page-level rules — defaults to true for non-root elements. */
componentMode: true,
/** Translated rule messages and guidance (e.g. "en", "es"). */
locale: "es",
/** Minimum impact that causes failure. "minor" (default) fails on anything. */
failOn: "serious",
/** Compare against a baseline instead of asserting zero violations. */
snapshot: "login-form",
/** Where to store snapshot files. Defaults to {cwd}/accessibility-snapshots/. */
snapshotDir: "./test/a11y-snapshots",
});Failure messages
Failures include impact, WCAG criterion, level, selector, optional context, suggested fix, and remediation guidance:
Expected element to have no accessibility violations, but found 2:
[critical] text-alternatives/img-alt (WCAG 1.1.1, A) — Images must have alternate text
selector: img
fix: add-attribute alt=""
guidance: Decorative images should have alt=""; informative images should describe content.
[serious] distinguishable/color-contrast (WCAG 1.4.3, AA) — Text must have sufficient color contrast
selector: p.subtitleSnapshot baselines
Lock in the current accessibility state of a component and fail only on new violations. Useful when adopting AccessLint in a legacy codebase.
expect(container).toBeAccessible({ snapshot: "login-form" });- First run creates
accessibility-snapshots/login-form.jsonand passes. - Subsequent runs fail if any new violation appears beyond the baseline.
- Ratchet-down — when only fixed violations are detected (none added), the baseline updates automatically.
- Force refresh the baseline with
ACCESSLINT_UPDATE=1or Vitest's-u/--updateflag.
Violation identity starts at ruleId + selector and falls back through a tiered multi-signal matcher powered by @accesslint/heal-diff:
- Exact selector match (fast path).
- Anchor attribute (
data-testid,id,name,href,for,aria-label). - Computed ARIA role + accessible name.
- HTML fingerprint (normalized outerHTML hash).
- Relative location (nearest landmark trail), uniqueness-gated.
When a non-exact tier matches the baseline's selector auto-heals to the current one, the snapshot is rewritten, and a "healed" event is logged to .history.ndjson. The test still passes. When no tier matches but weaker signals overlap, the failure message attaches a likely moved from: hint so you can tell "same issue, different selector" from "genuinely new issue" at a glance.
Trend reports
Every create / ratchet-down / force-update event appends a record to accessibility-snapshots/.history.ndjson alongside the baseline JSON files. Generate a trend report from that history with @accesslint/report:
npx @accesslint/report --format md > a11y-report.md
npx @accesslint/report --format html --out a11y-report.htmlThe report shows a stacked chart of total violations per snapshot over time, plus a per-rule movement table that joins in WCAG metadata from @accesslint/core. The sidecar file is append-only and safe to commit; merge conflicts on it resolve trivially by concatenating both sides.
Audit memoization (opt-in fixture)
By default every toBeAccessible() call runs a fresh audit. For component tests that chain multiple assertions against the same DOM state, import the fixture to reuse one audit across assertions:
import { test } from "@accesslint/vitest/fixture";
import { expect } from "vitest";
test("Form", ({ a11y }) => {
render(<Form />);
a11y.refresh(); // snapshot current DOM state
// both assertions reuse one runAudit() call
expect(screen.getByRole("form")).toBeAccessible();
expect(screen.getByRole("button")).toBeAccessible();
});Call a11y.refresh() after any DOM mutation that should be re-audited.
TypeScript
Types are included. Importing the package augments Vitest's expect with toBeAccessible() automatically.
What it checks
WCAG 2.2 Level A and AA rules via @accesslint/core, covering images, forms, ARIA attributes, color contrast, landmarks, links, tables, document language, and more. See the core rules table for the full list.
License
MIT
