Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:04 +08:00
commit dc6f26607c
8 changed files with 1184 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
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([])
})
})

View File

@@ -0,0 +1,134 @@
name: Accessibility Tests
on:
pull_request:
branches: [main, master]
push:
branches: [main, master]
jobs:
accessibility:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Start application server
run: |
npm start &
echo $! > .app-pid
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000 -t 60000
- name: Run accessibility tests
id: a11y_tests
continue-on-error: true
run: npm run test:a11y
- name: Setup Python
if: always()
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Generate accessibility report
if: always()
run: |
python scripts/generate_a11y_report.py \
--input test-results/a11y-results.json \
--output accessibility-report.md \
--format github
- name: Comment PR with report
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs')
try {
const report = fs.readFileSync('accessibility-report.md', 'utf8')
// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
})
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Accessibility Test Report')
)
const commentBody = report + '\n\n---\n*Automated accessibility check*'
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
})
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
})
}
} catch (error) {
console.error('Error posting comment:', error)
}
- name: Upload accessibility report
if: always()
uses: actions/upload-artifact@v4
with:
name: accessibility-report
path: |
accessibility-report.md
test-results/
retention-days: 30
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Stop application server
if: always()
run: |
if [ -f .app-pid ]; then
kill $(cat .app-pid) || true
fi
- name: Fail job if violations found
if: steps.a11y_tests.outcome == 'failure'
run: |
echo "[ERROR] Accessibility violations found"
exit 1

View File

@@ -0,0 +1,52 @@
{
"defaults": {
"timeout": 30000,
"wait": 1000,
"chromeLaunchConfig": {
"executablePath": "/usr/bin/chromium-browser",
"args": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage"
]
},
"standard": "WCAG2AA",
"runners": [
"axe",
"htmlcs"
],
"hideElements": "iframe, [role='presentation']",
"ignore": [
"notice",
"warning"
],
"includeNotices": false,
"includeWarnings": false,
"level": "error",
"reporter": "json",
"threshold": 0,
"screenCapture": "reports/screenshots/{url}-{datetime}.png",
"actions": [
"wait for element body to be visible",
"wait for element #main-content to be visible"
]
},
"urls": [
{
"url": "http://localhost:3000",
"screenCapture": "reports/screenshots/homepage.png"
},
{
"url": "http://localhost:3000/entities",
"screenCapture": "reports/screenshots/entities.png"
},
{
"url": "http://localhost:3000/timeline",
"screenCapture": "reports/screenshots/timeline.png"
},
{
"url": "http://localhost:3000/about",
"screenCapture": "reports/screenshots/about.png"
}
]
}