chore: add accessibility testing support with @axe-core/playwright and related utilities

This commit is contained in:
GitHub Actions
2026-04-20 17:37:38 +00:00
parent a74d10d138
commit 5f855ea779
6 changed files with 116 additions and 18 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View 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
View 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';

View 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([]);
}