Initial commit
This commit is contained in:
153
skills/a11y-checker-ci/assets/a11y-test.spec.ts
Normal file
153
skills/a11y-checker-ci/assets/a11y-test.spec.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
134
skills/a11y-checker-ci/assets/github-actions-a11y.yml
Normal file
134
skills/a11y-checker-ci/assets/github-actions-a11y.yml
Normal 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
|
||||
52
skills/a11y-checker-ci/assets/pa11y-config.json
Normal file
52
skills/a11y-checker-ci/assets/pa11y-config.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user