test(e2e): add axe-core accessibility fixture and smoke spec
Adds @axe-core/playwright with a shared fixture providing an `axe` helper. New a11y.spec.ts runs WCAG 2.1 AA checks on signin, dashboard, timeline, allocations, resources, and projects pages. Currently reports violations as warnings — upgrade to hard failures after fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
import { test as base, expect } from "@playwright/test";
|
||||||
|
import AxeBuilder from "@axe-core/playwright";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared Playwright fixture that adds an `axe` helper to every test.
|
||||||
|
* Usage:
|
||||||
|
* import { test, expect } from "./a11y-fixture.js";
|
||||||
|
* test("page is accessible", async ({ axe }) => {
|
||||||
|
* const results = await axe.analyze();
|
||||||
|
* expect(results.violations).toEqual([]);
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export const test = base.extend<{ axe: AxeBuilder }>({
|
||||||
|
axe: async ({ page }, use) => {
|
||||||
|
const builder = new AxeBuilder({ page })
|
||||||
|
// Exclude known third-party widgets that we cannot control
|
||||||
|
.exclude("#__next-build-indicator")
|
||||||
|
// Only check WCAG 2.1 AA — the standard most teams target
|
||||||
|
.withTags(["wcag2a", "wcag21a", "wcag2aa", "wcag21aa"]);
|
||||||
|
await use(builder);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { expect };
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { test, expect } from "./a11y-fixture.js";
|
||||||
|
|
||||||
|
test.describe("Accessibility (axe-core)", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto("/auth/signin");
|
||||||
|
await page.fill('input[type="email"]', "admin@capakraken.dev");
|
||||||
|
await page.fill('input[type="password"]', "admin123");
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
await expect(page).toHaveURL(/\/(dashboard|resources)/, { timeout: 15_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ name: "Dashboard", path: "/dashboard" },
|
||||||
|
{ name: "Timeline", path: "/timeline" },
|
||||||
|
{ name: "Allocations", path: "/allocations" },
|
||||||
|
{ name: "Resources", path: "/resources" },
|
||||||
|
{ name: "Projects", path: "/projects" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
test(`${route.name} page has no critical a11y violations`, async ({ page, axe }) => {
|
||||||
|
await page.goto(route.path);
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
const results = await axe
|
||||||
|
// Start with critical + serious only — warn on moderate/minor later
|
||||||
|
.options({ resultTypes: ["violations"] })
|
||||||
|
.analyze();
|
||||||
|
|
||||||
|
const critical = results.violations.filter(
|
||||||
|
(v) => v.impact === "critical" || v.impact === "serious",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (critical.length > 0) {
|
||||||
|
const summary = critical
|
||||||
|
.map((v) => `[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} nodes)`)
|
||||||
|
.join("\n");
|
||||||
|
console.warn(`A11y issues on ${route.path}:\n${summary}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, report but don't fail — upgrade to expect([]).toEqual([]) after fixes
|
||||||
|
expect(critical.length).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Sign-in page has no critical a11y violations (unauthenticated)", async ({ page, axe }) => {
|
||||||
|
await page.goto("/auth/signin");
|
||||||
|
await page.waitForLoadState("networkidle");
|
||||||
|
const results = await axe.options({ resultTypes: ["violations"] }).analyze();
|
||||||
|
|
||||||
|
const critical = results.violations.filter(
|
||||||
|
(v) => v.impact === "critical" || v.impact === "serious",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (critical.length > 0) {
|
||||||
|
const summary = critical
|
||||||
|
.map((v) => `[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} nodes)`)
|
||||||
|
.join("\n");
|
||||||
|
console.warn(`A11y issues on /auth/signin:\n${summary}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(critical.length).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@axe-core/playwright": "^4.11.1",
|
||||||
"@capakraken/eslint-config": "workspace:*",
|
"@capakraken/eslint-config": "workspace:*",
|
||||||
"@capakraken/tsconfig": "workspace:*",
|
"@capakraken/tsconfig": "workspace:*",
|
||||||
"@playwright/test": "^1.49.1",
|
"@playwright/test": "^1.49.1",
|
||||||
|
|||||||
Generated
+13
@@ -147,6 +147,9 @@ importers:
|
|||||||
specifier: ^3.23.8
|
specifier: ^3.23.8
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@axe-core/playwright':
|
||||||
|
specifier: ^4.11.1
|
||||||
|
version: 4.11.1(playwright-core@1.58.2)
|
||||||
'@capakraken/eslint-config':
|
'@capakraken/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../tooling/eslint
|
version: link:../../tooling/eslint
|
||||||
@@ -461,6 +464,11 @@ packages:
|
|||||||
nodemailer:
|
nodemailer:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@axe-core/playwright@4.11.1':
|
||||||
|
resolution: {integrity: sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==}
|
||||||
|
peerDependencies:
|
||||||
|
playwright-core: '>= 1.0.0'
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.0':
|
||||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -5453,6 +5461,11 @@ snapshots:
|
|||||||
preact: 10.24.3
|
preact: 10.24.3
|
||||||
preact-render-to-string: 6.5.11(preact@10.24.3)
|
preact-render-to-string: 6.5.11(preact@10.24.3)
|
||||||
|
|
||||||
|
'@axe-core/playwright@4.11.1(playwright-core@1.58.2)':
|
||||||
|
dependencies:
|
||||||
|
axe-core: 4.11.2
|
||||||
|
playwright-core: 1.58.2
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|||||||
Reference in New Issue
Block a user