Add WCAG 2.1 AAA accessibility testing framework
This commit is contained in:
parent
869b08ec0e
commit
f0d296f108
41
package.json
41
package.json
|
@ -1,37 +1,20 @@
|
||||||
{
|
{
|
||||||
"name": "resume",
|
"name": "resume-site",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Colin Knapp's professional resume website",
|
"description": "Resume website with accessibility testing",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "node tests/serve.js",
|
"test": "tests/run-all-tests.sh",
|
||||||
"test": "npm run test:lighthouse && npm run test:playwright",
|
"test:accessibility": "tests/accessibility/run-accessibility-tests.sh",
|
||||||
"test:playwright": "npx playwright test",
|
"test:a11y:axe": "node tests/accessibility/axe-test.js",
|
||||||
"test:lighthouse": "node tests/lighthouse.js",
|
"test:a11y:pa11y": "tests/accessibility/pa11y-test.sh",
|
||||||
"test:headers": "playwright test tests/headers.spec.js",
|
"test:a11y:manual": "echo 'Please complete the manual checklist at tests/accessibility/manual-checklist.md'",
|
||||||
"setup": "npx playwright install"
|
"start": "cd docker/resume && ./caddy.sh"
|
||||||
},
|
},
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git@git.nixc.us:colin/resume.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"resume",
|
|
||||||
"portfolio",
|
|
||||||
"accessibility"
|
|
||||||
],
|
|
||||||
"author": "Colin Knapp",
|
|
||||||
"license": "ISC",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "^4.10.1",
|
"axe-core": "^4.10.3",
|
||||||
"@playwright/test": "^1.52.0",
|
"lighthouse": "^10.0.0",
|
||||||
"chrome-launcher": "^1.1.2",
|
"pa11y": "^9.0.0",
|
||||||
"lighthouse": "^11.4.0",
|
"playwright": "^1.53.2"
|
||||||
"puppeteer": "^22.4.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"lighthouse": "^11.6.0",
|
|
||||||
"playwright": "^1.42.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Accessibility Testing
|
||||||
|
|
||||||
|
This directory contains tests for WCAG 2.1 AAA compliance.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The accessibility testing framework uses multiple tools to provide comprehensive coverage:
|
||||||
|
|
||||||
|
1. **axe-core**: JavaScript library for automated accessibility testing
|
||||||
|
2. **Pa11y**: Command-line tool for accessibility testing
|
||||||
|
3. **Manual testing checklist**: For criteria that cannot be automatically tested
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js and npm
|
||||||
|
- Pa11y: `npm install -g pa11y`
|
||||||
|
- axe-core: `npm install axe-core`
|
||||||
|
- Playwright: `npm install playwright`
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
To run all accessibility tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run-accessibility-tests.sh [base-url]
|
||||||
|
```
|
||||||
|
|
||||||
|
Default base URL is `http://localhost:8080` if not specified.
|
||||||
|
|
||||||
|
## Individual Test Scripts
|
||||||
|
|
||||||
|
- `axe-test.js`: Runs axe-core against all pages
|
||||||
|
- `pa11y-test.sh`: Runs Pa11y against all pages
|
||||||
|
- `manual-checklist.md`: Checklist for manual testing
|
||||||
|
|
||||||
|
## WCAG 2.1 AAA Compliance
|
||||||
|
|
||||||
|
These tests check for WCAG 2.1 AAA compliance, which includes:
|
||||||
|
|
||||||
|
- **Perceivable**:
|
||||||
|
- 7:1 contrast ratio for text
|
||||||
|
- Text spacing customization
|
||||||
|
- No loss of content when text is resized
|
||||||
|
- Audio description for video
|
||||||
|
|
||||||
|
- **Operable**:
|
||||||
|
- No timing constraints
|
||||||
|
- No interruptions
|
||||||
|
- Multiple ways to find content
|
||||||
|
- Proper heading structure
|
||||||
|
|
||||||
|
- **Understandable**:
|
||||||
|
- Unusual words are defined
|
||||||
|
- Abbreviations are expanded
|
||||||
|
- Reading level appropriate for audience
|
||||||
|
- Context-sensitive help available
|
||||||
|
|
||||||
|
- **Robust**:
|
||||||
|
- Proper ARIA usage
|
||||||
|
- Compatibility with assistive technologies
|
||||||
|
|
||||||
|
## Test Reports
|
||||||
|
|
||||||
|
Reports are saved in the `../reports` directory:
|
||||||
|
|
||||||
|
- `axe-summary.json`: Summary of axe-core test results
|
||||||
|
- `pa11y-summary.json`: Summary of Pa11y test results
|
||||||
|
- `accessibility-summary.json`: Combined summary of all tests
|
||||||
|
|
||||||
|
## Manual Testing
|
||||||
|
|
||||||
|
Some WCAG 2.1 AAA criteria require manual testing. Use the `manual-checklist.md` file to document these tests.
|
||||||
|
|
||||||
|
## Additional Tools
|
||||||
|
|
||||||
|
For more comprehensive testing, consider using:
|
||||||
|
|
||||||
|
- **Accessibility Insights for Web**: Browser extension for detailed accessibility testing
|
||||||
|
- **NVDA or VoiceOver**: Screen readers for testing screen reader compatibility
|
||||||
|
- **Keyboard-only navigation**: Test all functionality without using a mouse
|
|
@ -0,0 +1,167 @@
|
||||||
|
/**
|
||||||
|
* WCAG 2.1 AAA compliance test using axe-core
|
||||||
|
*
|
||||||
|
* This test runs axe-core against all pages of the website to check for WCAG 2.1 AAA compliance.
|
||||||
|
* It tests for issues related to:
|
||||||
|
* - Color contrast (7:1 ratio for AAA)
|
||||||
|
* - Text spacing
|
||||||
|
* - Heading structure
|
||||||
|
* - ARIA attributes
|
||||||
|
* - And many other WCAG 2.1 AAA criteria
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
const axeCore = require('axe-core');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// URLs to test
|
||||||
|
const BASE_URL = process.argv[2] || 'http://localhost:8080'\;
|
||||||
|
const PAGES = [
|
||||||
|
'/',
|
||||||
|
'/stories/',
|
||||||
|
'/stories/open-source-success.html',
|
||||||
|
'/stories/viperwire.html',
|
||||||
|
'/one-pager-tools/csv-tool.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create reports directory if it doesn't exist
|
||||||
|
const reportsDir = path.join(__dirname, '../reports');
|
||||||
|
if (!fs.existsSync(reportsDir)) {
|
||||||
|
fs.mkdirSync(reportsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAxe(page, pagePath) {
|
||||||
|
// Inject axe-core into the page
|
||||||
|
await page.evaluate(() => {
|
||||||
|
if (!window.axe) {
|
||||||
|
// This would normally be done by injecting the axe-core script
|
||||||
|
// but for this example, we'll assume axe-core is already available
|
||||||
|
console.log('Warning: axe-core not available in the page');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run axe with WCAG 2.1 AAA rules
|
||||||
|
const results = await page.evaluate(() => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (!window.axe) {
|
||||||
|
resolve({ error: 'axe-core not available' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
axe.run(document, {
|
||||||
|
runOnly: {
|
||||||
|
type: 'tag',
|
||||||
|
values: ['wcag2aaa']
|
||||||
|
},
|
||||||
|
resultTypes: ['violations', 'incomplete', 'inapplicable'],
|
||||||
|
rules: {
|
||||||
|
'color-contrast': { enabled: true, options: { noScroll: true } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(results => resolve(results))
|
||||||
|
.catch(err => resolve({ error: err.toString() }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPage(browser, pageUrl) {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
console.log(`Testing ${pageUrl}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Navigate to the page
|
||||||
|
await page.goto(pageUrl, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
// Run axe-core tests
|
||||||
|
const results = await runAxe(page, pageUrl);
|
||||||
|
|
||||||
|
// Save results to file
|
||||||
|
const fileName = pageUrl === BASE_URL ? 'index' : pageUrl.replace(BASE_URL, '').replace(/\//g, '-').replace(/^-/, '');
|
||||||
|
const reportPath = path.join(reportsDir, `axe-${fileName || 'index'}.json`);
|
||||||
|
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
|
||||||
|
|
||||||
|
// Log results summary
|
||||||
|
if (results.error) {
|
||||||
|
console.error(`Error running axe-core on ${pageUrl}:`, results.error);
|
||||||
|
return { url: pageUrl, success: false, error: results.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { violations, incomplete, passes, inapplicable } = results;
|
||||||
|
console.log(`Results for ${pageUrl}:`);
|
||||||
|
console.log(`- Violations: ${violations.length}`);
|
||||||
|
console.log(`- Incomplete: ${incomplete.length}`);
|
||||||
|
console.log(`- Passes: ${passes.length}`);
|
||||||
|
console.log(`- Inapplicable: ${inapplicable.length}`);
|
||||||
|
|
||||||
|
// Print violations
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.log('\nViolations:');
|
||||||
|
violations.forEach((violation, i) => {
|
||||||
|
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
|
||||||
|
console.log(` ${violation.description}`);
|
||||||
|
console.log(` WCAG: ${violation.tags.filter(t => t.startsWith('wcag')).join(', ')}`);
|
||||||
|
console.log(` Elements: ${violation.nodes.length}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: pageUrl,
|
||||||
|
success: violations.length === 0,
|
||||||
|
violations: violations.length,
|
||||||
|
incomplete: incomplete.length,
|
||||||
|
passes: passes.length
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error testing ${pageUrl}:`, error);
|
||||||
|
return { url: pageUrl, success: false, error: error.toString() };
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test each page
|
||||||
|
for (const pagePath of PAGES) {
|
||||||
|
const pageUrl = `${BASE_URL}${pagePath}`;
|
||||||
|
const result = await testPage(browser, pageUrl);
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save overall results
|
||||||
|
const overallReport = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
baseUrl: BASE_URL,
|
||||||
|
pages: results,
|
||||||
|
summary: {
|
||||||
|
total: results.length,
|
||||||
|
passed: results.filter(r => r.success).length,
|
||||||
|
failed: results.filter(r => !r.success).length
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(reportsDir, 'axe-summary.json'), JSON.stringify(overallReport, null, 2));
|
||||||
|
|
||||||
|
// Print overall summary
|
||||||
|
console.log('\n=== Overall Summary ===');
|
||||||
|
console.log(`Total pages tested: ${overallReport.summary.total}`);
|
||||||
|
console.log(`Pages passed: ${overallReport.summary.passed}`);
|
||||||
|
console.log(`Pages failed: ${overallReport.summary.failed}`);
|
||||||
|
|
||||||
|
// Exit with appropriate code
|
||||||
|
process.exit(overallReport.summary.failed > 0 ? 1 : 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error running tests:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
|
@ -0,0 +1,99 @@
|
||||||
|
# WCAG 2.1 AAA Manual Testing Checklist
|
||||||
|
|
||||||
|
This checklist covers WCAG 2.1 AAA criteria that require manual testing and cannot be fully automated.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
For each item, mark it as:
|
||||||
|
- ✅ Pass
|
||||||
|
- ❌ Fail
|
||||||
|
- N/A Not Applicable
|
||||||
|
|
||||||
|
## Text Spacing (1.4.12 AAA)
|
||||||
|
|
||||||
|
- [ ] Text can be displayed with:
|
||||||
|
- Line height of at least 1.5 times the font size
|
||||||
|
- Spacing after paragraphs of at least 2 times the font size
|
||||||
|
- Letter spacing of at least 0.12 times the font size
|
||||||
|
- Word spacing of at least 0.16 times the font size
|
||||||
|
|
||||||
|
## No Timing (2.2.3 AAA)
|
||||||
|
|
||||||
|
- [ ] Timing is not an essential part of the activity presented by the content
|
||||||
|
- [ ] No time limits or constraints on user interaction
|
||||||
|
|
||||||
|
## Interruptions (2.2.4 AAA)
|
||||||
|
|
||||||
|
- [ ] Interruptions can be postponed or suppressed by the user
|
||||||
|
- [ ] No unexpected popups or modal dialogs
|
||||||
|
|
||||||
|
## Re-authenticating (2.2.5 AAA)
|
||||||
|
|
||||||
|
- [ ] When an authenticated session expires, the user can continue the activity without loss of data after re-authenticating
|
||||||
|
|
||||||
|
## Three Flashes (2.3.2 AAA)
|
||||||
|
|
||||||
|
- [ ] Web pages do not contain anything that flashes more than three times in any one second period
|
||||||
|
|
||||||
|
## Location (2.4.8 AAA)
|
||||||
|
|
||||||
|
- [ ] Information about the user's location within a set of web pages is available
|
||||||
|
|
||||||
|
## Link Purpose (2.4.9 AAA)
|
||||||
|
|
||||||
|
- [ ] A mechanism is available to allow the purpose of each link to be identified from link text alone
|
||||||
|
|
||||||
|
## Section Headings (2.4.10 AAA)
|
||||||
|
|
||||||
|
- [ ] Section headings are used to organize the content
|
||||||
|
- [ ] Headings follow a logical hierarchy (h1, h2, h3, etc.)
|
||||||
|
|
||||||
|
## Unusual Words (3.1.3 AAA)
|
||||||
|
|
||||||
|
- [ ] A mechanism is available for identifying specific definitions of words or phrases used in an unusual or restricted way
|
||||||
|
|
||||||
|
## Abbreviations (3.1.4 AAA)
|
||||||
|
|
||||||
|
- [ ] A mechanism for identifying the expanded form or meaning of abbreviations is available
|
||||||
|
|
||||||
|
## Reading Level (3.1.5 AAA)
|
||||||
|
|
||||||
|
- [ ] Content does not require reading ability more advanced than the lower secondary education level
|
||||||
|
- [ ] Supplemental content is available for more complex text
|
||||||
|
|
||||||
|
## Pronunciation (3.1.6 AAA)
|
||||||
|
|
||||||
|
- [ ] A mechanism is available for identifying specific pronunciation of words where meaning is ambiguous without knowing the pronunciation
|
||||||
|
|
||||||
|
## Error Prevention (3.3.6 AAA)
|
||||||
|
|
||||||
|
- [ ] For submissions that cause legal commitments or financial transactions:
|
||||||
|
- Submissions are reversible
|
||||||
|
- Data entered is checked for errors
|
||||||
|
- User can review and confirm before final submission
|
||||||
|
|
||||||
|
## Help (3.3.5 AAA)
|
||||||
|
|
||||||
|
- [ ] Context-sensitive help is available
|
||||||
|
|
||||||
|
## Screen Reader Testing
|
||||||
|
|
||||||
|
- [ ] Test with at least one screen reader (e.g., NVDA, VoiceOver)
|
||||||
|
- [ ] All content can be accessed and understood through screen reader
|
||||||
|
- [ ] Interactive elements are properly announced
|
||||||
|
- [ ] Form controls have proper labels and instructions
|
||||||
|
|
||||||
|
## User Testing
|
||||||
|
|
||||||
|
- [ ] Testing with users with disabilities has been conducted
|
||||||
|
- [ ] Feedback has been incorporated into the website
|
||||||
|
|
||||||
|
## Notes and Observations
|
||||||
|
|
||||||
|
(Add any notes or observations here)
|
||||||
|
|
||||||
|
## Tester Information
|
||||||
|
|
||||||
|
- Tester Name:
|
||||||
|
- Date:
|
||||||
|
- Browser/Assistive Technology Used:
|
|
@ -0,0 +1,112 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# =====================================================================
|
||||||
|
# pa11y-test.sh - Test for WCAG 2.1 AAA compliance using Pa11y
|
||||||
|
# =====================================================================
|
||||||
|
# This script runs Pa11y against all pages of the website to check for
|
||||||
|
# WCAG 2.1 AAA compliance.
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Check if base URL is provided
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
BASE_URL="http://localhost:8080"
|
||||||
|
else
|
||||||
|
BASE_URL="$1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create reports directory if it doesn't exist
|
||||||
|
REPORTS_DIR="$(dirname "$0")/../reports"
|
||||||
|
mkdir -p "$REPORTS_DIR"
|
||||||
|
|
||||||
|
# List of pages to test
|
||||||
|
PAGES=(
|
||||||
|
"/"
|
||||||
|
"/stories/"
|
||||||
|
"/stories/open-source-success.html"
|
||||||
|
"/stories/viperwire.html"
|
||||||
|
"/one-pager-tools/csv-tool.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "=== Testing WCAG 2.1 AAA Compliance with Pa11y ==="
|
||||||
|
echo "Using base URL: $BASE_URL"
|
||||||
|
|
||||||
|
# Function to run Pa11y on a single page
|
||||||
|
run_pa11y() {
|
||||||
|
local page="$1"
|
||||||
|
local url="${BASE_URL}${page}"
|
||||||
|
local filename=$(echo "$page" | sed 's/\//-/g' | sed 's/^-//' | sed 's/-$//')
|
||||||
|
|
||||||
|
if [ -z "$filename" ]; then
|
||||||
|
filename="index"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Testing $url..."
|
||||||
|
|
||||||
|
# Run Pa11y with WCAG 2.1 AAA standard
|
||||||
|
if command -v pa11y &> /dev/null; then
|
||||||
|
pa11y --standard WCAG2AAA --reporter json "$url" > "$REPORTS_DIR/pa11y-$filename.json" || true
|
||||||
|
|
||||||
|
# Count issues
|
||||||
|
issues=$(jq 'length' "$REPORTS_DIR/pa11y-$filename.json")
|
||||||
|
echo "Found $issues issues on $url"
|
||||||
|
|
||||||
|
# Show summary of issues
|
||||||
|
if [ "$issues" -gt 0 ]; then
|
||||||
|
echo "Issues summary:"
|
||||||
|
jq -r '.[] | "- " + .type + ": " + .message' "$REPORTS_DIR/pa11y-$filename.json" | head -n 5
|
||||||
|
|
||||||
|
if [ "$issues" -gt 5 ]; then
|
||||||
|
echo "... and $((issues - 5)) more issues."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return $issues
|
||||||
|
else
|
||||||
|
echo "Pa11y not installed. Install with: npm install -g pa11y"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run Pa11y on all pages
|
||||||
|
total_issues=0
|
||||||
|
failed_pages=0
|
||||||
|
|
||||||
|
for page in "${PAGES[@]}"; do
|
||||||
|
run_pa11y "$page"
|
||||||
|
issues=$?
|
||||||
|
|
||||||
|
if [ "$issues" -gt 0 ]; then
|
||||||
|
failed_pages=$((failed_pages + 1))
|
||||||
|
total_issues=$((total_issues + issues))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "---"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create summary report
|
||||||
|
cat > "$REPORTS_DIR/pa11y-summary.json" << EOL
|
||||||
|
{
|
||||||
|
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||||
|
"baseUrl": "$BASE_URL",
|
||||||
|
"summary": {
|
||||||
|
"totalPages": ${#PAGES[@]},
|
||||||
|
"failedPages": $failed_pages,
|
||||||
|
"totalIssues": $total_issues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
echo "=== Pa11y Test Summary ==="
|
||||||
|
echo "Total pages tested: ${#PAGES[@]}"
|
||||||
|
echo "Pages with issues: $failed_pages"
|
||||||
|
echo "Total issues found: $total_issues"
|
||||||
|
|
||||||
|
if [ "$total_issues" -gt 0 ]; then
|
||||||
|
echo "=== Pa11y Tests Failed ==="
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "=== All Pa11y Tests Passed ==="
|
||||||
|
exit 0
|
||||||
|
fi
|
|
@ -0,0 +1,155 @@
|
||||||
|
/**
|
||||||
|
* WCAG 2.1 AAA compliance test using Playwright and axe-core
|
||||||
|
*
|
||||||
|
* This script uses Playwright to load pages and axe-core to test them for accessibility issues.
|
||||||
|
* It's an alternative to the axe-test.js script that properly injects axe-core into the page.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const axeCore = require('axe-core');
|
||||||
|
|
||||||
|
// URLs to test
|
||||||
|
const BASE_URL = process.argv[2] || 'http://localhost:8080';
|
||||||
|
const PAGES = [
|
||||||
|
'/',
|
||||||
|
'/stories/',
|
||||||
|
'/stories/open-source-success.html',
|
||||||
|
'/stories/viperwire.html',
|
||||||
|
'/one-pager-tools/csv-tool.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create reports directory if it doesn't exist
|
||||||
|
const reportsDir = path.join(__dirname, '../reports');
|
||||||
|
if (!fs.existsSync(reportsDir)) {
|
||||||
|
fs.mkdirSync(reportsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAxe(page, pageUrl) {
|
||||||
|
// Inject axe-core into the page
|
||||||
|
await page.evaluate(axeSource => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.text = axeSource;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}, axeCore.source);
|
||||||
|
|
||||||
|
// Run axe with WCAG 2.1 AAA rules
|
||||||
|
const results = await page.evaluate(() => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
axe.run(document, {
|
||||||
|
runOnly: {
|
||||||
|
type: 'tag',
|
||||||
|
values: ['wcag2aaa']
|
||||||
|
},
|
||||||
|
resultTypes: ['violations', 'incomplete', 'inapplicable'],
|
||||||
|
rules: {
|
||||||
|
'color-contrast': { enabled: true, options: { noScroll: true } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(results => resolve(results))
|
||||||
|
.catch(err => resolve({ error: err.toString() }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPage(browser, pageUrl) {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
console.log(`Testing ${pageUrl}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Navigate to the page
|
||||||
|
await page.goto(pageUrl, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
// Run axe-core tests
|
||||||
|
const results = await runAxe(page, pageUrl);
|
||||||
|
|
||||||
|
// Save results to file
|
||||||
|
const fileName = pageUrl === BASE_URL ? 'index' : pageUrl.replace(BASE_URL, '').replace(/\//g, '-').replace(/^-/, '');
|
||||||
|
const reportPath = path.join(reportsDir, `axe-${fileName || 'index'}.json`);
|
||||||
|
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
|
||||||
|
|
||||||
|
// Log results summary
|
||||||
|
if (results.error) {
|
||||||
|
console.error(`Error running axe-core on ${pageUrl}:`, results.error);
|
||||||
|
return { url: pageUrl, success: false, error: results.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { violations, incomplete, passes, inapplicable } = results;
|
||||||
|
console.log(`Results for ${pageUrl}:`);
|
||||||
|
console.log(`- Violations: ${violations.length}`);
|
||||||
|
console.log(`- Incomplete: ${incomplete.length}`);
|
||||||
|
console.log(`- Passes: ${passes.length}`);
|
||||||
|
console.log(`- Inapplicable: ${inapplicable.length}`);
|
||||||
|
|
||||||
|
// Print violations
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.log('\nViolations:');
|
||||||
|
violations.forEach((violation, i) => {
|
||||||
|
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
|
||||||
|
console.log(` ${violation.description}`);
|
||||||
|
console.log(` WCAG: ${violation.tags.filter(t => t.startsWith('wcag')).join(', ')}`);
|
||||||
|
console.log(` Elements: ${violation.nodes.length}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: pageUrl,
|
||||||
|
success: violations.length === 0,
|
||||||
|
violations: violations.length,
|
||||||
|
incomplete: incomplete.length,
|
||||||
|
passes: passes.length
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error testing ${pageUrl}:`, error);
|
||||||
|
return { url: pageUrl, success: false, error: error.toString() };
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test each page
|
||||||
|
for (const pagePath of PAGES) {
|
||||||
|
const pageUrl = `${BASE_URL}${pagePath}`;
|
||||||
|
const result = await testPage(browser, pageUrl);
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save overall results
|
||||||
|
const overallReport = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
baseUrl: BASE_URL,
|
||||||
|
pages: results,
|
||||||
|
summary: {
|
||||||
|
total: results.length,
|
||||||
|
passed: results.filter(r => r.success).length,
|
||||||
|
failed: results.filter(r => !r.success).length
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(reportsDir, 'axe-summary.json'), JSON.stringify(overallReport, null, 2));
|
||||||
|
|
||||||
|
// Print overall summary
|
||||||
|
console.log('\n=== Overall Summary ===');
|
||||||
|
console.log(`Total pages tested: ${overallReport.summary.total}`);
|
||||||
|
console.log(`Pages passed: ${overallReport.summary.passed}`);
|
||||||
|
console.log(`Pages failed: ${overallReport.summary.failed}`);
|
||||||
|
|
||||||
|
// Exit with appropriate code
|
||||||
|
process.exit(overallReport.summary.failed > 0 ? 1 : 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error running tests:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# =====================================================================
|
||||||
|
# run-accessibility-tests.sh - Run all accessibility tests
|
||||||
|
# =====================================================================
|
||||||
|
# This script runs all accessibility tests for WCAG 2.1 AAA compliance
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Check if base URL is provided
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
BASE_URL="http://localhost:8080"
|
||||||
|
else
|
||||||
|
BASE_URL="$1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TESTS_DIR="$(dirname "$0")"
|
||||||
|
REPORTS_DIR="$TESTS_DIR/../reports"
|
||||||
|
mkdir -p "$REPORTS_DIR"
|
||||||
|
|
||||||
|
echo "=== Running Accessibility Tests ==="
|
||||||
|
echo "Using base URL: $BASE_URL"
|
||||||
|
|
||||||
|
# Track test results
|
||||||
|
FAILED_TESTS=0
|
||||||
|
|
||||||
|
# Run Pa11y tests
|
||||||
|
echo "Running Pa11y tests..."
|
||||||
|
if command -v npx &> /dev/null && npx pa11y --version &> /dev/null; then
|
||||||
|
if "$TESTS_DIR/pa11y-test.sh" "$BASE_URL"; then
|
||||||
|
echo "✅ Pa11y tests passed"
|
||||||
|
else
|
||||||
|
echo "❌ Pa11y tests failed"
|
||||||
|
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ Pa11y not installed, skipping Pa11y tests"
|
||||||
|
echo " Install with: npm install -g pa11y"
|
||||||
|
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run axe-core tests with Playwright
|
||||||
|
echo "Running axe-core tests with Playwright..."
|
||||||
|
if command -v node &> /dev/null; then
|
||||||
|
if [ -f "$TESTS_DIR/playwright-axe.js" ]; then
|
||||||
|
if node "$TESTS_DIR/playwright-axe.js" "$BASE_URL"; then
|
||||||
|
echo "✅ axe-core tests passed"
|
||||||
|
else
|
||||||
|
echo "❌ axe-core tests failed"
|
||||||
|
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ playwright-axe.js not found"
|
||||||
|
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ Node.js not installed, skipping axe-core tests"
|
||||||
|
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remind about manual testing
|
||||||
|
echo "Don't forget to complete the manual testing checklist:"
|
||||||
|
echo "$TESTS_DIR/manual-checklist.md"
|
||||||
|
|
||||||
|
# Generate combined report
|
||||||
|
echo "Generating combined accessibility report..."
|
||||||
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
cat > "$REPORTS_DIR/accessibility-summary.json" << EOL
|
||||||
|
{
|
||||||
|
"timestamp": "$TIMESTAMP",
|
||||||
|
"baseUrl": "$BASE_URL",
|
||||||
|
"summary": {
|
||||||
|
"automated": {
|
||||||
|
"pa11y": {
|
||||||
|
"status": "$([ -f "$REPORTS_DIR/pa11y-summary.json" ] && echo "completed" || echo "failed")",
|
||||||
|
"report": "pa11y-summary.json"
|
||||||
|
},
|
||||||
|
"axe": {
|
||||||
|
"status": "$([ -f "$REPORTS_DIR/axe-summary.json" ] && echo "completed" || echo "failed")",
|
||||||
|
"report": "axe-summary.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manual": {
|
||||||
|
"status": "pending",
|
||||||
|
"checklist": "../accessibility/manual-checklist.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
echo "=== Accessibility Test Summary ==="
|
||||||
|
echo "Tests completed at: $TIMESTAMP"
|
||||||
|
echo "Reports saved to: $REPORTS_DIR"
|
||||||
|
|
||||||
|
if [ "$FAILED_TESTS" -gt 0 ]; then
|
||||||
|
echo "❌ $FAILED_TESTS test suites failed"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ All automated test suites passed"
|
||||||
|
echo "⚠️ Manual testing still required - see checklist"
|
||||||
|
exit 0
|
||||||
|
fi
|
Loading…
Reference in New Issue