chore: add accessibility testing support with @axe-core/playwright and related utilities
This commit is contained in:
18
frontend/package-lock.json
generated
18
frontend/package-lock.json
generated
@@ -2823,9 +2823,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2843,9 +2840,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2863,9 +2857,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2883,9 +2874,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2903,9 +2891,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2923,9 +2908,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"type-check": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.11.2",
|
||||
"@bgotink/playwright-coverage": "^0.3.2",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@types/eslint-plugin-jsx-a11y": "^6.10.1",
|
||||
@@ -24,6 +25,19 @@
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@axe-core/playwright": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.2.tgz",
|
||||
"integrity": "sha512-iP6hfNl9G0j/SEUSo8M7D80RbcDo9KRAAfDP4IT5OHB+Wm6zUHIrm8Y51BKI+Oyqduvipf9u1hcRy57zCBKzWQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"axe-core": "~4.11.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright-core": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
@@ -1024,6 +1038,16 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.11.3",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.3.tgz",
|
||||
"integrity": "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"smol-toml": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.11.2",
|
||||
"@bgotink/playwright-coverage": "^0.3.2",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@types/eslint-plugin-jsx-a11y": "^6.10.1",
|
||||
|
||||
15
tests/a11y/a11y-baseline.ts
Normal file
15
tests/a11y/a11y-baseline.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface BaselineEntry {
|
||||
ruleId: string;
|
||||
pages: string[];
|
||||
reason: string;
|
||||
ticket?: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export const A11Y_BASELINE: BaselineEntry[] = [];
|
||||
|
||||
export function getBaselinedRuleIds(currentPage: string): string[] {
|
||||
return A11Y_BASELINE
|
||||
.filter((entry) => entry.pages.some((p) => currentPage.startsWith(p)))
|
||||
.map((entry) => entry.ruleId);
|
||||
}
|
||||
18
tests/fixtures/a11y.ts
vendored
Normal file
18
tests/fixtures/a11y.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test as base } from './auth-fixtures';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
|
||||
interface A11yFixtures {
|
||||
makeAxeBuilder: () => AxeBuilder;
|
||||
}
|
||||
|
||||
export const test = base.extend<A11yFixtures>({
|
||||
makeAxeBuilder: async ({ page }, use) => {
|
||||
const makeAxeBuilder = () =>
|
||||
new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
|
||||
.exclude('.chart-container canvas');
|
||||
await use(makeAxeBuilder);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from './auth-fixtures';
|
||||
58
tests/utils/a11y-helpers.ts
Normal file
58
tests/utils/a11y-helpers.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { expect } from '../fixtures/test';
|
||||
import type { AxeResults, Result } from 'axe-core';
|
||||
|
||||
export type ViolationImpact = 'critical' | 'serious' | 'moderate' | 'minor';
|
||||
|
||||
export interface A11yAssertionOptions {
|
||||
failOn?: ViolationImpact[];
|
||||
knownViolations?: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_FAIL_ON: ViolationImpact[] = ['critical', 'serious'];
|
||||
|
||||
export function getFailingViolations(
|
||||
results: AxeResults,
|
||||
options: A11yAssertionOptions = {},
|
||||
): Result[] {
|
||||
const failOn = options.failOn ?? DEFAULT_FAIL_ON;
|
||||
const knownViolations = new Set(options.knownViolations ?? []);
|
||||
|
||||
return results.violations.filter(
|
||||
(v) =>
|
||||
failOn.includes(v.impact as ViolationImpact) &&
|
||||
!knownViolations.has(v.id),
|
||||
);
|
||||
}
|
||||
|
||||
export function formatViolation(violation: Result): string {
|
||||
const nodes = violation.nodes
|
||||
.map((node, i) => {
|
||||
const selector = node.target.join(' ');
|
||||
const html = node.html.length > 200
|
||||
? `${node.html.slice(0, 200)}…`
|
||||
: node.html;
|
||||
const fix = node.failureSummary ?? '';
|
||||
return ` Node ${i + 1}: ${selector}\n HTML: ${html}\n Fix: ${fix}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return [
|
||||
`[${violation.impact?.toUpperCase()}] ${violation.id}: ${violation.description}`,
|
||||
` Help: ${violation.helpUrl}`,
|
||||
` Affected nodes (${violation.nodes.length}):`,
|
||||
nodes,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function expectNoA11yViolations(
|
||||
results: AxeResults,
|
||||
options: A11yAssertionOptions = {},
|
||||
): void {
|
||||
const failing = getFailingViolations(results, options);
|
||||
|
||||
const message = failing.length > 0
|
||||
? `Found ${failing.length} accessibility violation(s):\n\n${failing.map(formatViolation).join('\n\n')}`
|
||||
: '';
|
||||
|
||||
expect(failing, message).toEqual([]);
|
||||
}
|
||||
Reference in New Issue
Block a user