Token导航 LogoToken导航TokenDH.com
A11y MCP Audit Demo logo
浏览器工具stdio官方级别未说明来源级核验

A11y MCP Audit Demo

MCP Server

playwright

基于Playwright和axe-core的无障碍测试工具,提供自动化扫描、WCAG合规性报告和回归测试功能,适用于网站无障碍性检查和开发流程集成。

工具数

0

提示词数

0

GitHub Stars

1

资源数

0
浏览器自动化TypeScript自动化测试

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

作者 / 组织

StevenG0211

提供方

StevenG0211

最后核验

2026/5/17 20:22

运行时

Node.js

快速接入

先看主来源和安装命令,再打开仓库或文档;下面只保留这个条目的关键接入事实。

命令预览

npx playwright install chromium

详细介绍

剧作家无障碍测试项目设置指南

本指南提供了使用Playwright和axe-core建立全面的可访问性测试项目的完整说明。该项目包括自动扫描、报告生成和WCAG合规性测试用例。

项目概述

该项目能够:

  • 使用axe-core对网站进行自动可访问性扫描
  • 全面的WCAG合规报告(2.0、2.1、2.2)
  • 详细的降价报告,包括问题、严重程度、建议和行动项
  • 防止退化的剧作家测试用例
  • 与CI/CD管道集成

先决条件

  • 已安装Node.js 18+
  • npm或yarn包管理器
  • TypeScript和剧作家的基本知识

步骤1:初始化项目

mkdir a11y-demo
cd a11y-demo
npm init -y

步骤2:安装依赖项

npm install --save-dev @playwright/test @axe-core/playwright typescript @types/node tsx
npx playwright install chromium

步骤3:创建项目结构

创建以下目录结构:

a11y-demo/
├── audits/              # Generated audit reports
├── scripts/             # Utility scripts
├── tests/               # Playwright test files
├── package.json
├── playwright.config.ts
└── tsconfig.json

步骤4:配置TypeScript

创建 tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "./dist"
  },
  "include": ["tests/**/*", "playwright.config.ts", "scripts/**/*"]
}

步骤5:配置剧作家

创建 playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

步骤6:更新package.json脚本

更新 scripts 章节 package.json:

{
  "scripts": {
    "test": "playwright test",
    "test:ui": "playwright test --ui",
    "test:debug": "playwright test --debug",
    "test:headed": "playwright test --headed",
    "scan:a11y": "tsx scripts/run-accessibility-scan.ts",
    "generate:report": "tsx scripts/generate-audit-report.ts"
  }
}

步骤7:创建辅助功能扫描脚本

创建 scripts/run-accessibility-scan.ts:

import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/playwright';
import * as fs from 'fs';
import * as path from 'path';

async function runAccessibilityScan() {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
  
  try {
    console.log('Navigating to https://sv.siman.com/...');
    await page.goto('https://sv.siman.com/', { waitUntil: 'networkidle' });
    
    // Wait for dynamic content to load
    await page.waitForTimeout(5000);
    
    console.log('Running accessibility scan...');
    const results = await new AxeBuilder({ page })
      .withTags([
        'wcag2a', 'wcag2aa', 'wcag2aaa',
        'wcag21a', 'wcag21aa', 'wcag21aaa',
        'wcag22a', 'wcag22aa', 'wcag22aaa',
        'section508',
        'cat.aria', 'cat.color', 'cat.forms', 'cat.keyboard',
        'cat.language', 'cat.name-role-value', 'cat.parsing',
        'cat.semantics', 'cat.sensory-and-visual-cues',
        'cat.structure', 'cat.tables', 'cat.text-alternatives',
        'cat.time-and-media'
      ])
      .analyze();
    
    // Ensure audits directory exists
    const auditsDir = path.join(__dirname, '..', 'audits');
    if (!fs.existsSync(auditsDir)) {
      fs.mkdirSync(auditsDir, { recursive: true });
    }
    
    // Save raw results as JSON
    const timestamp = new Date().toISOString().split('T')[0];
    const jsonPath = path.join(auditsDir, `sv-siman-audit-${timestamp}.json`);
    fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2));
    console.log(`Raw results saved to ${jsonPath}`);
    
    return results;
  } finally {
    await browser.close();
  }
}

runAccessibilityScan()
  .then((results) => {
    console.log(`\nScan complete! Found ${results.violations.length} violations.`);
    process.exit(0);
  })
  .catch((error) => {
    console.error('Error running scan:', error);
    process.exit(1);
  });

