From 64a6ff6a2000f458b86ba05805d87185245e8f3d Mon Sep 17 00:00:00 2001 From: Ninosaurier Date: Thu, 16 Jul 2026 15:33:28 +0200 Subject: [PATCH] feat(a11y): add extensible accessibility test infrastructure with axe-core" -m "- Add A11yChecker interface (types.ts) as the shared contract for accessibility checkers --- .../testing/a11y/checkers/axe-core.checker.ts | 21 +++++++++++ .../src/testing/a11y/checkers/types.ts | 17 +++++++++ .../luna-charts/src/testing/a11y/index.ts | 12 +++++++ .../luna-charts/src/testing/a11y/matchers.ts | 36 +++++++++++++++++++ packages/luna-charts/vitest-setup.ts | 1 + 5 files changed, 87 insertions(+) create mode 100644 packages/luna-charts/src/testing/a11y/checkers/axe-core.checker.ts create mode 100644 packages/luna-charts/src/testing/a11y/checkers/types.ts create mode 100644 packages/luna-charts/src/testing/a11y/index.ts create mode 100644 packages/luna-charts/src/testing/a11y/matchers.ts diff --git a/packages/luna-charts/src/testing/a11y/checkers/axe-core.checker.ts b/packages/luna-charts/src/testing/a11y/checkers/axe-core.checker.ts new file mode 100644 index 0000000..edef231 --- /dev/null +++ b/packages/luna-charts/src/testing/a11y/checkers/axe-core.checker.ts @@ -0,0 +1,21 @@ +import axe from 'axe-core'; +import type { A11yChecker, A11yCheckResult } from './types'; + +export const axeCoreChecker: A11yChecker = { + name: 'axe-core', + + async run(rootElement: Element): Promise { + const results = await axe.run(rootElement); + + return { + checkerName: 'axe-core', + violations: results.violations.map((violations) => ({ + id: violations.id, + impact: violations.impact ?? null, + description: violations.help, + helpUrl: violations.helpUrl, + nodes: violations.nodes.map((node) => node.target.join(' ')), + })), + }; + }, +}; \ No newline at end of file diff --git a/packages/luna-charts/src/testing/a11y/checkers/types.ts b/packages/luna-charts/src/testing/a11y/checkers/types.ts new file mode 100644 index 0000000..2254aaa --- /dev/null +++ b/packages/luna-charts/src/testing/a11y/checkers/types.ts @@ -0,0 +1,17 @@ +export interface A11yViolation { + id: string; + impact: 'minor' | 'moderate' | 'serious' | 'critical' | null; + description: string; + helpUrl?: string; + nodes: string[]; +} + +export interface A11yCheckResult { + checkerName: string; + violations: A11yViolation[]; +} + +export interface A11yChecker { + name: string; + run(root: Element): Promise; +} \ No newline at end of file diff --git a/packages/luna-charts/src/testing/a11y/index.ts b/packages/luna-charts/src/testing/a11y/index.ts new file mode 100644 index 0000000..7a840e8 --- /dev/null +++ b/packages/luna-charts/src/testing/a11y/index.ts @@ -0,0 +1,12 @@ +import type { A11yChecker, A11yCheckResult } from './checkers/types'; +import { axeCoreChecker } from './checkers/axe-core.checker'; + +const a11yCheckerRegistry: A11yChecker[] = [ + axeCoreChecker, +]; + +export async function runA11yChecks(rootElement: Element): Promise { + return Promise.all(a11yCheckerRegistry.map((a11yChecker) => a11yChecker.run(rootElement))); +} + +export type { A11yChecker, A11yCheckResult, A11yViolation } from './checkers/types'; \ No newline at end of file diff --git a/packages/luna-charts/src/testing/a11y/matchers.ts b/packages/luna-charts/src/testing/a11y/matchers.ts new file mode 100644 index 0000000..ac44fcb --- /dev/null +++ b/packages/luna-charts/src/testing/a11y/matchers.ts @@ -0,0 +1,36 @@ +import { expect } from 'vitest'; +import type { A11yCheckResult } from './checkers/types'; + +expect.extend({ + toHaveNoA11yViolations(results: A11yCheckResult[]) { + const failing = results.filter((results) => results.violations.length > 0); + + if (failing.length === 0) { + return { + pass: true, + message: () => 'expected accessibility violations, but none were found', + }; + } + + const message = failing + .map( + (results) => + `[${results.checkerName}] ${results.violations.length} violation(s):\n` + + results.violations + .map((validation) => ` - ${validation.id} (${validation.impact}): ${validation.description}\n affected: ${validation.nodes.join(', ')}`) + .join('\n') + ) + .join('\n\n'); + + return { + pass: false, + message: () => `Accessibility violations found:\n\n${message}`, + }; + }, +}); + +declare module 'vitest' { + interface Assertion { + toHaveNoA11yViolations(): void; + } +} \ No newline at end of file diff --git a/packages/luna-charts/vitest-setup.ts b/packages/luna-charts/vitest-setup.ts index 1c397f3..e046bc4 100644 --- a/packages/luna-charts/vitest-setup.ts +++ b/packages/luna-charts/vitest-setup.ts @@ -1,3 +1,4 @@ import { defineCustomElements } from "./loader"; +import './src/testing/a11y/matchers'; defineCustomElements(); \ No newline at end of file