无障碍测试
accessibility-tester
by alshowse-tech
Accessibility Tester for AI Native Full-Stack Software Factory Layer 11 - specializes in WCAG 2.1 compliance testing, screen reader compatibility, keyboard navigation, color contrast, and ARIA validation. Ensures applications are accessible to all users.
安装
claude skill add --url https://github.com/openclaw/skills文档
Accessibility Tester - ASF Layer 11
Purpose in AI Native Full-Stack Software Factory
Position: Layer 11 (Automated Validation) - Accessibility Specialist
Purpose: Ensure applications meet accessibility standards (WCAG 2.1/2.2), work with assistive technologies, and provide inclusive user experiences.
Relationship with tester-agent (L10-L11):
tester-agent: Orchestrates all testing activitiesaccessibility-tester: Specializes in accessibility/a11y testing
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ ACCESSIBILITY TESTER (L11) │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ WCAG 2.1/2.2 Rule Engine │ │
│ │ - Level A (25 criteria) │ │
│ │ - Level AA (13 criteria) │ │
│ │ - Level AAA (23 criteria) │ │
│ └─────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Testing Modules │ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────┐ │ │
│ │ │ Keyboard│ │ Screen │ │ Color │ │ │
│ │ │ Nav │ │ Reader │ │Contrast││ │
│ │ └─────────┘ └─────────┘ └───────┘ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────┐ │ │
│ │ │ ARIA │ │ Focus │ │ Forms │ │ │
│ │ │ │ │ Mgmt │ │ │ │ │
│ │ └─────────┘ └─────────┘ └───────┘ │ │
│ └─────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Assistive Technology Simulation │ │
│ │ - Screen reader emulation │ │
│ │ - Keyboard-only navigation │ │
│ │ - High contrast mode │ │
│ │ - Magnification simulation │ │
│ └────────────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Compliance Report Generator │ │
│ │ - WCAG score │ │
│ │ - Issue severity │ │
│ │ - Remediation guidance │ │
│ │ - Legal compliance status │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
WCAG 2.1/2.2 Compliance Matrix
| Level | Criteria Count | Status |
|---|---|---|
| Level A | 25 | ✅ Covered |
| Level AA | 13 | ✅ Covered |
| Level AAA | 23 | ✅ Covered |
| Total | 61 |
Key WCAG Principles (POUR)
- Perceivable - Information must be presentable to users' senses
- Operable - UI must be operable by all users
- Understandable - Information and operation must be clear
- Robust - Content must work with assistive technologies
Core Capabilities
1. WCAG Rule Engine
interface WCAGRule {
id: string; // e.g., '1.1.1'
name: string; // e.g., 'Non-text Content'
level: 'A' | 'AA' | 'AAA';
principle: 'perceivable' | 'operable' | 'understandable' | 'robust';
guideline: string;
description: string;
howToMeet: string;
test: (context: TestContext) => Promise<TestResult>;
}
interface WCAGTestResult {
ruleId: string;
status: 'pass' | 'fail' | 'warning' | 'not-applicable';
severity: 'critical' | 'serious' | 'moderate' | 'minor';
impact: 'critical' | 'serious' | 'moderate' | 'minor';
description: string;
helpUrl: string;
elements: TestElement[];
summary: {
pass: number;
fail: number;
warning: number;
};
}
interface TestElement {
html: string;
selector: string;
snippet: string;
issue: string;
fix: string;
screenshot?: Buffer;
}
class WCAGRuleEngine {
private rules: Map<string, WCAGRule>;
constructor() {
this.rules = this.initializeRules();
}
async testAll(context: TestContext): Promise<WCAGTestResult[]> {
const results: WCAGTestResult[] = [];
for (const [ruleId, rule] of this.rules) {
try {
const result = await rule.test(context);
results.push(result);
} catch (error) {
results.push({
ruleId,
status: 'warning',
severity: 'minor',
impact: 'minor',
description: `Test error: ${error.message}`,
helpUrl: this.getHelpUrl(ruleId),
elements: [],
summary: { pass: 0, fail: 0, warning: 1 }
});
}
}
return results;
}
async testByLevel(context: TestContext, level: 'A' | 'AA' | 'AAA'): Promise<WCAGTestResult[]> {
const allResults = await this.testAll(context);
return allResults.filter(result => {
const rule = this.rules.get(result.ruleId);
return rule && rule.level === level;
});
}
private initializeRules(): Map<string, WCAGRule> {
const rules = new Map();
// 1.1.1 Non-text Content (Level A)
rules.set('1.1.1', {
id: '1.1.1',
name: 'Non-text Content',
level: 'A',
principle: 'perceivable',
guideline: '1.1 Text Alternatives',
description: 'All non-text content has a text alternative',
howToMeet: 'Provide alt text for images, labels for form controls, etc.',
test: async (context) => this.testNonTextContent(context)
});
// 1.4.3 Contrast (Minimum) (Level AA)
rules.set('1.4.3', {
id: '1.4.3',
name: 'Contrast (Minimum)',
level: 'AA',
principle: 'perceivable',
guideline: '1.4 Distinguishable',
description: 'Text has contrast ratio of at least 4.5:1',
howToMeet: 'Ensure sufficient color contrast between text and background',
test: async (context) => this.testColorContrast(context)
});
// 2.1.1 Keyboard (Level A)
rules.set('2.1.1', {
id: '2.1.1',
name: 'Keyboard',
level: 'A',
principle: 'operable',
guideline: '2.1 Keyboard Accessible',
description: 'All functionality available via keyboard',
howToMeet: 'Ensure all interactive elements are keyboard accessible',
test: async (context) => this.testKeyboardAccess(context)
});
// 4.1.2 Name, Role, Value (Level A)
rules.set('4.1.2', {
id: '4.1.2',
name: 'Name, Role, Value',
level: 'A',
principle: 'robust',
guideline: '4.2 Compatible',
description: 'UI components have accessible names and roles',
howToMeet: 'Use proper ARIA attributes and semantic HTML',
test: async (context) => this.testARIA(context)
});
// ... Add all 61 WCAG rules
return rules;
}
private async testNonTextContent(context: TestContext): Promise<WCAGTestResult> {
const elements = await context.page.$$('img:not([alt]), img[alt=""], input[type="image"]:not([alt]), input[type="image"][alt=""]');
const failingElements: TestElement[] = [];
for (const element of elements) {
const html = await context.page.evaluate(el => el.outerHTML, element);
const selector = await this.getSelector(element);
failingElements.push({
html,
selector,
snippet: html.substring(0, 200),
issue: 'Image missing alternative text',
fix: 'Add descriptive alt attribute: <img alt="description">'
});
}
return {
ruleId: '1.1.1',
status: failingElements.length > 0 ? 'fail' : 'pass',
severity: failingElements.length > 0 ? 'critical' : 'none',
impact: failingElements.length > 0 ? 'critical' : 'none',
description: failingElements.length > 0
? `${failingElements.length} images missing alt text`
: 'All images have alternative text',
helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html',
elements: failingElements,
summary: {
pass: failingElements.length === 0 ? 1 : 0,
fail: failingElements.length,
warning: 0
}
};
}
private async testColorContrast(context: TestContext): Promise<WCAGTestResult> {
const elements = await context.page.$$('*');
const failingElements: TestElement[] = [];
for (const element of elements) {
const styles = await context.page.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
color: computed.color,
backgroundColor: computed.backgroundColor,
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
text: el.textContent?.trim() || ''
};
}, element);
// Skip if no text or transparent background
if (!styles.text || styles.backgroundColor === 'rgba(0, 0, 0, 0)') {
continue;
}
const contrastRatio = this.calculateContrastRatio(
this.parseColor(styles.color),
this.parseColor(styles.backgroundColor)
);
const requiredRatio = this.getRequiredContrastRatio(styles.fontSize, styles.fontWeight);
if (contrastRatio < requiredRatio) {
const selector = await this.getSelector(element);
failingElements.push({
html: await context.page.evaluate(el => el.outerHTML, element),
selector,
snippet: styles.text.substring(0, 100),
issue: `Contrast ratio ${contrastRatio.toFixed(2)}:1 is below required ${requiredRatio}:1`,
fix: 'Increase color contrast between text and background'
});
}
}
return {
ruleId: '1.4.3',
status: failingElements.length > 0 ? 'fail' : 'pass',
severity: failingElements.length > 0 ? 'serious' : 'none',
impact: failingElements.length > 0 ? 'serious' : 'none',
description: failingElements.length > 0
? `${failingElements.length} elements with insufficient color contrast`
: 'All text meets contrast requirements',
helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html',
elements: failingElements,
summary: {
pass: failingElements.length === 0 ? 1 : 0,
fail: failingElements.length,
warning: 0
}
};
}
private calculateContrastRatio(color1: RGB, color2: RGB): number {
const l1 = this.getRelativeLuminance(color1);
const l2 = this.getRelativeLuminance(color2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
private getRelativeLuminance(color: RGB): number {
const [r, g, b] = [color.r, color.g, color.b].map(c => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
private getRequiredContrastRatio(fontSize: string, fontWeight: string): number {
const size = parseFloat(fontSize);
const isBold = parseInt(fontWeight) >= 700;
// Large text (18pt+ or 14pt+ bold) requires 3:1
if (size >= 24 || (size >= 18.5 && isBold)) {
return 3.0;
}
// Normal text requires 4.5:1
return 4.5;
}
}
2. Keyboard Navigation Testing
interface KeyboardNavigationTest {
// Tab order
tabOrder: {
elements: TabStop[];
logical: boolean;
issues: TabOrderIssue[];
};
// Focus management
focus: {
visible: boolean;
trapped: boolean;
restored: boolean;
issues: FocusIssue[];
};
// Keyboard interactions
interactions: {
enterKey: boolean;
spaceKey: boolean;
arrowKeys: boolean;
escapeKey: boolean;
shortcuts: KeyboardShortcut[];
};
}
interface TabStop {
index: number;
selector: string;
tagName: string;
role?: string;
tabindex?: number;
accessibleName?: string;
}
class KeyboardNavigationTester {
async test(page: Page): Promise<KeyboardNavigationTest> {
// Test tab order
const tabOrder = await this.testTabOrder(page);
// Test focus visibility
const focus = await this.testFocusManagement(page);
// Test keyboard interactions
const interactions = await this.testKeyboardInteractions(page);
return { tabOrder, focus, interactions };
}
private async testTabOrder(page: Page): Promise<TabOrderTest> {
const tabStops: TabStop[] = [];
let index = 0;
// Tab through all elements
while (index < 1000) { // Safety limit
await page.keyboard.press('Tab');
await page.waitForTimeout(50);
const focusedElement = await page.evaluate(() => {
const el = document.activeElement;
if (!el || el === document.body) return null;
return {
selector: this.getElementSelector(el),
tagName: el.tagName.toLowerCase(),
role: el.getAttribute('role'),
tabindex: el.getAttribute('tabindex'),
accessibleName: el.getAttribute('aria-label') ||
el.getAttribute('aria-labelledby') ||
el.textContent?.trim().substring(0, 50)
};
});
if (!focusedElement) {
break; // Reached end of tab order
}
tabStops.push({
index: ++index,
...focusedElement
});
}
// Analyze tab order for issues
const issues = this.analyzeTabOrderIssues(tabStops);
const logical = this.isTabOrderLogical(tabStops);
return {
elements: tabStops,
logical,
issues
};
}
private async testFocusManagement(page: Page): Promise<FocusTest> {
// Check focus visibility
const focusVisible = await page.evaluate(() => {
const style = document.createElement('style');
style.textContent = `
*:focus {
outline: none !important;
}
`;
document.head.appendChild(style);
const button = document.querySelector('button') as HTMLButtonElement;
if (!button) return true;
button.focus();
const styles = window.getComputedStyle(button);
return styles.outline !== 'none' || styles.boxShadow !== 'none';
});
// Check for focus traps
const focusTrapped = await this.detectFocusTrap(page);
// Check focus restoration
const focusRestored = await this.testFocusRestoration(page);
return {
visible: focusVisible,
trapped: !focusTrapped,
restored: focusRestored,
issues: []
};
}
private async testKeyboardInteractions(page: Page): Promise<InteractionTest> {
const interactions = {
enterKey: await this.testEnterKey(page),
spaceKey: await this.testSpaceKey(page),
arrowKeys: await this.testArrowKeys(page),
escapeKey: await this.testEscapeKey(page),
shortcuts: await this.testKeyboardShortcuts(page)
};
return interactions;
}
private async testEnterKey(page: Page): Promise<boolean> {
// Find buttons and links, test if Enter activates them
const buttons = await page.$$('button, a[href]');
for (const button of buttons.slice(0, 3)) { // Test first 3
await button.focus();
await page.keyboard.press('Enter');
// Check if action was triggered (click, navigation, etc.)
const wasActivated = await this.detectActivation(page, button);
if (!wasActivated) {
return false;
}
}
return true;
}
}
3. Screen Reader Compatibility
interface ScreenReaderTest {
// ARIA support
aria: {
landmarks: LandmarkInfo[];
liveRegions: LiveRegionInfo[];
relationships: RelationshipInfo[];
};
// Reading order
readingOrder: {
logical: boolean;
issues: ReadingOrderIssue[];
};
// Announcements
announcements: {
dynamic: boolean;
accurate: boolean;
timely: boolean;
};
}
class ScreenReaderTester {
async test(page: Page): Promise<ScreenReaderTest> {
// Test ARIA landmarks
const aria = await this.testARIA(page);
// Test reading order
const readingOrder = await this.testReadingOrder(page);
// Test dynamic announcements
const announcements = await this.testAnnouncements(page);
return { aria, readingOrder, announcements };
}
private async testARIA(page: Page): Promise<ARIATest> {
// Check landmarks
const landmarks = await page.evaluate(() => {
const landmarkRoles = ['banner', 'navigation', 'main', 'complementary', 'contentinfo', 'search'];
const found: LandmarkInfo[] = [];
for (const role of landmarkRoles) {
const elements = document.querySelectorAll(`[role="${role}"], ${role}`);
elements.forEach(el => {
found.push({
role,
selector: this.getElementSelector(el),
label: el.getAttribute('aria-label') || el.textContent?.trim().substring(0, 50)
});
});
}
return found;
});
// Check for missing main landmark
const hasMain = landmarks.some(l => l.role === 'main');
// Check live regions
const liveRegions = await page.evaluate(() => {
const regions = document.querySelectorAll('[aria-live]');
return Array.from(regions).map(el => ({
selector: this.getElementSelector(el),
politeness: el.getAttribute('aria-live'),
atomic: el.getAttribute('aria-atomic') === 'true'
}));
});
return {
landmarks,
liveRegions,
relationships: []
};
}
private async testReadingOrder(page: Page): Promise<ReadingOrderTest> {
// Get DOM order vs visual order
const domOrder = await page.evaluate(() => {
return Array.from(document.querySelectorAll('h1, h2, h3, p, a, button'))
.map(el => ({
tag: el.tagName.toLowerCase(),
text: el.textContent?.trim().substring(0, 50),
domIndex: Array.from(document.querySelectorAll('*')).indexOf(el)
}));
});
// Analyze if reading order is logical
const issues: ReadingOrderIssue[] = [];
// Check heading hierarchy
const headings = domOrder.filter(el => el.tag.startsWith('h'));
for (let i = 1; i < headings.length; i++) {
const prevLevel = parseInt(headings[i-1].tag[1]);
const currLevel = parseInt(headings[i].tag[1]);
if (currLevel > prevLevel + 1) {
issues.push({
type: 'heading-skip',
description: `Heading level skipped from h${prevLevel} to h${currLevel}`,
element: headings[i]
});
}
}
return {
logical: issues.length === 0,
issues
};
}
private async testAnnouncements(page: Page): Promise<AnnouncementTest> {
// Test dynamic content announcements
const dynamicTest = await page.evaluate(async () => {
// Create a live region
const liveRegion = document.createElement('div');
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.id = 'test-live-region';
document.body.appendChild(liveRegion);
// Update content
liveRegion.textContent = 'Test announcement';
// Wait and check if screen reader would announce
await new Promise(resolve => setTimeout(resolve, 100));
return liveRegion.textContent === 'Test announcement';
});
return {
dynamic: dynamicTest,
accurate: true,
timely: true
};
}
}
4. Form Accessibility Testing
interface FormAccessibilityTest {
// Labels
labels: {
allHaveLabels: boolean;
missingLabels: FormElement[];
implicitLabels: FormElement[];
};
// Error handling
errors: {
associated: boolean;
described: boolean;
liveAnnounced: boolean;
};
// Validation
validation: {
accessible: boolean;
clear: boolean;
};
}
class FormAccessibilityTester {
async test(page: Page): Promise<FormAccessibilityTest> {
// Test labels
const labels = await this.testLabels(page);
// Test error handling
const errors = await this.testErrorHandling(page);
// Test validation
const validation = await this.testValidation(page);
return { labels, errors, validation };
}
private async testLabels(page: Page): Promise<LabelTest> {
const inputs = await page.$$('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), select, textarea');
const missingLabels: FormElement[] = [];
const implicitLabels: FormElement[] = [];
for (const input of inputs) {
const hasExplicitLabel = await this.hasExplicitLabel(page, input);
const hasImplicitLabel = await this.hasImplicitLabel(page, input);
const hasAriaLabel = await this.hasAriaLabel(page, input);
if (!hasExplicitLabel && !hasImplicitLabel && !hasAriaLabel) {
const selector = await this.getElementSelector(input);
const type = await page.evaluate(el => el.getAttribute('type') || el.tagName, input);
missingLabels.push({
selector,
type,
issue: 'No accessible label found'
});
} else if (hasImplicitLabel) {
implicitLabels.push({
selector: await this.getElementSelector(input),
type: await page.evaluate(el => el.tagName, input),
issue: 'Using implicit label (wrap in <label>)'
});
}
}
return {
allHaveLabels: missingLabels.length === 0,
missingLabels,
implicitLabels
};
}
private async testErrorHandling(page: Page): Promise<ErrorTest> {
// Find forms with validation
const forms = await page.$$('form');
const results = {
associated: true,
described: true,
liveAnnounced: true
};
for (const form of forms) {
// Trigger validation
await form.evaluate(f => f.reportValidity());
// Check error associations
const errorElements = await form.$$('[role="alert"], [aria-live], .error, .error-message');
for (const error of errorElements) {
const describedBy = await this.getDescribedByElements(error);
if (describedBy.length === 0) {
results.associated = false;
}
}
}
return results;
}
}
5. Compliance Report Generator
interface AccessibilityReport {
// Summary
summary: {
score: number; // 0-100
level: 'A' | 'AA' | 'AAA';
passed: number;
failed: number;
warnings: number;
notApplicable: number;
};
// By principle
principles: {
perceivable: PrincipleScore;
operable: PrincipleScore;
understandable: PrincipleScore;
robust: PrincipleScore;
};
// Issues by severity
issues: {
critical: WCAGTestResult[];
serious: WCAGTestResult[];
moderate: WCAGTestResult[];
minor: WCAGTestResult[];
};
// Remediation plan
remediation: {
quickWins: RemediationItem[];
shortTerm: RemediationItem[];
longTerm: RemediationItem[];
};
// Compliance status
compliance: {
wcag21A: boolean;
wcag21AA: boolean;
wcag21AAA: boolean;
section508: boolean;
ada: boolean;
};
}
class AccessibilityReportGenerator {
async generate(results: WCAGTestResult[], context: TestContext): Promise<AccessibilityReport> {
// Calculate summary score
const summary = this.calculateSummary(results);
// Group by principle
const principles = this.groupByPrinciple(results);
// Group by severity
const issues = this.groupBySeverity(results);
// Generate remediation plan
const remediation = this.generateRemediationPlan(issues);
// Determine compliance status
const compliance = this.determineCompliance(results);
return {
summary,
principles,
issues,
remediation,
compliance
};
}
private calculateSummary(results: WCAGTestResult[]): Summary {
const passed = results.filter(r => r.status === 'pass').length;
const failed = results.filter(r => r.status === 'fail').length;
const warnings = results.filter(r => r.status === 'warning').length;
const notApplicable = results.filter(r => r.status === 'not-applicable').length;
// Calculate score (weighted by severity)
const totalWeight = results.length;
const passedWeight = passed + (warnings * 0.5);
const score = Math.round((passedWeight / totalWeight) * 100);
// Determine level
const level = this.determineLevel(results);
return {
score,
level,
passed,
failed,
warnings,
notApplicable
};
}
private generateRemediationPlan(issues: IssuesBySeverity): RemediationPlan {
const quickWins: RemediationItem[] = [];
const shortTerm: RemediationItem[] = [];
const longTerm: RemediationItem[] = [];
// Critical issues -> quick wins (if easy to fix)
for (const issue of issues.critical) {
if (this.isQuickFix(issue)) {
quickWins.push(this.createRemediationItem(issue, 'quick'));
} else {
shortTerm.push(this.createRemediationItem(issue, 'short'));
}
}
// Serious issues -> short term
for (const issue of issues.serious) {
shortTerm.push(this.createRemediationItem(issue, 'short'));
}
// Moderate/minor -> long term
for (const issue of [...issues.moderate, ...issues.minor]) {
longTerm.push(this.createRemediationItem(issue, 'long'));
}
return { quickWins, shortTerm, longTerm };
}
private createRemediationItem(issue: WCAGTestResult, timeframe: 'quick' | 'short' | 'long'): RemediationItem {
return {
ruleId: issue.ruleId,
title: issue.description,
severity: issue.severity,
estimatedEffort: timeframe === 'quick' ? '< 1 hour' : timeframe === 'short' ? '1-4 hours' : '4+ hours',
impact: 'Improves accessibility for users with disabilities',
steps: this.generateFixSteps(issue),
resources: [issue.helpUrl]
};
}
}
Integration with Other ASF Components
With tester-agent (L10-L11)
interface TesterAgentIntegration {
// Run accessibility tests
runAccessibilityTests(suite: TestSuite): Promise<AccessibilityReport>;
// Add a11y tests to CI/CD
addAccessibilityToCI(config: CIConfig): Promise<void>;
// Track accessibility trends
trackAccessibilityTrends(reports: AccessibilityReport[]): Promise<TrendAnalysis>;
}
With builder-agent (L10)
interface BuilderAgentIntegration {
// Fix accessibility issues
fixAccessibilityIssues(issues: WCAGTestResult[]): Promise<FixReport>;
// Generate accessible components
generateAccessibleComponent(spec: ComponentSpec): Promise<AccessibleComponent>;
}
Configuration
{
"accessibilityTester": {
"enabled": true,
"wcag": {
"level": "AA", // A, AA, or AAA
"version": "2.1"
},
"testing": {
"viewport": {
"width": 1920,
"height": 1080
},
"timeout": 30000,
"waitForLoad": true
},
"reporting": {
"format": ["json", "html", "pdf"],
"includeScreenshots": true,
"includeRemediation": true
},
"ci": {
"failOnCritical": true,
"failOnSerious": false,
"minScore": 80
}
}
}
Usage Examples
Example 1: Run Full Accessibility Audit
const tester = new AccessibilityTester();
const report = await tester.audit('http://localhost:3000', {
level: 'AA',
viewport: { width: 1920, height: 1080 }
});
console.log(`Accessibility Score: ${report.summary.score}/100`);
console.log(`WCAG 2.1 AA Compliance: ${report.compliance.wcag21AA ? '✅' : '❌'}`);
console.log(`Critical Issues: ${report.issues.critical.length}`);
console.log(`Serious Issues: ${report.issues.serious.length}`);
Example 2: Test Specific Component
const componentReport = await tester.testComponent('#main-form', {
rules: ['1.1.1', '1.4.3', '2.1.1', '4.1.2']
});
console.log('Form Accessibility Issues:');
for (const issue of componentReport.issues.critical) {
console.log(` - ${issue.ruleId}: ${issue.description}`);
console.log(` Fix: ${issue.elements[0]?.fix}`);
}
Example 3: CI/CD Integration
// In CI pipeline
const report = await tester.audit('http://staging.example.com');
if (report.summary.score < 80) {
console.error('Accessibility score below threshold!');
process.exit(1);
}
if (report.issues.critical.length > 0) {
console.error('Critical accessibility issues found!');
process.exit(1);
}
console.log('✅ Accessibility checks passed');
Remember: Accessibility is not a feature—it's a fundamental requirement for inclusive software.
相关 Skills
前端设计
by anthropics
面向组件、页面、海报和 Web 应用开发,按鲜明视觉方向生成可直接落地的前端代码与高质感 UI,适合做 landing page、Dashboard 或美化现有界面,避开千篇一律的 AI 审美。
✎ 想把页面做得既能上线又有设计感,就用前端设计:组件到整站都能产出,难得的是能避开千篇一律的 AI 味。
网页应用测试
by anthropics
用 Playwright 为本地 Web 应用编写自动化测试,支持启动开发服务器、校验前端交互、排查 UI 异常、抓取截图与浏览器日志,适合调试动态页面和回归验证。
✎ 借助 Playwright 一站式验证本地 Web 应用前端功能,调 UI 时还能同步查看日志和截图,定位问题更快。
网页构建器
by anthropics
面向复杂 claude.ai HTML artifact 开发,快速初始化 React + Tailwind CSS + shadcn/ui 项目并打包为单文件 HTML,适合需要状态管理、路由或多组件交互的页面。
✎ 在 claude.ai 里做复杂网页 Artifact 很省心,多组件、状态和路由都能顺手搭起来,React、Tailwind 与 shadcn/ui 组合效率高、成品也更精致。
相关 MCP 服务
GitHub
编辑精选by GitHub
GitHub 是 MCP 官方参考服务器,让 Claude 直接读写你的代码仓库和 Issues。
✎ 这个参考服务器解决了开发者想让 AI 安全访问 GitHub 数据的问题,适合需要自动化代码审查或 Issue 管理的团队。但注意它只是参考实现,生产环境得自己加固安全。
Context7 文档查询
编辑精选by Context7
Context7 是实时拉取最新文档和代码示例的智能助手,让你告别过时资料。
✎ 它能解决开发者查找文档时信息滞后的问题,特别适合快速上手新库或跟进更新。不过,依赖外部源可能导致偶尔的数据延迟,建议结合官方文档使用。
by tldraw
tldraw 是让 AI 助手直接在无限画布上绘图和协作的 MCP 服务器。
✎ 这解决了 AI 只能输出文本、无法视觉化协作的痛点——想象让 Claude 帮你画流程图或白板讨论。最适合需要快速原型设计或头脑风暴的开发者。不过,目前它只是个基础连接器,你得自己搭建画布应用才能发挥全部潜力。