注: 更新脚本中的URL以定位您的网站。

步骤8:创建报告生成脚本

创建 scripts/generate-audit-report.ts:

import * as fs from 'fs';
import * as path from 'path';

interface AxeResult {
  violations: Violation[];
  passes: any[];
  inapplicable: any[];
  incomplete: any[];
  timestamp: string;
  url: string;
  testEngine?: { version: string };
}

interface Violation {
  id: string;
  impact: 'critical' | 'serious' | 'moderate' | 'minor';
  tags: string[];
  description: string;
  help: string;
  helpUrl: string;
  nodes: Node[];
}

interface Node {
  html: string;
  target: string[];
  any?: any[];
  all?: any[];
  none?: any[];
}

function getWCAGLevel(tags: string[]): { version: string; level: string }[] {
  const levels: { version: string; level: string }[] = [];
  
  tags.forEach(tag => {
    if (tag.startsWith('wcag2')) {
      const match = tag.match(/wcag(\d+)([a-z]+)/);
      if (match) {
        levels.push({ version: `WCAG ${match[1]}.0`, level: match[2].toUpperCase() });
      }
    } else if (tag.startsWith('wcag')) {
      const match = tag.match(/wcag(\d+)([a-z]+)/);
      if (match) {
        levels.push({ version: `WCAG ${match[1]}.0`, level: match[2].toUpperCase() });
      }
    }
  });
  
  return levels.length > 0 ? levels : [{ version: 'WCAG 2.1', level: 'A' }];
}

function getSeverityPriority(impact: string): number {
  const priorities: Record = {
    'critical': 1,
    'serious': 2,
    'moderate': 3,
    'minor': 4
  };
  return priorities[impact] || 5;
}

function calculateCompliance(violations: Violation[]): Record {
  const compliance: Record = {};
  
  const wcagLevels = ['wcag2a', 'wcag2aa', 'wcag2aaa', 'wcag21a', 'wcag21aa', 'wcag21aaa', 'wcag22a', 'wcag22aa', 'wcag22aaa'];
  
  wcagLevels.forEach(level => {
    const violationsForLevel = violations.filter(v => v.tags.includes(level));
    const total = 100;
    const passed = Math.max(0, total - violationsForLevel.length);
    compliance[level] = {
      total,
      passed,
      percentage: Math.round((passed / total) * 100)
    };
  });
  
  return compliance;
}

function generateRecommendations(violation: Violation): string {
  const recommendations: Record = {
    'aria-command-name': 'Add an accessible name to ARIA commands using aria-label, aria-labelledby, or visible text content.',
    'color-contrast': 'Ensure text has sufficient color contrast ratio (4.5:1 for normal text, 3:1 for large text).',
    'image-alt': 'Add descriptive alt text to all images that convey meaning. Use empty alt="" for decorative images.',
    'link-name': 'Ensure all links have descriptive text that makes sense out of context.',
    'button-name': 'Ensure all buttons have accessible names via text content, aria-label, or aria-labelledby.',
    'heading-order': 'Use heading elements (h1-h6) in sequential order without skipping levels.',
    'landmark-one-main': 'Ensure the page has one main landmark or use aria-label to distinguish multiple main regions.',
    'page-has-heading-one': 'Ensure the page has a level 1 heading that describes the main content.',
    'region': 'Ensure all page content is contained within landmarks (main, nav, aside, etc.).',
    'html-has-lang': 'Specify a valid language attribute on the html element.'
  };
  
  return recommendations[violation.id] || `Review the ${violation.id} rule and follow the guidance at ${violation.helpUrl}`;
}

function generateActionItems(violations: Violation[]): string[] {
  const actionItems: string[] = [];
  const groupedViolations = new Map();
  
  violations.forEach(v => {
    if (!groupedViolations.has(v.id)) {
      groupedViolations.set(v.id, []);
    }
    groupedViolations.get(v.id)!.push(v);
  });
  
  groupedViolations.forEach((violations, id) => {
    const violation = violations[0];
    const count = violations.length;
    const effort = violation.impact === 'critical' ? 'High' : 
                   violation.impact === 'serious' ? 'Medium' : 'Low';
    
    actionItems.push(`[${violation.impact.toUpperCase()}] Fix ${violation.id}: ${violation.help} (${count} instance${count > 1 ? 's' : ''}) - Effort: ${effort}`);
  });
  
  return actionItems.sort((a, b) => {
    const priorityA = getSeverityPriority(violations.find(v => a.includes(v.id))?.impact || 'minor');
    const priorityB = getSeverityPriority(violations.find(v => b.includes(v.id))?.impact || 'minor');
    return priorityA - priorityB;
  });
}

