← 返回 Skills 市场
alshowse-tech

Accessibility Tester

作者 alshowse-tech · GitHub ↗ · v1.0.0 · MIT-0
cross-platform ✓ 安全检测通过
95
总下载
0
收藏
1
当前安装
1
版本数
在 OpenClaw 中安装
/install accessibility-tester
功能描述
Accessibility Tester for AI Native Full-Stack Software Factory Layer 11 - specializes in WCAG 2.1 compliance testing, screen reader compatibility, keyboard n...
使用说明 (SKILL.md)

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 activities
  • accessibility-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)

  1. Perceivable - Information must be presentable to users' senses
  2. Operable - UI must be operable by all users
  3. Understandable - Information and operation must be clear
  4. 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\x3CTestResult>;
}

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\x3Cstring, WCAGRule>;
  
  constructor() {
    this.rules = this.initializeRules();
  }
  
  async testAll(context: TestContext): Promise\x3CWCAGTestResult[]> {
    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\x3CWCAGTestResult[]> {
    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\x3Cstring, 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\x3CWCAGTestResult> {
    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: \x3Cimg 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\x3CWCAGTestResult> {
    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 \x3C 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 \x3C= 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\x3CKeyboardNavigationTest> {
    // 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\x3CTabOrderTest> {
    const tabStops: TabStop[] = [];
    let index = 0;
    
    // Tab through all elements
    while (index \x3C 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\x3CFocusTest> {
    // 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\x3CInteractionTest> {
    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\x3Cboolean> {
    // 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\x3CScreenReaderTest> {
    // 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\x3CARIATest> {
    // 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\x3CReadingOrderTest> {
    // 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 \x3C 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\x3CAnnouncementTest> {
    // 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\x3CFormAccessibilityTest> {
    // 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\x3CLabelTest> {
    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 \x3Clabel>)'
        });
      }
    }
    
    return {
      allHaveLabels: missingLabels.length === 0,
      missingLabels,
      implicitLabels
    };
  }
  
  private async testErrorHandling(page: Page): Promise\x3CErrorTest> {
    // 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\x3CAccessibilityReport> {
    // 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' ? '\x3C 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\x3CAccessibilityReport>;
  
  // Add a11y tests to CI/CD
  addAccessibilityToCI(config: CIConfig): Promise\x3Cvoid>;
  
  // Track accessibility trends
  trackAccessibilityTrends(reports: AccessibilityReport[]): Promise\x3CTrendAnalysis>;
}

With builder-agent (L10)

interface BuilderAgentIntegration {
  // Fix accessibility issues
  fixAccessibilityIssues(issues: WCAGTestResult[]): Promise\x3CFixReport>;
  
  // Generate accessible components
  generateAccessibleComponent(spec: ComponentSpec): Promise\x3CAccessibleComponent>;
}

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 \x3C 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.

安全使用建议
This skill appears coherent and contains only local TypeScript implementations of accessibility checks — no network calls, secrets, or installers are present. However, the implementation is largely simulated/stubbed (functions operate on provided context objects and tests call localhost), so it will not perform automated browser-driven audits out of the box. If you plan to use it to audit real sites, review and/or extend the code to add safe browser automation (e.g., Puppeteer) in a controlled environment, and run it in a sandbox. Also confirm what the SKILL.md 'scope: system' means in your agent platform before granting any elevated runtime permissions.
功能分析
Type: OpenClaw Skill Name: accessibility-tester Version: 1.0.0 The accessibility-tester skill bundle is a legitimate tool designed for WCAG 2.1/2.2 compliance testing. The code in index.ts and the instructions in SKILL.md focus on accessibility metrics such as color contrast ratios, keyboard navigation, and ARIA validation. There are no signs of data exfiltration, malicious execution, or prompt injection; the logic is entirely consistent with its stated purpose as an automated accessibility specialist.
能力评估
Purpose & Capability
Name/description (WCAG/a11y testing) align with the included TypeScript code: ColorContrastCalculator, WCAGRuleEngine, KeyboardNavigationTester, and AccessibilityTester implement accessibility checks. There are no unrelated requirements (no cloud credentials, no unrelated binaries) requested by the skill.
Instruction Scope
SKILL.md and code are self-contained and describe tests and rules; they do not instruct the agent to read arbitrary system files, environment variables, or contact external endpoints. However the SKILL.md metadata includes 'scope: system' (likely indicating agent-level role) — this is a metadata note rather than executable behavior, but you may want to confirm intended runtime privileges. The code itself uses simulated contexts (no real browser automation or DOM scraping) so it won't perform active site crawling as-is.
Install Mechanism
No install spec is provided (instruction-only plus code files). Nothing is downloaded or installed by the skill; all code is included in the bundle. This is low risk from an installation perspective.
Credentials
The skill declares no required environment variables, no credentials, and no config paths. The implementation does not reference process.env or other secrets. Credential/request burden is proportional to the stated purpose.
Persistence & Privilege
Flags show always:false and model invocation not disabled (normal). The skill does not request persistent system presence or modify other skills/configs. It does not write to disk or create background services in the provided source.
如何使用
  1. 确保已安装 OpenClaw(本地或 Docker 部署)
  2. 在对话框中输入安装命令:/install accessibility-tester
  3. 安装完成后,直接呼叫该 Skill 的名称或使用 /accessibility-tester 触发
  4. 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
版本历史
v1.0.0
Initial release: Automated accessibility testing for Layer 11. - Provides automated WCAG 2.1 and 2.2 compliance testing (Levels A, AA, AAA) - Covers screen reader compatibility, keyboard navigation, color contrast, and ARIA validation - Simulates assistive technologies (screen reader, keyboard-only navigation, high contrast, magnification) - Generates structured accessibility compliance reports with remediation guidance - Integrates with broader testing orchestration in the ASF system
元数据
Slug accessibility-tester
版本 1.0.0
许可证 MIT-0
累计安装 1
当前安装数 1
历史版本数 1
常见问题

Accessibility Tester 是什么?

Accessibility Tester for AI Native Full-Stack Software Factory Layer 11 - specializes in WCAG 2.1 compliance testing, screen reader compatibility, keyboard n... 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 95 次。

如何安装 Accessibility Tester?

在 OpenClaw 或 Claude Code 对话框中运行命令「/install accessibility-tester」即可一键安装,无需额外配置。

Accessibility Tester 是免费的吗?

是的,Accessibility Tester 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。

Accessibility Tester 支持哪些平台?

Accessibility Tester 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。

谁开发了 Accessibility Tester?

由 alshowse-tech(@alshowse-tech)开发并维护,当前版本 v1.0.0。

💬 留言讨论