play.wenpai.net/scripts/play-a11y
elementary-qa 36b7ad5288
All checks were successful
Deploy play.wenpai.net / deploy (push) Successful in 3s
feat: add 7-layer test infrastructure for play.wenpai.net
- Schema validation (validate-plugins): field/type/enum/blueprint existence checks
- Deep verification (play-verify): admin notice bar, plugin activation, JS error capture
- Performance baseline (play-verify): navigation + playground load timing
- Visual regression (play-visual): 3 viewports, ImageMagick pixel diff
- Cross-browser (play-verify): --browser firefox/webkit, --mobile viewport
- Accessibility (play-a11y): axe-core WCAG 2 AA audit
- Error monitoring (play-smoke): blueprint URL reachability for all zip references
- Fix: color contrast on featured card (opacity 0.45→0.7) for WCAG AA compliance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:51:08 +08:00

104 lines
3.6 KiB
JavaScript
Executable file

#!/usr/bin/env node
// play-a11y — play.wenpai.net 可访问性测试
// 用法: play-a11y [--verbose]
// 使用 axe-core 引擎扫描首页,输出违规项
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const SITE = 'https://play.wenpai.net';
const args = process.argv.slice(2);
const verbose = args.includes('--verbose');
const RED = '\x1b[31m', GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m', NC = '\x1b[0m';
// 找到 axe-core 的路径
function findAxeCore() {
const candidates = [
'/usr/lib/node_modules/axe-core/axe.min.js',
'/usr/lib/node_modules/@axe-core/cli/node_modules/axe-core/axe.min.js',
path.join(process.env.HOME, 'node_modules/axe-core/axe.min.js')
];
for (const p of candidates) {
if (fs.existsSync(p)) return p;
}
return null;
}
(async () => {
console.log('\n=== play.wenpai.net 可访问性测试 (axe-core) ===\n');
const axePath = findAxeCore();
if (!axePath) {
console.error(`${RED}axe-core 未找到。安装: npm i -g axe-core${NC}`);
process.exit(1);
}
console.log(`axe-core: ${axePath}`);
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto(SITE, { waitUntil: 'networkidle', timeout: 30000 });
// 等待动态渲染完成
await page.waitForTimeout(2000);
// 注入 axe-core 并运行
const axeSource = fs.readFileSync(axePath, 'utf8');
await page.evaluate(axeSource);
const results = await page.evaluate(() => {
return new Promise((resolve) => {
// eslint-disable-next-line no-undef
axe.run(document, {
runOnly: ['wcag2a', 'wcag2aa', 'best-practice'],
resultTypes: ['violations', 'incomplete']
}).then(resolve);
});
});
await browser.close();
// 输出结果
const violations = results.violations || [];
const incomplete = results.incomplete || [];
if (violations.length === 0) {
console.log(`\n${GREEN}[✓] 无可访问性违规${NC}`);
} else {
console.log(`\n${RED}发现 ${violations.length} 项违规:${NC}\n`);
for (const v of violations) {
const impact = v.impact === 'critical' || v.impact === 'serious' ? RED : YELLOW;
console.log(` ${impact}[${v.impact}]${NC} ${v.id}: ${v.description}`);
console.log(` 帮助: ${v.helpUrl}`);
if (verbose) {
for (const node of v.nodes.slice(0, 3)) {
console.log(` 元素: ${node.html.substring(0, 100)}`);
}
if (v.nodes.length > 3) console.log(` ... 还有 ${v.nodes.length - 3} 个元素`);
} else {
console.log(` 影响 ${v.nodes.length} 个元素`);
}
}
}
if (incomplete.length > 0) {
console.log(`\n${YELLOW}${incomplete.length} 项需人工复查:${NC}`);
for (const i of incomplete.slice(0, 5)) {
console.log(` [?] ${i.id}: ${i.description} (${i.nodes.length} 个元素)`);
}
if (incomplete.length > 5) console.log(` ... 还有 ${incomplete.length - 5}`);
}
// 保存完整结果
const outDir = path.join(process.env.HOME, 'test-results', new Date().toISOString().slice(0, 10));
fs.mkdirSync(outDir, { recursive: true });
const outPath = path.join(outDir, 'a11y-results.json');
fs.writeFileSync(outPath, JSON.stringify(results, null, 2));
console.log(`\n完整结果: ${outPath}`);
const criticalCount = violations.filter(v => v.impact === 'critical' || v.impact === 'serious').length;
console.log(`\n=== 结果: ${violations.length} 违规 (${criticalCount} 严重), ${incomplete.length} 待复查 ===`);
process.exit(criticalCount > 0 ? 1 : 0);
})();