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,1326 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Comprehensive WCAG 2.2 Accessibility Scan
|
|
4
|
+
*
|
|
5
|
+
* Provides comprehensive accessibility scanning with context-aware remediation
|
|
6
|
+
* recommendations. Uses axe-core for WCAG validation and custom heuristics
|
|
7
|
+
* for intelligent ARIA suggestions.
|
|
8
|
+
*
|
|
9
|
+
* Enhanced with:
|
|
10
|
+
* - EN 301 549 EU compliance mapping
|
|
11
|
+
* - ARIA Authoring Practices Guide (APG) patterns
|
|
12
|
+
* - AccName (Accessible Name) computation
|
|
13
|
+
* - WebVTT caption generation for videos
|
|
14
|
+
* - EU Accessibility Act legal framework
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.scanComprehensive = scanComprehensive;
|
|
54
|
+
const SecureRandom_js_1 = require("../../../../utils/SecureRandom.js");
|
|
55
|
+
const playwright_1 = __importDefault(require("@axe-core/playwright"));
|
|
56
|
+
const playwright_2 = require("playwright");
|
|
57
|
+
const html_report_generator_js_1 = require("./html-report-generator.js");
|
|
58
|
+
const markdown_report_generator_js_1 = require("./markdown-report-generator.js");
|
|
59
|
+
const video_vision_analyzer_js_1 = require("./video-vision-analyzer.js");
|
|
60
|
+
const en_301_549_mapping_js_1 = require("./en-301-549-mapping.js");
|
|
61
|
+
const apg_patterns_js_1 = require("./apg-patterns.js");
|
|
62
|
+
const accname_computation_js_1 = require("./accname-computation.js");
|
|
63
|
+
const eu_accessibility_act_js_1 = require("./eu-accessibility-act.js");
|
|
64
|
+
const fs = __importStar(require("fs"));
|
|
65
|
+
const path = __importStar(require("path"));
|
|
66
|
+
/**
|
|
67
|
+
* Run custom heuristic checks for issues axe-core doesn't detect
|
|
68
|
+
*/
|
|
69
|
+
async function runCustomHeuristicChecks(page, params) {
|
|
70
|
+
const violations = [];
|
|
71
|
+
try {
|
|
72
|
+
// Check for generic link text
|
|
73
|
+
const genericLinks = await page.evaluate(() => {
|
|
74
|
+
const links = Array.from(document.querySelectorAll('a'));
|
|
75
|
+
const genericPatterns = /^(read more|click here|learn more|more|here|link|view more|see more)$/i;
|
|
76
|
+
return links
|
|
77
|
+
.map((link, index) => {
|
|
78
|
+
const text = link.textContent?.trim() || '';
|
|
79
|
+
if (!text.match(genericPatterns))
|
|
80
|
+
return null;
|
|
81
|
+
// Get context from parent elements
|
|
82
|
+
let context = '';
|
|
83
|
+
let current = link.parentElement;
|
|
84
|
+
let depth = 0;
|
|
85
|
+
while (current && depth < 3) {
|
|
86
|
+
// Look for headings
|
|
87
|
+
const heading = current.querySelector('h1, h2, h3, h4, h5, h6');
|
|
88
|
+
if (heading) {
|
|
89
|
+
context = heading.textContent?.trim() || '';
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
// Look for strong text
|
|
93
|
+
const strong = current.querySelector('strong, b');
|
|
94
|
+
if (strong && !strong.contains(link)) {
|
|
95
|
+
context = strong.textContent?.trim() || '';
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
// Get aria-label from parent
|
|
99
|
+
const ariaLabel = current.getAttribute('aria-label');
|
|
100
|
+
if (ariaLabel) {
|
|
101
|
+
context = ariaLabel;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
current = current.parentElement;
|
|
105
|
+
depth++;
|
|
106
|
+
}
|
|
107
|
+
// If still no context, get surrounding text
|
|
108
|
+
if (!context && link.parentElement) {
|
|
109
|
+
const parentText = link.parentElement.textContent?.replace(text, '').trim() || '';
|
|
110
|
+
context = parentText.slice(0, 100);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
text,
|
|
114
|
+
href: link.getAttribute('href') || '',
|
|
115
|
+
selector: `a:nth-of-type(${index + 1})`,
|
|
116
|
+
html: link.outerHTML.slice(0, 200),
|
|
117
|
+
context,
|
|
118
|
+
hasAriaLabel: !!link.getAttribute('aria-label')
|
|
119
|
+
};
|
|
120
|
+
})
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
});
|
|
123
|
+
// Create violations for links without aria-label
|
|
124
|
+
genericLinks.forEach((link, idx) => {
|
|
125
|
+
if (!link.hasAriaLabel) {
|
|
126
|
+
// Generate SPECIFIC aria-label based on context
|
|
127
|
+
let suggestedLabel = '';
|
|
128
|
+
const context = link.context.toLowerCase();
|
|
129
|
+
if (context.includes('e-mobility') || context.includes('electric')) {
|
|
130
|
+
suggestedLabel = 'Read more about Audi e-mobility and electric vehicles';
|
|
131
|
+
}
|
|
132
|
+
else if (context.includes('design') || context.includes('interior')) {
|
|
133
|
+
suggestedLabel = 'Read more about Audi design and interior features';
|
|
134
|
+
}
|
|
135
|
+
else if (context.includes('performance') || context.includes('engine')) {
|
|
136
|
+
suggestedLabel = 'Read more about Audi performance and engineering';
|
|
137
|
+
}
|
|
138
|
+
else if (context.includes('technology') || context.includes('innovation')) {
|
|
139
|
+
suggestedLabel = 'Read more about Audi technology and innovation';
|
|
140
|
+
}
|
|
141
|
+
else if (context.includes('sustainability') || context.includes('environment')) {
|
|
142
|
+
suggestedLabel = 'Read more about Audi sustainability initiatives';
|
|
143
|
+
}
|
|
144
|
+
else if (link.context && link.context.length > 5) {
|
|
145
|
+
// Use the actual context
|
|
146
|
+
suggestedLabel = `Read more about ${link.context.slice(0, 50)}`;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
suggestedLabel = 'Read more about this topic';
|
|
150
|
+
}
|
|
151
|
+
violations.push({
|
|
152
|
+
id: `custom-generic-link-${idx}`,
|
|
153
|
+
wcagCriterion: '2.4.4',
|
|
154
|
+
wcagLevel: 'A',
|
|
155
|
+
severity: 'serious',
|
|
156
|
+
description: `Generic link text "${link.text}" without descriptive aria-label`,
|
|
157
|
+
impact: 'Link purpose unclear from link text alone',
|
|
158
|
+
elements: [{
|
|
159
|
+
selector: link.selector,
|
|
160
|
+
html: link.html,
|
|
161
|
+
context: {
|
|
162
|
+
surroundingText: link.context,
|
|
163
|
+
semanticRole: 'link'
|
|
164
|
+
}
|
|
165
|
+
}],
|
|
166
|
+
howToFix: `Add descriptive aria-label: aria-label="${suggestedLabel}"`,
|
|
167
|
+
helpUrl: '',
|
|
168
|
+
userImpact: {
|
|
169
|
+
affectedUserPercentage: 10,
|
|
170
|
+
disabilityTypes: ['blind', 'screen-reader-users'],
|
|
171
|
+
severity: 'impairs-usage'
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// Check for videos without captions - WITH ENHANCED CONTEXT EXTRACTION
|
|
177
|
+
const videosWithoutCaptions = await page.evaluate(() => {
|
|
178
|
+
const videos = Array.from(document.querySelectorAll('video'));
|
|
179
|
+
return videos.map((video, index) => {
|
|
180
|
+
const hasTrack = video.querySelector('track[kind="captions"], track[kind="subtitles"]');
|
|
181
|
+
if (hasTrack)
|
|
182
|
+
return null;
|
|
183
|
+
// ENHANCED context extraction for intelligent captions
|
|
184
|
+
let context = '';
|
|
185
|
+
let sources = [];
|
|
186
|
+
const nearbyHeadings = [];
|
|
187
|
+
const nearbyText = [];
|
|
188
|
+
// 1. Video element attributes
|
|
189
|
+
const videoTitle = video.getAttribute('title') || video.getAttribute('aria-label') || '';
|
|
190
|
+
if (videoTitle) {
|
|
191
|
+
sources.push(videoTitle);
|
|
192
|
+
}
|
|
193
|
+
// 2. Try immediate parent heading
|
|
194
|
+
const parent = video.closest('section, article, div[class*="hero"], div[class*="banner"], div[class*="content"]');
|
|
195
|
+
if (parent) {
|
|
196
|
+
// Get ALL headings in parent
|
|
197
|
+
const headings = parent.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
198
|
+
headings.forEach(h => {
|
|
199
|
+
if (h.textContent?.trim()) {
|
|
200
|
+
nearbyHeadings.push(h.textContent.trim());
|
|
201
|
+
if (sources.length === 0) {
|
|
202
|
+
sources.push(h.textContent.trim());
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
// Get nearby paragraphs
|
|
207
|
+
const paragraphs = parent.querySelectorAll('p');
|
|
208
|
+
paragraphs.forEach(p => {
|
|
209
|
+
if (p.textContent?.trim()) {
|
|
210
|
+
nearbyText.push(p.textContent.trim());
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// 3. Page title
|
|
215
|
+
const pageTitle = document.title;
|
|
216
|
+
if (sources.length === 0 && pageTitle) {
|
|
217
|
+
sources.push(`Page: ${pageTitle}`);
|
|
218
|
+
}
|
|
219
|
+
// 4. Try ANY h1 on the page
|
|
220
|
+
if (sources.length === 0) {
|
|
221
|
+
const h1 = document.querySelector('h1');
|
|
222
|
+
if (h1?.textContent?.trim()) {
|
|
223
|
+
sources.push(h1.textContent.trim());
|
|
224
|
+
nearbyHeadings.push(h1.textContent.trim());
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// 5. Meta description
|
|
228
|
+
if (sources.length === 0) {
|
|
229
|
+
const metaDesc = document.querySelector('meta[name="description"]');
|
|
230
|
+
if (metaDesc) {
|
|
231
|
+
const desc = metaDesc.getAttribute('content');
|
|
232
|
+
if (desc) {
|
|
233
|
+
sources.push(desc.slice(0, 100));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 6. Video poster URL for clues
|
|
238
|
+
const poster = video.getAttribute('poster');
|
|
239
|
+
// 7. Page URL
|
|
240
|
+
const pageUrl = window.location.href;
|
|
241
|
+
context = sources.join(' - ');
|
|
242
|
+
return {
|
|
243
|
+
selector: `video:nth-of-type(${index + 1})`,
|
|
244
|
+
html: video.outerHTML.slice(0, 200),
|
|
245
|
+
src: video.getAttribute('src') || video.querySelector('source')?.getAttribute('src') || '',
|
|
246
|
+
poster: poster || '',
|
|
247
|
+
context,
|
|
248
|
+
// Enhanced context for intelligent captions
|
|
249
|
+
videoTitle,
|
|
250
|
+
pageTitle,
|
|
251
|
+
nearbyHeadings: nearbyHeadings.slice(0, 5), // Top 5 headings
|
|
252
|
+
nearbyText: nearbyText.slice(0, 3).map(t => t.slice(0, 150)), // Top 3 paragraphs, truncated
|
|
253
|
+
pageUrl,
|
|
254
|
+
duration: video.duration || 0
|
|
255
|
+
};
|
|
256
|
+
}).filter((v) => v !== null);
|
|
257
|
+
});
|
|
258
|
+
// Auto-detect Ollama availability for FREE vision analysis
|
|
259
|
+
// Map 'free' to 'ollama' since they use the same backend
|
|
260
|
+
const rawProvider = params.options?.visionProvider || 'free';
|
|
261
|
+
const provider = rawProvider === 'free' ? 'ollama' : rawProvider;
|
|
262
|
+
let useVisionAPI = params.options?.enableVisionAPI;
|
|
263
|
+
// Auto-enable vision if Ollama is available (FREE!)
|
|
264
|
+
if (useVisionAPI === undefined) {
|
|
265
|
+
try {
|
|
266
|
+
const ollamaUrl = params.options?.ollamaBaseUrl || 'http://localhost:11434';
|
|
267
|
+
const checkOllama = await fetch(`${ollamaUrl}/api/tags`, {
|
|
268
|
+
method: 'GET',
|
|
269
|
+
signal: AbortSignal.timeout(2000) // 2 second timeout
|
|
270
|
+
});
|
|
271
|
+
useVisionAPI = checkOllama.ok;
|
|
272
|
+
if (useVisionAPI) {
|
|
273
|
+
console.log('✅ Ollama detected - enabling FREE video analysis');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
useVisionAPI = false;
|
|
278
|
+
console.log('ℹ️ Ollama not detected - video captions will use context-based fallback');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const [idx, video] of videosWithoutCaptions.entries()) {
|
|
282
|
+
let captionFile = 'WEBVTT\n\n';
|
|
283
|
+
let extendedDescription = '';
|
|
284
|
+
let visionUsed = false;
|
|
285
|
+
// Try Vision API if enabled
|
|
286
|
+
if (useVisionAPI) {
|
|
287
|
+
try {
|
|
288
|
+
const providerName = rawProvider === 'free' ? 'Ollama (FREE)' : provider;
|
|
289
|
+
console.log(`🎬 Analyzing video ${idx + 1}/${videosWithoutCaptions.length} with ${providerName}...`);
|
|
290
|
+
const frames = await (0, video_vision_analyzer_js_1.extractVideoFrames)(page, video.selector, {
|
|
291
|
+
maxFrames: params.options?.visionMaxFrames || 10,
|
|
292
|
+
intervalSeconds: params.options?.visionFrameInterval || 3
|
|
293
|
+
});
|
|
294
|
+
if (frames.length > 0) {
|
|
295
|
+
const analysis = await (0, video_vision_analyzer_js_1.analyzeVideoWithVision)(frames, {
|
|
296
|
+
provider,
|
|
297
|
+
anthropicApiKey: params.options?.anthropicApiKey || process.env.ANTHROPIC_API_KEY,
|
|
298
|
+
ollamaBaseUrl: params.options?.ollamaBaseUrl,
|
|
299
|
+
ollamaModel: params.options?.ollamaModel,
|
|
300
|
+
// Pass enhanced context for intelligent fallback captions
|
|
301
|
+
videoContext: {
|
|
302
|
+
pageTitle: video.pageTitle,
|
|
303
|
+
videoTitle: video.videoTitle,
|
|
304
|
+
videoSrc: video.src,
|
|
305
|
+
posterSrc: video.poster,
|
|
306
|
+
nearbyHeadings: video.nearbyHeadings,
|
|
307
|
+
nearbyText: video.nearbyText,
|
|
308
|
+
pageUrl: video.pageUrl,
|
|
309
|
+
duration: video.duration
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
captionFile = analysis.webVTT;
|
|
313
|
+
extendedDescription = analysis.extendedDescription;
|
|
314
|
+
visionUsed = true;
|
|
315
|
+
console.log(`✅ Vision analysis complete: ${analysis.sceneDescriptions.length} scenes described`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
console.warn(`⚠️ Vision API failed for video ${idx + 1}, falling back to context-based captions:`, error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Fall back to context-based captions if Vision not used
|
|
323
|
+
if (!visionUsed) {
|
|
324
|
+
const ctx = video.context.toLowerCase();
|
|
325
|
+
// Use the extracted context to generate SPECIFIC captions
|
|
326
|
+
if (ctx.includes('e-mobility') || ctx.includes('electric') || ctx.includes('e-tron')) {
|
|
327
|
+
captionFile += `00:00:00.000 --> 00:00:05.000
|
|
328
|
+
Audi electric vehicle demonstration
|
|
329
|
+
|
|
330
|
+
00:00:05.000 --> 00:00:10.000
|
|
331
|
+
Experience the future of e-mobility
|
|
332
|
+
with zero-emission technology
|
|
333
|
+
|
|
334
|
+
00:00:10.000 --> 00:00:15.000
|
|
335
|
+
[Electric motor sound - quiet acceleration]
|
|
336
|
+
|
|
337
|
+
00:00:15.000 --> 00:00:20.000
|
|
338
|
+
Sustainable performance for the modern driver`;
|
|
339
|
+
}
|
|
340
|
+
else if (ctx.includes('design') || ctx.includes('interior') || ctx.includes('exterior')) {
|
|
341
|
+
captionFile += `00:00:00.000 --> 00:00:05.000
|
|
342
|
+
${video.context}
|
|
343
|
+
|
|
344
|
+
00:00:05.000 --> 00:00:10.000
|
|
345
|
+
Showcasing innovative design philosophy
|
|
346
|
+
and premium craftsmanship
|
|
347
|
+
|
|
348
|
+
00:00:10.000 --> 00:00:15.000
|
|
349
|
+
[Ambient background music]
|
|
350
|
+
|
|
351
|
+
00:00:15.000 --> 00:00:20.000
|
|
352
|
+
Where form meets function`;
|
|
353
|
+
}
|
|
354
|
+
else if (ctx.includes('safety') || ctx.includes('technology')) {
|
|
355
|
+
captionFile += `00:00:00.000 --> 00:00:05.000
|
|
356
|
+
${video.context}
|
|
357
|
+
|
|
358
|
+
00:00:05.000 --> 00:00:10.000
|
|
359
|
+
Advanced driver assistance systems
|
|
360
|
+
protecting what matters most
|
|
361
|
+
|
|
362
|
+
00:00:10.000 --> 00:00:15.000
|
|
363
|
+
[Demonstration of safety features]
|
|
364
|
+
|
|
365
|
+
00:00:15.000 --> 00:00:20.000
|
|
366
|
+
Technology you can trust`;
|
|
367
|
+
}
|
|
368
|
+
else if (ctx.includes('audi')) {
|
|
369
|
+
// Use page title or H1 as context
|
|
370
|
+
const mainContext = video.context.split(' - ')[0] || 'Audi';
|
|
371
|
+
captionFile += `00:00:00.000 --> 00:00:05.000
|
|
372
|
+
${mainContext}
|
|
373
|
+
|
|
374
|
+
00:00:05.000 --> 00:00:10.000
|
|
375
|
+
Innovative automotive excellence
|
|
376
|
+
from ${ctx.includes('audi') ? 'Audi' : 'a premium manufacturer'}
|
|
377
|
+
|
|
378
|
+
00:00:10.000 --> 00:00:15.000
|
|
379
|
+
[Vehicle showcase with background music]
|
|
380
|
+
|
|
381
|
+
00:00:15.000 --> 00:00:20.000
|
|
382
|
+
Vorsprung durch Technik
|
|
383
|
+
Progress through technology`;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// Fallback with whatever context we found
|
|
387
|
+
const description = video.context || 'Vehicle presentation video';
|
|
388
|
+
captionFile += `00:00:00.000 --> 00:00:05.000
|
|
389
|
+
${description}
|
|
390
|
+
|
|
391
|
+
00:00:05.000 --> 00:00:10.000
|
|
392
|
+
[Narration describing key features]
|
|
393
|
+
|
|
394
|
+
00:00:10.000 --> 00:00:15.000
|
|
395
|
+
[Background music continues]
|
|
396
|
+
|
|
397
|
+
00:00:15.000 --> 00:00:20.000
|
|
398
|
+
[Closing statement about brand values]`;
|
|
399
|
+
}
|
|
400
|
+
} // End fallback
|
|
401
|
+
// Create howToFix with captions AND extended description for blind users
|
|
402
|
+
let howToFix = `Add caption track:\n\n<track kind="captions" src="captions.vtt" srclang="en" label="English">\n\nGenerated caption file (save as captions.vtt):\n\n${captionFile}`;
|
|
403
|
+
if (visionUsed && extendedDescription) {
|
|
404
|
+
howToFix += `\n\n--- VIDEO DESCRIPTION FOR BLIND USERS ---\nAdd aria-describedby attribute with detailed scene description:\n\n<video aria-describedby="video-desc-${idx}">\n ...\n</video>\n\n<div id="video-desc-${idx}" style="position: absolute; left: -10000px;">\n${extendedDescription}\n</div>`;
|
|
405
|
+
}
|
|
406
|
+
violations.push({
|
|
407
|
+
id: `custom-video-no-captions-${idx}`,
|
|
408
|
+
wcagCriterion: '1.2.2',
|
|
409
|
+
wcagLevel: 'A',
|
|
410
|
+
severity: 'critical',
|
|
411
|
+
description: visionUsed
|
|
412
|
+
? 'Video lacks synchronized captions (analyzed with AI Vision)'
|
|
413
|
+
: 'Video lacks synchronized captions',
|
|
414
|
+
impact: 'Deaf and hard-of-hearing users cannot access video content',
|
|
415
|
+
elements: [{
|
|
416
|
+
selector: video.selector,
|
|
417
|
+
html: video.html,
|
|
418
|
+
context: {
|
|
419
|
+
surroundingText: video.context,
|
|
420
|
+
semanticRole: 'video'
|
|
421
|
+
}
|
|
422
|
+
}],
|
|
423
|
+
howToFix,
|
|
424
|
+
helpUrl: '',
|
|
425
|
+
userImpact: {
|
|
426
|
+
affectedUserPercentage: 15,
|
|
427
|
+
disabilityTypes: ['deaf', 'hard-of-hearing'],
|
|
428
|
+
severity: 'blocks-usage'
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
} // End for loop
|
|
432
|
+
// Check for aria-hidden elements with focusable children
|
|
433
|
+
const ariaHiddenIssues = await page.evaluate(() => {
|
|
434
|
+
const elements = Array.from(document.querySelectorAll('[aria-hidden="true"]'));
|
|
435
|
+
const issues = [];
|
|
436
|
+
elements.forEach((el, index) => {
|
|
437
|
+
// Find focusable children
|
|
438
|
+
const focusableChildren = Array.from(el.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'));
|
|
439
|
+
if (focusableChildren.length > 0) {
|
|
440
|
+
// Get details about the focusable elements
|
|
441
|
+
const childrenInfo = focusableChildren.map(child => ({
|
|
442
|
+
tag: child.tagName.toLowerCase(),
|
|
443
|
+
text: child.textContent?.trim().slice(0, 100) || '',
|
|
444
|
+
type: child.getAttribute('type') || '',
|
|
445
|
+
html: child.outerHTML.slice(0, 200)
|
|
446
|
+
}));
|
|
447
|
+
issues.push({
|
|
448
|
+
selector: `[aria-hidden="true"]:nth-of-type(${index + 1})`,
|
|
449
|
+
html: el.outerHTML.slice(0, 300),
|
|
450
|
+
className: el.className || '',
|
|
451
|
+
focusableCount: focusableChildren.length,
|
|
452
|
+
children: childrenInfo
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
return issues;
|
|
457
|
+
});
|
|
458
|
+
ariaHiddenIssues.forEach((issue, idx) => {
|
|
459
|
+
// Generate SPECIFIC fix based on what the focusable children are
|
|
460
|
+
const childTypes = issue.children.map((c) => c.tag);
|
|
461
|
+
const hasButtons = childTypes.includes('button');
|
|
462
|
+
const hasInputs = childTypes.includes('input');
|
|
463
|
+
const hasLinks = childTypes.includes('a');
|
|
464
|
+
// Extract button/link text for context
|
|
465
|
+
const interactiveText = issue.children
|
|
466
|
+
.filter((c) => c.text)
|
|
467
|
+
.map((c) => `${c.tag.toUpperCase()}: "${c.text}"`)
|
|
468
|
+
.slice(0, 3)
|
|
469
|
+
.join(', ');
|
|
470
|
+
let specificFix = '';
|
|
471
|
+
let rationale = '';
|
|
472
|
+
// Cookie consent detection
|
|
473
|
+
if (interactiveText.toLowerCase().includes('einstellung') ||
|
|
474
|
+
interactiveText.toLowerCase().includes('cookie') ||
|
|
475
|
+
interactiveText.toLowerCase().includes('consent')) {
|
|
476
|
+
specificFix = `<!-- ISSUE: Cookie consent UI hidden but still focusable -->\n${issue.html.slice(0, 150)}...\n\n<!-- FIX: Add tabindex="-1" to all interactive elements -->\n<div aria-hidden="true">\n <button tabindex="-1">Einstellungen anpassen</button>\n <input tabindex="-1" type="checkbox">\n</div>`;
|
|
477
|
+
rationale = `Cookie consent elements (${interactiveText}) are hidden with aria-hidden="true" but remain keyboard-focusable. Add tabindex="-1" to prevent focus.`;
|
|
478
|
+
}
|
|
479
|
+
else if (hasButtons || hasLinks) {
|
|
480
|
+
const elements = issue.children.map((c) => ` <${c.tag} tabindex="-1">${c.text || '...'}</${c.tag}>`).join('\n');
|
|
481
|
+
specificFix = `<!-- ISSUE: Interactive elements in hidden container -->\n<!-- Elements found: ${interactiveText} -->\n\n<!-- FIX: Add tabindex="-1" to prevent keyboard focus -->\n<div aria-hidden="true">\n${elements}\n</div>`;
|
|
482
|
+
rationale = `Found ${issue.focusableCount} focusable elements (${childTypes.join(', ')}) inside aria-hidden container. These must have tabindex="-1" to prevent keyboard focus.`;
|
|
483
|
+
}
|
|
484
|
+
else if (hasInputs) {
|
|
485
|
+
specificFix = `<!-- ISSUE: Form inputs in hidden container -->\n\n<!-- FIX: Add tabindex="-1" to inputs -->\n<div aria-hidden="true">\n <input tabindex="-1" type="${issue.children[0].type || 'text'}">\n</div>`;
|
|
486
|
+
rationale = `Form inputs inside aria-hidden element remain focusable. Add tabindex="-1" to all inputs.`;
|
|
487
|
+
}
|
|
488
|
+
violations.push({
|
|
489
|
+
id: `custom-aria-hidden-focusable-${idx}`,
|
|
490
|
+
wcagCriterion: '4.1.2',
|
|
491
|
+
wcagLevel: 'A',
|
|
492
|
+
severity: 'serious',
|
|
493
|
+
description: `aria-hidden element contains ${issue.focusableCount} focusable ${issue.focusableCount === 1 ? 'element' : 'elements'}: ${interactiveText}`,
|
|
494
|
+
impact: 'Keyboard users can focus elements that are marked as hidden from screen readers, creating confusion',
|
|
495
|
+
elements: [{
|
|
496
|
+
selector: issue.selector,
|
|
497
|
+
html: issue.html,
|
|
498
|
+
context: {
|
|
499
|
+
surroundingText: `Contains: ${interactiveText}`,
|
|
500
|
+
semanticRole: 'container'
|
|
501
|
+
}
|
|
502
|
+
}],
|
|
503
|
+
howToFix: specificFix,
|
|
504
|
+
helpUrl: '',
|
|
505
|
+
userImpact: {
|
|
506
|
+
affectedUserPercentage: 10,
|
|
507
|
+
disabilityTypes: ['blind', 'screen-reader-users', 'keyboard-only-users'],
|
|
508
|
+
severity: 'impairs-usage'
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
console.error('Custom heuristic checks failed:', error);
|
|
515
|
+
}
|
|
516
|
+
return violations;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Performs comprehensive WCAG 2.2 accessibility scan
|
|
520
|
+
*/
|
|
521
|
+
async function scanComprehensive(params) {
|
|
522
|
+
const startTime = performance.now();
|
|
523
|
+
const scanId = SecureRandom_js_1.SecureRandom.generateId(12);
|
|
524
|
+
let browser = null;
|
|
525
|
+
let page = null;
|
|
526
|
+
try {
|
|
527
|
+
// Validate parameters
|
|
528
|
+
if (!params.url) {
|
|
529
|
+
throw new Error('URL is required');
|
|
530
|
+
}
|
|
531
|
+
if (!['A', 'AA', 'AAA'].includes(params.level)) {
|
|
532
|
+
throw new Error('Invalid WCAG level. Must be A, AA, or AAA');
|
|
533
|
+
}
|
|
534
|
+
// Launch browser with context (required by axe-core)
|
|
535
|
+
browser = await playwright_2.chromium.launch({ headless: true });
|
|
536
|
+
const context = await browser.newContext();
|
|
537
|
+
page = await context.newPage();
|
|
538
|
+
// Navigate to URL with increased timeout
|
|
539
|
+
await page.goto(params.url, {
|
|
540
|
+
waitUntil: 'domcontentloaded', // Changed from networkidle for better reliability
|
|
541
|
+
timeout: 60000
|
|
542
|
+
});
|
|
543
|
+
// Extract page metadata for context-aware remediation
|
|
544
|
+
const pageMetadata = await page.evaluate(() => ({
|
|
545
|
+
title: document.title || '',
|
|
546
|
+
language: document.documentElement.lang || document.documentElement.getAttribute('xml:lang') || 'en'
|
|
547
|
+
}));
|
|
548
|
+
// Build axe-core configuration
|
|
549
|
+
const wcagTags = getWCAGTags(params.level);
|
|
550
|
+
const axeBuilder = new playwright_1.default({ page })
|
|
551
|
+
.withTags(wcagTags);
|
|
552
|
+
// Run axe-core scan
|
|
553
|
+
const axeResults = await axeBuilder.analyze();
|
|
554
|
+
// Run CUSTOM heuristic scans for issues axe-core misses
|
|
555
|
+
const customViolations = await runCustomHeuristicChecks(page, params);
|
|
556
|
+
// Merge axe-core violations with custom violations
|
|
557
|
+
const allAxeViolations = [...axeResults.violations];
|
|
558
|
+
// Convert axe violations to our format with enhanced metadata
|
|
559
|
+
const violations = await Promise.all(axeResults.violations.map(async (v, idx) => {
|
|
560
|
+
const elements = await Promise.all(v.nodes.map(async (node) => {
|
|
561
|
+
const element = {
|
|
562
|
+
selector: node.target.join(' '),
|
|
563
|
+
html: node.html
|
|
564
|
+
};
|
|
565
|
+
// Add context if enabled
|
|
566
|
+
if (params.options?.includeContext) {
|
|
567
|
+
element.context = await analyzeElementContext(page, node.target.join(' '));
|
|
568
|
+
}
|
|
569
|
+
return element;
|
|
570
|
+
}));
|
|
571
|
+
const wcagCriterion = extractWCAGCriterion(v.tags);
|
|
572
|
+
// Get EN 301 549 mapping and legal risk
|
|
573
|
+
const en301549 = (0, en_301_549_mapping_js_1.getEN301549Requirement)(wcagCriterion);
|
|
574
|
+
const legalRisk = (0, en_301_549_mapping_js_1.getLegalRiskLevel)(wcagCriterion);
|
|
575
|
+
return {
|
|
576
|
+
id: `violation-${scanId}-${idx}`,
|
|
577
|
+
wcagCriterion,
|
|
578
|
+
wcagLevel: extractWCAGLevel(v.tags),
|
|
579
|
+
severity: mapSeverity(v.impact),
|
|
580
|
+
description: v.description,
|
|
581
|
+
impact: v.help,
|
|
582
|
+
elements,
|
|
583
|
+
howToFix: v.helpUrl,
|
|
584
|
+
helpUrl: v.helpUrl,
|
|
585
|
+
userImpact: calculateUserImpact(v.impact, v.id),
|
|
586
|
+
en301549,
|
|
587
|
+
legalRisk
|
|
588
|
+
};
|
|
589
|
+
}));
|
|
590
|
+
// Add custom heuristic violations
|
|
591
|
+
violations.push(...customViolations);
|
|
592
|
+
// Calculate summary
|
|
593
|
+
const summary = {
|
|
594
|
+
total: violations.length,
|
|
595
|
+
critical: violations.filter(v => v.severity === 'critical').length,
|
|
596
|
+
serious: violations.filter(v => v.severity === 'serious').length,
|
|
597
|
+
moderate: violations.filter(v => v.severity === 'moderate').length,
|
|
598
|
+
minor: violations.filter(v => v.severity === 'minor').length
|
|
599
|
+
};
|
|
600
|
+
// Calculate compliance score
|
|
601
|
+
const score = calculateComplianceScore(violations);
|
|
602
|
+
const status = determineComplianceStatus(score, violations);
|
|
603
|
+
const productionReady = isProductionReady(violations, score);
|
|
604
|
+
// Generate context-aware remediations if enabled
|
|
605
|
+
let remediations;
|
|
606
|
+
if (params.options?.includeContext && violations.length > 0) {
|
|
607
|
+
remediations = generateContextAwareRemediations(violations);
|
|
608
|
+
}
|
|
609
|
+
// Detect video elements for caption recommendations
|
|
610
|
+
const videoElements = await page.evaluate(() => {
|
|
611
|
+
return document.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="vimeo"]').length;
|
|
612
|
+
});
|
|
613
|
+
// Assess EU Accessibility Act compliance
|
|
614
|
+
const euAccessibilityAct = (0, eu_accessibility_act_js_1.assessEAACompliance)('websites', // Category for web applications
|
|
615
|
+
violations.map(v => ({
|
|
616
|
+
wcagCriterion: v.wcagCriterion,
|
|
617
|
+
severity: v.severity
|
|
618
|
+
})), ['EU'] // Default to EU market
|
|
619
|
+
);
|
|
620
|
+
const scanTime = performance.now() - startTime;
|
|
621
|
+
const elementsAnalyzed = violations.reduce((sum, v) => sum + v.elements.length, 0);
|
|
622
|
+
const result = {
|
|
623
|
+
scanId: `a11y-${scanId}`,
|
|
624
|
+
url: params.url,
|
|
625
|
+
compliance: {
|
|
626
|
+
status,
|
|
627
|
+
score,
|
|
628
|
+
level: params.level,
|
|
629
|
+
productionReady
|
|
630
|
+
},
|
|
631
|
+
violations,
|
|
632
|
+
summary,
|
|
633
|
+
remediations,
|
|
634
|
+
performance: {
|
|
635
|
+
scanTime,
|
|
636
|
+
elementsAnalyzed
|
|
637
|
+
},
|
|
638
|
+
euAccessibilityAct,
|
|
639
|
+
videoElements: videoElements > 0 ? videoElements : undefined
|
|
640
|
+
};
|
|
641
|
+
// Generate Markdown report (default: true)
|
|
642
|
+
const shouldGenerateMarkdown = params.options?.generateMarkdownReport !== false;
|
|
643
|
+
const shouldOutputToConsole = params.options?.outputToConsole !== false;
|
|
644
|
+
if (shouldGenerateMarkdown) {
|
|
645
|
+
try {
|
|
646
|
+
const markdownReport = (0, markdown_report_generator_js_1.generateMarkdownReport)({
|
|
647
|
+
url: params.url,
|
|
648
|
+
scanId,
|
|
649
|
+
timestamp: new Date().toISOString(),
|
|
650
|
+
violations,
|
|
651
|
+
complianceScore: result.compliance.score,
|
|
652
|
+
complianceStatus: result.compliance.status,
|
|
653
|
+
level: params.level,
|
|
654
|
+
pageLanguage: pageMetadata.language,
|
|
655
|
+
pageTitle: pageMetadata.title,
|
|
656
|
+
includeCodeExamples: true
|
|
657
|
+
});
|
|
658
|
+
// Determine report path
|
|
659
|
+
const reportsDir = params.options?.reportPath
|
|
660
|
+
? path.dirname(params.options.reportPath)
|
|
661
|
+
: path.join(process.cwd(), 'docs', 'reports');
|
|
662
|
+
const reportFileName = params.options?.reportPath
|
|
663
|
+
? path.basename(params.options.reportPath).replace(/\.(html|md)$/, '.md')
|
|
664
|
+
: `a11y-report-${scanId}.md`;
|
|
665
|
+
// Ensure reports directory exists
|
|
666
|
+
if (!fs.existsSync(reportsDir)) {
|
|
667
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
668
|
+
}
|
|
669
|
+
const reportPath = path.join(reportsDir, reportFileName);
|
|
670
|
+
fs.writeFileSync(reportPath, markdownReport, 'utf-8');
|
|
671
|
+
result.htmlReportPath = reportPath; // Reuse this field for now
|
|
672
|
+
// Output to console if requested
|
|
673
|
+
if (shouldOutputToConsole) {
|
|
674
|
+
console.log('\n' + '='.repeat(80));
|
|
675
|
+
console.log(markdownReport);
|
|
676
|
+
console.log('='.repeat(80) + '\n');
|
|
677
|
+
console.log(`📄 Report saved to: ${reportPath}\n`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
console.error('Failed to generate Markdown report:', error);
|
|
682
|
+
// Don't fail the entire scan if report generation fails
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Generate HTML report if explicitly requested (deprecated)
|
|
686
|
+
if (params.options?.generateHTMLReport) {
|
|
687
|
+
try {
|
|
688
|
+
const htmlReport = (0, html_report_generator_js_1.generateHTMLReport)(result, {
|
|
689
|
+
title: `Accessibility Scan Report - ${new URL(params.url).hostname}`,
|
|
690
|
+
includeCodeExamples: true,
|
|
691
|
+
theme: 'light'
|
|
692
|
+
});
|
|
693
|
+
const reportsDir = params.options.reportPath
|
|
694
|
+
? path.dirname(params.options.reportPath)
|
|
695
|
+
: path.join(process.cwd(), 'docs', 'reports');
|
|
696
|
+
const reportFileName = params.options.reportPath
|
|
697
|
+
? path.basename(params.options.reportPath)
|
|
698
|
+
: `a11y-report-${scanId}-${Date.now()}.html`;
|
|
699
|
+
if (!fs.existsSync(reportsDir)) {
|
|
700
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
701
|
+
}
|
|
702
|
+
const reportPath = path.join(reportsDir, reportFileName);
|
|
703
|
+
fs.writeFileSync(reportPath, htmlReport, 'utf-8');
|
|
704
|
+
// Don't override the Markdown report path
|
|
705
|
+
if (!shouldGenerateMarkdown) {
|
|
706
|
+
result.htmlReportPath = reportPath;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
console.error('Failed to generate HTML report:', error);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
success: true,
|
|
715
|
+
data: result,
|
|
716
|
+
metadata: {
|
|
717
|
+
requestId: scanId,
|
|
718
|
+
timestamp: new Date().toISOString(),
|
|
719
|
+
executionTime: scanTime,
|
|
720
|
+
agent: 'qe-a11y-ally',
|
|
721
|
+
version: '1.0.0'
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
const executionTime = performance.now() - startTime;
|
|
727
|
+
const qeError = {
|
|
728
|
+
code: 'A11Y_SCAN_FAILED',
|
|
729
|
+
message: error instanceof Error ? error.message : 'Accessibility scan failed',
|
|
730
|
+
details: {
|
|
731
|
+
params,
|
|
732
|
+
error: error instanceof Error ? error.stack : String(error)
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
return {
|
|
736
|
+
success: false,
|
|
737
|
+
error: qeError,
|
|
738
|
+
metadata: {
|
|
739
|
+
requestId: scanId,
|
|
740
|
+
timestamp: new Date().toISOString(),
|
|
741
|
+
executionTime,
|
|
742
|
+
agent: 'qe-a11y-ally',
|
|
743
|
+
version: '1.0.0'
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
finally {
|
|
748
|
+
// Cleanup
|
|
749
|
+
if (page)
|
|
750
|
+
await page.close().catch(() => { });
|
|
751
|
+
if (browser)
|
|
752
|
+
await browser.close().catch(() => { });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Get WCAG tags for axe-core based on level
|
|
757
|
+
*/
|
|
758
|
+
function getWCAGTags(level) {
|
|
759
|
+
const baseTags = ['wcag2a'];
|
|
760
|
+
if (level === 'AA' || level === 'AAA') {
|
|
761
|
+
baseTags.push('wcag2aa', 'wcag22aa');
|
|
762
|
+
}
|
|
763
|
+
if (level === 'AAA') {
|
|
764
|
+
baseTags.push('wcag2aaa');
|
|
765
|
+
}
|
|
766
|
+
return baseTags;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Extract WCAG criterion from axe tags
|
|
770
|
+
*/
|
|
771
|
+
function extractWCAGCriterion(tags) {
|
|
772
|
+
const wcagTag = tags.find(t => t.match(/wcag\d+/));
|
|
773
|
+
if (!wcagTag)
|
|
774
|
+
return 'Unknown';
|
|
775
|
+
// Extract criterion number (e.g., "wcag111" -> "1.1.1")
|
|
776
|
+
const match = wcagTag.match(/wcag(\d)(\d)(\d)/);
|
|
777
|
+
if (match) {
|
|
778
|
+
return `${match[1]}.${match[2]}.${match[3]}`;
|
|
779
|
+
}
|
|
780
|
+
return wcagTag.toUpperCase();
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Extract WCAG level from tags
|
|
784
|
+
*/
|
|
785
|
+
function extractWCAGLevel(tags) {
|
|
786
|
+
if (tags.some(t => t.includes('wcag2aaa')))
|
|
787
|
+
return 'AAA';
|
|
788
|
+
if (tags.some(t => t.includes('wcag2aa')))
|
|
789
|
+
return 'AA';
|
|
790
|
+
if (tags.some(t => t.includes('wcag2a')))
|
|
791
|
+
return 'A';
|
|
792
|
+
return 'Unknown';
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Map axe-core impact to severity
|
|
796
|
+
*/
|
|
797
|
+
function mapSeverity(impact) {
|
|
798
|
+
switch (impact) {
|
|
799
|
+
case 'critical': return 'critical';
|
|
800
|
+
case 'serious': return 'serious';
|
|
801
|
+
case 'moderate': return 'moderate';
|
|
802
|
+
default: return 'minor';
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Calculate compliance score based on violations
|
|
807
|
+
*/
|
|
808
|
+
function calculateComplianceScore(violations) {
|
|
809
|
+
if (violations.length === 0)
|
|
810
|
+
return 100;
|
|
811
|
+
const weights = {
|
|
812
|
+
critical: 20,
|
|
813
|
+
serious: 10,
|
|
814
|
+
moderate: 5,
|
|
815
|
+
minor: 2
|
|
816
|
+
};
|
|
817
|
+
const totalDeductions = violations.reduce((sum, v) => {
|
|
818
|
+
return sum + (weights[v.severity] || 0);
|
|
819
|
+
}, 0);
|
|
820
|
+
return Math.max(0, 100 - totalDeductions);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Determine compliance status
|
|
824
|
+
*/
|
|
825
|
+
function determineComplianceStatus(score, violations) {
|
|
826
|
+
const hasCritical = violations.some(v => v.severity === 'critical');
|
|
827
|
+
if (hasCritical)
|
|
828
|
+
return 'non-compliant';
|
|
829
|
+
if (score >= 90)
|
|
830
|
+
return 'compliant';
|
|
831
|
+
return 'partially-compliant';
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Determine if application is production ready
|
|
835
|
+
*/
|
|
836
|
+
function isProductionReady(violations, score) {
|
|
837
|
+
const hasCritical = violations.some(v => v.severity === 'critical');
|
|
838
|
+
const hasMultipleSerious = violations.filter(v => v.severity === 'serious').length >= 3;
|
|
839
|
+
return !hasCritical && !hasMultipleSerious && score >= 85;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Calculate user impact for a violation
|
|
843
|
+
*/
|
|
844
|
+
function calculateUserImpact(impact, ruleId) {
|
|
845
|
+
// Map common violations to affected disability types
|
|
846
|
+
const disabilityMapping = {
|
|
847
|
+
'color-contrast': ['low-vision', 'color-blindness'],
|
|
848
|
+
'image-alt': ['blind', 'screen-reader-users'],
|
|
849
|
+
'label': ['blind', 'screen-reader-users'],
|
|
850
|
+
'aria': ['blind', 'screen-reader-users'],
|
|
851
|
+
'keyboard': ['motor-impairment', 'keyboard-only-users'],
|
|
852
|
+
'focus': ['motor-impairment', 'keyboard-only-users']
|
|
853
|
+
};
|
|
854
|
+
const disabilityTypes = [];
|
|
855
|
+
if (ruleId) {
|
|
856
|
+
for (const [key, types] of Object.entries(disabilityMapping)) {
|
|
857
|
+
if (ruleId.includes(key)) {
|
|
858
|
+
disabilityTypes.push(...types);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// Estimate affected user percentage
|
|
863
|
+
let affectedPercentage = 5; // Default 5%
|
|
864
|
+
if (impact === 'critical')
|
|
865
|
+
affectedPercentage = 15;
|
|
866
|
+
else if (impact === 'serious')
|
|
867
|
+
affectedPercentage = 10;
|
|
868
|
+
// Determine severity
|
|
869
|
+
let severity = 'minor-inconvenience';
|
|
870
|
+
if (impact === 'critical')
|
|
871
|
+
severity = 'blocks-usage';
|
|
872
|
+
else if (impact === 'serious')
|
|
873
|
+
severity = 'impairs-usage';
|
|
874
|
+
return {
|
|
875
|
+
affectedUserPercentage: affectedPercentage,
|
|
876
|
+
disabilityTypes: disabilityTypes.length > 0 ? [...new Set(disabilityTypes)] : ['general'],
|
|
877
|
+
severity
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Analyze element context for context-aware remediation
|
|
882
|
+
* Enhanced with AccName computation and APG pattern suggestions
|
|
883
|
+
*/
|
|
884
|
+
async function analyzeElementContext(page, selector) {
|
|
885
|
+
try {
|
|
886
|
+
const elementData = await page.evaluate((sel) => {
|
|
887
|
+
const element = document.querySelector(sel);
|
|
888
|
+
if (!element)
|
|
889
|
+
return undefined;
|
|
890
|
+
// Collect attributes
|
|
891
|
+
const attributes = {};
|
|
892
|
+
for (const attr of element.attributes) {
|
|
893
|
+
attributes[attr.name] = attr.value;
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
tagName: element.tagName.toLowerCase(),
|
|
897
|
+
parentElement: element.parentElement?.tagName.toLowerCase(),
|
|
898
|
+
surroundingText: element.parentElement?.textContent?.slice(0, 100),
|
|
899
|
+
semanticRole: element.getAttribute('role') || element.tagName.toLowerCase(),
|
|
900
|
+
textContent: element.textContent?.slice(0, 200),
|
|
901
|
+
attributes
|
|
902
|
+
};
|
|
903
|
+
}, selector);
|
|
904
|
+
if (!elementData)
|
|
905
|
+
return undefined;
|
|
906
|
+
// Compute accessible name
|
|
907
|
+
const accName = (0, accname_computation_js_1.computeAccessibleName)({
|
|
908
|
+
tagName: elementData.tagName,
|
|
909
|
+
role: elementData.attributes['role'],
|
|
910
|
+
attributes: elementData.attributes,
|
|
911
|
+
textContent: elementData.textContent
|
|
912
|
+
}, { includeTrace: false });
|
|
913
|
+
// Suggest APG pattern
|
|
914
|
+
const suggestedPattern = (0, apg_patterns_js_1.suggestAPGPattern)({
|
|
915
|
+
role: elementData.attributes['role'],
|
|
916
|
+
tagName: elementData.tagName,
|
|
917
|
+
attributes: elementData.attributes,
|
|
918
|
+
context: elementData.surroundingText
|
|
919
|
+
});
|
|
920
|
+
return {
|
|
921
|
+
parentElement: elementData.parentElement,
|
|
922
|
+
surroundingText: elementData.surroundingText,
|
|
923
|
+
semanticRole: elementData.semanticRole,
|
|
924
|
+
accName,
|
|
925
|
+
suggestedPattern: suggestedPattern || undefined
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
return undefined;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Generate context-aware remediations
|
|
934
|
+
*/
|
|
935
|
+
function generateContextAwareRemediations(violations) {
|
|
936
|
+
return violations.map((violation, index) => {
|
|
937
|
+
// Calculate priority based on severity and user impact
|
|
938
|
+
const priorityScore = calculatePriorityScore(violation);
|
|
939
|
+
// Estimate remediation effort
|
|
940
|
+
const effort = estimateRemediationEffort(violation);
|
|
941
|
+
// Generate remediation recommendations
|
|
942
|
+
const recommendations = generateRecommendations(violation);
|
|
943
|
+
// Calculate ROI (priority / effort)
|
|
944
|
+
const roi = priorityScore / effort.hours;
|
|
945
|
+
return {
|
|
946
|
+
violationId: violation.id,
|
|
947
|
+
priority: priorityScore,
|
|
948
|
+
estimatedEffort: effort,
|
|
949
|
+
recommendations,
|
|
950
|
+
roi
|
|
951
|
+
};
|
|
952
|
+
}).sort((a, b) => b.roi - a.roi); // Sort by ROI descending
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Calculate priority score (1-10)
|
|
956
|
+
*/
|
|
957
|
+
function calculatePriorityScore(violation) {
|
|
958
|
+
const severityScores = {
|
|
959
|
+
critical: 10,
|
|
960
|
+
serious: 7,
|
|
961
|
+
moderate: 4,
|
|
962
|
+
minor: 2
|
|
963
|
+
};
|
|
964
|
+
const baseScore = severityScores[violation.severity];
|
|
965
|
+
const impactMultiplier = (violation.userImpact?.affectedUserPercentage || 5) / 10;
|
|
966
|
+
return Math.min(10, baseScore + impactMultiplier);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Estimate remediation effort
|
|
970
|
+
*/
|
|
971
|
+
function estimateRemediationEffort(violation) {
|
|
972
|
+
// Simple heuristic based on violation type
|
|
973
|
+
const elementCount = violation.elements.length;
|
|
974
|
+
let baseHours = 0.5;
|
|
975
|
+
let complexity = 'simple';
|
|
976
|
+
if (violation.wcagCriterion.startsWith('1.1')) {
|
|
977
|
+
// Alt text violations - simple
|
|
978
|
+
baseHours = 0.25 * elementCount;
|
|
979
|
+
complexity = 'trivial';
|
|
980
|
+
}
|
|
981
|
+
else if (violation.wcagCriterion.startsWith('4.1')) {
|
|
982
|
+
// ARIA violations - moderate
|
|
983
|
+
baseHours = 0.5 * elementCount;
|
|
984
|
+
complexity = 'moderate';
|
|
985
|
+
}
|
|
986
|
+
else if (violation.wcagCriterion.startsWith('2.1')) {
|
|
987
|
+
// Keyboard navigation - can be complex
|
|
988
|
+
baseHours = 1 * elementCount;
|
|
989
|
+
complexity = 'complex';
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
hours: Math.max(0.25, baseHours),
|
|
993
|
+
complexity
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Generate remediation recommendations
|
|
998
|
+
* Enhanced with APG patterns and AccName intelligence
|
|
999
|
+
*/
|
|
1000
|
+
function generateRecommendations(violation) {
|
|
1001
|
+
const recommendations = [];
|
|
1002
|
+
const element = violation.elements[0];
|
|
1003
|
+
const context = element?.context;
|
|
1004
|
+
// PRIORITY 1: Check if this is a CUSTOM violation with specific howToFix code
|
|
1005
|
+
// Custom violations from runCustomHeuristicChecks() have detailed, ready-to-use code
|
|
1006
|
+
if (violation.howToFix && violation.id.startsWith('custom-')) {
|
|
1007
|
+
// Extract code from howToFix field
|
|
1008
|
+
// Format can be:
|
|
1009
|
+
// 1. "Add descriptive aria-label: aria-label="...""
|
|
1010
|
+
// 2. "Add caption track:\n\n<track ...>\n\nGenerated caption file..."
|
|
1011
|
+
const howToFix = violation.howToFix;
|
|
1012
|
+
// For generic link violations with aria-label recommendations
|
|
1013
|
+
if (howToFix.includes('aria-label=')) {
|
|
1014
|
+
const ariaMatch = howToFix.match(/aria-label="([^"]+)"/);
|
|
1015
|
+
if (ariaMatch) {
|
|
1016
|
+
const ariaLabel = ariaMatch[1];
|
|
1017
|
+
recommendations.push({
|
|
1018
|
+
approach: 'aria-enhancement',
|
|
1019
|
+
priority: 1,
|
|
1020
|
+
code: `aria-label="${ariaLabel}"`,
|
|
1021
|
+
rationale: violation.description,
|
|
1022
|
+
wcagCriteria: [violation.wcagCriterion],
|
|
1023
|
+
confidence: 0.9
|
|
1024
|
+
});
|
|
1025
|
+
return recommendations; // Return immediately - don't generate generic recommendations
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// For video caption violations with WebVTT files
|
|
1029
|
+
if (howToFix.includes('<track') && howToFix.includes('WEBVTT')) {
|
|
1030
|
+
recommendations.push({
|
|
1031
|
+
approach: 'semantic-html',
|
|
1032
|
+
priority: 1,
|
|
1033
|
+
code: howToFix, // Use the entire howToFix as it contains both HTML and WebVTT
|
|
1034
|
+
rationale: violation.description,
|
|
1035
|
+
wcagCriteria: [violation.wcagCriterion],
|
|
1036
|
+
confidence: 0.95
|
|
1037
|
+
});
|
|
1038
|
+
return recommendations; // Return immediately - this is complete, copy-paste-ready code
|
|
1039
|
+
}
|
|
1040
|
+
// For aria-hidden focusability violations with tabindex fixes
|
|
1041
|
+
if (howToFix.includes('<!-- ISSUE:') && howToFix.includes('tabindex="-1"')) {
|
|
1042
|
+
recommendations.push({
|
|
1043
|
+
approach: 'semantic-html',
|
|
1044
|
+
priority: 1,
|
|
1045
|
+
code: howToFix, // Use the entire howToFix with issue + fix comments
|
|
1046
|
+
rationale: violation.description,
|
|
1047
|
+
wcagCriteria: [violation.wcagCriterion],
|
|
1048
|
+
confidence: 0.9
|
|
1049
|
+
});
|
|
1050
|
+
return recommendations; // Return immediately - this is complete, copy-paste-ready code
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
// Use APG pattern if suggested
|
|
1054
|
+
if (context?.suggestedPattern) {
|
|
1055
|
+
const { pattern, confidence, reason } = context.suggestedPattern;
|
|
1056
|
+
recommendations.push({
|
|
1057
|
+
approach: 'semantic-html',
|
|
1058
|
+
priority: 1,
|
|
1059
|
+
code: (0, apg_patterns_js_1.generatePatternCodeExample)(pattern.name, {
|
|
1060
|
+
includeJavaScript: true,
|
|
1061
|
+
includeCSS: false
|
|
1062
|
+
}),
|
|
1063
|
+
rationale: `Use W3C APG ${pattern.name} pattern: ${reason}. Reference: ${pattern.apgUrl}`,
|
|
1064
|
+
wcagCriteria: pattern.wcagCriteria,
|
|
1065
|
+
confidence
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
// Use AccName recommendation if available
|
|
1069
|
+
if (context?.accName && !context.accName.sufficient) {
|
|
1070
|
+
const accNameRec = (0, accname_computation_js_1.generateAccessibleNameRecommendation)(context.accName, {
|
|
1071
|
+
tagName: element.html.match(/<(\w+)/)?.[1] || 'div',
|
|
1072
|
+
attributes: {},
|
|
1073
|
+
context: context.surroundingText
|
|
1074
|
+
});
|
|
1075
|
+
if (accNameRec.priority === 'critical' || accNameRec.priority === 'high') {
|
|
1076
|
+
recommendations.push({
|
|
1077
|
+
approach: context.accName.source.type === 'aria-label' ? 'aria-enhancement' : 'semantic-html',
|
|
1078
|
+
priority: accNameRec.priority === 'critical' ? 1 : 2,
|
|
1079
|
+
code: accNameRec.codeExample,
|
|
1080
|
+
rationale: `${accNameRec.recommendation}. Current accessible name: "${context.accName.accessibleName}" (quality: ${context.accName.quality}/100)`,
|
|
1081
|
+
wcagCriteria: [violation.wcagCriterion, '4.1.2'],
|
|
1082
|
+
confidence: 0.9
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
// Generate recommendations based on violation type
|
|
1087
|
+
if (violation.description.toLowerCase().includes('aria-label')) {
|
|
1088
|
+
if (recommendations.length === 0) {
|
|
1089
|
+
recommendations.push({
|
|
1090
|
+
approach: 'aria-enhancement',
|
|
1091
|
+
priority: 1,
|
|
1092
|
+
code: generateARIARecommendation(violation),
|
|
1093
|
+
rationale: 'Add descriptive ARIA label for screen reader users',
|
|
1094
|
+
wcagCriteria: [violation.wcagCriterion],
|
|
1095
|
+
confidence: 0.85
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (violation.description.toLowerCase().includes('alt') || violation.description.toLowerCase().includes('image')) {
|
|
1100
|
+
const element = violation.elements[0];
|
|
1101
|
+
const html = element?.html || '';
|
|
1102
|
+
const selector = element?.selector || '';
|
|
1103
|
+
// Extract actual image URL and parent link context
|
|
1104
|
+
const srcMatch = html.match(/src="([^"]+)"/);
|
|
1105
|
+
// Check BOTH HTML and CSS selector for parent aria-label
|
|
1106
|
+
let parentMatch = html.match(/aria-label="([^"]+)"/);
|
|
1107
|
+
if (!parentMatch && selector) {
|
|
1108
|
+
// Extract from CSS selector like: .parent[aria-label="Text"] > .child
|
|
1109
|
+
parentMatch = selector.match(/aria-label=["']([^"']+)["']/);
|
|
1110
|
+
}
|
|
1111
|
+
let specificAlt = '';
|
|
1112
|
+
let rationale = 'Image requires descriptive alt text for screen reader users.';
|
|
1113
|
+
if (srcMatch) {
|
|
1114
|
+
const src = srcMatch[1];
|
|
1115
|
+
// Analyze parent link context
|
|
1116
|
+
if (parentMatch) {
|
|
1117
|
+
const parentLabel = parentMatch[1];
|
|
1118
|
+
// Generate SPECIFIC alt text based on parent link
|
|
1119
|
+
if (parentLabel.toLowerCase().includes('e-mobility') || parentLabel.toLowerCase().includes('electric')) {
|
|
1120
|
+
specificAlt = 'alt="Audi electric vehicle showcasing e-mobility technology"';
|
|
1121
|
+
rationale = `Parent link: "${parentLabel}". Image shows Audi's electric vehicle for this e-mobility section.`;
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
specificAlt = `alt="Visual for: ${parentLabel}"`;
|
|
1125
|
+
rationale = `Parent link says "${parentLabel}". Image provides visual context for this link.`;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
// Analyze image URL for brand/product context
|
|
1129
|
+
else if (src.includes('audi.com') || src.includes('dam.audi')) {
|
|
1130
|
+
if (src.includes('mobility') || src.includes('e-tron') || src.includes('electric')) {
|
|
1131
|
+
specificAlt = 'alt="Audi electric vehicle showcasing e-mobility technology"';
|
|
1132
|
+
rationale = 'Image URL suggests Audi e-mobility content. Alt text describes the electric vehicle shown.';
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
specificAlt = 'alt="Audi promotional image"';
|
|
1136
|
+
rationale = 'Image from Audi Digital Asset Management. Describe the specific Audi product or feature shown.';
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
// Generic but more specific than placeholder
|
|
1140
|
+
else {
|
|
1141
|
+
const filename = src.split('/').pop()?.split('?')[0] || '';
|
|
1142
|
+
const cleanName = filename
|
|
1143
|
+
.replace(/\.(jpg|png|svg|webp|gif)$/i, '')
|
|
1144
|
+
.replace(/[-_]/g, ' ')
|
|
1145
|
+
.replace(/\d{2,}/g, '')
|
|
1146
|
+
.trim();
|
|
1147
|
+
if (cleanName && cleanName.length > 3) {
|
|
1148
|
+
specificAlt = `alt="${cleanName}"`;
|
|
1149
|
+
rationale = `Inferred from filename "${cleanName}". Verify this accurately describes the image content.`;
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
specificAlt = generateAltTextRecommendation(violation);
|
|
1153
|
+
rationale = 'Analyze the image content and provide specific description.';
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
specificAlt = generateAltTextRecommendation(violation);
|
|
1159
|
+
}
|
|
1160
|
+
recommendations.push({
|
|
1161
|
+
approach: 'semantic-html',
|
|
1162
|
+
priority: 1,
|
|
1163
|
+
code: `<!-- Current HTML -->\n${element?.html?.slice(0, 150) || '<img ...>'}${element?.html && element.html.length > 150 ? '...' : ''}\n\n<!-- RECOMMENDED FIX -->\n${specificAlt}`,
|
|
1164
|
+
rationale,
|
|
1165
|
+
wcagCriteria: [violation.wcagCriterion],
|
|
1166
|
+
confidence: parentMatch ? 0.85 : 0.75
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
// Default generic recommendation
|
|
1170
|
+
if (recommendations.length === 0) {
|
|
1171
|
+
recommendations.push({
|
|
1172
|
+
approach: 'semantic-html',
|
|
1173
|
+
priority: 2,
|
|
1174
|
+
code: `<!-- Fix required for ${violation.wcagCriterion} -->\n<!-- See: ${violation.helpUrl} -->`,
|
|
1175
|
+
rationale: violation.impact,
|
|
1176
|
+
wcagCriteria: [violation.wcagCriterion],
|
|
1177
|
+
confidence: 0.7
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
return recommendations.slice(0, 3); // Limit to top 3 recommendations
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Generate ARIA label recommendation with SPECIFIC context-aware suggestions
|
|
1184
|
+
*/
|
|
1185
|
+
function generateARIARecommendation(violation) {
|
|
1186
|
+
const element = violation.elements[0];
|
|
1187
|
+
if (!element)
|
|
1188
|
+
return 'aria-label="[Describe the action this element performs]"';
|
|
1189
|
+
const html = element.html.toLowerCase();
|
|
1190
|
+
const context = element.context;
|
|
1191
|
+
let suggestedLabel = '';
|
|
1192
|
+
// Analyze actual element content and context
|
|
1193
|
+
if (html.includes('close') || html.includes('×') || html.includes('x')) {
|
|
1194
|
+
const parentContext = context?.surroundingText?.toLowerCase() || '';
|
|
1195
|
+
if (parentContext.includes('modal') || parentContext.includes('dialog')) {
|
|
1196
|
+
suggestedLabel = 'Close dialog';
|
|
1197
|
+
}
|
|
1198
|
+
else if (parentContext.includes('menu')) {
|
|
1199
|
+
suggestedLabel = 'Close menu';
|
|
1200
|
+
}
|
|
1201
|
+
else if (parentContext.includes('banner') || parentContext.includes('notification')) {
|
|
1202
|
+
suggestedLabel = 'Close notification';
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
suggestedLabel = 'Close';
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
else if (html.includes('menu') || html.includes('☰') || html.includes('hamburger')) {
|
|
1209
|
+
suggestedLabel = 'Open navigation menu';
|
|
1210
|
+
}
|
|
1211
|
+
else if (html.includes('search')) {
|
|
1212
|
+
suggestedLabel = 'Search';
|
|
1213
|
+
}
|
|
1214
|
+
else if (html.includes('cart') || html.includes('shopping')) {
|
|
1215
|
+
suggestedLabel = 'View shopping cart';
|
|
1216
|
+
}
|
|
1217
|
+
else if (html.includes('user') || html.includes('account') || html.includes('profile')) {
|
|
1218
|
+
suggestedLabel = 'My account';
|
|
1219
|
+
}
|
|
1220
|
+
else if (html.includes('chevron') || html.includes('arrow')) {
|
|
1221
|
+
if (html.includes('right') || html.includes('next')) {
|
|
1222
|
+
suggestedLabel = 'Next';
|
|
1223
|
+
}
|
|
1224
|
+
else if (html.includes('left') || html.includes('prev')) {
|
|
1225
|
+
suggestedLabel = 'Previous';
|
|
1226
|
+
}
|
|
1227
|
+
else if (html.includes('down')) {
|
|
1228
|
+
suggestedLabel = 'Expand';
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
suggestedLabel = 'Navigate';
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
else if (html.includes('play')) {
|
|
1235
|
+
suggestedLabel = 'Play video';
|
|
1236
|
+
}
|
|
1237
|
+
else if (html.includes('pause')) {
|
|
1238
|
+
suggestedLabel = 'Pause video';
|
|
1239
|
+
}
|
|
1240
|
+
else if (context?.surroundingText) {
|
|
1241
|
+
// Extract meaningful text from context
|
|
1242
|
+
const text = context.surroundingText
|
|
1243
|
+
.replace(/\s+/g, ' ')
|
|
1244
|
+
.trim()
|
|
1245
|
+
.slice(0, 50);
|
|
1246
|
+
if (text && text.length > 3) {
|
|
1247
|
+
suggestedLabel = text;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (!suggestedLabel) {
|
|
1251
|
+
suggestedLabel = '[Describe the specific action: e.g., "Submit contact form", "Download PDF", "Open filters"]';
|
|
1252
|
+
}
|
|
1253
|
+
return `aria-label="${suggestedLabel}"`;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Generate SPECIFIC alt text recommendation based on actual image context
|
|
1257
|
+
*/
|
|
1258
|
+
function generateAltTextRecommendation(violation) {
|
|
1259
|
+
const element = violation.elements[0];
|
|
1260
|
+
if (!element)
|
|
1261
|
+
return 'alt="[Describe what this specific image shows]"';
|
|
1262
|
+
const html = element.html.toLowerCase();
|
|
1263
|
+
let suggestedAlt = '';
|
|
1264
|
+
// Extract image src for context
|
|
1265
|
+
const srcMatch = html.match(/src="([^"]+)"/);
|
|
1266
|
+
const src = srcMatch ? srcMatch[1].toLowerCase() : '';
|
|
1267
|
+
// Analyze image filename and path for clues
|
|
1268
|
+
if (src) {
|
|
1269
|
+
if (src.includes('logo')) {
|
|
1270
|
+
// Extract brand name from URL if possible
|
|
1271
|
+
const urlParts = src.split('/');
|
|
1272
|
+
const domain = urlParts.find(part => part.includes('.com') || part.includes('.org'));
|
|
1273
|
+
if (domain) {
|
|
1274
|
+
const brandName = domain.split('.')[0];
|
|
1275
|
+
suggestedAlt = `${brandName.charAt(0).toUpperCase() + brandName.slice(1)} logo`;
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
suggestedAlt = 'Company logo';
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
else if (src.includes('icon')) {
|
|
1282
|
+
suggestedAlt = '[Decorative icon - use alt="" if purely decorative, or describe its meaning]';
|
|
1283
|
+
}
|
|
1284
|
+
else if (src.includes('product') || src.includes('item')) {
|
|
1285
|
+
suggestedAlt = '[Product name and key features, e.g., "Audi e-tron GT electric vehicle in metallic silver"]';
|
|
1286
|
+
}
|
|
1287
|
+
else if (src.includes('hero') || src.includes('banner')) {
|
|
1288
|
+
suggestedAlt = '[Main subject of banner image, e.g., "Customer using mobile app to track delivery"]';
|
|
1289
|
+
}
|
|
1290
|
+
else if (src.includes('team') || src.includes('person') || src.includes('profile')) {
|
|
1291
|
+
suggestedAlt = '[Person\'s name and role, e.g., "Jane Smith, Chief Technology Officer"]';
|
|
1292
|
+
}
|
|
1293
|
+
else if (src.includes('chart') || src.includes('graph') || src.includes('diagram')) {
|
|
1294
|
+
suggestedAlt = '[Describe the data shown, e.g., "Bar chart showing 40% increase in sales from 2023 to 2024"]';
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
// Try to infer from URL structure
|
|
1298
|
+
const filename = src.split('/').pop()?.split('?')[0] || '';
|
|
1299
|
+
const cleanName = filename
|
|
1300
|
+
.replace(/\.(jpg|png|svg|webp|gif)$/i, '')
|
|
1301
|
+
.replace(/[-_]/g, ' ')
|
|
1302
|
+
.trim();
|
|
1303
|
+
if (cleanName && cleanName.length > 3) {
|
|
1304
|
+
suggestedAlt = cleanName;
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
suggestedAlt = '[Describe what this image shows and its purpose on the page]';
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
// Check if it's in a link to provide more context
|
|
1312
|
+
const context = element.context;
|
|
1313
|
+
if (context?.parentElement === 'a' || html.includes('<a ')) {
|
|
1314
|
+
if (suggestedAlt.startsWith('[')) {
|
|
1315
|
+
suggestedAlt = '[Image shows: ... ] - Link destination: [where this link goes]';
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
suggestedAlt = `${suggestedAlt} - [add link destination, e.g., "Learn more about ${suggestedAlt}"]`;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (!suggestedAlt) {
|
|
1322
|
+
suggestedAlt = '[Describe the specific content of this image]';
|
|
1323
|
+
}
|
|
1324
|
+
return `alt="${suggestedAlt}"`;
|
|
1325
|
+
}
|
|
1326
|
+
//# sourceMappingURL=scan-comprehensive.js.map
|