feat(bf-21nd): add axe-core Playwright integration with shared helper

- Add shared accessibility test helper (tests/accessibility/helper.js/ts)
  - scanAccessibility(): runs axe-core scans with WCAG 2.1 AA tags
  - formatViolations(): formats violation details for error reporting
  - expectNoAccessibilityViolations(): asserts no violations
- Add smoke tests demonstrating helper usage (tests/accessibility/smoke.spec.js/ts)
- Refactor existing accessibility tests to use shared helper
  - Simplified a11y.spec.js to use expectNoAccessibilityViolations()
  - Maintains all existing test coverage

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-07-06 00:25:39 -04:00
parent 669d3793aa
commit fb6e81e135
5 changed files with 183 additions and 21 deletions

View file

@ -1,5 +1,5 @@
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
const { test } = require('@playwright/test');
const { expectNoAccessibilityViolations } = require('./accessibility/helper');
const pages = [
{ name: 'index', path: '/index.html' },
@ -16,25 +16,8 @@ for (const page of pages) {
const pg = await context.newPage();
await pg.goto(page.path, { waitUntil: 'load' });
const results = await new AxeBuilder({ page: pg })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
const violations = results.violations;
if (violations.length > 0) {
const details = violations
.map((v) => ` ${v.id}: ${v.nodes.length} node(s) — ${v.description}`)
.join('\n');
// Attach full report for debugging
await test.info().attach('axe-violations', {
body: JSON.stringify(violations, null, 2),
contentType: 'application/json',
});
}
await expect(violations, `${violations.length} WCAG AA violation(s) on ${page.path}:\n${
violations.map((v) => ` ${v.id}: ${v.description}`).join('\n')
}`).toEqual([]);
// Use the shared accessibility helper
await expectNoAccessibilityViolations(pg, page.path);
await context.close();
});

View file

@ -0,0 +1,69 @@
const AxeBuilder = require('@axe-core/playwright').default;
/**
* Run an axe-core accessibility scan with WCAG 2.1 AA tags
*
* @param {Page} page - Playwright Page object to scan
* @param {string} context - Optional context selector to limit the scan scope
* @returns {Promise<Object>} Axe scan results with violations, passes, and incomplete results
*/
async function scanAccessibility(page, context) {
const builder = new AxeBuilder({ page });
// Configure with WCAG 2.1 AA tags
builder.withTags(['wcag2a', 'wcag2aa']);
// Limit scan scope if context is provided
if (context) {
builder.include(context);
}
return await builder.analyze();
}
/**
* Format accessibility violations for error reporting
*
* @param {Array} violations - Array of axe violation results
* @returns {string} Formatted string with violation details
*/
function formatViolations(violations) {
return violations
.map((v) => ` ${v.id}: ${v.nodes.length} node(s) — ${v.description}`)
.join('\n');
}
/**
* Assert that a page has no accessibility violations
*
* @param {Page} page - Playwright Page object to scan
* @param {string} pageName - Name of the page for error reporting
* @param {string} context - Optional context selector to limit the scan scope
*/
async function expectNoAccessibilityViolations(page, pageName, context) {
const results = await scanAccessibility(page, context);
const violations = results.violations;
if (violations.length > 0) {
// Attach full report for debugging
const { test } = require('@playwright/test');
await test.info().attach('axe-violations', {
body: JSON.stringify(violations, null, 2),
contentType: 'application/json',
});
}
const { expect } = require('@playwright/test');
await expect(
violations,
`${violations.length} WCAG AA violation(s) on ${pageName}:\n${
violations.map((v) => ` ${v.id}: ${v.description}`).join('\n')
}`
).toEqual([]);
}
module.exports = {
scanAccessibility,
formatViolations,
expectNoAccessibilityViolations,
};

View file

@ -0,0 +1,68 @@
import { Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
/**
* Run an axe-core accessibility scan with WCAG 2.1 AA tags
*
* @param page - Playwright Page object to scan
* @param context - Optional context selector to limit the scan scope
* @returns Axe scan results with violations, passes, and incomplete results
*/
export async function scanAccessibility(page: Page, context?: string) {
const builder = new AxeBuilder({ page });
// Configure with WCAG 2.1 AA tags
builder.withTags(['wcag2a', 'wcag2aa']);
// Limit scan scope if context is provided
if (context) {
builder.include(context);
}
return await builder.analyze();
}
/**
* Format accessibility violations for error reporting
*
* @param violations - Array of axe violation results
* @returns Formatted string with violation details
*/
export function formatViolations(violations: any[]): string {
return violations
.map((v) => ` ${v.id}: ${v.nodes.length} node(s) — ${v.description}`)
.join('\n');
}
/**
* Assert that a page has no accessibility violations
*
* @param page - Playwright Page object to scan
* @param pageName - Name of the page for error reporting
* @param context - Optional context selector to limit the scan scope
*/
export async function expectNoAccessibilityViolations(
page: Page,
pageName: string,
context?: string
): Promise<void> {
const results = await scanAccessibility(page, context);
const violations = results.violations;
if (violations.length > 0) {
// Attach full report for debugging
const { test } = require('@playwright/test');
await test.info().attach('axe-violations', {
body: JSON.stringify(violations, null, 2),
contentType: 'application/json',
});
}
const { expect } = require('@playwright/test');
await expect(
violations,
`${violations.length} WCAG AA violation(s) on ${pageName}:\n${
violations.map((v) => ` ${v.id}: ${v.description}`).join('\n')
}`
).toEqual([]);
}

View file

@ -0,0 +1,21 @@
const { test, expect } = require('@playwright/test');
const { scanAccessibility, expectNoAccessibilityViolations } = require('./helper');
test.describe('Accessibility Helper Smoke Test', () => {
test('helper functions are available', async ({ page }) => {
// Basic smoke test to verify the helper is working
await page.goto('/index.html', { waitUntil: 'load' });
// Test the scanAccessibility function
const results = await scanAccessibility(page);
expect(results).toHaveProperty('violations');
expect(results).toHaveProperty('passes');
expect(Array.isArray(results.violations)).toBe(true);
expect(Array.isArray(results.passes)).toBe(true);
});
test('index page has no WCAG AA violations (using helper)', async ({ page }) => {
await page.goto('/index.html', { waitUntil: 'load' });
await expectNoAccessibilityViolations(page, 'index page');
});
});

View file

@ -0,0 +1,21 @@
import { test, expect } from '@playwright/test';
import { scanAccessibility, expectNoAccessibilityViolations } from './helper';
test.describe('Accessibility Helper Smoke Test', () => {
test('helper functions are available', async ({ page }) => {
// Basic smoke test to verify the helper is working
await page.goto('/index.html', { waitUntil: 'load' });
// Test the scanAccessibility function
const results = await scanAccessibility(page);
expect(results).toHaveProperty('violations');
expect(results).toHaveProperty('passes');
expect(Array.isArray(results.violations)).toBe(true);
expect(Array.isArray(results.passes)).toBe(true);
});
test('index page has no WCAG AA violations (using helper)', async ({ page }) => {
await page.goto('/index.html', { waitUntil: 'load' });
await expectNoAccessibilityViolations(page, 'index page');
});
});