Files
2025-11-29 18:46:04 +08:00

154 lines
4.2 KiB
TypeScript

import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
import * as fs from 'fs'
import * as path from 'path'
/**
* Accessibility test suite using axe-core
*
* Tests pages against WCAG 2.1 Level AA standards
* Generates JSON results for report generation
*/
// Pages to test
const PAGES = [
{ url: '/', name: 'Homepage' },
{ url: '/entities', name: 'Entity List' },
{ url: '/timeline', name: 'Timeline' },
{ url: '/about', name: 'About' }
]
// WCAG levels to test
const WCAG_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']
// Result storage
const results: any[] = []
test.describe('Accessibility Tests', () => {
test.afterAll(async () => {
// Save results to file for report generation
const resultsDir = path.join(process.cwd(), 'test-results')
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true })
}
const resultsFile = path.join(resultsDir, 'a11y-results.json')
fs.writeFileSync(resultsFile, JSON.stringify(results, null, 2))
console.log(`Accessibility results saved to: ${resultsFile}`)
})
for (const { url, name } of PAGES) {
test(`${name} meets WCAG standards`, async ({ page }) => {
await page.goto(url)
// Wait for page to be fully loaded
await page.waitForLoadState('networkidle')
// Run accessibility scan
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(WCAG_TAGS)
.analyze()
// Store results
results.push({
url,
name,
timestamp: new Date().toISOString(),
violations: accessibilityScanResults.violations,
passes: accessibilityScanResults.passes,
incomplete: accessibilityScanResults.incomplete
})
// Log violations for immediate feedback
if (accessibilityScanResults.violations.length > 0) {
console.log(`\n[ERROR] ${name} has ${accessibilityScanResults.violations.length} violations:`)
accessibilityScanResults.violations.forEach(violation => {
console.log(` - [${violation.impact}] ${violation.id}: ${violation.description}`)
console.log(` Affected: ${violation.nodes.length} elements`)
})
} else {
console.log(`\n[OK] ${name} has no violations`)
}
// Fail test if violations found
expect(accessibilityScanResults.violations).toEqual([])
})
}
test('scan for color contrast issues', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.withRules(['color-contrast'])
.analyze()
expect(results.violations).toEqual([])
})
test('scan for keyboard navigation', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.withRules(['keyboard'])
.analyze()
expect(results.violations).toEqual([])
})
test('scan for ARIA usage', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.withTags(['cat.aria'])
.analyze()
expect(results.violations).toEqual([])
})
})
test.describe('Form Accessibility', () => {
test('forms have proper labels', async ({ page }) => {
// Test pages with forms
const formPages = ['/signup', '/login', '/entities/new']
for (const url of formPages) {
try {
await page.goto(url, { timeout: 5000 })
const results = await new AxeBuilder({ page })
.withRules(['label', 'label-title-only'])
.analyze()
expect(results.violations).toEqual([])
} catch (e) {
// Skip if page doesn't exist
console.log(`Skipping ${url} - page not found`)
}
}
})
})
test.describe('Interactive Elements', () => {
test('buttons have accessible names', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.withRules(['button-name'])
.analyze()
expect(results.violations).toEqual([])
})
test('links have accessible text', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.withRules(['link-name'])
.analyze()
expect(results.violations).toEqual([])
})
})