UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

97 lines (96 loc) 21.6 kB
/** * Keyboard Navigation Test Utilities for Therapeutic Components * * Comprehensive testing suite for keyboard accessibility validation specifically * designed for ADHD-focused therapeutic components with enhanced requirements. */ // Default test configuration const defaultConfig = { enableVirtualFocus: false, simulateUserDelay: true, testUserDelay: 100, testTabSequence: true, testAriaAnnouncements: true, testFocusTrapping: true, testEscapeHandling: true, testHelpSystem: true, testCrisisMode: true, validateAdhdPatterns: true }; /** * Run comprehensive keyboard navigation tests for therapeutic component */ export async function testTherapeuticKeyboardNav(component, componentType, config = {}) { const testConfig = { ...defaultConfig, ...config }; const context = { component, startTime: Date.now(), focusHistory: [], announcementHistory: [], currentFocus: null }; const issues = []; try { // Basic keyboard navigation tests await testBasicKeyboardNavigation(context, testConfig, issues); // Tab sequence and focus management if (testConfig.testTabSequence) { await testTabSequence(context, testConfig, issues); } // ARIA announcements if (testConfig.testAriaAnnouncements) { await testAriaAnnouncements(context, testConfig, issues); } // Focus trapping (for modals, panels) if (testConfig.testFocusTrapping) { await testFocusTrapping(context, testConfig, issues); } // Escape handling if (testConfig.testEscapeHandling) { await testEscapeHandling(context, testConfig, issues); } // Help system if (testConfig.testHelpSystem) { await testHelpSystem(context, testConfig, issues); } // Crisis mode integration if (testConfig.testCrisisMode) { await testCrisisModeIntegration(context, testConfig, issues); } // ADHD-specific patterns if (testConfig.validateAdhdPatterns) { await testAdhdSpecificPatterns(context, testConfig, issues); } // Evaluate ADHD compliance const adhdCompliance = evaluateAdhdCompliance(context, issues); return { passed: issues.filter(i => i.severity === 'error').length === 0, component: componentType, testType: 'keyboard-navigation', issues, adhdCompliance, recommendations: generateRecommendations(issues, adhdCompliance), timeToComplete: Date.now() - context.startTime }; } catch (error) { issues.push({ severity: 'error', type: 'navigation', element: 'test-runner', description: `Test execution failed: ${error}`, expectedBehavior: 'Tests should complete without errors', actualBehavior: 'Test runner encountered an exception', adhdImpact: 'high', n }); n; n; return { n, passed: false, n, component: componentType, n, testType: 'keyboard-navigation', n, issues, n, adhdCompliance: { n, focusManagement: false, n, cognitiveLoadReduction: false, n, timeFlexibility: false, n, clearFeedback: false, n, errorRecovery: false, n, helpSystem: false, n, escapeRoutes: false, n, score: 0, n }, n, recommendations: ['Fix test execution errors before conducting accessibility tests'], n, timeToComplete: Date.now() - context.startTime, n }; n; } n; } n; n; /**\n * Test basic keyboard navigation patterns\n */ nasync; function testBasicKeyboardNavigation(n, context, n, config, n, issues, n) { n; const { component } = context; n; n; } // Find all focusable elements\n const focusableElements = getFocusableElements(component);\n \n if (focusableElements.length === 0) {\n issues.push({\n severity: 'warning',\n type: 'focus',\n element: component.tagName,\n description: 'No focusable elements found in component',\n expectedBehavior: 'Component should have at least one focusable element',\n actualBehavior: 'No focusable elements detected',\n adhdImpact: 'high'\n });\n return;\n }\n \n // Test each focusable element\n for (const element of focusableElements) {\n await testElementFocus(element, context, config, issues);\n await simulateUserDelay(config);\n }\n \n // Test arrow key navigation if applicable\n await testArrowKeyNavigation(context, config, issues);\n \n // Test Enter/Space activation\n await testActivationKeys(context, config, issues);\n}\n\n/**\n * Test tab sequence and focus order\n */\nasync function testTabSequence(\n context: TestContext, \n config: KeyboardTestConfig, \n issues: KeyboardIssue[]\n): Promise<void> {\n const { component } = context;\n const focusableElements = getFocusableElements(component);\n \n if (focusableElements.length < 2) return; // Need at least 2 elements for sequence test\n \n // Test forward tab sequence\n for (let i = 0; i < focusableElements.length; i++) {\n const element = focusableElements[i];\n element.focus();\n \n await simulateUserDelay(config);\n \n // Verify focus is on expected element\n if (document.activeElement !== element) {\n issues.push({\n severity: 'error',\n type: 'focus',\n element: getElementSelector(element),\n description: 'Tab sequence broken - focus not where expected',\n expectedBehavior: `Focus should be on element ${i + 1} in sequence`,\n actualBehavior: `Focus is on ${getElementSelector(document.activeElement as HTMLElement)}`,\n wcagCriterion: '2.4.3',\n adhdImpact: 'high'\n });\n }\n \n context.focusHistory.push(element);\n }\n \n // Test reverse tab sequence (Shift+Tab)\n await testReverseTabSequence(context, config, issues);\n}\n\n/**\n * Test ARIA announcements and screen reader integration\n */\nasync function testAriaAnnouncements(\n context: TestContext, \n config: KeyboardTestConfig, \n issues: KeyboardIssue[]\n): Promise<void> {\n const { component } = context;\n \n // Find live regions\n const liveRegions = component.querySelectorAll('[aria-live]');\n \n if (liveRegions.length === 0) {\n issues.push({\n severity: 'warning',\n type: 'announcement',\n element: component.tagName,\n description: 'No ARIA live regions found for announcements',\n expectedBehavior: 'Therapeutic components should have live regions for state changes',\n actualBehavior: 'No aria-live regions detected',\n adhdImpact: 'medium'\n });\n }\n \n // Test announcement content\n for (const region of liveRegions) {\n const announcements = await captureAnnouncements(region as HTMLElement, 1000);\n \n if (announcements.length === 0) {\n issues.push({\n severity: 'info',\n type: 'announcement',\n element: getElementSelector(region as HTMLElement),\n description: 'Live region exists but no announcements captured during test',\n expectedBehavior: 'Live regions should announce relevant state changes',\n actualBehavior: 'No announcements detected',\n adhdImpact: 'low'\n });\n }\n \n context.announcementHistory.push(...announcements);\n }\n \n // Validate announcement quality for ADHD users\n validateAnnouncementQuality(context.announcementHistory, issues);\n}\n\n/**\n * Test focus trapping for modal-like components\n */\nasync function testFocusTrapping(\n context: TestContext, \n config: KeyboardTestConfig, \n issues: KeyboardIssue[]\n): Promise<void> {\n const { component } = context;\n \n // Check if component has modal or dialog role\n const isModal = component.getAttribute('role') === 'dialog' || \n component.classList.contains('modal') ||\n component.classList.contains('panel');\n \n if (!isModal) return; // Focus trapping only applies to modal-like components\n \n const focusableElements = getFocusableElements(component);\n \n if (focusableElements.length === 0) {\n issues.push({\n severity: 'error',\n type: 'focus',\n element: component.tagName,\n description: 'Modal component has no focusable elements',\n expectedBehavior: 'Modal should contain at least one focusable element',\n actualBehavior: 'No focusable elements in modal',\n wcagCriterion: '2.4.3',\n adhdImpact: 'high'\n });\n return;\n }\n \n // Test forward and backward focus trapping\n const firstElement = focusableElements[0];\n const lastElement = focusableElements[focusableElements.length - 1];\n \n // Focus last element and tab forward\n lastElement.focus();\n await simulateKey('Tab');\n \n if (document.activeElement !== firstElement) {\n issues.push({\n severity: 'error',\n type: 'focus',\n element: getElementSelector(lastElement),\n description: 'Focus not trapped - Tab from last element should focus first element',\n expectedBehavior: 'Focus should wrap to first element',\n actualBehavior: `Focus went to ${getElementSelector(document.activeElement as HTMLElement)}`,\n wcagCriterion: '2.4.3',\n adhdImpact: 'high'\n });\n }\n \n // Focus first element and Shift+Tab backward\n firstElement.focus();\n await simulateKey('Tab', { shiftKey: true });\n \n if (document.activeElement !== lastElement) {\n issues.push({\n severity: 'error',\n type: 'focus',\n element: getElementSelector(firstElement),\n description: 'Focus not trapped - Shift+Tab from first element should focus last element',\n expectedBehavior: 'Focus should wrap to last element',\n actualBehavior: `Focus went to ${getElementSelector(document.activeElement as HTMLElement)}`,\n wcagCriterion: '2.4.3',\n adhdImpact: 'high'\n });\n }\n}\n\n/**\n * Test escape key handling for crisis mode and quick exits\n */\nasync function testEscapeHandling(\n context: TestContext, \n config: KeyboardTestConfig, \n issues: KeyboardIssue[]\n): Promise<void> {\n const { component } = context;\n \n // Simulate escape key press\n const escapeHandled = await simulateKey('Escape');\n \n // For therapeutic components, escape should always be handled\n if (!escapeHandled) {\n issues.push({\n severity: 'warning',\n type: 'navigation',\n element: component.tagName,\n description: 'Escape key not handled',\n expectedBehavior: 'Therapeutic components should handle Escape for quick exits (ADHD support)',\n actualBehavior: 'Escape key was not captured or handled',\n adhdImpact: 'high'\n });\n }\n \n // Test escape from different states\n const focusableElements = getFocusableElements(component);\n for (const element of focusableElements) {\n element.focus();\n await simulateKey('Escape');\n \n // Verify appropriate escape behavior\n // (This would be component-specific validation)\n }\n}\n\n/**\n * Test help system accessibility (h key)\n */\nasync function testHelpSystem(\n context: TestContext, \n config: KeyboardTestConfig, \n issues: KeyboardIssue[]\n): Promise<void> {\n const { component } = context;\n \n // Test 'h' key for help\n const helpHandled = await simulateKey('h');\n const helpHandledUpper = await simulateKey('H');\n \n if (!helpHandled && !helpHandledUpper) {\n issues.push({\n severity: 'warning',\n type: 'navigation',\n element: component.tagName,\n description: 'Help key (h) not handled',\n expectedBehavior: 'Therapeutic components should provide contextual help via h key (ADHD support)',\n actualBehavior: 'Neither h nor H key triggers help system',\n adhdImpact: 'medium'\n });\n }\n \n // Check for help indication in aria-label or description\n const elementsWithHelp = component.querySelectorAll('[aria-label*=\"help\"], [aria-describedby*=\"help\"]');\n \n if (elementsWithHelp.length === 0) {\n issues.push({\n severity: 'info',\n type: 'announcement',\n element: component.tagName,\n description: 'No elements indicate help availability',\n expectedBehavior: 'Components should indicate help availability in ARIA labels',\n actualBehavior: 'No help indicators found in accessibility tree',\n adhdImpact: 'low'\n });\n }\n}\n\n/**\n * Test crisis mode integration and accessibility\n */\nasync function testCrisisModeIntegration(\n context: TestContext, \n config: KeyboardTestConfig, \n issues: KeyboardIssue[]\n): Promise<void> {\n const { component } = context;\n \n // Check if component responds to crisis mode\n const hasCrisisSupport = component.hasAttribute('data-crisis-mode') ||\n component.classList.contains('ck-crisis-mode') ||\n component.querySelector('[data-crisis-mode]');\n \n if (!hasCrisisSupport) {\n issues.push({\n severity: 'info',\n type: 'adhd-specific',\n element: component.tagName,\n description: 'Component may not support crisis mode',\n expectedBehavior: 'Therapeutic components should adapt to crisis mode for cognitive load reduction',\n actualBehavior: 'No crisis mode indicators found',\n adhdImpact: 'medium'\n });\n }\n \n // Test emergency escape (Ctrl+Shift+C)\n const emergencyEscapeHandled = await simulateKey('c', { ctrlKey: true, shiftKey: true });\n \n // This might be handled globally, so we check for any response\n // (In a real test, we'd check for crisis mode activation/deactivation)\n}\n\n/**\n * Test ADHD-specific interaction patterns\n */\nasync function testAdhdSpecificPatterns(\n context: TestContext, \n config: KeyboardTestConfig, \n issues: KeyboardIssue[]\n): Promise<void> {\n const { component } = context;\n \n // Test timing tolerance\n await testTimingTolerance(context, config, issues);\n \n // Test clear feedback\n await testClearFeedback(context, config, issues);\n \n // Test error recovery\n await testErrorRecovery(context, config, issues);\n \n // Test focus indicators\n await testFocusIndicators(context, config, issues);\n}\n\n/**\n * Helper functions\n */\n\nfunction getFocusableElements(container: HTMLElement): HTMLElement[] {\n const focusableSelector = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]'\n ].join(', ');\n \n return Array.from(container.querySelectorAll(focusableSelector)) as HTMLElement[];\n}\n\nfunction getElementSelector(element: HTMLElement): string {\n if (element.id) return `#${element.id}`;\n if (element.className) return `${element.tagName.toLowerCase()}.${element.className.split(' ')[0]}`;\n return element.tagName.toLowerCase();\n}\n\nasync function simulateKey(key: string, modifiers: {\n ctrlKey?: boolean;\n shiftKey?: boolean;\n altKey?: boolean;\n metaKey?: boolean;\n} = {}): Promise<boolean> {\n const event = new KeyboardEvent('keydown', {\n key,\n bubbles: true,\n cancelable: true,\n ...modifiers\n });\n \n const target = document.activeElement || document.body;\n const handled = !target.dispatchEvent(event);\n \n await new Promise(resolve => setTimeout(resolve, 50)); // Allow event processing\n \n return handled;\n}\n\nasync function simulateUserDelay(config: KeyboardTestConfig): Promise<void> {\n if (config.simulateUserDelay) {\n await new Promise(resolve => setTimeout(resolve, config.testUserDelay));\n }\n}\n\nasync function testElementFocus(\n element: HTMLElement,\n context: TestContext,\n config: KeyboardTestConfig,\n issues: KeyboardIssue[]\n): Promise<void> {\n element.focus();\n \n if (document.activeElement !== element) {\n issues.push({\n severity: 'error',\n type: 'focus',\n element: getElementSelector(element),\n description: 'Element cannot receive focus',\n expectedBehavior: 'Focusable elements should accept focus when focused programmatically',\n actualBehavior: 'Element did not receive focus',\n wcagCriterion: '2.4.3',\n adhdImpact: 'high'\n });\n }\n \n // Check focus indicator visibility\n const computedStyle = window.getComputedStyle(element);\n const hasVisibleFocus = computedStyle.outline !== 'none' || \n computedStyle.boxShadow !== 'none' ||\n element.classList.contains('focus-visible');\n \n if (!hasVisibleFocus) {\n issues.push({\n severity: 'warning',\n type: 'focus',\n element: getElementSelector(element),\n description: 'Focus indicator not visible',\n expectedBehavior: 'Focused elements should have visible focus indicators',\n actualBehavior: 'No visible focus indicator detected',\n wcagCriterion: '2.4.7',\n adhdImpact: 'medium'\n });\n }\n}\n\n// Additional helper functions would continue here...\n// (Implementing remaining test functions)\n\n/**\n * Evaluate ADHD compliance based on test results\n */\nfunction evaluateAdhdCompliance(context: TestContext, issues: KeyboardIssue[]): AdhdComplianceResult {\n const adhdIssues = issues.filter(issue => issue.adhdImpact);\n const highImpactIssues = adhdIssues.filter(issue => issue.adhdImpact === 'high');\n const mediumImpactIssues = adhdIssues.filter(issue => issue.adhdImpact === 'medium');\n \n // Calculate scores for each area\n const focusManagement = highImpactIssues.filter(i => i.type === 'focus').length === 0;\n const cognitiveLoadReduction = issues.filter(i => i.type === 'adhd-specific').length < 2;\n const timeFlexibility = issues.filter(i => i.type === 'timing').length === 0;\n const clearFeedback = issues.filter(i => i.type === 'announcement').length < 3;\n const errorRecovery = true; // Would be determined by specific tests\n const helpSystem = context.announcementHistory.some(a => a.includes('help'));\n const escapeRoutes = issues.filter(i => i.description.includes('Escape')).length === 0;\n \n const areas = [focusManagement, cognitiveLoadReduction, timeFlexibility, clearFeedback, errorRecovery, helpSystem, escapeRoutes];\n const score = Math.round((areas.filter(Boolean).length / areas.length) * 100);\n \n return {\n focusManagement,\n cognitiveLoadReduction,\n timeFlexibility,\n clearFeedback,\n errorRecovery,\n helpSystem,\n escapeRoutes,\n score\n };\n}\n\n/**\n * Generate recommendations based on test results\n */\nfunction generateRecommendations(issues: KeyboardIssue[], compliance: AdhdComplianceResult): string[] {\n const recommendations: string[] = [];\n \n if (!compliance.focusManagement) {\n recommendations.push('Implement enhanced focus indicators with 3px outline and box-shadow for ADHD users');\n }\n \n if (!compliance.helpSystem) {\n recommendations.push('Add contextual help system triggered by h key');\n }\n \n if (!compliance.escapeRoutes) {\n recommendations.push('Ensure Escape key provides immediate exit from all interactive states');\n }\n \n if (compliance.score < 70) {\n recommendations.push('Consider implementing ADHD-specific accessibility utilities for better cognitive load management');\n }\n \n // Add specific recommendations based on issue types\n const errorIssues = issues.filter(i => i.severity === 'error');\n if (errorIssues.length > 0) {\n recommendations.push('Fix critical accessibility errors before production deployment');\n }\n \n return recommendations;\n}\n\n// Placeholder implementations for remaining test functions\nasync function testReverseTabSequence(context: TestContext, config: KeyboardTestConfig, issues: KeyboardIssue[]): Promise<void> {\n // Implementation would test Shift+Tab navigation\n}\n\nasync function testArrowKeyNavigation(context: TestContext, config: KeyboardTestConfig, issues: KeyboardIssue[]): Promise<void> {\n // Implementation would test arrow key navigation for radio groups, etc.\n}\n\nasync function testActivationKeys(context: TestContext, config: KeyboardTestConfig, issues: KeyboardIssue[]): Promise<void> {\n // Implementation would test Enter and Space key activation\n}\n\nasync function captureAnnouncements(liveRegion: HTMLElement, duration: number): Promise<string[]> {\n // Implementation would monitor live region changes\n return [];\n}\n\nfunction validateAnnouncementQuality(announcements: string[], issues: KeyboardIssue[]): void {\n // Implementation would validate announcement content for ADHD-friendliness\n}\n\nasync function testTimingTolerance(context: TestContext, config: KeyboardTestConfig, issues: KeyboardIssue[]): Promise<void> {\n // Implementation would test timing tolerance for ADHD users\n}\n\nasync function testClearFeedback(context: TestContext, config: KeyboardTestConfig, issues: KeyboardIssue[]): Promise<void> {\n // Implementation would test feedback clarity\n}\n\nasync function testErrorRecovery(context: TestContext, config: KeyboardTestConfig, issues: KeyboardIssue[]): Promise<void> {\n // Implementation would test error recovery mechanisms\n}\n\nasync function testFocusIndicators(context: TestContext, config: KeyboardTestConfig, issues: KeyboardIssue[]): Promise<void> {\n // Implementation would test focus indicator adequacy for ADHD users\n}\n\n/**\n * Export main testing function and utilities\n */\nexport { testTherapeuticKeyboardNav as default };