agentic-qe 2.4.0 → 2.5.1

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.
Files changed (174) hide show
  1. package/.claude/agents/qe-a11y-ally.md +855 -0
  2. package/.claude/agents/qx-partner.md +120 -4
  3. package/.claude/skills/testability-scoring/SKILL.md +107 -6
  4. package/CHANGELOG.md +135 -0
  5. package/README.md +7 -6
  6. package/dist/agents/AccessibilityAllyAgent.d.ts +168 -0
  7. package/dist/agents/AccessibilityAllyAgent.d.ts.map +1 -0
  8. package/dist/agents/AccessibilityAllyAgent.js +462 -0
  9. package/dist/agents/AccessibilityAllyAgent.js.map +1 -0
  10. package/dist/agents/SONAIntegration.d.ts +109 -0
  11. package/dist/agents/SONAIntegration.d.ts.map +1 -0
  12. package/dist/agents/SONAIntegration.js +167 -0
  13. package/dist/agents/SONAIntegration.js.map +1 -0
  14. package/dist/agents/index.d.ts +3 -0
  15. package/dist/agents/index.d.ts.map +1 -1
  16. package/dist/agents/index.js +93 -2
  17. package/dist/agents/index.js.map +1 -1
  18. package/dist/cli/init/agents.js +1 -1
  19. package/dist/cli/init/claude-config.js +2 -2
  20. package/dist/cli/init/database-init.js +1 -1
  21. package/dist/core/cache/BinaryCacheImpl.d.ts +161 -0
  22. package/dist/core/cache/BinaryCacheImpl.d.ts.map +1 -0
  23. package/dist/core/cache/BinaryCacheImpl.js +685 -0
  24. package/dist/core/cache/BinaryCacheImpl.js.map +1 -0
  25. package/dist/core/cache/BinaryMetadataCache.d.ts +244 -0
  26. package/dist/core/cache/BinaryMetadataCache.d.ts.map +1 -1
  27. package/dist/core/cache/BinaryMetadataCache.js +63 -1
  28. package/dist/core/cache/BinaryMetadataCache.js.map +1 -1
  29. package/dist/core/cache/index.d.ts +1 -0
  30. package/dist/core/cache/index.d.ts.map +1 -1
  31. package/dist/core/cache/index.js +10 -1
  32. package/dist/core/cache/index.js.map +1 -1
  33. package/dist/core/memory/AgentDBService.d.ts +30 -4
  34. package/dist/core/memory/AgentDBService.d.ts.map +1 -1
  35. package/dist/core/memory/AgentDBService.js +122 -12
  36. package/dist/core/memory/AgentDBService.js.map +1 -1
  37. package/dist/core/memory/CachedHNSWVectorMemory.d.ts +153 -0
  38. package/dist/core/memory/CachedHNSWVectorMemory.d.ts.map +1 -0
  39. package/dist/core/memory/CachedHNSWVectorMemory.js +329 -0
  40. package/dist/core/memory/CachedHNSWVectorMemory.js.map +1 -0
  41. package/dist/core/memory/HNSWVectorMemory.js +1 -1
  42. package/dist/core/memory/RuVectorPatternStore.d.ts.map +1 -1
  43. package/dist/core/memory/RuVectorPatternStore.js +8 -2
  44. package/dist/core/memory/RuVectorPatternStore.js.map +1 -1
  45. package/dist/core/memory/UnifiedMemoryCoordinator.d.ts +50 -0
  46. package/dist/core/memory/UnifiedMemoryCoordinator.d.ts.map +1 -1
  47. package/dist/core/memory/UnifiedMemoryCoordinator.js +206 -0
  48. package/dist/core/memory/UnifiedMemoryCoordinator.js.map +1 -1
  49. package/dist/core/memory/index.d.ts +2 -0
  50. package/dist/core/memory/index.d.ts.map +1 -1
  51. package/dist/core/memory/index.js +8 -1
  52. package/dist/core/memory/index.js.map +1 -1
  53. package/dist/core/optimization/RecursiveOptimizer.d.ts +233 -0
  54. package/dist/core/optimization/RecursiveOptimizer.d.ts.map +1 -0
  55. package/dist/core/optimization/RecursiveOptimizer.js +509 -0
  56. package/dist/core/optimization/RecursiveOptimizer.js.map +1 -0
  57. package/dist/core/strategies/SONALearningStrategy.d.ts +115 -0
  58. package/dist/core/strategies/SONALearningStrategy.d.ts.map +1 -0
  59. package/dist/core/strategies/SONALearningStrategy.js +656 -0
  60. package/dist/core/strategies/SONALearningStrategy.js.map +1 -0
  61. package/dist/core/strategies/TRMLearningStrategy.d.ts +162 -0
  62. package/dist/core/strategies/TRMLearningStrategy.d.ts.map +1 -0
  63. package/dist/core/strategies/TRMLearningStrategy.js +670 -0
  64. package/dist/core/strategies/TRMLearningStrategy.js.map +1 -0
  65. package/dist/core/strategies/index.d.ts +10 -1
  66. package/dist/core/strategies/index.d.ts.map +1 -1
  67. package/dist/core/strategies/index.js +4 -1
  68. package/dist/core/strategies/index.js.map +1 -1
  69. package/dist/learning/SONAFeedbackLoop.d.ts +168 -0
  70. package/dist/learning/SONAFeedbackLoop.d.ts.map +1 -0
  71. package/dist/learning/SONAFeedbackLoop.js +344 -0
  72. package/dist/learning/SONAFeedbackLoop.js.map +1 -0
  73. package/dist/learning/baselines/BaselineCollector.d.ts +1 -1
  74. package/dist/learning/baselines/BaselineCollector.js +1 -1
  75. package/dist/learning/baselines/StandardTaskSuite.d.ts +1 -1
  76. package/dist/learning/baselines/StandardTaskSuite.js +1 -1
  77. package/dist/learning/index.d.ts +2 -0
  78. package/dist/learning/index.d.ts.map +1 -1
  79. package/dist/learning/index.js +6 -1
  80. package/dist/learning/index.js.map +1 -1
  81. package/dist/mcp/server-instructions.d.ts +1 -1
  82. package/dist/mcp/server-instructions.js +1 -1
  83. package/dist/mcp/server.d.ts.map +1 -1
  84. package/dist/mcp/server.js +23 -16
  85. package/dist/mcp/server.js.map +1 -1
  86. package/dist/mcp/services/AgentRegistry.d.ts.map +1 -1
  87. package/dist/mcp/services/AgentRegistry.js +6 -1
  88. package/dist/mcp/services/AgentRegistry.js.map +1 -1
  89. package/dist/mcp/tools/qe/accessibility/accname-computation.d.ts +114 -0
  90. package/dist/mcp/tools/qe/accessibility/accname-computation.d.ts.map +1 -0
  91. package/dist/mcp/tools/qe/accessibility/accname-computation.js +566 -0
  92. package/dist/mcp/tools/qe/accessibility/accname-computation.js.map +1 -0
  93. package/dist/mcp/tools/qe/accessibility/apg-patterns.d.ts +103 -0
  94. package/dist/mcp/tools/qe/accessibility/apg-patterns.d.ts.map +1 -0
  95. package/dist/mcp/tools/qe/accessibility/apg-patterns.js +1028 -0
  96. package/dist/mcp/tools/qe/accessibility/apg-patterns.js.map +1 -0
  97. package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.d.ts +48 -0
  98. package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.d.ts.map +1 -0
  99. package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.js +565 -0
  100. package/dist/mcp/tools/qe/accessibility/en-301-549-mapping.js.map +1 -0
  101. package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.d.ts +117 -0
  102. package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.d.ts.map +1 -0
  103. package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.js +571 -0
  104. package/dist/mcp/tools/qe/accessibility/eu-accessibility-act.js.map +1 -0
  105. package/dist/mcp/tools/qe/accessibility/html-report-generator.d.ts +23 -0
  106. package/dist/mcp/tools/qe/accessibility/html-report-generator.d.ts.map +1 -0
  107. package/dist/mcp/tools/qe/accessibility/html-report-generator.js +1152 -0
  108. package/dist/mcp/tools/qe/accessibility/html-report-generator.js.map +1 -0
  109. package/dist/mcp/tools/qe/accessibility/index.d.ts +22 -0
  110. package/dist/mcp/tools/qe/accessibility/index.d.ts.map +1 -0
  111. package/dist/mcp/tools/qe/accessibility/index.js +38 -0
  112. package/dist/mcp/tools/qe/accessibility/index.js.map +1 -0
  113. package/dist/mcp/tools/qe/accessibility/markdown-report-generator.d.ts +18 -0
  114. package/dist/mcp/tools/qe/accessibility/markdown-report-generator.d.ts.map +1 -0
  115. package/dist/mcp/tools/qe/accessibility/markdown-report-generator.js +549 -0
  116. package/dist/mcp/tools/qe/accessibility/markdown-report-generator.js.map +1 -0
  117. package/dist/mcp/tools/qe/accessibility/remediation-code-generator.d.ts +139 -0
  118. package/dist/mcp/tools/qe/accessibility/remediation-code-generator.d.ts.map +1 -0
  119. package/dist/mcp/tools/qe/accessibility/remediation-code-generator.js +1300 -0
  120. package/dist/mcp/tools/qe/accessibility/remediation-code-generator.js.map +1 -0
  121. package/dist/mcp/tools/qe/accessibility/scan-comprehensive.d.ts +138 -0
  122. package/dist/mcp/tools/qe/accessibility/scan-comprehensive.d.ts.map +1 -0
  123. package/dist/mcp/tools/qe/accessibility/scan-comprehensive.js +1298 -0
  124. package/dist/mcp/tools/qe/accessibility/scan-comprehensive.js.map +1 -0
  125. package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.d.ts +50 -0
  126. package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.d.ts.map +1 -0
  127. package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.js +469 -0
  128. package/dist/mcp/tools/qe/accessibility/video-vision-analyzer.js.map +1 -0
  129. package/dist/mcp/tools/qe/accessibility/webvtt-generator.d.ts +193 -0
  130. package/dist/mcp/tools/qe/accessibility/webvtt-generator.d.ts.map +1 -0
  131. package/dist/mcp/tools/qe/accessibility/webvtt-generator.js +511 -0
  132. package/dist/mcp/tools/qe/accessibility/webvtt-generator.js.map +1 -0
  133. package/dist/mcp/tools.d.ts +1 -0
  134. package/dist/mcp/tools.d.ts.map +1 -1
  135. package/dist/mcp/tools.js +61 -0
  136. package/dist/mcp/tools.js.map +1 -1
  137. package/dist/providers/HybridRouter.d.ts +34 -3
  138. package/dist/providers/HybridRouter.d.ts.map +1 -1
  139. package/dist/providers/HybridRouter.js +69 -4
  140. package/dist/providers/HybridRouter.js.map +1 -1
  141. package/dist/providers/LLMProviderFactory.d.ts +68 -1
  142. package/dist/providers/LLMProviderFactory.d.ts.map +1 -1
  143. package/dist/providers/LLMProviderFactory.js +173 -6
  144. package/dist/providers/LLMProviderFactory.js.map +1 -1
  145. package/dist/providers/OpenRouterProvider.d.ts +150 -0
  146. package/dist/providers/OpenRouterProvider.d.ts.map +1 -0
  147. package/dist/providers/OpenRouterProvider.js +545 -0
  148. package/dist/providers/OpenRouterProvider.js.map +1 -0
  149. package/dist/providers/RuvllmProvider.d.ts +130 -16
  150. package/dist/providers/RuvllmProvider.d.ts.map +1 -1
  151. package/dist/providers/RuvllmProvider.js +399 -83
  152. package/dist/providers/RuvllmProvider.js.map +1 -1
  153. package/dist/providers/index.d.ts +33 -4
  154. package/dist/providers/index.d.ts.map +1 -1
  155. package/dist/providers/index.js +72 -21
  156. package/dist/providers/index.js.map +1 -1
  157. package/dist/telemetry/instrumentation/agent.d.ts +1 -1
  158. package/dist/telemetry/instrumentation/agent.js +1 -1
  159. package/dist/telemetry/instrumentation/index.d.ts +1 -1
  160. package/dist/telemetry/instrumentation/index.js +1 -1
  161. package/dist/types/index.d.ts +2 -1
  162. package/dist/types/index.d.ts.map +1 -1
  163. package/dist/types/index.js +2 -0
  164. package/dist/types/index.js.map +1 -1
  165. package/dist/types/ruvllm.d.ts +97 -0
  166. package/dist/types/ruvllm.d.ts.map +1 -0
  167. package/dist/types/ruvllm.js +46 -0
  168. package/dist/types/ruvllm.js.map +1 -0
  169. package/dist/utils/ruvllm-loader.d.ts +94 -0
  170. package/dist/utils/ruvllm-loader.d.ts.map +1 -0
  171. package/dist/utils/ruvllm-loader.js +87 -0
  172. package/dist/utils/ruvllm-loader.js.map +1 -0
  173. package/docs/reference/agents.md +36 -1
  174. package/package.json +5 -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