All checks were successful
Deploy play.wenpai.net / deploy (push) Successful in 3s
- 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>
104 lines
3.6 KiB
JavaScript
Executable file
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);
|
|
})();
|