function generateTestCases(violations: Violation[]): string {
  const testCases: string[] = [];
  const uniqueViolations = new Map();
  
  violations.forEach(v => {
    if (!uniqueViolations.has(v.id)) {
      uniqueViolations.set(v.id, v);
    }
  });
  
  uniqueViolations.forEach((violation, id) => {
    const wcagLevels = getWCAGLevel(violation.tags);
    const levelStr = wcagLevels.map(l => `${l.version} ${l.level}`).join(', ');
    
    testCases.push(`test('should not have ${violation.id} violations (${levelStr})', async ({ page }) => {`);
    testCases.push(`  await page.goto('https://sv.siman.com/');`);
    testCases.push(`  await page.waitForLoadState('networkidle');`);
    testCases.push(`  `);
    testCases.push(`  const accessibilityScanResults = await new AxeBuilder({ page })`);
    testCases.push(`    .withTags(['${violation.tags.filter(t => t.startsWith('wcag')).join("', '")}'])`);
    testCases.push(`    .analyze();`);
    testCases.push(`  `);
    testCases.push(`  const violations = accessibilityScanResults.violations.filter(`);
    testCases.push(`    v => v.id === '${violation.id}'`);
    testCases.push(`  );`);
    testCases.push(`  `);
    testCases.push(`  expect(violations).toHaveLength(0);`);
    testCases.push(`});`);
    testCases.push('');
  });
  
  return testCases.join('\n');
}

