From 4b84f280653a7ac8b6406db15f1d32d1caf3e460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Fri, 6 Mar 2026 10:05:10 +0800 Subject: [PATCH] feat: add accessibility testing script with axe-core --- scripts/accessibility-test.js | 92 +++++++++++++++++++++++++++++++++++ scripts/utils/axe-runner.js | 71 +++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 scripts/accessibility-test.js create mode 100644 scripts/utils/axe-runner.js diff --git a/scripts/accessibility-test.js b/scripts/accessibility-test.js new file mode 100644 index 0000000..fd8bf46 --- /dev/null +++ b/scripts/accessibility-test.js @@ -0,0 +1,92 @@ +const { runAxeTest, generateReport } = require('./utils/axe-runner'); +const fs = require('fs'); +const path = require('path'); + +const PAGES = [ + { name: '首页', url: 'http://localhost:3000' }, + { name: '关于我们', url: 'http://localhost:3000/about' }, + { name: '联系我们', url: 'http://localhost:3000/contact' }, + { name: '服务', url: 'http://localhost:3000/services' }, + { name: '产品', url: 'http://localhost:3000/products' }, + { name: '案例', url: 'http://localhost:3000/cases' }, + { name: '新闻', url: 'http://localhost:3000/news' } +]; + +const WCAG_STANDARD = 'WCAG 2.1 AA'; +const MIN_SCORE = 80; + +async function testAllPages() { + console.log('♿ 开始可访问性测试...\n'); + console.log(`标准: ${WCAG_STANDARD}`); + console.log(`最低分数: ${MIN_SCORE}\n`); + + const results = []; + const reportDir = 'test-results/accessibility'; + + if (!fs.existsSync(reportDir)) { + fs.mkdirSync(reportDir, { recursive: true }); + } + + for (const page of PAGES) { + console.log(`📄 测试页面: ${page.name} (${page.url})`); + + try { + const result = await runAxeTest(page.url, page.name); + results.push(result); + + const status = parseFloat(result.score) >= MIN_SCORE ? '✅' : '⚠️'; + console.log(` ${status} 分数: ${result.score}\n`); + + } catch (error) { + console.error(` ❌ 测试失败: ${error.message}\n`); + results.push({ + page: page.name, + url: page.url, + error: error.message + }); + } + } + + return results; +} + +function generateSummary(results) { + console.log('\n📊 可访问性测试摘要\n'); + console.log('─'.repeat(80)); + + const table = results.map(r => { + if (r.error) { + return `| ${r.page.padEnd(20)} | ❌ 失败 |`; + } + const status = parseFloat(r.score) >= MIN_SCORE ? '✅' : '⚠️'; + const violations = r.violations.length; + return `| ${r.page.padEnd(20)} | ${status} ${r.score.padStart(6)} | ${violations.toString().padStart(3)} |`; + }); + + console.log('| 页面'.padEnd(20) + ' | 状态 | 违规 |'); + console.log('─'.repeat(80)); + table.forEach(row => console.log(row)); + console.log('─'.repeat(80)); + + const passed = results.filter(r => !r.error && parseFloat(r.score) >= MIN_SCORE).length; + const total = results.length; + const avgScore = results + .filter(r => !r.error) + .reduce((sum, r) => sum + parseFloat(r.score), 0) / results.length; + + console.log(`\n📈 统计:`); + console.log(` 平均分数: ${avgScore.toFixed(1)}`); + console.log(` 达标页面: ${passed}/${total} (${(passed/total*100).toFixed(1)}%)`); + console.log(` 总违规数: ${results.reduce((sum, r) => sum + (r.violations?.length || 0), 0)}`); +} + +async function main() { + const results = await testAllPages(); + generateSummary(results); + + const reportPath = 'test-results/accessibility-summary.json'; + generateReport(results, reportPath); + console.log(`\n💾 详细报告已保存到: ${reportPath}`); +} + +main().catch(console.error); \ No newline at end of file diff --git a/scripts/utils/axe-runner.js b/scripts/utils/axe-runner.js new file mode 100644 index 0000000..786cd5d --- /dev/null +++ b/scripts/utils/axe-runner.js @@ -0,0 +1,71 @@ +const { chromium } = require('playwright'); +const AxeBuilder = require('@axe-core/playwright').default; +const fs = require('fs'); +const path = require('path'); + +async function runAxeTest(url, pageName) { + const browser = await chromium.launch(); + const page = await browser.newPage(); + const results = []; + + try { + await page.goto(url, { waitUntil: 'networkidle' }); + + const accessibilityScanResults = await new AxeBuilder({ page }) + .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) + .analyze(); + + const violations = accessibilityScanResults.violations.map(v => ({ + id: v.id, + impact: v.impact, + description: v.description, + help: v.help, + helpUrl: v.helpUrl, + nodes: v.nodes.length + })); + + const passes = accessibilityScanResults.passes.length; + const incomplete = accessibilityScanResults.incomplete.length; + + results.push({ + page: pageName, + url, + violations, + passes, + incomplete, + score: calculateScore(violations, passes, incomplete) + }); + + console.log(` ✅ 扫描完成: ${violations.length} 个违规, ${passes} 个通过, ${incomplete} 个未完成`); + + await browser.close(); + return results[0]; + + } catch (error) { + await browser.close(); + throw new Error(`可访问性测试失败: ${error.message}`); + } +} + +function calculateScore(violations, passes, incomplete) { + const total = violations.length + passes.length + incomplete.length; + if (total === 0) return 100; + return ((passes / total) * 100).toFixed(1); +} + +function generateReport(results, outputPath) { + const report = { + timestamp: new Date().toISOString(), + summary: { + totalPages: results.length, + totalViolations: results.reduce((sum, r) => sum + r.violations.length, 0), + averageScore: (results.reduce((sum, r) => sum + parseFloat(r.score), 0) / results.length).toFixed(1) + }, + pages: results + }; + + fs.writeFileSync(outputPath, JSON.stringify(report, null, 2)); + return outputPath; +} + +module.exports = { runAxeTest, generateReport }; \ No newline at end of file