Skip to main content
Glama
accessibility-testing.test.ts17.8 kB
import { AccessibilityTester, accessibilityTester, } from '../../src/utils/accessibility-testing'; describe('AccessibilityTester', () => { let tester: AccessibilityTester; beforeEach(() => { tester = new AccessibilityTester(); }); describe('testHTML', () => { it('should pass for fully compliant HTML', () => { const compliantHTML = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Test</title> <style> button:focus { outline: 2px solid blue; } @media (prefers-reduced-motion: reduce) { * { animation: none; } } @media (prefers-contrast: high) { body { background: white; } } </style> </head> <body> <main> <h1>Main Heading</h1> <button aria-label="Test button" tabindex="0">Click me</button> </main> </body> </html> `; const result = tester.testHTML(compliantHTML); expect(result.score).toBeGreaterThan(80); expect(result.issues.filter(i => i.type === 'error')).toHaveLength(0); // Note: passes might be false due to warnings, but no errors is what matters }); it('should fail for HTML missing DOCTYPE', () => { const html = '<html><body>Content</body></html>'; const result = tester.testHTML(html); expect(result.passes).toBe(false); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'error', code: 'DOCTYPE_MISSING', wcagLevel: 'A', }) ); }); it('should fail for HTML missing lang attribute', () => { const html = '<!DOCTYPE html><html><body>Content</body></html>'; const result = tester.testHTML(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'error', code: 'LANG_MISSING', wcagLevel: 'A', }) ); }); it('should warn for missing viewport meta tag', () => { const html = '<!DOCTYPE html><html lang="en"><body>Content</body></html>'; const result = tester.testHTML(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'warning', code: 'VIEWPORT_MISSING', wcagLevel: 'AA', }) ); }); it('should warn for missing headings', () => { const html = '<!DOCTYPE html><html lang="en"><body><p>Content</p></body></html>'; const result = tester.testHTML(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'warning', code: 'NO_HEADINGS', wcagLevel: 'AA', }) ); }); it('should detect missing ARIA labels on interactive elements', () => { const html = ` <!DOCTYPE html> <html lang="en"> <body> <button>No label</button> <input type="text"> <select><option>Option</option></select> <textarea></textarea> <div role="button">Custom button</div> <div tabindex="0">Focusable div</div> </body> </html> `; const result = tester.testHTML(html); const ariaIssues = result.issues.filter( i => i.code === 'MISSING_ARIA_LABEL' ); expect(ariaIssues.length).toBeGreaterThan(0); }); it('should detect missing tabindex', () => { const html = '<!DOCTYPE html><html lang="en"><body><p>Content</p></body></html>'; const result = tester.testHTML(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'info', code: 'NO_TABINDEX', wcagLevel: 'AA', }) ); }); it('should detect missing focus styles', () => { const html = ` <!DOCTYPE html> <html lang="en"> <body> <button>Button</button> </body> </html> `; const result = tester.testHTML(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'error', code: 'NO_FOCUS_STYLES', wcagLevel: 'AA', }) ); }); it('should detect missing reduced motion support', () => { const html = '<!DOCTYPE html><html lang="en"><body>Content</body></html>'; const result = tester.testHTML(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'info', code: 'NO_REDUCED_MOTION', wcagLevel: 'AAA', }) ); }); it('should detect missing high contrast support', () => { const html = '<!DOCTYPE html><html lang="en"><body>Content</body></html>'; const result = tester.testHTML(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'info', code: 'NO_HIGH_CONTRAST', wcagLevel: 'AAA', }) ); }); it('should detect missing semantic HTML elements', () => { const html = '<!DOCTYPE html><html lang="en"><body><div>Content</div></body></html>'; const result = tester.testHTML(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'warning', code: 'NO_SEMANTIC_HTML', wcagLevel: 'AA', }) ); }); it('should pass with semantic HTML elements', () => { const html = ` <!DOCTYPE html> <html lang="en"> <body> <main>Content</main> </body> </html> `; const result = tester.testHTML(html); const semanticIssues = result.issues.filter( i => i.code === 'NO_SEMANTIC_HTML' ); expect(semanticIssues).toHaveLength(0); }); it('should pass with role-based semantic elements', () => { const html = ` <!DOCTYPE html> <html lang="en"> <body> <div role="main">Content</div> </body> </html> `; const result = tester.testHTML(html); const semanticIssues = result.issues.filter( i => i.code === 'NO_SEMANTIC_HTML' ); expect(semanticIssues).toHaveLength(0); }); it('should generate appropriate recommendations', () => { const html = ` <!DOCTYPE html> <html lang="en"> <body> <button>No label</button> </body> </html> `; const result = tester.testHTML(html); expect(result.recommendations).toContain( 'Add aria-label attributes to all interactive elements' ); expect(result.recommendations).toContain( 'Add visible focus indicators for keyboard navigation' ); }); it('should calculate score based on issues', () => { const htmlWithErrors = '<!DOCTYPE html><body>Content</body>'; const result = tester.testHTML(htmlWithErrors); expect(result.score).toBeLessThan(100); expect(typeof result.score).toBe('number'); expect(result.score).toBeGreaterThanOrEqual(0); }); }); describe('testColorContrast', () => { it('should pass for high contrast colors', () => { // Mock the private method to return a high contrast ratio const originalMethod = (tester as any).calculateContrastRatio; (tester as any).calculateContrastRatio = jest.fn().mockReturnValue(21); const result = tester.testColorContrast('#000000', '#ffffff'); expect(result.passes).toBe(true); expect(result.score).toBe(100); expect(result.issues.filter(i => i.type === 'error')).toHaveLength(0); // Restore original method (tester as any).calculateContrastRatio = originalMethod; }); it('should fail for low contrast colors', () => { const originalMethod = (tester as any).calculateContrastRatio; (tester as any).calculateContrastRatio = jest.fn().mockReturnValue(2.5); const result = tester.testColorContrast('#888888', '#999999'); expect(result.passes).toBe(false); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'error', code: 'CONTRAST_AA_FAIL', wcagLevel: 'AA', }) ); (tester as any).calculateContrastRatio = originalMethod; }); it('should handle different text sizes', () => { const originalMethod = (tester as any).calculateContrastRatio; (tester as any).calculateContrastRatio = jest.fn().mockReturnValue(3.5); const normalResult = tester.testColorContrast( '#666666', '#ffffff', 'normal' ); const largeResult = tester.testColorContrast( '#666666', '#ffffff', 'large' ); expect(normalResult.passes).toBe(false); // 3.5 < 4.5 for normal text expect(largeResult.passes).toBe(true); // 3.5 > 3.0 for large text (tester as any).calculateContrastRatio = originalMethod; }); it('should warn for AAA failures', () => { const originalMethod = (tester as any).calculateContrastRatio; (tester as any).calculateContrastRatio = jest.fn().mockReturnValue(5.0); const result = tester.testColorContrast('#666666', '#ffffff'); expect(result.passes).toBe(true); // Passes AA expect(result.issues).toContainEqual( expect.objectContaining({ type: 'warning', code: 'CONTRAST_AAA_FAIL', wcagLevel: 'AAA', }) ); (tester as any).calculateContrastRatio = originalMethod; }); it('should calculate appropriate scores', () => { const originalMethod = (tester as any).calculateContrastRatio; // Test AAA level (tester as any).calculateContrastRatio = jest.fn().mockReturnValue(8.0); let result = tester.testColorContrast('#000000', '#ffffff'); expect(result.score).toBe(100); // Test AA level (tester as any).calculateContrastRatio = jest.fn().mockReturnValue(5.0); result = tester.testColorContrast('#000000', '#ffffff'); expect(result.score).toBe(80); // Test below AA (tester as any).calculateContrastRatio = jest.fn().mockReturnValue(2.0); result = tester.testColorContrast('#000000', '#ffffff'); expect(result.score).toBeLessThan(60); (tester as any).calculateContrastRatio = originalMethod; }); }); describe('testKeyboardNavigation', () => { it('should pass for proper keyboard navigation', () => { const html = ` <div tabindex="0" onkeydown="handleKey()"> <button>Button</button> </div> <script> document.addEventListener('keydown', function(e) { if (e.key === 'ArrowUp') { /* handle */ } }); </script> `; const result = tester.testKeyboardNavigation(html); expect(result.passes).toBe(true); expect(result.score).toBe(100); }); it('should warn for missing tab order', () => { const html = '<div><button>Button</button></div>'; const result = tester.testKeyboardNavigation(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'warning', code: 'NO_TAB_ORDER', wcagLevel: 'AA', }) ); }); it('should fail for missing keyboard handlers', () => { const html = '<div tabindex="0"><button>Button</button></div>'; const result = tester.testKeyboardNavigation(html); expect(result.passes).toBe(false); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'error', code: 'NO_KEYBOARD_HANDLERS', wcagLevel: 'A', }) ); }); it('should detect various keyboard handler patterns', () => { const patterns = [ { pattern: 'onkeydown="test()"', description: 'inline onkeydown' }, { pattern: 'onkeyup="test()"', description: 'inline onkeyup' }, { pattern: 'onkeypress="test()"', description: 'inline onkeypress' }, ]; patterns.forEach(({ pattern }) => { const html = `<div ${pattern}><button>Button</button></div>`; const result = tester.testKeyboardNavigation(html); const keyboardIssues = result.issues.filter( i => i.code === 'NO_KEYBOARD_HANDLERS' ); expect(keyboardIssues).toHaveLength(0); }); // Test addEventListener patterns in script tags const scriptPatterns = [ 'addEventListener("keydown"', 'addEventListener("keyup"', ]; scriptPatterns.forEach(pattern => { const html = ` <div><button>Button</button></div> <script> document.${pattern}, function() {}); </script> `; const result = tester.testKeyboardNavigation(html); const keyboardIssues = result.issues.filter( i => i.code === 'NO_KEYBOARD_HANDLERS' ); expect(keyboardIssues).toHaveLength(0); }); }); it('should suggest arrow key navigation', () => { const html = '<div tabindex="0" onkeydown="test()"><button>Button</button></div>'; const result = tester.testKeyboardNavigation(html); expect(result.issues).toContainEqual( expect.objectContaining({ type: 'info', code: 'NO_ARROW_NAVIGATION', wcagLevel: 'AAA', }) ); }); it('should pass with arrow key navigation', () => { const html = ` <div tabindex="0" onkeydown="test()"> <script> if (e.key === 'ArrowUp') { /* handle */ } </script> </div> `; const result = tester.testKeyboardNavigation(html); const arrowIssues = result.issues.filter( i => i.code === 'NO_ARROW_NAVIGATION' ); expect(arrowIssues).toHaveLength(0); }); }); describe('generateAccessibilityReport', () => { it('should generate comprehensive report', () => { const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> button:focus { outline: 2px solid blue; } </style> </head> <body> <main> <h1>Title</h1> <button aria-label="Test" tabindex="0" onkeydown="test()">Button</button> </main> </body> </html> `; const report = tester.generateAccessibilityReport(html); expect(report).toHaveProperty('overall'); expect(report).toHaveProperty('html'); expect(report).toHaveProperty('keyboard'); expect(report).toHaveProperty('summary'); expect(report.summary).toHaveProperty('totalIssues'); expect(report.summary).toHaveProperty('criticalIssues'); expect(report.summary).toHaveProperty('overallScore'); expect(report.summary).toHaveProperty('wcagLevel'); }); it('should calculate WCAG levels correctly', () => { // Test AAA level const excellentHTML = ` <!DOCTYPE html> <html lang="en"> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> button:focus { outline: 2px solid blue; } @media (prefers-reduced-motion: reduce) { * { animation: none; } } @media (prefers-contrast: high) { body { background: white; } } </style> </head> <body> <main> <h1>Title</h1> <button aria-label="Test" tabindex="0" onkeydown="handleKey(event)">Button</button> </main> <script> function handleKey(e) { if (e.key === 'ArrowUp') { /* handle */ } } </script> </body> </html> `; const report = tester.generateAccessibilityReport(excellentHTML); expect(['AAA', 'AA', 'A']).toContain(report.summary.wcagLevel); // Test failing level const poorHTML = '<div>No structure</div>'; const poorReport = tester.generateAccessibilityReport(poorHTML); expect(poorReport.summary.wcagLevel).toBe('Fail'); }); it('should merge recommendations without duplicates', () => { const html = ` <button>No label</button> <input type="text"> `; const report = tester.generateAccessibilityReport(html); const recommendations = report.overall.recommendations; // Should not have duplicate recommendations const uniqueRecommendations = [...new Set(recommendations)]; expect(recommendations).toEqual(uniqueRecommendations); }); }); describe('calculateContrastRatio (private method)', () => { it('should handle identical colors', () => { const result = (tester as any).calculateContrastRatio( '#000000', '#000000' ); expect(result).toBe(1); }); it('should handle black and white', () => { const result1 = (tester as any).calculateContrastRatio( '#000000', '#ffffff' ); const result2 = (tester as any).calculateContrastRatio( '#ffffff', '#000000' ); expect(result1).toBe(21); expect(result2).toBe(21); }); it('should return reasonable values for other colors', () => { const result = (tester as any).calculateContrastRatio( '#ff0000', '#00ff00' ); expect(result).toBeGreaterThan(3); expect(result).toBeLessThan(13); }); }); describe('singleton instance', () => { it('should export a singleton instance', () => { expect(accessibilityTester).toBeInstanceOf(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