Skip to main content
Glama
accessibility-testing.ts12.2 kB
/** * Comprehensive accessibility testing utilities for HTML visualizations */ export interface AccessibilityTestResult { passes: boolean; score: number; issues: AccessibilityIssue[]; recommendations: string[]; } export interface AccessibilityIssue { type: 'error' | 'warning' | 'info'; code: string; message: string; element?: string; wcagLevel?: 'A' | 'AA' | 'AAA'; } export class AccessibilityTester { /** * Test HTML content for accessibility compliance */ public testHTML(html: string): AccessibilityTestResult { const issues: AccessibilityIssue[] = []; const recommendations: string[] = []; // Test 1: Check for proper DOCTYPE if (!html.includes('<!DOCTYPE html>')) { issues.push({ type: 'error', code: 'DOCTYPE_MISSING', message: 'HTML5 DOCTYPE declaration is missing', wcagLevel: 'A', }); } // Test 2: Check for lang attribute if (!html.includes('<html lang=')) { issues.push({ type: 'error', code: 'LANG_MISSING', message: 'HTML lang attribute is missing', wcagLevel: 'A', }); } // Test 3: Check for viewport meta tag if (!html.includes('name="viewport"')) { issues.push({ type: 'warning', code: 'VIEWPORT_MISSING', message: 'Viewport meta tag is missing for responsive design', wcagLevel: 'AA', }); } // Test 4: Check for proper heading hierarchy const headingMatches = html.match(/<h[1-6][^>]*>/g) || []; if (headingMatches.length === 0) { issues.push({ type: 'warning', code: 'NO_HEADINGS', message: 'No heading elements found', wcagLevel: 'AA', }); } // Test 5: Check for ARIA labels on interactive elements // Check for button elements const buttonMatches = html.match(/<button[^>]*>/gi) || []; buttonMatches.forEach(match => { if ( !match.includes('aria-label=') && !match.includes('aria-labelledby=') ) { issues.push({ type: 'warning', code: 'MISSING_ARIA_LABEL', message: `Button element missing aria-label: ${match.substring(0, 50)}...`, element: 'button', wcagLevel: 'AA', }); } }); // Check for input elements const inputMatches = html.match(/<input[^>]*>/gi) || []; inputMatches.forEach(match => { if ( !match.includes('aria-label=') && !match.includes('aria-labelledby=') && !match.includes('id=') // inputs can be labeled by associated labels ) { issues.push({ type: 'warning', code: 'MISSING_ARIA_LABEL', message: `Input element missing aria-label: ${match.substring(0, 50)}...`, element: 'input', wcagLevel: 'AA', }); } }); // Check for elements with role="button" const roleButtonMatches = html.match(/role=["']button["'][^>]*>/gi) || []; roleButtonMatches.forEach(match => { if ( !match.includes('aria-label=') && !match.includes('aria-labelledby=') ) { issues.push({ type: 'warning', code: 'MISSING_ARIA_LABEL', message: `Element with role="button" missing aria-label: ${match.substring(0, 50)}...`, element: '[role="button"]', wcagLevel: 'AA', }); } }); // Test 6: Check for keyboard navigation support if (!html.includes('tabindex=')) { issues.push({ type: 'info', code: 'NO_TABINDEX', message: 'No explicit tab order defined', wcagLevel: 'AA', }); } // Test 7: Check for focus indicators in CSS if (!html.includes(':focus') && !html.includes(':focus-visible')) { issues.push({ type: 'error', code: 'NO_FOCUS_STYLES', message: 'No focus styles defined for keyboard navigation', wcagLevel: 'AA', }); } // Test 8: Check for reduced motion support if (!html.includes('prefers-reduced-motion')) { issues.push({ type: 'info', code: 'NO_REDUCED_MOTION', message: 'No reduced motion support for users with vestibular disorders', wcagLevel: 'AAA', }); } // Test 9: Check for high contrast mode support if (!html.includes('prefers-contrast')) { issues.push({ type: 'info', code: 'NO_HIGH_CONTRAST', message: 'No high contrast mode support', wcagLevel: 'AAA', }); } // Test 10: Check for semantic HTML elements const semanticElements = [ 'main', 'header', 'nav', 'section', 'article', 'aside', 'footer', ]; const hasSemanticElements = semanticElements.some( element => html.includes(`<${element}`) || html.includes(`role="${element}"`) ); if (!hasSemanticElements) { issues.push({ type: 'warning', code: 'NO_SEMANTIC_HTML', message: 'No semantic HTML elements found', wcagLevel: 'AA', }); } // Generate recommendations based on issues if (issues.some(issue => issue.code === 'MISSING_ARIA_LABEL')) { recommendations.push( 'Add aria-label attributes to all interactive elements' ); } if (issues.some(issue => issue.code === 'NO_FOCUS_STYLES')) { recommendations.push( 'Add visible focus indicators for keyboard navigation' ); } if (issues.some(issue => issue.code === 'NO_REDUCED_MOTION')) { recommendations.push('Add support for users who prefer reduced motion'); } if (issues.some(issue => issue.code === 'NO_HIGH_CONTRAST')) { recommendations.push( 'Add high contrast mode support for better visibility' ); } // Calculate accessibility score const errorCount = issues.filter(issue => issue.type === 'error').length; const warningCount = issues.filter( issue => issue.type === 'warning' ).length; const score = Math.max(0, 100 - errorCount * 15 - warningCount * 5); const passes = errorCount === 0 && warningCount <= 2; return { passes, score, issues, recommendations, }; } /** * Test color contrast ratios for accessibility */ public testColorContrast( foreground: string, background: string, textSize: 'normal' | 'large' = 'normal' ): AccessibilityTestResult { const issues: AccessibilityIssue[] = []; const recommendations: string[] = []; // Calculate contrast ratio (simplified - would use actual color calculation in real implementation) const contrastRatio = this.calculateContrastRatio(foreground, background); const aaThreshold = textSize === 'large' ? 3.0 : 4.5; const aaaThreshold = textSize === 'large' ? 4.5 : 7.0; if (contrastRatio < aaThreshold) { issues.push({ type: 'error', code: 'CONTRAST_AA_FAIL', message: `Contrast ratio ${contrastRatio.toFixed(2)}:1 fails WCAG AA (requires ${aaThreshold}:1)`, wcagLevel: 'AA', }); recommendations.push('Increase color contrast to meet WCAG AA standards'); } if (contrastRatio < aaaThreshold) { issues.push({ type: 'warning', code: 'CONTRAST_AAA_FAIL', message: `Contrast ratio ${contrastRatio.toFixed(2)}:1 fails WCAG AAA (requires ${aaaThreshold}:1)`, wcagLevel: 'AAA', }); recommendations.push( 'Consider increasing contrast for WCAG AAA compliance' ); } const score = contrastRatio >= aaaThreshold ? 100 : contrastRatio >= aaThreshold ? 80 : Math.max(0, (contrastRatio / aaThreshold) * 60); return { passes: contrastRatio >= aaThreshold, score, issues, recommendations, }; } /** * Test keyboard navigation functionality */ public testKeyboardNavigation(html: string): AccessibilityTestResult { const issues: AccessibilityIssue[] = []; const recommendations: string[] = []; // Check for tabindex attributes const tabindexElements = (html.match(/tabindex="[^"]*"/g) || []).length; // Check for keyboard event handlers const keyboardHandlers = [ /onkeydown/i, /onkeyup/i, /onkeypress/i, /addEventListener\s*\(\s*["']keydown["']/i, /addEventListener\s*\(\s*["']keyup["']/i, /addEventListener\s*\(\s*["']keypress["']/i, ]; const hasKeyboardHandlers = keyboardHandlers.some(handler => handler.test(html) ); if (tabindexElements === 0) { issues.push({ type: 'warning', code: 'NO_TAB_ORDER', message: 'No explicit tab order defined', wcagLevel: 'AA', }); } if (!hasKeyboardHandlers) { issues.push({ type: 'error', code: 'NO_KEYBOARD_HANDLERS', message: 'No keyboard event handlers found', wcagLevel: 'A', }); recommendations.push( 'Add keyboard event handlers for interactive elements' ); } // Check for arrow key navigation if (!html.includes('ArrowUp') && !html.includes('ArrowDown')) { issues.push({ type: 'info', code: 'NO_ARROW_NAVIGATION', message: 'No arrow key navigation implemented', wcagLevel: 'AAA', }); recommendations.push( 'Consider adding arrow key navigation for grid layouts' ); } const errorCount = issues.filter(issue => issue.type === 'error').length; const score = errorCount === 0 ? 100 : Math.max(0, 100 - errorCount * 25); return { passes: errorCount === 0, score, issues, recommendations, }; } /** * Generate comprehensive accessibility report */ public generateAccessibilityReport(html: string): { overall: AccessibilityTestResult; html: AccessibilityTestResult; keyboard: AccessibilityTestResult; summary: { totalIssues: number; criticalIssues: number; overallScore: number; wcagLevel: 'A' | 'AA' | 'AAA' | 'Fail'; }; } { const htmlTest = this.testHTML(html); const keyboardTest = this.testKeyboardNavigation(html); const allIssues = [...htmlTest.issues, ...keyboardTest.issues]; const allRecommendations = [ ...new Set([ ...htmlTest.recommendations, ...keyboardTest.recommendations, ]), ]; const criticalIssues = allIssues.filter( issue => issue.type === 'error' ).length; const totalIssues = allIssues.length; const overallScore = Math.round((htmlTest.score + keyboardTest.score) / 2); let wcagLevel: 'A' | 'AA' | 'AAA' | 'Fail' = 'Fail'; if (criticalIssues === 0) { if (overallScore >= 95) wcagLevel = 'AAA'; else if (overallScore >= 80) wcagLevel = 'AA'; else wcagLevel = 'A'; } const overall: AccessibilityTestResult = { passes: criticalIssues === 0, score: overallScore, issues: allIssues, recommendations: allRecommendations, }; return { overall, html: htmlTest, keyboard: keyboardTest, summary: { totalIssues, criticalIssues, overallScore, wcagLevel, }, }; } /** * Simplified contrast ratio calculation * In a real implementation, this would use proper color space calculations */ private calculateContrastRatio( foreground: string, background: string ): number { // This is a simplified calculation for demonstration // Real implementation would parse colors and calculate luminance properly // For now, return a mock value based on color similarity if (foreground === background) return 1; if (foreground === '#000000' && background === '#ffffff') return 21; if (foreground === '#ffffff' && background === '#000000') return 21; // Mock calculation - in real implementation would use proper luminance formula return Math.random() * 10 + 3; // Random value between 3-13 for testing } } export const accessibilityTester = new AccessibilityTester();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ColorMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server