From 8245b03b436a6b20f4195dcfa455521cce734e97 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 27 Sep 2026 12:04:37 -0700 Subject: [PATCH] test(e2e): run an axe sweep over every Ladle story test:e2e builds and serves the stories and fails on any serious or critical axe violation. --- CONTRIBUTING.md | 3 ++- bun.lock | 3 +++ e2e/stories-a11y.spec.ts | 32 ++++++++++++++++++++++++++++++++ package.json | 1 + playwright.config.ts | 23 ++++++++++++++++------- 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 e2e/stories-a11y.spec.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73162046..f404dc96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,8 @@ Contributors sign the [CLA](CLA.md) on their first PR; the CLA bot explains how. `bun run test:e2e` checks the built package: every `exports` target exists and the root entry loads under Node with every optional peer blocked, then packs the package into a Vite host and runs the Playwright specs in `e2e/` against it. Each -scenario is a route in `e2e/fixtures.tsx`. Run `bun run build` first, and +scenario is a route in `e2e/fixtures.tsx`. The same run builds the Ladle stories +and fails on any serious or critical axe violation in any story. Run `bun run build` first, and `bunx playwright install chromium` once. CI also runs `bun run contrast-test` (theme contrast floors, reads `dist/styles.css`) and `bun run dep-guard` (forbidden imports, see `AGENTS.md`) after the build. diff --git a/bun.lock b/bun.lock index c627c396..f36f7996 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "tailwind-merge": "^3.3.1", }, "devDependencies": { + "@axe-core/playwright": "^4.13.0", "@happy-dom/global-registrator": "^20.11.2", "@ladle/react": "^5.1.1", "@playwright/test": "^1.63.0", @@ -51,6 +52,8 @@ }, }, "packages": { + "@axe-core/playwright": ["@axe-core/playwright@4.13.0", "", { "dependencies": { "axe-core": "~4.13.0" }, "peerDependencies": { "playwright-core": ">= 1.0.0" } }, "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], diff --git a/e2e/stories-a11y.spec.ts b/e2e/stories-a11y.spec.ts new file mode 100644 index 00000000..32330343 --- /dev/null +++ b/e2e/stories-a11y.spec.ts @@ -0,0 +1,32 @@ +import AxeBuilder from "@axe-core/playwright"; +import { expect, test } from "@playwright/test"; + +const STORIES = "http://localhost:61000"; + +test("every Ladle story is free of serious and critical axe violations", async ({ + page, + request, +}) => { + test.setTimeout(600_000); + const meta = (await (await request.get(`${STORIES}/meta.json`)).json()) as { + stories: Record; + }; + const failures: string[] = []; + + for (const theme of ["light", "dark"]) { + for (const story of Object.keys(meta.stories)) { + await page.goto(`${STORIES}/?story=${story}&mode=preview&theme=${theme}`); + await page.locator("[data-storyloaded]").waitFor(); + const { violations } = await new AxeBuilder({ page }).analyze(); + for (const violation of violations) { + if (violation.impact === "serious" || violation.impact === "critical") { + failures.push( + `${story} (${theme}): ${violation.id} (${violation.nodes.length})`, + ); + } + } + } + } + + expect(failures).toEqual([]); +}); diff --git a/package.json b/package.json index 17404fde..6fc6db07 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "tailwind-merge": "^3.3.1" }, "devDependencies": { + "@axe-core/playwright": "^4.13.0", "@happy-dom/global-registrator": "^20.11.2", "@ladle/react": "^5.1.1", "@playwright/test": "^1.63.0", diff --git a/playwright.config.ts b/playwright.config.ts index 4e2a30cc..e18b3720 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,6 +1,7 @@ import { defineConfig, devices } from "@playwright/test"; -// Serves the Vite host e2e/helpers.ts bootstraps from the packed tarball. +// Serves the Vite host e2e/helpers.ts bootstraps from the packed tarball, and +// the Ladle stories build for the accessibility sweep. export default defineConfig({ testDir: "e2e", testMatch: "*.spec.ts", @@ -10,10 +11,18 @@ export default defineConfig({ snapshotPathTemplate: "{testDir}/__screenshots__/{arg}{ext}", use: { baseURL: "http://localhost:4173" }, projects: [{ name: "chromium", use: devices["Desktop Chrome"] }], - webServer: { - command: "bun e2e/helpers.ts", - url: "http://localhost:4173/smoke", - reuseExistingServer: false, - timeout: 180_000, - }, + webServer: [ + { + command: "bun e2e/helpers.ts", + url: "http://localhost:4173/smoke", + reuseExistingServer: false, + timeout: 180_000, + }, + { + command: "bun run stories:build && bunx ladle preview --port 61000", + url: "http://localhost:61000/meta.json", + reuseExistingServer: false, + timeout: 180_000, + }, + ], });