function generateReport(data: AxeResult): string {
  const violations = data.violations;
  const compliance = calculateCompliance(violations);
  const actionItems = generateActionItems(violations);
  const testCases = generateTestCases(violations);
  
  const timestamp = new Date(data.timestamp).toLocaleString();
  const dateStr = new Date(data.timestamp).toISOString().split('T')[0];
  
  let report = `# Accessibility Audit Report - ${new URL(data.url).hostname}\n\n`;
  report += `**Audit Date:** ${timestamp}  \n`;
  report += `**URL:** ${data.url}  \n`;
  report += `**Tool:** axe-core ${data.testEngine?.version || 'unknown'}\n\n`;
  report += `---\n\n`;
  
  // Executive Summary
  report += `## Executive Summary\n\n`;
  report += `This accessibility audit identified **${violations.length} violation${violations.length !== 1 ? 's' : ''}** across the homepage.\n\n`;
  
  const criticalCount = violations.filter(v => v.impact === 'critical').length;
  const seriousCount = violations.filter(v => v.impact === 'serious').length;
  const moderateCount = violations.filter(v => v.impact === 'moderate').length;
  const minorCount = violations.filter(v => v.impact === 'minor').length;
  
  report += `### Severity Breakdown\n`;
  report += `- **Critical:** ${criticalCount}\n`;
  report += `- **Serious:** ${seriousCount}\n`;
  report += `- **Moderate:** ${moderateCount}\n`;
  report += `- **Minor:** ${minorCount}\n\n`;
  
  // Compliance Levels
  report += `## Compliance Levels\n\n`;
  report += `### WCAG 2.0 Compliance\n`;
  report += `- **Level A:** ${compliance.wcag2a?.percentage || 0}% compliant\n`;
  report += `- **Level AA:** ${compliance.wcag2aa?.percentage || 0}% compliant\n`;
  report += `- **Level AAA:** ${compliance.wcag2aaa?.percentage || 0}% compliant\n\n`;
  
  report += `### WCAG 2.1 Compliance\n`;
  report += `- **Level A:** ${compliance.wcag21a?.percentage || 0}% compliant\n`;
  report += `- **Level AA:** ${compliance.wcag21aa?.percentage || 0}% compliant\n`;
  report += `- **Level AAA:** ${compliance.wcag21aaa?.percentage || 0}% compliant\n\n`;
  
  report += `### WCAG 2.2 Compliance\n`;
  report += `- **Level A:** ${compliance.wcag22a?.percentage || 0}% compliant\n`;
  report += `- **Level AA:** ${compliance.wcag22aa?.percentage || 0}% compliant\n`;
  report += `- **Level AAA:** ${compliance.wcag22aaa?.percentage || 0}% compliant\n\n`;
  
  // Issues Found
  report += `## Issues Found\n\n`;
  
  violations.sort((a, b) => getSeverityPriority(a.impact) - getSeverityPriority(b.impact));
  
  violations.forEach((violation, index) => {
    const wcagLevels = getWCAGLevel(violation.tags);
    const levelStr = wcagLevels.map(l => `${l.version} ${l.level}`).join(', ');
    
    report += `### ${index + 1}. ${violation.id} (${violation.impact.toUpperCase()})\n\n`;
    report += `**Description:** ${violation.description}\n\n`;
    report += `**Help:** ${violation.help}\n\n`;
    report += `**WCAG Level:** ${levelStr}\n\n`;
    report += `**Affected Elements:** ${violation.nodes.length} instance${violation.nodes.length !== 1 ? 's' : ''}\n\n`;
    
    if (violation.nodes.length > 0) {
      report += `**Example Elements:**\n\n`;
      violation.nodes.slice(0, 3).forEach((node, nodeIndex) => {
        report += `${nodeIndex + 1}. Selector: \`${node.target[0] || 'N/A'}\`\n`;
        report += `   HTML: \`${node.html.substring(0, 100)}${node.html.length > 100 ? '...' : ''}\`\n\n`;
      });
      if (violation.nodes.length > 3) {
        report += `*... and ${violation.nodes.length - 3} more instance(s)*\n\n`;
      }
    }
    
    report += `**Recommendation:** ${generateRecommendations(violation)}\n\n`;
    report += `**Help URL:** ${violation.helpUrl}\n\n`;
    report += `---\n\n`;
  });
  
  // Recommendations
  report += `## Recommendations\n\n`;
  report += `### Priority Fixes (by Severity)\n\n`;
  
  const groupedBySeverity = new Map();
  violations.forEach(v => {
    if (!groupedBySeverity.has(v.impact)) {
      groupedBySeverity.set(v.impact, []);
    }
    groupedBySeverity.get(v.impact)!.push(v);
  });
  
  ['critical', 'serious', 'moderate', 'minor'].forEach(severity => {
    const violationsForSeverity = groupedBySeverity.get(severity) || [];
    if (violationsForSeverity.length > 0) {
      report += `#### ${severity.toUpperCase()} Issues\n\n`;
      violationsForSeverity.forEach(v => {
        report += `- **${v.id}**: ${generateRecommendations(v)}\n`;
      });
      report += `\n`;
    }
  });
  
  // Action Items
  report += `## Action Items for Developers\n\n`;
  report += `Use this checklist to track progress on fixing accessibility issues:\n\n`;
  
  actionItems.forEach((item, index) => {
    report += `${index + 1}. [ ] ${item}\n`;
  });
  
  report += `\n---\n\n`;
  
  // Test Cases
  report += `## Initial Test Cases for Playwright\n\n`;
  report += `The following test cases can be added to your Playwright test suite to prevent regression:\n\n`;
  report += `\`\`\`typescript\n`;
  report += `import { test, expect } from '@playwright/test';\n`;
  report += `import AxeBuilder from '@axe-core/playwright';\n\n`;
  report += testCases;
  report += `\`\`\`\n\n`;
  
  report += `---\n\n`;
  report += `*Report generated on ${timestamp}*\n`;
  
  return report;
}

// Main execution
const auditsDir = path.join(__dirname, '..', 'audits');
const files = fs.readdirSync(auditsDir).filter(f => f.endsWith('.json') && f.includes('audit'));

if (files.length === 0) {
  console.error('No audit JSON files found. Please run the scan first: npm run scan:a11y');
  process.exit(1);
}

// Use the most recent file
const latestFile = files.sort().reverse()[0];
const jsonPath = path.join(auditsDir, latestFile);
const data: AxeResult = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));

const report = generateReport(data);
const dateStr = new Date(data.timestamp).toISOString().split('T')[0];
const reportPath = path.join(auditsDir, `audit-${dateStr}.md`);

fs.writeFileSync(reportPath, report);
console.log(`\n✅ Report generated successfully: ${reportPath}`);
console.log(`\nSummary:`);
console.log(`- Total violations: ${data.violations.length}`);
console.log(`- Critical: ${data.violations.filter(v => v.impact === 'critical').length}`);
console.log(`- Serious: ${data.violations.filter(v => v.impact === 'serious').length}`);
console.log(`- Moderate: ${data.violations.filter(v => v.impact === 'moderate').length}`);
console.log(`- Minor: ${data.violations.filter(v => v.impact === 'minor').length}`);

