agentic-qe 2.3.5 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/qe-a11y-ally.md +751 -0
- package/.claude/agents/qx-partner.md +120 -4
- package/.claude/skills/testability-scoring/SKILL.md +107 -6
- package/CHANGELOG.md +133 -0
- package/README.md +7 -6
- package/dist/agents/AccessibilityAllyAgent.d.ts +168 -0
- package/dist/agents/AccessibilityAllyAgent.d.ts.map +1 -0
- package/dist/agents/AccessibilityAllyAgent.js +462 -0
- package/dist/agents/AccessibilityAllyAgent.js.map +1 -0
- package/dist/agents/BaseAgent.d.ts +56 -49
- package/dist/agents/BaseAgent.d.ts.map +1 -1
- package/dist/agents/BaseAgent.js +208 -604
- package/dist/agents/BaseAgent.js.map +1 -1
- package/dist/agents/FlakyTestHunterAgent.d.ts +2 -2
- package/dist/agents/FlakyTestHunterAgent.d.ts.map +1 -1
- package/dist/agents/FlakyTestHunterAgent.js +8 -104
- package/dist/agents/FlakyTestHunterAgent.js.map +1 -1
- package/dist/agents/SONAIntegration.d.ts +109 -0
- package/dist/agents/SONAIntegration.d.ts.map +1 -0
- package/dist/agents/SONAIntegration.js +167 -0
- package/dist/agents/SONAIntegration.js.map +1 -0
- package/dist/agents/TestExecutorAgent.d.ts +1 -1
- package/dist/agents/TestExecutorAgent.d.ts.map +1 -1
- package/dist/agents/TestExecutorAgent.js +4 -46
- package/dist/agents/TestExecutorAgent.js.map +1 -1
- package/dist/agents/TestGeneratorAgent.d.ts.map +1 -1
- package/dist/agents/TestGeneratorAgent.js +15 -35
- package/dist/agents/TestGeneratorAgent.js.map +1 -1
- package/dist/agents/adapters/CoordinatorAdapter.d.ts +47 -0
- package/dist/agents/adapters/CoordinatorAdapter.d.ts.map +1 -0
- package/dist/agents/adapters/CoordinatorAdapter.js +266 -0
- package/dist/agents/adapters/CoordinatorAdapter.js.map +1 -0
- package/dist/agents/adapters/LearningEngineAdapter.d.ts +45 -0
- package/dist/agents/adapters/LearningEngineAdapter.d.ts.map +1 -0
- package/dist/agents/adapters/LearningEngineAdapter.js +279 -0
- package/dist/agents/adapters/LearningEngineAdapter.js.map +1 -0
- package/dist/agents/adapters/LifecycleManagerAdapter.d.ts +45 -0
- package/dist/agents/adapters/LifecycleManagerAdapter.d.ts.map +1 -0
- package/dist/agents/adapters/LifecycleManagerAdapter.js +128 -0
- package/dist/agents/adapters/LifecycleManagerAdapter.js.map +1 -0
- package/dist/agents/adapters/MemoryServiceAdapter.d.ts +96 -0
- package/dist/agents/adapters/MemoryServiceAdapter.d.ts.map +1 -0
- package/dist/agents/adapters/MemoryServiceAdapter.js +266 -0
- package/dist/agents/adapters/MemoryServiceAdapter.js.map +1 -0
- package/dist/agents/adapters/index.d.ts +14 -0
- package/dist/agents/adapters/index.d.ts.map +1 -0
- package/dist/agents/adapters/index.js +25 -0
- package/dist/agents/adapters/index.js.map +1 -0
- package/dist/agents/index.d.ts +3 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +93 -2
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/lifecycle/AgentLifecycleManager.d.ts.map +1 -1
- package/dist/agents/lifecycle/AgentLifecycleManager.js +12 -6
- package/dist/agents/lifecycle/AgentLifecycleManager.js.map +1 -1
- package/dist/cli/init/agents.js +1 -1
- package/dist/cli/init/claude-config.js +2 -2
- package/dist/cli/init/database-init.js +1 -1
- package/dist/core/cache/BinaryCacheBuilder.d.ts +84 -0
- package/dist/core/cache/BinaryCacheBuilder.d.ts.map +1 -0
- package/dist/core/cache/BinaryCacheBuilder.js +225 -0
- package/dist/core/cache/BinaryCacheBuilder.js.map +1 -0
- package/dist/core/cache/BinaryCacheImpl.d.ts +161 -0
- package/dist/core/cache/BinaryCacheImpl.d.ts.map +1 -0
- package/dist/core/cache/BinaryCacheImpl.js +685 -0
- package/dist/core/cache/BinaryCacheImpl.js.map +1 -0
- package/dist/core/cache/BinaryCacheManager.d.ts +142 -0
- package/dist/core/cache/BinaryCacheManager.d.ts.map +1 -0
- package/dist/core/cache/BinaryCacheManager.js +354 -0
- package/dist/core/cache/BinaryCacheManager.js.map +1 -0
- package/dist/core/cache/BinaryCacheReader.d.ts +129 -0
- package/dist/core/cache/BinaryCacheReader.d.ts.map +1 -0
- package/dist/core/cache/BinaryCacheReader.js +243 -0
- package/dist/core/cache/BinaryCacheReader.js.map +1 -0
- package/dist/core/cache/BinaryMetadataCache.d.ts +777 -0
- package/dist/core/cache/BinaryMetadataCache.d.ts.map +1 -0
- package/dist/core/cache/BinaryMetadataCache.js +204 -0
- package/dist/core/cache/BinaryMetadataCache.js.map +1 -0
- package/dist/core/cache/CacheInvalidator.d.ts +103 -0
- package/dist/core/cache/CacheInvalidator.d.ts.map +1 -0
- package/dist/core/cache/CacheInvalidator.js +152 -0
- package/dist/core/cache/CacheInvalidator.js.map +1 -0
- package/dist/core/cache/CacheValidator.d.ts +69 -0
- package/dist/core/cache/CacheValidator.d.ts.map +1 -0
- package/dist/core/cache/CacheValidator.js +189 -0
- package/dist/core/cache/CacheValidator.js.map +1 -0
- package/dist/core/cache/MessagePackSerializer.d.ts +163 -0
- package/dist/core/cache/MessagePackSerializer.d.ts.map +1 -0
- package/dist/core/cache/MessagePackSerializer.js +274 -0
- package/dist/core/cache/MessagePackSerializer.js.map +1 -0
- package/dist/core/cache/index.d.ts +59 -0
- package/dist/core/cache/index.d.ts.map +1 -0
- package/dist/core/cache/index.js +111 -0
- package/dist/core/cache/index.js.map +1 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +18 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/memory/AgentDBService.d.ts +30 -4
- package/dist/core/memory/AgentDBService.d.ts.map +1 -1
- package/dist/core/memory/AgentDBService.js +122 -12
- package/dist/core/memory/AgentDBService.js.map +1 -1
- package/dist/core/memory/CachedHNSWVectorMemory.d.ts +153 -0
- package/dist/core/memory/CachedHNSWVectorMemory.d.ts.map +1 -0
- package/dist/core/memory/CachedHNSWVectorMemory.js +329 -0
- package/dist/core/memory/CachedHNSWVectorMemory.js.map +1 -0
- package/dist/core/memory/HNSWVectorMemory.js +1 -1
- package/dist/core/memory/PatternStoreFactory.d.ts +28 -0
- package/dist/core/memory/PatternStoreFactory.d.ts.map +1 -1
- package/dist/core/memory/PatternStoreFactory.js +68 -3
- package/dist/core/memory/PatternStoreFactory.js.map +1 -1
- package/dist/core/memory/RuVectorPatternStore.d.ts.map +1 -1
- package/dist/core/memory/RuVectorPatternStore.js +8 -2
- package/dist/core/memory/RuVectorPatternStore.js.map +1 -1
- package/dist/core/memory/UnifiedMemoryCoordinator.d.ts +50 -0
- package/dist/core/memory/UnifiedMemoryCoordinator.d.ts.map +1 -1
- package/dist/core/memory/UnifiedMemoryCoordinator.js +206 -0
- package/dist/core/memory/UnifiedMemoryCoordinator.js.map +1 -1
- package/dist/core/memory/index.d.ts +2 -0
- package/dist/core/memory/index.d.ts.map +1 -1
- package/dist/core/memory/index.js +8 -1
- package/dist/core/memory/index.js.map +1 -1
- package/dist/core/optimization/RecursiveOptimizer.d.ts +233 -0
- package/dist/core/optimization/RecursiveOptimizer.d.ts.map +1 -0
- package/dist/core/optimization/RecursiveOptimizer.js +509 -0
- package/dist/core/optimization/RecursiveOptimizer.js.map +1 -0
- package/dist/core/platform/FileOperations.d.ts +124 -0
- package/dist/core/platform/FileOperations.d.ts.map +1 -0
- package/dist/core/platform/FileOperations.js +371 -0
- package/dist/core/platform/FileOperations.js.map +1 -0
- package/dist/core/platform/PlatformDetector.d.ts +53 -0
- package/dist/core/platform/PlatformDetector.d.ts.map +1 -0
- package/dist/core/platform/PlatformDetector.js +251 -0
- package/dist/core/platform/PlatformDetector.js.map +1 -0
- package/dist/core/platform/index.d.ts +12 -0
- package/dist/core/platform/index.d.ts.map +1 -0
- package/dist/core/platform/index.js +28 -0
- package/dist/core/platform/index.js.map +1 -0
- package/dist/core/strategies/AcceleratedLearningStrategy.d.ts +74 -0
- package/dist/core/strategies/AcceleratedLearningStrategy.d.ts.map +1 -0
- package/dist/core/strategies/AcceleratedLearningStrategy.js +200 -0
- package/dist/core/strategies/AcceleratedLearningStrategy.js.map +1 -0
- package/dist/core/strategies/AgentCoordinationStrategy.d.ts +157 -0
- package/dist/core/strategies/AgentCoordinationStrategy.d.ts.map +1 -0
- package/dist/core/strategies/AgentCoordinationStrategy.js +12 -0
- package/dist/core/strategies/AgentCoordinationStrategy.js.map +1 -0
- package/dist/core/strategies/AgentLearningStrategy.d.ts +200 -0
- package/dist/core/strategies/AgentLearningStrategy.d.ts.map +1 -0
- package/dist/core/strategies/AgentLearningStrategy.js +13 -0
- package/dist/core/strategies/AgentLearningStrategy.js.map +1 -0
- package/dist/core/strategies/AgentLifecycleStrategy.d.ts +116 -0
- package/dist/core/strategies/AgentLifecycleStrategy.d.ts.map +1 -0
- package/dist/core/strategies/AgentLifecycleStrategy.js +12 -0
- package/dist/core/strategies/AgentLifecycleStrategy.js.map +1 -0
- package/dist/core/strategies/AgentMemoryStrategy.d.ts +154 -0
- package/dist/core/strategies/AgentMemoryStrategy.d.ts.map +1 -0
- package/dist/core/strategies/AgentMemoryStrategy.js +13 -0
- package/dist/core/strategies/AgentMemoryStrategy.js.map +1 -0
- package/dist/core/strategies/DefaultCoordinationStrategy.d.ts +105 -0
- package/dist/core/strategies/DefaultCoordinationStrategy.d.ts.map +1 -0
- package/dist/core/strategies/DefaultCoordinationStrategy.js +332 -0
- package/dist/core/strategies/DefaultCoordinationStrategy.js.map +1 -0
- package/dist/core/strategies/DefaultLearningStrategy.d.ts +120 -0
- package/dist/core/strategies/DefaultLearningStrategy.d.ts.map +1 -0
- package/dist/core/strategies/DefaultLearningStrategy.js +365 -0
- package/dist/core/strategies/DefaultLearningStrategy.js.map +1 -0
- package/dist/core/strategies/DefaultLifecycleStrategy.d.ts +129 -0
- package/dist/core/strategies/DefaultLifecycleStrategy.d.ts.map +1 -0
- package/dist/core/strategies/DefaultLifecycleStrategy.js +297 -0
- package/dist/core/strategies/DefaultLifecycleStrategy.js.map +1 -0
- package/dist/core/strategies/DefaultMemoryStrategy.d.ts +91 -0
- package/dist/core/strategies/DefaultMemoryStrategy.d.ts.map +1 -0
- package/dist/core/strategies/DefaultMemoryStrategy.js +257 -0
- package/dist/core/strategies/DefaultMemoryStrategy.js.map +1 -0
- package/dist/core/strategies/DistributedMemoryStrategy.d.ts +85 -0
- package/dist/core/strategies/DistributedMemoryStrategy.d.ts.map +1 -0
- package/dist/core/strategies/DistributedMemoryStrategy.js +126 -0
- package/dist/core/strategies/DistributedMemoryStrategy.js.map +1 -0
- package/dist/core/strategies/SONALearningStrategy.d.ts +115 -0
- package/dist/core/strategies/SONALearningStrategy.d.ts.map +1 -0
- package/dist/core/strategies/SONALearningStrategy.js +656 -0
- package/dist/core/strategies/SONALearningStrategy.js.map +1 -0
- package/dist/core/strategies/TRMLearningStrategy.d.ts +162 -0
- package/dist/core/strategies/TRMLearningStrategy.d.ts.map +1 -0
- package/dist/core/strategies/TRMLearningStrategy.js +670 -0
- package/dist/core/strategies/TRMLearningStrategy.js.map +1 -0
- package/dist/core/strategies/index.d.ts +104 -0
- package/dist/core/strategies/index.d.ts.map +1 -0
- package/dist/core/strategies/index.js +68 -0
- package/dist/core/strategies/index.js.map +1 -0
- package/dist/learning/PerformanceOptimizer.js +2 -2
- package/dist/learning/PerformanceOptimizer.js.map +1 -1
- package/dist/learning/SONAFeedbackLoop.d.ts +168 -0
- package/dist/learning/SONAFeedbackLoop.d.ts.map +1 -0
- package/dist/learning/SONAFeedbackLoop.js +344 -0
- package/dist/learning/SONAFeedbackLoop.js.map +1 -0
- package/dist/learning/baselines/BaselineCollector.d.ts +1 -1
- package/dist/learning/baselines/BaselineCollector.js +1 -1
- package/dist/learning/baselines/StandardTaskSuite.d.ts +1 -1
- package/dist/learning/baselines/StandardTaskSuite.js +1 -1
- package/dist/learning/index.d.ts +2 -0
- package/dist/learning/index.d.ts.map +1 -1
- package/dist/learning/index.js +6 -1
- package/dist/learning/index.js.map +1 -1
- package/dist/mcp/handlers/base-handler.d.ts +28 -1
- package/dist/mcp/handlers/base-handler.d.ts.map +1 -1
- package/dist/mcp/handlers/base-handler.js +41 -0
- package/dist/mcp/handlers/base-handler.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +1 -1
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +23 -16
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/services/AgentRegistry.d.ts.map +1 -1
- package/dist/mcp/services/AgentRegistry.js +6 -1
- package/dist/mcp/services/AgentRegistry.js.map +1 -1
- package/dist/mcp/tools/qe/accessibility/accname-computation.d.ts +114 -0
- package/dist/mcp/tools/qe/accessibility/accname-computation.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/accname-computation.js +566 -0
- package/dist/mcp/tools/qe/accessibility/accname-computation.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/apg-patterns.d.ts +103 -0
- package/dist/mcp/tools/qe/accessibility/apg-patterns.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/apg-patterns.js +1028 -0
- package/dist/mcp/tools/qe/accessibility/apg-patterns.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.d.ts +48 -0
- package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.js +565 -0
- package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.d.ts +117 -0
- package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.js +571 -0
- package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/html-report-generator.d.ts +23 -0
- package/dist/mcp/tools/qe/accessibility/html-report-generator.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/html-report-generator.js +1152 -0
- package/dist/mcp/tools/qe/accessibility/html-report-generator.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/index.d.ts +22 -0
- package/dist/mcp/tools/qe/accessibility/index.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/index.js +38 -0
- package/dist/mcp/tools/qe/accessibility/index.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/markdown-report-generator.d.ts +18 -0
- package/dist/mcp/tools/qe/accessibility/markdown-report-generator.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/markdown-report-generator.js +549 -0
- package/dist/mcp/tools/qe/accessibility/markdown-report-generator.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/remediation-code-generator.d.ts +139 -0
- package/dist/mcp/tools/qe/accessibility/remediation-code-generator.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/remediation-code-generator.js +1300 -0
- package/dist/mcp/tools/qe/accessibility/remediation-code-generator.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/scan-comprehensive.d.ts +138 -0
- package/dist/mcp/tools/qe/accessibility/scan-comprehensive.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/scan-comprehensive.js +1326 -0
- package/dist/mcp/tools/qe/accessibility/scan-comprehensive.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.d.ts +50 -0
- package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.js +469 -0
- package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.js.map +1 -0
- package/dist/mcp/tools/qe/accessibility/webvtt-generator.d.ts +193 -0
- package/dist/mcp/tools/qe/accessibility/webvtt-generator.d.ts.map +1 -0
- package/dist/mcp/tools/qe/accessibility/webvtt-generator.js +511 -0
- package/dist/mcp/tools/qe/accessibility/webvtt-generator.js.map +1 -0
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +61 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/output/AIActionSuggester.d.ts +98 -0
- package/dist/output/AIActionSuggester.d.ts.map +1 -0
- package/dist/output/AIActionSuggester.js +499 -0
- package/dist/output/AIActionSuggester.js.map +1 -0
- package/dist/output/CLIOutputHelper.d.ts +169 -0
- package/dist/output/CLIOutputHelper.d.ts.map +1 -0
- package/dist/output/CLIOutputHelper.js +320 -0
- package/dist/output/CLIOutputHelper.js.map +1 -0
- package/dist/output/OutputFormatter.d.ts +764 -0
- package/dist/output/OutputFormatter.d.ts.map +1 -0
- package/dist/output/OutputFormatter.js +125 -0
- package/dist/output/OutputFormatter.js.map +1 -0
- package/dist/output/OutputFormatterImpl.d.ts +131 -0
- package/dist/output/OutputFormatterImpl.d.ts.map +1 -0
- package/dist/output/OutputFormatterImpl.js +556 -0
- package/dist/output/OutputFormatterImpl.js.map +1 -0
- package/dist/output/examples.d.ts +38 -0
- package/dist/output/examples.d.ts.map +1 -0
- package/dist/output/examples.js +503 -0
- package/dist/output/examples.js.map +1 -0
- package/dist/output/index.d.ts +16 -0
- package/dist/output/index.d.ts.map +1 -0
- package/dist/output/index.js +58 -0
- package/dist/output/index.js.map +1 -0
- package/dist/providers/HybridRouter.d.ts +34 -3
- package/dist/providers/HybridRouter.d.ts.map +1 -1
- package/dist/providers/HybridRouter.js +69 -4
- package/dist/providers/HybridRouter.js.map +1 -1
- package/dist/providers/LLMProviderFactory.d.ts +68 -1
- package/dist/providers/LLMProviderFactory.d.ts.map +1 -1
- package/dist/providers/LLMProviderFactory.js +173 -6
- package/dist/providers/LLMProviderFactory.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts +150 -0
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -0
- package/dist/providers/OpenRouterProvider.js +545 -0
- package/dist/providers/OpenRouterProvider.js.map +1 -0
- package/dist/providers/RuvllmProvider.d.ts +130 -16
- package/dist/providers/RuvllmProvider.d.ts.map +1 -1
- package/dist/providers/RuvllmProvider.js +399 -83
- package/dist/providers/RuvllmProvider.js.map +1 -1
- package/dist/providers/index.d.ts +33 -4
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +72 -21
- package/dist/providers/index.js.map +1 -1
- package/dist/telemetry/instrumentation/agent.d.ts +1 -1
- package/dist/telemetry/instrumentation/agent.js +1 -1
- package/dist/telemetry/instrumentation/index.d.ts +1 -1
- package/dist/telemetry/instrumentation/index.js +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/ruvllm.d.ts +97 -0
- package/dist/types/ruvllm.d.ts.map +1 -0
- package/dist/types/ruvllm.js +46 -0
- package/dist/types/ruvllm.js.map +1 -0
- package/dist/utils/ruvllm-loader.d.ts +94 -0
- package/dist/utils/ruvllm-loader.d.ts.map +1 -0
- package/dist/utils/ruvllm-loader.js +87 -0
- package/dist/utils/ruvllm-loader.js.map +1 -0
- package/docs/reference/agents.md +36 -1
- package/package.json +7 -2
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Remediation Code Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates context-aware, copy-paste ready code examples for accessibility violations.
|
|
6
|
+
* Includes framework detection, brand color extraction, and multi-language support.
|
|
7
|
+
*
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
* @author Agentic QE Team
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.detectFramework = detectFramework;
|
|
13
|
+
exports.analyzeColors = analyzeColors;
|
|
14
|
+
exports.generateVideoCaptionRemediation = generateVideoCaptionRemediation;
|
|
15
|
+
exports.generateAriaHiddenFocusRemediation = generateAriaHiddenFocusRemediation;
|
|
16
|
+
exports.generateColorContrastRemediation = generateColorContrastRemediation;
|
|
17
|
+
exports.generatePlaywrightTest = generatePlaywrightTest;
|
|
18
|
+
exports.generateAccessibilityCSSUtilities = generateAccessibilityCSSUtilities;
|
|
19
|
+
exports.generateRemediationCodes = generateRemediationCodes;
|
|
20
|
+
exports.generateImageAltRemediation = generateImageAltRemediation;
|
|
21
|
+
exports.generateLinkNameRemediation = generateLinkNameRemediation;
|
|
22
|
+
exports.generateListStructureRemediation = generateListStructureRemediation;
|
|
23
|
+
exports.generateTouchTargetRemediation = generateTouchTargetRemediation;
|
|
24
|
+
/**
|
|
25
|
+
* Detect carousel/slider framework from HTML
|
|
26
|
+
*/
|
|
27
|
+
function detectFramework(html, selectors) {
|
|
28
|
+
const checks = [
|
|
29
|
+
{
|
|
30
|
+
framework: 'slick',
|
|
31
|
+
patterns: [/slick-slider/i, /slick-slide/i, /slick-track/i, /slick-list/i],
|
|
32
|
+
confidence: 0.95
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
framework: 'swiper',
|
|
36
|
+
patterns: [/swiper-container/i, /swiper-slide/i, /swiper-wrapper/i],
|
|
37
|
+
confidence: 0.95
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
framework: 'bootstrap',
|
|
41
|
+
patterns: [/carousel-inner/i, /carousel-item/i, /data-bs-ride/i, /data-ride="carousel"/i],
|
|
42
|
+
confidence: 0.9
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
framework: 'owl-carousel',
|
|
46
|
+
patterns: [/owl-carousel/i, /owl-item/i, /owl-stage/i],
|
|
47
|
+
confidence: 0.95
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
framework: 'flickity',
|
|
51
|
+
patterns: [/flickity-slider/i, /flickity-viewport/i],
|
|
52
|
+
confidence: 0.95
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
framework: 'glide',
|
|
56
|
+
patterns: [/glide__slides/i, /glide__track/i],
|
|
57
|
+
confidence: 0.95
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
framework: 'splide',
|
|
61
|
+
patterns: [/splide__slide/i, /splide__list/i],
|
|
62
|
+
confidence: 0.95
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
const combinedContent = html + ' ' + selectors.join(' ');
|
|
66
|
+
for (const check of checks) {
|
|
67
|
+
const matches = check.patterns.filter(p => p.test(combinedContent));
|
|
68
|
+
if (matches.length >= 2) {
|
|
69
|
+
return {
|
|
70
|
+
framework: check.framework,
|
|
71
|
+
confidence: check.confidence,
|
|
72
|
+
selectors: selectors.filter(s => check.patterns.some(p => p.test(s)))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
framework: 'unknown',
|
|
78
|
+
confidence: 0.5,
|
|
79
|
+
selectors
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Calculate color contrast ratio
|
|
84
|
+
*/
|
|
85
|
+
function getContrastRatio(fg, bg) {
|
|
86
|
+
const getLuminance = (hex) => {
|
|
87
|
+
const rgb = hex.replace('#', '').match(/.{2}/g)?.map(x => parseInt(x, 16) / 255) || [0, 0, 0];
|
|
88
|
+
const [r, g, b] = rgb.map(c => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
89
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
90
|
+
};
|
|
91
|
+
const l1 = getLuminance(fg);
|
|
92
|
+
const l2 = getLuminance(bg);
|
|
93
|
+
const lighter = Math.max(l1, l2);
|
|
94
|
+
const darker = Math.min(l1, l2);
|
|
95
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Suggest accessible color alternatives
|
|
99
|
+
*/
|
|
100
|
+
function analyzeColors(foreground, background, level = 'AA') {
|
|
101
|
+
const currentRatio = getContrastRatio(foreground, background);
|
|
102
|
+
const requiredRatio = level === 'AAA' ? 7 : 4.5;
|
|
103
|
+
// Common accessible color alternatives
|
|
104
|
+
const accessibleColors = [
|
|
105
|
+
{ color: '#000000', name: 'Black' },
|
|
106
|
+
{ color: '#333333', name: 'Dark Gray' },
|
|
107
|
+
{ color: '#595959', name: 'Medium Gray' },
|
|
108
|
+
{ color: '#1a1a1a', name: 'Near Black' },
|
|
109
|
+
{ color: '#0d47a1', name: 'Dark Blue' },
|
|
110
|
+
{ color: '#1b5e20', name: 'Dark Green' },
|
|
111
|
+
{ color: '#b71c1c', name: 'Dark Red' }
|
|
112
|
+
];
|
|
113
|
+
let suggestedForeground = foreground;
|
|
114
|
+
let newRatio = currentRatio;
|
|
115
|
+
if (currentRatio < requiredRatio) {
|
|
116
|
+
// Find first color that meets contrast requirement
|
|
117
|
+
for (const { color } of accessibleColors) {
|
|
118
|
+
const ratio = getContrastRatio(color, background);
|
|
119
|
+
if (ratio >= requiredRatio) {
|
|
120
|
+
suggestedForeground = color;
|
|
121
|
+
newRatio = ratio;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
foreground,
|
|
128
|
+
background,
|
|
129
|
+
currentRatio: Math.round(currentRatio * 10) / 10,
|
|
130
|
+
requiredRatio,
|
|
131
|
+
suggestedForeground,
|
|
132
|
+
suggestedBackground: background,
|
|
133
|
+
newRatio: Math.round(newRatio * 10) / 10
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Generate video caption remediation code
|
|
138
|
+
*/
|
|
139
|
+
function generateVideoCaptionRemediation(violation, pageLanguage = 'en', pageTitle = '') {
|
|
140
|
+
const codes = [];
|
|
141
|
+
for (const element of violation.elements) {
|
|
142
|
+
const videoHtml = element.html || '<video src="video.mp4"></video>';
|
|
143
|
+
// Generate HTML fix
|
|
144
|
+
codes.push({
|
|
145
|
+
violationId: violation.id,
|
|
146
|
+
wcagCriterion: violation.wcagCriterion,
|
|
147
|
+
title: 'Add Caption Track to Video',
|
|
148
|
+
language: 'html',
|
|
149
|
+
beforeCode: videoHtml.slice(0, 200) + (videoHtml.length > 200 ? '...' : ''),
|
|
150
|
+
afterCode: `<video autoplay loop controls playsinline
|
|
151
|
+
aria-describedby="video-desc-${codes.length + 1}">
|
|
152
|
+
<source src="video.mp4" type="video/mp4">
|
|
153
|
+
|
|
154
|
+
<!-- Caption tracks for deaf/hard-of-hearing users -->
|
|
155
|
+
<track kind="captions"
|
|
156
|
+
src="/captions/video-${codes.length + 1}-${pageLanguage}.vtt"
|
|
157
|
+
srclang="${pageLanguage}"
|
|
158
|
+
label="${getLanguageName(pageLanguage)}"
|
|
159
|
+
default>
|
|
160
|
+
<track kind="captions"
|
|
161
|
+
src="/captions/video-${codes.length + 1}-en.vtt"
|
|
162
|
+
srclang="en"
|
|
163
|
+
label="English">
|
|
164
|
+
</video>
|
|
165
|
+
|
|
166
|
+
<!-- Extended description for screen readers -->
|
|
167
|
+
<div id="video-desc-${codes.length + 1}" class="visually-hidden">
|
|
168
|
+
${pageTitle ? `Video from: ${pageTitle}. ` : ''}Detailed description of the video content
|
|
169
|
+
including key visual elements, actions, and any on-screen text.
|
|
170
|
+
</div>`,
|
|
171
|
+
explanation: 'Add a <track> element with kind="captions" to provide synchronized text for deaf and hard-of-hearing users. The aria-describedby attribute links to a detailed description for screen reader users.',
|
|
172
|
+
estimatedTime: '4 hours',
|
|
173
|
+
notes: [
|
|
174
|
+
'Create WebVTT file with timestamps matching video content',
|
|
175
|
+
'Include descriptions of sounds, music, and speaker identification',
|
|
176
|
+
'Test caption display in multiple browsers'
|
|
177
|
+
],
|
|
178
|
+
relatedCriteria: ['1.2.2 Captions (Prerecorded)', '1.2.5 Audio Description']
|
|
179
|
+
});
|
|
180
|
+
// Generate WebVTT template
|
|
181
|
+
codes.push({
|
|
182
|
+
violationId: violation.id,
|
|
183
|
+
wcagCriterion: violation.wcagCriterion,
|
|
184
|
+
title: `WebVTT Caption File (${getLanguageName(pageLanguage)})`,
|
|
185
|
+
language: 'vtt',
|
|
186
|
+
beforeCode: '<!-- No captions file exists -->',
|
|
187
|
+
afterCode: `WEBVTT
|
|
188
|
+
|
|
189
|
+
NOTE ${pageTitle || 'Video'} - ${getLanguageName(pageLanguage)} Captions
|
|
190
|
+
|
|
191
|
+
00:00:00.000 --> 00:00:03.000
|
|
192
|
+
${pageTitle || '[Opening scene]'}
|
|
193
|
+
|
|
194
|
+
00:00:03.000 --> 00:00:06.000
|
|
195
|
+
[Description of visual content]
|
|
196
|
+
|
|
197
|
+
00:00:06.000 --> 00:00:09.000
|
|
198
|
+
[Ambient background music]
|
|
199
|
+
|
|
200
|
+
00:00:09.000 --> 00:00:12.000
|
|
201
|
+
[Key visual element or action]
|
|
202
|
+
|
|
203
|
+
00:00:12.000 --> 00:00:15.000
|
|
204
|
+
[On-screen text or dialogue]
|
|
205
|
+
|
|
206
|
+
00:00:15.000 --> 00:00:18.000
|
|
207
|
+
[Continuing description]
|
|
208
|
+
|
|
209
|
+
00:00:18.000 --> 00:00:21.000
|
|
210
|
+
[Important visual details]
|
|
211
|
+
|
|
212
|
+
00:00:21.000 --> 00:00:24.000
|
|
213
|
+
[Conclusion or call-to-action]`,
|
|
214
|
+
explanation: 'WebVTT (Web Video Text Tracks) is the standard format for video captions. Each cue has a timestamp and text content.',
|
|
215
|
+
estimatedTime: '2 hours',
|
|
216
|
+
notes: [
|
|
217
|
+
'Timestamps should match video content precisely',
|
|
218
|
+
'Include [brackets] for non-speech audio like music or sounds',
|
|
219
|
+
'Keep lines under 42 characters for readability'
|
|
220
|
+
]
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return codes;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Generate ARIA hidden focus remediation code
|
|
227
|
+
*/
|
|
228
|
+
function generateAriaHiddenFocusRemediation(violation, framework) {
|
|
229
|
+
const codes = [];
|
|
230
|
+
// HTML fix
|
|
231
|
+
const element = violation.elements[0];
|
|
232
|
+
codes.push({
|
|
233
|
+
violationId: violation.id,
|
|
234
|
+
wcagCriterion: violation.wcagCriterion,
|
|
235
|
+
title: 'Remove Focusable Elements from ARIA Hidden Container',
|
|
236
|
+
language: 'html',
|
|
237
|
+
beforeCode: `<div aria-hidden="true">
|
|
238
|
+
<!-- PROBLEM: Focusable elements inside hidden container -->
|
|
239
|
+
<a href="/link">Link Text</a>
|
|
240
|
+
<button>Button</button>
|
|
241
|
+
</div>`,
|
|
242
|
+
afterCode: `<div aria-hidden="true">
|
|
243
|
+
<!-- FIX: Add tabindex="-1" to prevent keyboard focus -->
|
|
244
|
+
<a href="/link" tabindex="-1">Link Text</a>
|
|
245
|
+
<button tabindex="-1">Button</button>
|
|
246
|
+
</div>`,
|
|
247
|
+
explanation: 'Elements with aria-hidden="true" are hidden from assistive technology but can still receive keyboard focus. Adding tabindex="-1" prevents keyboard focus while maintaining the visual layout.',
|
|
248
|
+
estimatedTime: '30 minutes',
|
|
249
|
+
notes: [
|
|
250
|
+
'Apply to all focusable elements: a, button, input, select, textarea, [tabindex]',
|
|
251
|
+
'Remember to restore tabindex when elements become visible'
|
|
252
|
+
],
|
|
253
|
+
relatedCriteria: ['4.1.2 Name, Role, Value', '2.1.1 Keyboard']
|
|
254
|
+
});
|
|
255
|
+
// Framework-specific JavaScript fix
|
|
256
|
+
if (framework.framework !== 'unknown') {
|
|
257
|
+
codes.push(generateFrameworkFix(framework));
|
|
258
|
+
}
|
|
259
|
+
return codes;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Generate framework-specific JavaScript fix
|
|
263
|
+
*/
|
|
264
|
+
function generateFrameworkFix(framework) {
|
|
265
|
+
const fixes = {
|
|
266
|
+
slick: {
|
|
267
|
+
code: `/**
|
|
268
|
+
* Slick Carousel Accessibility Fix
|
|
269
|
+
* Prevents focus on hidden slides
|
|
270
|
+
*/
|
|
271
|
+
(function() {
|
|
272
|
+
'use strict';
|
|
273
|
+
|
|
274
|
+
function fixSlickA11y() {
|
|
275
|
+
const carousels = document.querySelectorAll('.slick-slider');
|
|
276
|
+
|
|
277
|
+
carousels.forEach(carousel => {
|
|
278
|
+
updateFocusability(carousel);
|
|
279
|
+
|
|
280
|
+
$(carousel).on('afterChange', function() {
|
|
281
|
+
updateFocusability(carousel);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function updateFocusability(carousel) {
|
|
287
|
+
const focusable = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
288
|
+
|
|
289
|
+
// Hidden slides - remove from tab order
|
|
290
|
+
carousel.querySelectorAll('.slick-slide[aria-hidden="true"]').forEach(slide => {
|
|
291
|
+
slide.querySelectorAll(focusable).forEach(el => {
|
|
292
|
+
el.setAttribute('tabindex', '-1');
|
|
293
|
+
el.setAttribute('data-a11y-restored-tabindex', el.getAttribute('tabindex') || '0');
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Visible slides - restore tab order
|
|
298
|
+
carousel.querySelectorAll('.slick-slide[aria-hidden="false"], .slick-active').forEach(slide => {
|
|
299
|
+
slide.querySelectorAll('[data-a11y-restored-tabindex]').forEach(el => {
|
|
300
|
+
const original = el.getAttribute('data-a11y-restored-tabindex');
|
|
301
|
+
if (original === '0') {
|
|
302
|
+
el.removeAttribute('tabindex');
|
|
303
|
+
} else {
|
|
304
|
+
el.setAttribute('tabindex', original);
|
|
305
|
+
}
|
|
306
|
+
el.removeAttribute('data-a11y-restored-tabindex');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (document.readyState === 'loading') {
|
|
312
|
+
document.addEventListener('DOMContentLoaded', fixSlickA11y);
|
|
313
|
+
} else {
|
|
314
|
+
fixSlickA11y();
|
|
315
|
+
}
|
|
316
|
+
})();`,
|
|
317
|
+
notes: ['Requires jQuery (Slick dependency)', 'Hooks into afterChange event']
|
|
318
|
+
},
|
|
319
|
+
swiper: {
|
|
320
|
+
code: `/**
|
|
321
|
+
* Swiper Accessibility Fix
|
|
322
|
+
*/
|
|
323
|
+
const swiper = new Swiper('.swiper-container', {
|
|
324
|
+
// ... existing config ...
|
|
325
|
+
|
|
326
|
+
a11y: {
|
|
327
|
+
enabled: true,
|
|
328
|
+
prevSlideMessage: 'Previous slide',
|
|
329
|
+
nextSlideMessage: 'Next slide',
|
|
330
|
+
firstSlideMessage: 'This is the first slide',
|
|
331
|
+
lastSlideMessage: 'This is the last slide',
|
|
332
|
+
paginationBulletMessage: 'Go to slide {{index}}'
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
on: {
|
|
336
|
+
slideChange: function() {
|
|
337
|
+
// Hide focusable elements in non-active slides
|
|
338
|
+
this.slides.forEach((slide, index) => {
|
|
339
|
+
const isActive = index === this.activeIndex;
|
|
340
|
+
slide.querySelectorAll('a, button').forEach(el => {
|
|
341
|
+
el.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});`,
|
|
347
|
+
notes: ['Swiper has built-in a11y module', 'Enable with a11y: { enabled: true }']
|
|
348
|
+
},
|
|
349
|
+
bootstrap: {
|
|
350
|
+
code: `/**
|
|
351
|
+
* Bootstrap Carousel Accessibility Fix
|
|
352
|
+
*/
|
|
353
|
+
document.querySelectorAll('.carousel').forEach(carousel => {
|
|
354
|
+
carousel.addEventListener('slid.bs.carousel', function() {
|
|
355
|
+
// Update tabindex on slide change
|
|
356
|
+
this.querySelectorAll('.carousel-item').forEach(item => {
|
|
357
|
+
const isActive = item.classList.contains('active');
|
|
358
|
+
item.querySelectorAll('a, button').forEach(el => {
|
|
359
|
+
el.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Initial fix
|
|
365
|
+
carousel.querySelectorAll('.carousel-item:not(.active) a, .carousel-item:not(.active) button')
|
|
366
|
+
.forEach(el => el.setAttribute('tabindex', '-1'));
|
|
367
|
+
});`,
|
|
368
|
+
notes: ['Uses Bootstrap 5 event system', 'Adjust for Bootstrap 4 if needed']
|
|
369
|
+
},
|
|
370
|
+
'owl-carousel': {
|
|
371
|
+
code: `/**
|
|
372
|
+
* Owl Carousel Accessibility Fix
|
|
373
|
+
*/
|
|
374
|
+
$('.owl-carousel').on('changed.owl.carousel', function(event) {
|
|
375
|
+
$(this).find('.owl-item').each(function() {
|
|
376
|
+
const isActive = $(this).hasClass('active');
|
|
377
|
+
$(this).find('a, button').attr('tabindex', isActive ? '0' : '-1');
|
|
378
|
+
});
|
|
379
|
+
});`,
|
|
380
|
+
notes: ['Requires jQuery', 'Uses Owl Carousel event system']
|
|
381
|
+
},
|
|
382
|
+
flickity: {
|
|
383
|
+
code: `/**
|
|
384
|
+
* Flickity Accessibility Fix
|
|
385
|
+
*/
|
|
386
|
+
const flkty = new Flickity('.carousel', {
|
|
387
|
+
// ... existing config ...
|
|
388
|
+
accessibility: true
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
flkty.on('change', function(index) {
|
|
392
|
+
flkty.cells.forEach((cell, i) => {
|
|
393
|
+
const isActive = i === index;
|
|
394
|
+
cell.element.querySelectorAll('a, button').forEach(el => {
|
|
395
|
+
el.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});`,
|
|
399
|
+
notes: ['Flickity has built-in accessibility option']
|
|
400
|
+
},
|
|
401
|
+
glide: {
|
|
402
|
+
code: `/**
|
|
403
|
+
* Glide.js Accessibility Fix
|
|
404
|
+
*/
|
|
405
|
+
const glide = new Glide('.glide').mount();
|
|
406
|
+
|
|
407
|
+
glide.on('run.after', function() {
|
|
408
|
+
document.querySelectorAll('.glide__slide').forEach((slide, index) => {
|
|
409
|
+
const isActive = index === glide.index;
|
|
410
|
+
slide.querySelectorAll('a, button').forEach(el => {
|
|
411
|
+
el.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});`,
|
|
415
|
+
notes: ['Uses Glide.js event system']
|
|
416
|
+
},
|
|
417
|
+
splide: {
|
|
418
|
+
code: `/**
|
|
419
|
+
* Splide Accessibility Fix
|
|
420
|
+
*/
|
|
421
|
+
const splide = new Splide('.splide', {
|
|
422
|
+
// ... existing config ...
|
|
423
|
+
accessibility: true
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
splide.on('moved', function(newIndex) {
|
|
427
|
+
splide.Components.Slides.forEach((slide, index) => {
|
|
428
|
+
const isActive = index === newIndex;
|
|
429
|
+
slide.slide.querySelectorAll('a, button').forEach(el => {
|
|
430
|
+
el.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
splide.mount();`,
|
|
436
|
+
notes: ['Splide has built-in accessibility support']
|
|
437
|
+
},
|
|
438
|
+
vanilla: {
|
|
439
|
+
code: `/**
|
|
440
|
+
* Vanilla JavaScript Carousel Accessibility Fix
|
|
441
|
+
*/
|
|
442
|
+
function fixCarouselA11y(carousel) {
|
|
443
|
+
const slides = carousel.querySelectorAll('[data-slide]');
|
|
444
|
+
const focusable = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
445
|
+
|
|
446
|
+
function updateFocusability() {
|
|
447
|
+
slides.forEach(slide => {
|
|
448
|
+
const isHidden = slide.getAttribute('aria-hidden') === 'true';
|
|
449
|
+
slide.querySelectorAll(focusable).forEach(el => {
|
|
450
|
+
el.setAttribute('tabindex', isHidden ? '-1' : '0');
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Watch for changes
|
|
456
|
+
const observer = new MutationObserver(updateFocusability);
|
|
457
|
+
slides.forEach(slide => {
|
|
458
|
+
observer.observe(slide, { attributes: true, attributeFilter: ['aria-hidden'] });
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
updateFocusability();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
document.querySelectorAll('.carousel').forEach(fixCarouselA11y);`,
|
|
465
|
+
notes: ['No dependencies', 'Uses MutationObserver for automatic updates']
|
|
466
|
+
},
|
|
467
|
+
unknown: {
|
|
468
|
+
code: `/**
|
|
469
|
+
* Generic Carousel Accessibility Fix
|
|
470
|
+
*/
|
|
471
|
+
function fixCarouselA11y() {
|
|
472
|
+
const hiddenContainers = document.querySelectorAll('[aria-hidden="true"]');
|
|
473
|
+
const focusable = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
474
|
+
|
|
475
|
+
hiddenContainers.forEach(container => {
|
|
476
|
+
container.querySelectorAll(focusable).forEach(el => {
|
|
477
|
+
el.setAttribute('tabindex', '-1');
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Run on load and after any dynamic content changes
|
|
483
|
+
fixCarouselA11y();`,
|
|
484
|
+
notes: ['Generic solution for unknown frameworks', 'May need customization']
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
const fix = fixes[framework.framework];
|
|
488
|
+
return {
|
|
489
|
+
violationId: 'aria-hidden-focus',
|
|
490
|
+
wcagCriterion: '4.1.2',
|
|
491
|
+
title: `${capitalizeFramework(framework.framework)} Carousel Accessibility Fix`,
|
|
492
|
+
language: 'javascript',
|
|
493
|
+
beforeCode: '// No accessibility handling',
|
|
494
|
+
afterCode: fix.code,
|
|
495
|
+
explanation: `Framework-specific fix for ${framework.framework} carousel. This script automatically manages tabindex on focusable elements within hidden slides.`,
|
|
496
|
+
estimatedTime: '1 hour',
|
|
497
|
+
notes: fix.notes,
|
|
498
|
+
relatedCriteria: ['4.1.2 Name, Role, Value', '2.1.1 Keyboard', '2.4.3 Focus Order']
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Generate color contrast remediation code
|
|
503
|
+
*/
|
|
504
|
+
function generateColorContrastRemediation(violation, colorAnalysis) {
|
|
505
|
+
const codes = [];
|
|
506
|
+
const element = violation.elements[0];
|
|
507
|
+
const selector = element.selector || '.element';
|
|
508
|
+
codes.push({
|
|
509
|
+
violationId: violation.id,
|
|
510
|
+
wcagCriterion: violation.wcagCriterion,
|
|
511
|
+
title: 'Fix Color Contrast',
|
|
512
|
+
language: 'css',
|
|
513
|
+
beforeCode: `${selector} {
|
|
514
|
+
color: ${colorAnalysis?.foreground || '#999999'}; /* Contrast: ${colorAnalysis?.currentRatio || '< 4.5'}:1 - FAILS */
|
|
515
|
+
background: ${colorAnalysis?.background || '#ffffff'};
|
|
516
|
+
}`,
|
|
517
|
+
afterCode: `${selector} {
|
|
518
|
+
color: ${colorAnalysis?.suggestedForeground || '#595959'}; /* Contrast: ${colorAnalysis?.newRatio || '7'}:1 - PASSES */
|
|
519
|
+
background: ${colorAnalysis?.suggestedBackground || '#ffffff'};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* Alternative: High contrast option */
|
|
523
|
+
${selector}--high-contrast {
|
|
524
|
+
color: #000000; /* Contrast: 21:1 - WCAG AAA */
|
|
525
|
+
background: #ffffff;
|
|
526
|
+
}`,
|
|
527
|
+
explanation: 'Increase the contrast ratio between foreground (text) and background colors to meet WCAG requirements. AA requires 4.5:1 for normal text, AAA requires 7:1.',
|
|
528
|
+
estimatedTime: '2 hours',
|
|
529
|
+
notes: [
|
|
530
|
+
'Test with WebAIM Contrast Checker',
|
|
531
|
+
'Consider users with low vision and color blindness',
|
|
532
|
+
'Check in different lighting conditions'
|
|
533
|
+
],
|
|
534
|
+
relatedCriteria: ['1.4.3 Contrast (Minimum)', '1.4.6 Contrast (Enhanced)']
|
|
535
|
+
});
|
|
536
|
+
// Add CSS custom properties for consistent theming
|
|
537
|
+
codes.push({
|
|
538
|
+
violationId: violation.id,
|
|
539
|
+
wcagCriterion: violation.wcagCriterion,
|
|
540
|
+
title: 'Accessible Color System (CSS Custom Properties)',
|
|
541
|
+
language: 'css',
|
|
542
|
+
beforeCode: '/* No color system defined */',
|
|
543
|
+
afterCode: `:root {
|
|
544
|
+
/* Primary Colors - WCAG AAA on white */
|
|
545
|
+
--color-text-primary: #000000; /* 21:1 on white */
|
|
546
|
+
--color-text-secondary: #333333; /* 12.6:1 on white */
|
|
547
|
+
--color-text-muted: #595959; /* 7:1 on white - AAA minimum */
|
|
548
|
+
|
|
549
|
+
/* Background Colors */
|
|
550
|
+
--color-bg-primary: #ffffff;
|
|
551
|
+
--color-bg-secondary: #f5f5f5;
|
|
552
|
+
|
|
553
|
+
/* Interactive Colors - WCAG AA on white */
|
|
554
|
+
--color-link: #0d47a1; /* 8.5:1 on white */
|
|
555
|
+
--color-link-hover: #1565c0; /* 6.4:1 on white */
|
|
556
|
+
|
|
557
|
+
/* Status Colors - WCAG AA on white */
|
|
558
|
+
--color-error: #b71c1c; /* 7.8:1 on white */
|
|
559
|
+
--color-success: #1b5e20; /* 8.2:1 on white */
|
|
560
|
+
--color-warning: #e65100; /* 4.6:1 on white - AA only */
|
|
561
|
+
|
|
562
|
+
/* NEVER use for text (decorative only) */
|
|
563
|
+
--color-decorative: #999999; /* 2.8:1 - FAILS */
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* Usage example */
|
|
567
|
+
.text-primary { color: var(--color-text-primary); }
|
|
568
|
+
.text-secondary { color: var(--color-text-secondary); }
|
|
569
|
+
.text-muted { color: var(--color-text-muted); }`,
|
|
570
|
+
explanation: 'Define a consistent color system using CSS custom properties. This ensures all text meets WCAG contrast requirements throughout the application.',
|
|
571
|
+
estimatedTime: '4 hours',
|
|
572
|
+
notes: [
|
|
573
|
+
'Document which colors are safe for text vs decorative use',
|
|
574
|
+
'Include in design system documentation',
|
|
575
|
+
'Test all color combinations'
|
|
576
|
+
]
|
|
577
|
+
});
|
|
578
|
+
return codes;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Generate Playwright test for a violation
|
|
582
|
+
*/
|
|
583
|
+
function generatePlaywrightTest(violation, url) {
|
|
584
|
+
const testName = violation.id.replace(/-/g, ' ');
|
|
585
|
+
return {
|
|
586
|
+
violationId: violation.id,
|
|
587
|
+
wcagCriterion: violation.wcagCriterion,
|
|
588
|
+
title: `Playwright Test: ${testName}`,
|
|
589
|
+
language: 'typescript',
|
|
590
|
+
beforeCode: '// No automated test exists',
|
|
591
|
+
afterCode: `import { test, expect } from '@playwright/test';
|
|
592
|
+
import AxeBuilder from '@axe-core/playwright';
|
|
593
|
+
|
|
594
|
+
test.describe('Accessibility: ${testName}', () => {
|
|
595
|
+
test('should have no ${violation.id} violations', async ({ page }) => {
|
|
596
|
+
await page.goto('${url}');
|
|
597
|
+
|
|
598
|
+
const results = await new AxeBuilder({ page })
|
|
599
|
+
.withRules(['${violation.id}'])
|
|
600
|
+
.analyze();
|
|
601
|
+
|
|
602
|
+
// Log violations for debugging
|
|
603
|
+
if (results.violations.length > 0) {
|
|
604
|
+
console.log('Violations found:');
|
|
605
|
+
results.violations.forEach(v => {
|
|
606
|
+
console.log(\`- \${v.id}: \${v.nodes.length} elements\`);
|
|
607
|
+
v.nodes.forEach(n => console.log(\` \${n.html.slice(0, 100)}\`));
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
expect(results.violations).toHaveLength(0);
|
|
612
|
+
});
|
|
613
|
+
${violation.id === 'video-caption' ? `
|
|
614
|
+
test('videos should have caption tracks', async ({ page }) => {
|
|
615
|
+
await page.goto('${url}');
|
|
616
|
+
|
|
617
|
+
const videos = await page.locator('video').all();
|
|
618
|
+
|
|
619
|
+
for (const video of videos) {
|
|
620
|
+
const captionTrack = video.locator('track[kind="captions"]');
|
|
621
|
+
await expect(captionTrack).toHaveCount(1, {
|
|
622
|
+
message: 'Video must have a caption track'
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
});` : ''}
|
|
626
|
+
${violation.id.includes('aria-hidden') ? `
|
|
627
|
+
test('no focus should be possible on aria-hidden elements', async ({ page }) => {
|
|
628
|
+
await page.goto('${url}');
|
|
629
|
+
|
|
630
|
+
// Tab through the page
|
|
631
|
+
for (let i = 0; i < 50; i++) {
|
|
632
|
+
await page.keyboard.press('Tab');
|
|
633
|
+
|
|
634
|
+
// Check if focused element is inside aria-hidden container
|
|
635
|
+
const isHiddenFocused = await page.evaluate(() => {
|
|
636
|
+
const focused = document.activeElement;
|
|
637
|
+
return focused?.closest('[aria-hidden="true"]') !== null;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
expect(isHiddenFocused).toBe(false);
|
|
641
|
+
}
|
|
642
|
+
});` : ''}
|
|
643
|
+
});`,
|
|
644
|
+
explanation: 'Automated Playwright test using axe-core to verify the accessibility violation is fixed. Run this test in CI/CD to prevent regressions.',
|
|
645
|
+
estimatedTime: '30 minutes',
|
|
646
|
+
notes: [
|
|
647
|
+
'Add to your CI/CD pipeline',
|
|
648
|
+
'Run before each deployment',
|
|
649
|
+
'Requires @axe-core/playwright package'
|
|
650
|
+
]
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Generate complete CSS utilities for accessibility
|
|
655
|
+
*/
|
|
656
|
+
function generateAccessibilityCSSUtilities() {
|
|
657
|
+
return {
|
|
658
|
+
violationId: 'utilities',
|
|
659
|
+
wcagCriterion: 'Multiple',
|
|
660
|
+
title: 'Accessibility CSS Utilities',
|
|
661
|
+
language: 'css',
|
|
662
|
+
beforeCode: '/* No accessibility utilities */',
|
|
663
|
+
afterCode: `/**
|
|
664
|
+
* Accessibility CSS Utilities
|
|
665
|
+
* Include in your global stylesheet
|
|
666
|
+
*/
|
|
667
|
+
|
|
668
|
+
/* ==========================================================================
|
|
669
|
+
Screen Reader Only (Visually Hidden)
|
|
670
|
+
========================================================================== */
|
|
671
|
+
|
|
672
|
+
.visually-hidden,
|
|
673
|
+
.sr-only {
|
|
674
|
+
position: absolute !important;
|
|
675
|
+
width: 1px !important;
|
|
676
|
+
height: 1px !important;
|
|
677
|
+
padding: 0 !important;
|
|
678
|
+
margin: -1px !important;
|
|
679
|
+
overflow: hidden !important;
|
|
680
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
681
|
+
white-space: nowrap !important;
|
|
682
|
+
border: 0 !important;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/* Focusable variant for skip links */
|
|
686
|
+
.visually-hidden.focusable:focus,
|
|
687
|
+
.sr-only.focusable:focus {
|
|
688
|
+
position: static !important;
|
|
689
|
+
width: auto !important;
|
|
690
|
+
height: auto !important;
|
|
691
|
+
overflow: visible !important;
|
|
692
|
+
clip: auto !important;
|
|
693
|
+
white-space: normal !important;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* ==========================================================================
|
|
697
|
+
Skip Link
|
|
698
|
+
========================================================================== */
|
|
699
|
+
|
|
700
|
+
.skip-link {
|
|
701
|
+
position: absolute;
|
|
702
|
+
top: -100%;
|
|
703
|
+
left: 50%;
|
|
704
|
+
transform: translateX(-50%);
|
|
705
|
+
background: #000;
|
|
706
|
+
color: #fff;
|
|
707
|
+
padding: 1rem 2rem;
|
|
708
|
+
text-decoration: none;
|
|
709
|
+
font-weight: bold;
|
|
710
|
+
z-index: 10000;
|
|
711
|
+
transition: top 0.2s ease;
|
|
712
|
+
border-radius: 0 0 4px 4px;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.skip-link:focus {
|
|
716
|
+
top: 0;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* ==========================================================================
|
|
720
|
+
Focus Indicators
|
|
721
|
+
========================================================================== */
|
|
722
|
+
|
|
723
|
+
/* High-visibility focus for all interactive elements */
|
|
724
|
+
:focus-visible {
|
|
725
|
+
outline: 3px solid #0066cc !important;
|
|
726
|
+
outline-offset: 2px !important;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/* Remove default outline, keep focus-visible */
|
|
730
|
+
:focus:not(:focus-visible) {
|
|
731
|
+
outline: none;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/* Focus within for complex components */
|
|
735
|
+
.focus-within:focus-within {
|
|
736
|
+
outline: 2px solid #0066cc;
|
|
737
|
+
outline-offset: 2px;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/* ==========================================================================
|
|
741
|
+
Reduced Motion
|
|
742
|
+
========================================================================== */
|
|
743
|
+
|
|
744
|
+
@media (prefers-reduced-motion: reduce) {
|
|
745
|
+
*,
|
|
746
|
+
*::before,
|
|
747
|
+
*::after {
|
|
748
|
+
animation-duration: 0.01ms !important;
|
|
749
|
+
animation-iteration-count: 1 !important;
|
|
750
|
+
transition-duration: 0.01ms !important;
|
|
751
|
+
scroll-behavior: auto !important;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* ==========================================================================
|
|
756
|
+
High Contrast Mode Support
|
|
757
|
+
========================================================================== */
|
|
758
|
+
|
|
759
|
+
@media (prefers-contrast: high) {
|
|
760
|
+
:root {
|
|
761
|
+
--color-text: #000;
|
|
762
|
+
--color-bg: #fff;
|
|
763
|
+
--color-link: #0000EE;
|
|
764
|
+
--color-border: #000;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
* {
|
|
768
|
+
border-color: var(--color-border) !important;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/* ==========================================================================
|
|
773
|
+
Print Styles for Accessibility
|
|
774
|
+
========================================================================== */
|
|
775
|
+
|
|
776
|
+
@media print {
|
|
777
|
+
.visually-hidden,
|
|
778
|
+
.sr-only {
|
|
779
|
+
position: static !important;
|
|
780
|
+
width: auto !important;
|
|
781
|
+
height: auto !important;
|
|
782
|
+
overflow: visible !important;
|
|
783
|
+
clip: auto !important;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
a[href]::after {
|
|
787
|
+
content: " (" attr(href) ")";
|
|
788
|
+
}
|
|
789
|
+
}`,
|
|
790
|
+
explanation: 'Reusable CSS utilities for common accessibility patterns. Include this in your global stylesheet.',
|
|
791
|
+
estimatedTime: '1 hour',
|
|
792
|
+
notes: [
|
|
793
|
+
'Add skip link HTML: <a href="#main-content" class="skip-link">Skip to content</a>',
|
|
794
|
+
'Use .visually-hidden for screen reader only content',
|
|
795
|
+
'Supports prefers-reduced-motion and prefers-contrast'
|
|
796
|
+
]
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Get human-readable language name
|
|
801
|
+
*/
|
|
802
|
+
function getLanguageName(code) {
|
|
803
|
+
const languages = {
|
|
804
|
+
en: 'English',
|
|
805
|
+
de: 'Deutsch',
|
|
806
|
+
fr: 'Français',
|
|
807
|
+
es: 'Español',
|
|
808
|
+
it: 'Italiano',
|
|
809
|
+
pt: 'Português',
|
|
810
|
+
nl: 'Nederlands',
|
|
811
|
+
pl: 'Polski',
|
|
812
|
+
ru: 'Русский',
|
|
813
|
+
zh: '中文',
|
|
814
|
+
ja: '日本語',
|
|
815
|
+
ko: '한국어'
|
|
816
|
+
};
|
|
817
|
+
return languages[code] || code.toUpperCase();
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Capitalize framework name
|
|
821
|
+
*/
|
|
822
|
+
function capitalizeFramework(framework) {
|
|
823
|
+
const names = {
|
|
824
|
+
slick: 'Slick',
|
|
825
|
+
swiper: 'Swiper',
|
|
826
|
+
bootstrap: 'Bootstrap',
|
|
827
|
+
'owl-carousel': 'Owl Carousel',
|
|
828
|
+
flickity: 'Flickity',
|
|
829
|
+
glide: 'Glide.js',
|
|
830
|
+
splide: 'Splide',
|
|
831
|
+
vanilla: 'Vanilla JS',
|
|
832
|
+
unknown: 'Generic'
|
|
833
|
+
};
|
|
834
|
+
return names[framework];
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Generate all remediation codes for a violation
|
|
838
|
+
*/
|
|
839
|
+
function generateRemediationCodes(violation, options = {}) {
|
|
840
|
+
const codes = [];
|
|
841
|
+
const { url = '', pageLanguage = 'en', pageTitle = '', framework, colorAnalysis } = options;
|
|
842
|
+
const pageContext = { url, pageLanguage, pageTitle };
|
|
843
|
+
// Route to appropriate generator based on violation type
|
|
844
|
+
const violationId = violation.id.toLowerCase();
|
|
845
|
+
const description = violation.description.toLowerCase();
|
|
846
|
+
// Video caption violations
|
|
847
|
+
if (violationId.includes('video') || violation.wcagCriterion?.includes('1.2')) {
|
|
848
|
+
codes.push(...generateVideoCaptionRemediation(violation, pageLanguage, pageTitle));
|
|
849
|
+
}
|
|
850
|
+
// Image alt text violations
|
|
851
|
+
if (violationId.includes('image-alt') || violationId.includes('alt') ||
|
|
852
|
+
description.includes('alternative text') || description.includes('alt text') ||
|
|
853
|
+
description.includes('<img>') || violation.wcagCriterion === '1.1.1') {
|
|
854
|
+
codes.push(...generateImageAltRemediation(violation, pageContext));
|
|
855
|
+
}
|
|
856
|
+
// Link name violations
|
|
857
|
+
if (violationId.includes('link-name') || violationId.includes('link') ||
|
|
858
|
+
description.includes('link') && description.includes('text') ||
|
|
859
|
+
description.includes('discernible text') ||
|
|
860
|
+
violation.wcagCriterion === '2.4.4') {
|
|
861
|
+
codes.push(...generateLinkNameRemediation(violation, pageContext));
|
|
862
|
+
}
|
|
863
|
+
// List structure violations
|
|
864
|
+
if (violationId.includes('list') ||
|
|
865
|
+
description.includes('<ul>') || description.includes('<ol>') ||
|
|
866
|
+
description.includes('<li>') || description.includes('list') ||
|
|
867
|
+
violation.wcagCriterion === '1.3.1' && description.includes('list')) {
|
|
868
|
+
codes.push(...generateListStructureRemediation(violation));
|
|
869
|
+
}
|
|
870
|
+
// Touch target size violations
|
|
871
|
+
if (violationId.includes('target-size') || violationId.includes('touch') ||
|
|
872
|
+
description.includes('touch target') || description.includes('target size') ||
|
|
873
|
+
violation.wcagCriterion === '2.5.8') {
|
|
874
|
+
codes.push(...generateTouchTargetRemediation(violation));
|
|
875
|
+
}
|
|
876
|
+
// ARIA hidden focus violations
|
|
877
|
+
if (violationId.includes('aria-hidden') || violationId.includes('focus')) {
|
|
878
|
+
const detectedFramework = framework || detectFramework(violation.elements.map(e => e.html).join(' '), violation.elements.map(e => e.selector));
|
|
879
|
+
codes.push(...generateAriaHiddenFocusRemediation(violation, detectedFramework));
|
|
880
|
+
}
|
|
881
|
+
// Color contrast violations
|
|
882
|
+
if (violationId.includes('color-contrast') || violationId.includes('contrast')) {
|
|
883
|
+
codes.push(...generateColorContrastRemediation(violation, colorAnalysis));
|
|
884
|
+
}
|
|
885
|
+
// Always generate a Playwright test
|
|
886
|
+
if (url) {
|
|
887
|
+
codes.push(generatePlaywrightTest(violation, url));
|
|
888
|
+
}
|
|
889
|
+
return codes;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Generate image alt text remediation with context-specific suggestions
|
|
893
|
+
*/
|
|
894
|
+
function generateImageAltRemediation(violation, pageContext = {}) {
|
|
895
|
+
const codes = [];
|
|
896
|
+
const { url = '', pageTitle = '', pageLanguage = 'en' } = pageContext;
|
|
897
|
+
// Extract domain/brand context from URL
|
|
898
|
+
let brandContext = '';
|
|
899
|
+
if (url) {
|
|
900
|
+
try {
|
|
901
|
+
const urlObj = new URL(url);
|
|
902
|
+
const domain = urlObj.hostname.replace('www.', '');
|
|
903
|
+
brandContext = domain.split('.')[0];
|
|
904
|
+
// Capitalize first letter
|
|
905
|
+
brandContext = brandContext.charAt(0).toUpperCase() + brandContext.slice(1);
|
|
906
|
+
}
|
|
907
|
+
catch { /* ignore */ }
|
|
908
|
+
}
|
|
909
|
+
for (const element of violation.elements) {
|
|
910
|
+
const html = element.html || '<img src="image.jpg">';
|
|
911
|
+
const selector = element.selector || 'img';
|
|
912
|
+
const context = element.context;
|
|
913
|
+
// Extract image src
|
|
914
|
+
const srcMatch = html.match(/src="([^"]+)"/);
|
|
915
|
+
const src = srcMatch ? srcMatch[1] : '';
|
|
916
|
+
// Extract parent link context
|
|
917
|
+
let parentLinkText = '';
|
|
918
|
+
let parentLinkHref = '';
|
|
919
|
+
// Check for parent link in selector or HTML
|
|
920
|
+
const ariaLabelMatch = selector.match(/aria-label=["']([^"']+)["']/i) ||
|
|
921
|
+
html.match(/aria-label=["']([^"']+)["']/i);
|
|
922
|
+
if (ariaLabelMatch) {
|
|
923
|
+
parentLinkText = ariaLabelMatch[1];
|
|
924
|
+
}
|
|
925
|
+
const hrefMatch = html.match(/href="([^"]+)"/);
|
|
926
|
+
if (hrefMatch) {
|
|
927
|
+
parentLinkHref = hrefMatch[1];
|
|
928
|
+
}
|
|
929
|
+
// Generate context-specific alt text
|
|
930
|
+
let specificAlt = '';
|
|
931
|
+
let rationale = '';
|
|
932
|
+
// Priority 1: Use parent link context
|
|
933
|
+
if (parentLinkText) {
|
|
934
|
+
specificAlt = `Visual representation of: ${parentLinkText}`;
|
|
935
|
+
rationale = `Parent link says "${parentLinkText}". Alt text should describe what the image shows in this context.`;
|
|
936
|
+
}
|
|
937
|
+
// Priority 2: Analyze image filename
|
|
938
|
+
else if (src) {
|
|
939
|
+
const filename = src.split('/').pop()?.split('?')[0] || '';
|
|
940
|
+
const cleanName = filename
|
|
941
|
+
.replace(/\.(jpg|jpeg|png|svg|webp|gif|avif)$/i, '')
|
|
942
|
+
.replace(/[-_]/g, ' ')
|
|
943
|
+
.replace(/\d{4,}/g, '') // Remove long numbers
|
|
944
|
+
.trim();
|
|
945
|
+
// Check for common patterns in filename
|
|
946
|
+
if (src.toLowerCase().includes('logo')) {
|
|
947
|
+
specificAlt = `${brandContext || 'Company'} logo`;
|
|
948
|
+
rationale = 'Image URL contains "logo" - this appears to be a brand logo.';
|
|
949
|
+
}
|
|
950
|
+
else if (src.toLowerCase().includes('hero') || src.toLowerCase().includes('banner')) {
|
|
951
|
+
specificAlt = `${brandContext || 'Website'} hero banner - [describe the main subject and action]`;
|
|
952
|
+
rationale = 'This is a hero/banner image. Describe the key visual message it conveys.';
|
|
953
|
+
}
|
|
954
|
+
else if (src.toLowerCase().includes('product') || src.toLowerCase().includes('item')) {
|
|
955
|
+
specificAlt = `${brandContext || 'Product'} - [product name and key features]`;
|
|
956
|
+
rationale = 'This appears to be a product image. Include product name and distinguishing features.';
|
|
957
|
+
}
|
|
958
|
+
else if (src.toLowerCase().includes('team') || src.toLowerCase().includes('person') || src.toLowerCase().includes('portrait')) {
|
|
959
|
+
specificAlt = `[Person's name and role/title]`;
|
|
960
|
+
rationale = 'This appears to be a person/team photo. Include the person\'s name and role.';
|
|
961
|
+
}
|
|
962
|
+
else if (cleanName && cleanName.length > 3) {
|
|
963
|
+
specificAlt = cleanName;
|
|
964
|
+
rationale = `Inferred from filename "${filename}". Verify this accurately describes the image.`;
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
specificAlt = `${brandContext ? brandContext + ' - ' : ''}[Describe what this image shows]`;
|
|
968
|
+
rationale = 'Unable to infer context. Manually describe what the image depicts.';
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// Priority 3: Use surrounding text context
|
|
972
|
+
else if (context?.surroundingText) {
|
|
973
|
+
const contextText = context.surroundingText.slice(0, 50).trim();
|
|
974
|
+
specificAlt = `Image related to: ${contextText}`;
|
|
975
|
+
rationale = `Based on surrounding text. Verify this matches the actual image content.`;
|
|
976
|
+
}
|
|
977
|
+
// Fallback
|
|
978
|
+
else {
|
|
979
|
+
specificAlt = `${brandContext ? brandContext + ' - ' : ''}[Describe the image content and purpose]`;
|
|
980
|
+
rationale = 'No context available. Manually describe what the image shows.';
|
|
981
|
+
}
|
|
982
|
+
codes.push({
|
|
983
|
+
violationId: violation.id,
|
|
984
|
+
wcagCriterion: violation.wcagCriterion || '1.1.1',
|
|
985
|
+
title: 'Add Descriptive Alt Text to Image',
|
|
986
|
+
language: 'html',
|
|
987
|
+
beforeCode: html.slice(0, 200) + (html.length > 200 ? '...' : ''),
|
|
988
|
+
afterCode: `<!-- Context: ${rationale} -->
|
|
989
|
+
<img src="${src || 'image.jpg'}"
|
|
990
|
+
alt="${specificAlt}"
|
|
991
|
+
loading="lazy">
|
|
992
|
+
|
|
993
|
+
<!-- If this is a decorative image with no informational content: -->
|
|
994
|
+
<img src="${src || 'image.jpg'}"
|
|
995
|
+
alt=""
|
|
996
|
+
role="presentation">`,
|
|
997
|
+
explanation: rationale,
|
|
998
|
+
estimatedTime: '5 minutes per image',
|
|
999
|
+
notes: [
|
|
1000
|
+
'Alt text should describe what the image shows, not be a caption',
|
|
1001
|
+
'For decorative images, use alt="" (empty string)',
|
|
1002
|
+
'Keep alt text under 125 characters for screen reader compatibility',
|
|
1003
|
+
`If image is inside a link, alt should describe the link destination`
|
|
1004
|
+
],
|
|
1005
|
+
relatedCriteria: ['1.1.1 Non-text Content']
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
return codes;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Generate link name remediation with context-specific aria-labels
|
|
1012
|
+
*/
|
|
1013
|
+
function generateLinkNameRemediation(violation, pageContext = {}) {
|
|
1014
|
+
const codes = [];
|
|
1015
|
+
const { url = '', pageTitle = '', pageLanguage = 'en' } = pageContext;
|
|
1016
|
+
// Extract domain for brand context
|
|
1017
|
+
let brandContext = '';
|
|
1018
|
+
if (url) {
|
|
1019
|
+
try {
|
|
1020
|
+
const urlObj = new URL(url);
|
|
1021
|
+
brandContext = urlObj.hostname.replace('www.', '').split('.')[0];
|
|
1022
|
+
brandContext = brandContext.charAt(0).toUpperCase() + brandContext.slice(1);
|
|
1023
|
+
}
|
|
1024
|
+
catch { /* ignore */ }
|
|
1025
|
+
}
|
|
1026
|
+
for (const element of violation.elements) {
|
|
1027
|
+
const html = element.html || '<a href="#">Link</a>';
|
|
1028
|
+
const selector = element.selector || 'a';
|
|
1029
|
+
const context = element.context;
|
|
1030
|
+
// Extract link href
|
|
1031
|
+
const hrefMatch = html.match(/href="([^"]+)"/);
|
|
1032
|
+
const href = hrefMatch ? hrefMatch[1] : '#';
|
|
1033
|
+
// Extract existing title (often empty)
|
|
1034
|
+
const titleMatch = html.match(/title="([^"]*)"/);
|
|
1035
|
+
const existingTitle = titleMatch ? titleMatch[1] : '';
|
|
1036
|
+
// Generate context-specific aria-label
|
|
1037
|
+
let specificLabel = '';
|
|
1038
|
+
let rationale = '';
|
|
1039
|
+
// Priority 1: Analyze href for destination context
|
|
1040
|
+
if (href && href !== '#') {
|
|
1041
|
+
const hrefLower = href.toLowerCase();
|
|
1042
|
+
// Product/category pages
|
|
1043
|
+
if (hrefLower.includes('/produkt') || hrefLower.includes('/product')) {
|
|
1044
|
+
const productSlug = href.split('/').filter(Boolean).pop() || '';
|
|
1045
|
+
const productName = productSlug.replace(/-/g, ' ').replace(/\..*$/, '');
|
|
1046
|
+
specificLabel = `View ${productName || 'product'} details`;
|
|
1047
|
+
rationale = `Link goes to product page: ${href}`;
|
|
1048
|
+
}
|
|
1049
|
+
// Solution/service pages
|
|
1050
|
+
else if (hrefLower.includes('/lösung') || hrefLower.includes('/solution') || hrefLower.includes('/service')) {
|
|
1051
|
+
const solutionSlug = href.split('/').filter(Boolean).pop() || '';
|
|
1052
|
+
const solutionName = solutionSlug.replace(/-/g, ' ').replace(/\..*$/, '');
|
|
1053
|
+
specificLabel = `Learn about ${solutionName || 'our solutions'}`;
|
|
1054
|
+
rationale = `Link goes to solutions/services page: ${href}`;
|
|
1055
|
+
}
|
|
1056
|
+
// About pages
|
|
1057
|
+
else if (hrefLower.includes('/about') || hrefLower.includes('/über') || hrefLower.includes('/ueber')) {
|
|
1058
|
+
specificLabel = `Learn about ${brandContext || 'us'}`;
|
|
1059
|
+
rationale = `Link goes to about page: ${href}`;
|
|
1060
|
+
}
|
|
1061
|
+
// Contact pages
|
|
1062
|
+
else if (hrefLower.includes('/contact') || hrefLower.includes('/kontakt')) {
|
|
1063
|
+
specificLabel = `Contact ${brandContext || 'us'}`;
|
|
1064
|
+
rationale = `Link goes to contact page: ${href}`;
|
|
1065
|
+
}
|
|
1066
|
+
// News/blog pages
|
|
1067
|
+
else if (hrefLower.includes('/news') || hrefLower.includes('/blog') || hrefLower.includes('/artikel') || hrefLower.includes('/article')) {
|
|
1068
|
+
const articleSlug = href.split('/').filter(Boolean).pop() || '';
|
|
1069
|
+
const articleTitle = articleSlug.replace(/-/g, ' ').replace(/\..*$/, '');
|
|
1070
|
+
specificLabel = `Read: ${articleTitle || 'news article'}`;
|
|
1071
|
+
rationale = `Link goes to news/article page: ${href}`;
|
|
1072
|
+
}
|
|
1073
|
+
// Career pages
|
|
1074
|
+
else if (hrefLower.includes('/career') || hrefLower.includes('/karriere') || hrefLower.includes('/job')) {
|
|
1075
|
+
specificLabel = `Explore careers at ${brandContext || 'our company'}`;
|
|
1076
|
+
rationale = `Link goes to careers page: ${href}`;
|
|
1077
|
+
}
|
|
1078
|
+
// Home page
|
|
1079
|
+
else if (hrefLower === '/' || hrefLower.includes('/home')) {
|
|
1080
|
+
specificLabel = `Go to ${brandContext || 'website'} homepage`;
|
|
1081
|
+
rationale = `Link goes to homepage: ${href}`;
|
|
1082
|
+
}
|
|
1083
|
+
// Generic page - extract from path
|
|
1084
|
+
else {
|
|
1085
|
+
const pathParts = href.split('/').filter(Boolean);
|
|
1086
|
+
const lastPart = pathParts[pathParts.length - 1] || '';
|
|
1087
|
+
const pageName = lastPart.replace(/-/g, ' ').replace(/\..*$/, '');
|
|
1088
|
+
if (pageName && pageName.length > 2) {
|
|
1089
|
+
specificLabel = `Go to ${pageName}`;
|
|
1090
|
+
rationale = `Link destination inferred from URL path: ${href}`;
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
specificLabel = `[Describe where this link goes]`;
|
|
1094
|
+
rationale = `Could not determine link purpose from URL: ${href}`;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
// Priority 2: Use surrounding context
|
|
1099
|
+
else if (context?.surroundingText) {
|
|
1100
|
+
const contextText = context.surroundingText.slice(0, 50).trim();
|
|
1101
|
+
specificLabel = `More about ${contextText}`;
|
|
1102
|
+
rationale = `Based on surrounding text. Verify this matches the link destination.`;
|
|
1103
|
+
}
|
|
1104
|
+
// Fallback
|
|
1105
|
+
else {
|
|
1106
|
+
specificLabel = `[Describe the link destination and purpose]`;
|
|
1107
|
+
rationale = 'No context available. Manually describe where this link goes.';
|
|
1108
|
+
}
|
|
1109
|
+
// Handle German language context
|
|
1110
|
+
if (pageLanguage === 'de' && specificLabel.startsWith('[')) {
|
|
1111
|
+
specificLabel = `[Beschreiben Sie das Linkziel]`;
|
|
1112
|
+
}
|
|
1113
|
+
codes.push({
|
|
1114
|
+
violationId: violation.id,
|
|
1115
|
+
wcagCriterion: violation.wcagCriterion || '2.4.4',
|
|
1116
|
+
title: 'Add Accessible Name to Link',
|
|
1117
|
+
language: 'html',
|
|
1118
|
+
beforeCode: html.slice(0, 200) + (html.length > 200 ? '...' : ''),
|
|
1119
|
+
afterCode: `<!-- ${rationale} -->
|
|
1120
|
+
<a href="${href}"
|
|
1121
|
+
aria-label="${specificLabel}">
|
|
1122
|
+
<!-- Existing content (icon, image, etc.) -->
|
|
1123
|
+
</a>
|
|
1124
|
+
|
|
1125
|
+
<!-- Alternative: Add visually hidden text -->
|
|
1126
|
+
<a href="${href}">
|
|
1127
|
+
<span class="visually-hidden">${specificLabel}</span>
|
|
1128
|
+
<!-- Existing visual content -->
|
|
1129
|
+
</a>`,
|
|
1130
|
+
explanation: rationale,
|
|
1131
|
+
estimatedTime: '2 minutes per link',
|
|
1132
|
+
notes: [
|
|
1133
|
+
'aria-label should describe where the link goes, not just "click here"',
|
|
1134
|
+
'For links with images, the aria-label should be on the link, not the image',
|
|
1135
|
+
'Links should make sense out of context (imagine a list of all links on the page)',
|
|
1136
|
+
`Current href: ${href}`
|
|
1137
|
+
],
|
|
1138
|
+
relatedCriteria: ['2.4.4 Link Purpose (In Context)', '2.4.9 Link Purpose (Link Only)']
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
return codes;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Generate list structure remediation code
|
|
1145
|
+
*/
|
|
1146
|
+
function generateListStructureRemediation(violation) {
|
|
1147
|
+
const codes = [];
|
|
1148
|
+
for (const element of violation.elements) {
|
|
1149
|
+
const html = element.html || '<ul><div>Invalid</div></ul>';
|
|
1150
|
+
const selector = element.selector || 'ul';
|
|
1151
|
+
// Detect what type of invalid content is present
|
|
1152
|
+
const hasDiv = html.includes('<div');
|
|
1153
|
+
const hasSpan = html.includes('<span');
|
|
1154
|
+
const isNavigation = selector.includes('nav') || html.includes('nav');
|
|
1155
|
+
codes.push({
|
|
1156
|
+
violationId: violation.id,
|
|
1157
|
+
wcagCriterion: violation.wcagCriterion || '1.3.1',
|
|
1158
|
+
title: 'Fix List Structure - Only <li> Children Allowed',
|
|
1159
|
+
language: 'html',
|
|
1160
|
+
beforeCode: `<!-- INVALID: <ul> can only contain <li>, <script>, or <template> -->
|
|
1161
|
+
${html.slice(0, 300)}${html.length > 300 ? '...' : ''}`,
|
|
1162
|
+
afterCode: `<!-- OPTION 1: Move wrapper outside the list -->
|
|
1163
|
+
<div class="container">
|
|
1164
|
+
<ul${isNavigation ? ' role="menubar"' : ''}>
|
|
1165
|
+
<li>Item 1</li>
|
|
1166
|
+
<li>Item 2</li>
|
|
1167
|
+
<li>Item 3</li>
|
|
1168
|
+
</ul>
|
|
1169
|
+
</div>
|
|
1170
|
+
|
|
1171
|
+
<!-- OPTION 2: Move styling div inside each <li> -->
|
|
1172
|
+
<ul${isNavigation ? ' role="menubar"' : ''}>
|
|
1173
|
+
<li>
|
|
1174
|
+
<div class="item-wrapper">Item 1</div>
|
|
1175
|
+
</li>
|
|
1176
|
+
<li>
|
|
1177
|
+
<div class="item-wrapper">Item 2</div>
|
|
1178
|
+
</li>
|
|
1179
|
+
</ul>
|
|
1180
|
+
|
|
1181
|
+
<!-- OPTION 3: For carousels - use proper slide structure -->
|
|
1182
|
+
<div class="carousel-wrapper" role="group" aria-label="Image carousel">
|
|
1183
|
+
<ul class="slides" aria-live="polite">
|
|
1184
|
+
<li class="slide" aria-hidden="false">Slide 1</li>
|
|
1185
|
+
<li class="slide" aria-hidden="true">Slide 2</li>
|
|
1186
|
+
</ul>
|
|
1187
|
+
</div>`,
|
|
1188
|
+
explanation: 'HTML specification requires <ul> and <ol> to only contain <li> elements as direct children. Wrapper divs must be placed outside the list or inside each list item.',
|
|
1189
|
+
estimatedTime: '30 minutes',
|
|
1190
|
+
notes: [
|
|
1191
|
+
'Screen readers announce "list with X items" - invalid structure breaks this',
|
|
1192
|
+
'For navigation menus, consider using role="menubar" and role="menuitem"',
|
|
1193
|
+
'For carousels, ensure slide items are proper <li> elements'
|
|
1194
|
+
],
|
|
1195
|
+
relatedCriteria: ['1.3.1 Info and Relationships', '4.1.1 Parsing']
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
return codes;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Generate touch target size remediation code
|
|
1202
|
+
*/
|
|
1203
|
+
function generateTouchTargetRemediation(violation) {
|
|
1204
|
+
const codes = [];
|
|
1205
|
+
for (const element of violation.elements) {
|
|
1206
|
+
const html = element.html || '<button>Small</button>';
|
|
1207
|
+
const selector = element.selector || 'button';
|
|
1208
|
+
// Detect element type
|
|
1209
|
+
const isPagination = selector.includes('pagination') || html.includes('pagination');
|
|
1210
|
+
const isIcon = html.includes('icon') || html.includes('svg') || html.includes('<i ');
|
|
1211
|
+
codes.push({
|
|
1212
|
+
violationId: violation.id,
|
|
1213
|
+
wcagCriterion: violation.wcagCriterion || '2.5.8',
|
|
1214
|
+
title: 'Increase Touch Target Size to Minimum 24×24px',
|
|
1215
|
+
language: 'css',
|
|
1216
|
+
beforeCode: `/* Current: Touch target too small (< 24px) */
|
|
1217
|
+
${selector} {
|
|
1218
|
+
/* Likely: width/height not set, padding too small */
|
|
1219
|
+
padding: 2px;
|
|
1220
|
+
}`,
|
|
1221
|
+
afterCode: `/* WCAG 2.2 Level AA: Minimum 24×24px touch target */
|
|
1222
|
+
${selector} {
|
|
1223
|
+
min-width: 24px;
|
|
1224
|
+
min-height: 24px;
|
|
1225
|
+
padding: 8px; /* Ensures adequate size */
|
|
1226
|
+
|
|
1227
|
+
/* For better mobile UX, use 44×44px (iOS) or 48×48px (Material) */
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/* RECOMMENDED: Best practice touch targets */
|
|
1231
|
+
${selector} {
|
|
1232
|
+
min-width: 44px;
|
|
1233
|
+
min-height: 44px;
|
|
1234
|
+
padding: 12px;
|
|
1235
|
+
|
|
1236
|
+
/* Ensure spacing between adjacent targets */
|
|
1237
|
+
margin: 4px;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
${isPagination ? `
|
|
1241
|
+
/* Pagination-specific fix: Keep visual dot small, expand hit area */
|
|
1242
|
+
.pagination-bullet {
|
|
1243
|
+
position: relative;
|
|
1244
|
+
width: 44px;
|
|
1245
|
+
height: 44px;
|
|
1246
|
+
margin: 0 8px;
|
|
1247
|
+
background: transparent;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
.pagination-bullet::before {
|
|
1251
|
+
content: '';
|
|
1252
|
+
position: absolute;
|
|
1253
|
+
top: 50%;
|
|
1254
|
+
left: 50%;
|
|
1255
|
+
transform: translate(-50%, -50%);
|
|
1256
|
+
width: 12px; /* Visual size */
|
|
1257
|
+
height: 12px;
|
|
1258
|
+
background: #999;
|
|
1259
|
+
border-radius: 50%;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.pagination-bullet.active::before {
|
|
1263
|
+
background: #000;
|
|
1264
|
+
}` : ''}
|
|
1265
|
+
|
|
1266
|
+
/* Responsive: Larger targets on touch devices */
|
|
1267
|
+
@media (pointer: coarse) {
|
|
1268
|
+
${selector} {
|
|
1269
|
+
min-width: 48px;
|
|
1270
|
+
min-height: 48px;
|
|
1271
|
+
}
|
|
1272
|
+
}`,
|
|
1273
|
+
explanation: 'WCAG 2.2 Level AA requires interactive elements to be at least 24×24 CSS pixels. This helps users with motor impairments and improves mobile usability.',
|
|
1274
|
+
estimatedTime: '1 hour',
|
|
1275
|
+
notes: [
|
|
1276
|
+
'iOS Human Interface Guidelines recommend 44×44pt minimum',
|
|
1277
|
+
'Material Design recommends 48×48dp minimum',
|
|
1278
|
+
'Ensure adequate spacing (8px+) between adjacent targets',
|
|
1279
|
+
'Test on actual mobile devices with different finger sizes'
|
|
1280
|
+
],
|
|
1281
|
+
relatedCriteria: ['2.5.8 Target Size (Minimum)']
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
return codes;
|
|
1285
|
+
}
|
|
1286
|
+
exports.default = {
|
|
1287
|
+
detectFramework,
|
|
1288
|
+
analyzeColors,
|
|
1289
|
+
generateRemediationCodes,
|
|
1290
|
+
generateVideoCaptionRemediation,
|
|
1291
|
+
generateAriaHiddenFocusRemediation,
|
|
1292
|
+
generateColorContrastRemediation,
|
|
1293
|
+
generateImageAltRemediation,
|
|
1294
|
+
generateLinkNameRemediation,
|
|
1295
|
+
generateListStructureRemediation,
|
|
1296
|
+
generateTouchTargetRemediation,
|
|
1297
|
+
generatePlaywrightTest,
|
|
1298
|
+
generateAccessibilityCSSUtilities
|
|
1299
|
+
};
|
|
1300
|
+
//# sourceMappingURL=remediation-code-generator.js.map
|