步骤9:创建剧作家测试用例

创建 tests/accessibility.spec.ts:

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Accessibility Tests', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://sv.siman.com/');
    await page.waitForLoadState('networkidle');
    await page.waitForTimeout(3000);
  });

  test('should pass all WCAG 2.0 Level A checks', async ({ page }) => {
    const accessibilityScanResults = await new AxeBuilder({ page })
      .withTags(['wcag2a'])
      .analyze();

    expect(accessibilityScanResults.violations).toHaveLength(0);
  });

  test('should pass all WCAG 2.1 Level A checks', async ({ page }) => {
    const accessibilityScanResults = await new AxeBuilder({ page })
      .withTags(['wcag21a'])
      .analyze();

    expect(accessibilityScanResults.violations).toHaveLength(0);
  });

  test('should pass all WCAG 2.2 Level A checks', async ({ page }) => {
    const accessibilityScanResults = await new AxeBuilder({ page })
      .withTags(['wcag22a'])
      .analyze();

    expect(accessibilityScanResults.violations).toHaveLength(0);
  });

  // Add specific violation tests based on your audit results
  test('should not have image-alt violations', async ({ page }) => {
    const accessibilityScanResults = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag244', 'wcag412'])
      .analyze();

    const violations = accessibilityScanResults.violations.filter(
      v => v.id === 'image-alt'
    );

    if (violations.length > 0) {
      console.log('Image-alt violations found:');
      violations.forEach(v => {
        console.log(`- ${v.help}: ${v.nodes.length} instance(s)`);
      });
    }

    expect(violations).toHaveLength(0);
  });
});

注: 根据您的审计结果更新URL并添加特定的测试用例。

步骤10:创建审核目录README

创建 audits/README.md:

# Accessibility Audit Reports

This directory contains accessibility audit reports generated using axe-core and Playwright.

## Running an Audit

npm run scan:a11y


## 生成报告

npm run generate:report


报告保存为markdown文件,并进行全面分析,包括:

- 执行摘要
- 详细的问题描述
- 合规级别(WCAG 2.0、2.1、2.2)
- 建议
- 开发人员的行动项目
- Playwright测试用例

Step 11: Create .gitignore

Create .gitignore:


节点模块/
剧作家报告/
测试结果/
\*.log
.DS_Store

Usage

Run Accessibility Scan

npm run scan:a11y

这将:

  1. 导航到目标网站
  2. 运行全面的辅助功能扫描
  3. 将原始结果保存为JSON格式 audits/ 目录

生成报告

npm run generate:report

这将:

  1. 读取最新的审计JSON文件
  2. 生成全面的降价报告
  3. 将报告保存在 audits/ 目录

运行测试

npm test

或者使用UI:

npm run test:ui

定制

更改目标URL

在以下位置更新URL:

  • scripts/run-accessibility-scan.ts (第12行)
  • tests/accessibility.spec.ts (在每个之前)

修改WCAG标签

在中编辑标签数组 scripts/run-accessibility-scan.ts 专注于特定的WCAG级别或类别。

添加自定义建议

扩展 generateRecommendations 功能在 scripts/generate-audit-report.ts 为特定的违规类型添加自定义建议。

项目特点

✅ 使用斧头核心进行自动可访问性扫描\ ✅ 全面的WCAG 2.0、2.1和2.2合规性检查\ ✅ 详细的降价报告,提供可操作的见解\ ✅ 防止退化的剧作家测试用例\ ✅ 基于严重性的问题优先级\ ✅ 开发人员友好的操作项\ ✅ CI/CD就绪测试套件

资源

支持

对于问题或疑问:

  1. 检查
  2. 审查 剧作家可访问性测试指南
  3. 详细要求请参考WCAG指南

目录标签

目录标签

浏览器自动化TypeScript自动化测试无障碍测试本地部署WCAG合规Playwrightaxe-core

接入字段

传输方式(transport,传输协议)

stdio

鉴权方式(authType,认证方式)

none

运行时(runtime,运行环境)

Node.js

来源包(packageName,安装包名)

playwright

工具数量(toolCount,工具数)

0

资源数量(resourceCount,资源数)

0

提示词数量(promptCount,提示词数)

0

权限和风险

stdionone部署方式未说明

接入前请确认传输方式、认证方式和部署位置,并根据实际工具能力限制访问范围。

安装前确认

不要直接授予不必要的文件、网络或账号权限;先核对安装命令和配置内容。

来源信息

继续浏览同类 MCP