cbrowser 8.10.0 → 9.1.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/README.md +121 -0
- package/dist/analysis/accessibility-empathy.d.ts +32 -0
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -0
- package/dist/analysis/accessibility-empathy.js +1005 -0
- package/dist/analysis/accessibility-empathy.js.map +1 -0
- package/dist/analysis/agent-ready-audit.d.ts +11 -0
- package/dist/analysis/agent-ready-audit.d.ts.map +1 -0
- package/dist/analysis/agent-ready-audit.js +900 -0
- package/dist/analysis/agent-ready-audit.js.map +1 -0
- package/dist/analysis/competitive-benchmark.d.ts +11 -0
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -0
- package/dist/analysis/competitive-benchmark.js +1122 -0
- package/dist/analysis/competitive-benchmark.js.map +1 -0
- package/dist/analysis/index.d.ts +5 -1
- package/dist/analysis/index.d.ts.map +1 -1
- package/dist/analysis/index.js +5 -1
- package/dist/analysis/index.js.map +1 -1
- package/dist/cli.js +191 -2
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/focus-hierarchies.d.ts +113 -0
- package/dist/cognitive/focus-hierarchies.d.ts.map +1 -0
- package/dist/cognitive/focus-hierarchies.js +500 -0
- package/dist/cognitive/focus-hierarchies.js.map +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +100 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/personas.d.ts +14 -0
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +393 -0
- package/dist/personas.js.map +1 -1
- package/dist/types.d.ts +429 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/METHODOLOGY.md +245 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility Empathy Audit Module
|
|
3
|
+
*
|
|
4
|
+
* Simulate how people with disabilities EXPERIENCE a site — motor impairments,
|
|
5
|
+
* cognitive differences, sensory limitations — not just WCAG compliance checking.
|
|
6
|
+
*
|
|
7
|
+
* METHODOLOGY DISCLAIMER:
|
|
8
|
+
*
|
|
9
|
+
* Empathy scores are HEURISTIC estimates based on barrier detection and
|
|
10
|
+
* persona simulation, not actual user testing with disabled individuals.
|
|
11
|
+
*
|
|
12
|
+
* Research-backed thresholds:
|
|
13
|
+
* - Touch targets: 44x44px minimum (WCAG 2.5.5 AAA, 2.5.8 AA)
|
|
14
|
+
* - Contrast ratios: 4.5:1 normal text, 3:1 large text (WCAG 1.4.3)
|
|
15
|
+
* - Form complexity: 7-8 fields optimal before cognitive overload (Baymard)
|
|
16
|
+
* - Screen reader success: ~55.6% task completion rate (WebAIM 2024)
|
|
17
|
+
*
|
|
18
|
+
* Heuristic components (interpret as directional, not precise):
|
|
19
|
+
* - Barrier detection: Identifies patterns known to cause difficulties
|
|
20
|
+
* - Friction points: Simulated based on persona traits, not actual user data
|
|
21
|
+
* - Empathy score: Composite for comparison, letter grades recommended
|
|
22
|
+
*
|
|
23
|
+
* For actual accessibility validation, combine with:
|
|
24
|
+
* - Automated WCAG checkers (axe, WAVE)
|
|
25
|
+
* - Manual testing with assistive technologies
|
|
26
|
+
* - User testing with people who have disabilities
|
|
27
|
+
*/
|
|
28
|
+
import { chromium } from "playwright";
|
|
29
|
+
import { getAccessibilityPersona } from "../personas.js";
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// WCAG Mapping
|
|
32
|
+
// ============================================================================
|
|
33
|
+
const WCAG_CRITERIA = {
|
|
34
|
+
"1.1.1": { level: "A", description: "Non-text Content" },
|
|
35
|
+
"1.3.1": { level: "A", description: "Info and Relationships" },
|
|
36
|
+
"1.4.1": { level: "A", description: "Use of Color" },
|
|
37
|
+
"1.4.3": { level: "AA", description: "Contrast (Minimum)" },
|
|
38
|
+
"1.4.4": { level: "AA", description: "Resize Text" },
|
|
39
|
+
"1.4.6": { level: "AAA", description: "Contrast (Enhanced)" },
|
|
40
|
+
"1.4.10": { level: "AA", description: "Reflow" },
|
|
41
|
+
"2.1.1": { level: "A", description: "Keyboard" },
|
|
42
|
+
"2.1.2": { level: "A", description: "No Keyboard Trap" },
|
|
43
|
+
"2.2.1": { level: "A", description: "Timing Adjustable" },
|
|
44
|
+
"2.2.2": { level: "A", description: "Pause, Stop, Hide" },
|
|
45
|
+
"2.3.1": { level: "A", description: "Three Flashes or Below" },
|
|
46
|
+
"2.4.1": { level: "A", description: "Bypass Blocks" },
|
|
47
|
+
"2.4.3": { level: "A", description: "Focus Order" },
|
|
48
|
+
"2.4.4": { level: "A", description: "Link Purpose" },
|
|
49
|
+
"2.4.6": { level: "AA", description: "Headings and Labels" },
|
|
50
|
+
"2.4.7": { level: "AA", description: "Focus Visible" },
|
|
51
|
+
"2.5.5": { level: "AAA", description: "Target Size" },
|
|
52
|
+
"2.5.8": { level: "AA", description: "Target Size (Minimum)" },
|
|
53
|
+
"3.1.1": { level: "A", description: "Language of Page" },
|
|
54
|
+
"3.2.1": { level: "A", description: "On Focus" },
|
|
55
|
+
"3.2.2": { level: "A", description: "On Input" },
|
|
56
|
+
"3.3.1": { level: "A", description: "Error Identification" },
|
|
57
|
+
"3.3.2": { level: "A", description: "Labels or Instructions" },
|
|
58
|
+
"4.1.1": { level: "A", description: "Parsing" },
|
|
59
|
+
"4.1.2": { level: "A", description: "Name, Role, Value" },
|
|
60
|
+
};
|
|
61
|
+
function getWcagCriteriaForBarrier(barrierType) {
|
|
62
|
+
switch (barrierType) {
|
|
63
|
+
case "motor_precision":
|
|
64
|
+
return ["2.5.5", "2.5.8"];
|
|
65
|
+
case "visual_clarity":
|
|
66
|
+
return ["1.4.3", "1.4.6", "1.4.4"];
|
|
67
|
+
case "cognitive_load":
|
|
68
|
+
return ["2.4.6", "3.3.2"];
|
|
69
|
+
case "temporal":
|
|
70
|
+
return ["2.2.1", "2.2.2"];
|
|
71
|
+
case "sensory":
|
|
72
|
+
return ["1.1.1", "1.4.1"];
|
|
73
|
+
case "contrast":
|
|
74
|
+
return ["1.4.3", "1.4.6"];
|
|
75
|
+
case "touch_target":
|
|
76
|
+
return ["2.5.5", "2.5.8"];
|
|
77
|
+
case "timing":
|
|
78
|
+
return ["2.2.1", "2.2.2"];
|
|
79
|
+
default:
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Detect small touch targets that are hard for motor-impaired users
|
|
85
|
+
*/
|
|
86
|
+
async function detectSmallTouchTargets(ctx) {
|
|
87
|
+
const { page, persona, barriers } = ctx;
|
|
88
|
+
// Skip if persona has full motor control
|
|
89
|
+
if (persona.accessibilityTraits.motorControl && persona.accessibilityTraits.motorControl > 0.8) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const smallTargets = await page.$$eval('button, a, input[type="checkbox"], input[type="radio"], [role="button"], [onclick]', (elements) => elements.map(el => {
|
|
93
|
+
const rect = el.getBoundingClientRect();
|
|
94
|
+
return {
|
|
95
|
+
selector: el.tagName.toLowerCase() + (el.id ? `#${el.id}` : ''),
|
|
96
|
+
width: rect.width,
|
|
97
|
+
height: rect.height,
|
|
98
|
+
text: el.textContent?.trim().slice(0, 30) || '',
|
|
99
|
+
area: rect.width * rect.height,
|
|
100
|
+
};
|
|
101
|
+
}).filter(el => el.area > 0 && (el.width < 44 || el.height < 44)));
|
|
102
|
+
for (const target of smallTargets.slice(0, 10)) {
|
|
103
|
+
const severity = target.width < 24 || target.height < 24 ? "critical" :
|
|
104
|
+
target.width < 32 || target.height < 32 ? "major" : "minor";
|
|
105
|
+
barriers.push({
|
|
106
|
+
type: "touch_target",
|
|
107
|
+
element: target.selector,
|
|
108
|
+
description: `Touch target too small (${Math.round(target.width)}x${Math.round(target.height)}px) - minimum 44x44px recommended`,
|
|
109
|
+
affectedPersonas: ["motor-impairment-tremor", "elderly-low-vision"],
|
|
110
|
+
wcagCriteria: ["2.5.5", "2.5.8"],
|
|
111
|
+
severity,
|
|
112
|
+
remediation: `Increase clickable area to at least 44x44 pixels, or add padding/margin to increase touch target`,
|
|
113
|
+
});
|
|
114
|
+
ctx.wcagViolations.add("2.5.8");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Detect low contrast elements that are hard for low-vision users
|
|
119
|
+
*/
|
|
120
|
+
async function detectLowContrast(ctx) {
|
|
121
|
+
const { page, persona, barriers } = ctx;
|
|
122
|
+
// Skip if persona has full vision
|
|
123
|
+
if (persona.accessibilityTraits.visionLevel && persona.accessibilityTraits.visionLevel > 0.8) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const contrastSensitivity = persona.accessibilityTraits.contrastSensitivity || 1;
|
|
127
|
+
// Check text elements for contrast (simplified check - real check needs computed colors)
|
|
128
|
+
const lowContrastElements = await page.$$eval('p, span, h1, h2, h3, h4, h5, h6, a, button, label', (elements) => {
|
|
129
|
+
const results = [];
|
|
130
|
+
for (const el of elements.slice(0, 100)) {
|
|
131
|
+
const styles = window.getComputedStyle(el);
|
|
132
|
+
const color = styles.color;
|
|
133
|
+
const bgColor = styles.backgroundColor;
|
|
134
|
+
// Simplified: flag light gray text as potential issue
|
|
135
|
+
if (color.includes('rgb')) {
|
|
136
|
+
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
137
|
+
if (match) {
|
|
138
|
+
const [, r, g, b] = match.map(Number);
|
|
139
|
+
// Light gray text (common contrast issue)
|
|
140
|
+
if (r > 150 && g > 150 && b > 150 && r < 200 && g < 200 && b < 200) {
|
|
141
|
+
results.push({
|
|
142
|
+
selector: el.tagName.toLowerCase() + (el.id ? `#${el.id}` : ''),
|
|
143
|
+
text: el.textContent?.trim().slice(0, 30) || '',
|
|
144
|
+
fontSize: styles.fontSize,
|
|
145
|
+
color,
|
|
146
|
+
bgColor,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return results;
|
|
153
|
+
});
|
|
154
|
+
for (const el of lowContrastElements.slice(0, 5)) {
|
|
155
|
+
barriers.push({
|
|
156
|
+
type: "contrast",
|
|
157
|
+
element: el.selector,
|
|
158
|
+
description: `Low contrast text may be difficult to read (color: ${el.color})`,
|
|
159
|
+
affectedPersonas: ["low-vision-magnified", "elderly-low-vision"],
|
|
160
|
+
wcagCriteria: ["1.4.3", "1.4.6"],
|
|
161
|
+
severity: contrastSensitivity > 2 ? "major" : "minor",
|
|
162
|
+
remediation: "Increase text contrast to at least 4.5:1 for normal text, 3:1 for large text",
|
|
163
|
+
});
|
|
164
|
+
ctx.wcagViolations.add("1.4.3");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Detect cognitive load issues
|
|
169
|
+
*/
|
|
170
|
+
async function detectCognitiveLoad(ctx) {
|
|
171
|
+
const { page, persona, barriers } = ctx;
|
|
172
|
+
// Check if persona has cognitive traits
|
|
173
|
+
const hasLowAttention = persona.accessibilityTraits.attentionSpan &&
|
|
174
|
+
persona.accessibilityTraits.attentionSpan < 0.5;
|
|
175
|
+
const hasLowProcessing = persona.accessibilityTraits.processingSpeed &&
|
|
176
|
+
persona.accessibilityTraits.processingSpeed < 0.6;
|
|
177
|
+
if (!hasLowAttention && !hasLowProcessing)
|
|
178
|
+
return;
|
|
179
|
+
const cognitiveIssues = await page.evaluate(() => {
|
|
180
|
+
const issues = [];
|
|
181
|
+
// Check for long forms
|
|
182
|
+
const forms = Array.from(document.querySelectorAll('form'));
|
|
183
|
+
for (const form of forms) {
|
|
184
|
+
const inputs = form.querySelectorAll('input:not([type="hidden"]), textarea, select');
|
|
185
|
+
if (inputs.length > 7) {
|
|
186
|
+
issues.push({
|
|
187
|
+
type: "long-form",
|
|
188
|
+
description: `Form with ${inputs.length} fields may overwhelm users with attention difficulties`,
|
|
189
|
+
count: inputs.length,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Check for text walls
|
|
194
|
+
const paragraphs = Array.from(document.querySelectorAll('p'));
|
|
195
|
+
for (const p of paragraphs) {
|
|
196
|
+
if (p.textContent && p.textContent.length > 500) {
|
|
197
|
+
issues.push({
|
|
198
|
+
type: "text-wall",
|
|
199
|
+
description: "Long paragraph without breaks may be difficult to process",
|
|
200
|
+
});
|
|
201
|
+
break; // Only flag once
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Check for animations/movement
|
|
205
|
+
const animations = document.querySelectorAll('[class*="animate"], [class*="slider"], [class*="carousel"]');
|
|
206
|
+
if (animations.length > 0) {
|
|
207
|
+
issues.push({
|
|
208
|
+
type: "animation",
|
|
209
|
+
description: "Animated content may distract users with attention difficulties",
|
|
210
|
+
count: animations.length,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Check for complex navigation
|
|
214
|
+
const navItems = document.querySelectorAll('nav a, header a, [role="navigation"] a');
|
|
215
|
+
if (navItems.length > 10) {
|
|
216
|
+
issues.push({
|
|
217
|
+
type: "complex-nav",
|
|
218
|
+
description: `Navigation with ${navItems.length} items may be overwhelming`,
|
|
219
|
+
count: navItems.length,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return issues;
|
|
223
|
+
});
|
|
224
|
+
for (const issue of cognitiveIssues) {
|
|
225
|
+
barriers.push({
|
|
226
|
+
type: "cognitive_load",
|
|
227
|
+
element: issue.type,
|
|
228
|
+
description: issue.description,
|
|
229
|
+
affectedPersonas: ["cognitive-adhd", "dyslexic-user"],
|
|
230
|
+
wcagCriteria: ["2.4.6", "3.3.2"],
|
|
231
|
+
severity: issue.type === "long-form" ? "major" : "minor",
|
|
232
|
+
remediation: issue.type === "long-form"
|
|
233
|
+
? "Break form into multiple steps or sections"
|
|
234
|
+
: issue.type === "text-wall"
|
|
235
|
+
? "Break text into smaller paragraphs with headings"
|
|
236
|
+
: issue.type === "animation"
|
|
237
|
+
? "Provide controls to pause/stop animations, or use prefers-reduced-motion"
|
|
238
|
+
: "Simplify navigation structure",
|
|
239
|
+
});
|
|
240
|
+
ctx.wcagViolations.add("3.3.2");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Detect timing-based barriers
|
|
245
|
+
*/
|
|
246
|
+
async function detectTimingIssues(ctx) {
|
|
247
|
+
const { page, persona, barriers } = ctx;
|
|
248
|
+
// Check for elements that might have timing constraints
|
|
249
|
+
const timingElements = await page.$$eval('[data-timeout], [class*="countdown"], [class*="timer"], [class*="auto-"], [class*="session"]', (elements) => elements.map(el => ({
|
|
250
|
+
selector: el.tagName.toLowerCase() + (el.className ? `.${el.className.split(' ')[0]}` : ''),
|
|
251
|
+
text: el.textContent?.trim().slice(0, 50) || '',
|
|
252
|
+
})));
|
|
253
|
+
if (timingElements.length > 0) {
|
|
254
|
+
for (const el of timingElements) {
|
|
255
|
+
barriers.push({
|
|
256
|
+
type: "timing",
|
|
257
|
+
element: el.selector,
|
|
258
|
+
description: `Time-limited content detected - may not allow enough time for users who need longer`,
|
|
259
|
+
affectedPersonas: ["motor-impairment-tremor", "low-vision-magnified", "cognitive-adhd", "dyslexic-user", "elderly-low-vision"],
|
|
260
|
+
wcagCriteria: ["2.2.1", "2.2.2"],
|
|
261
|
+
severity: "major",
|
|
262
|
+
remediation: "Allow users to extend, adjust, or disable time limits",
|
|
263
|
+
});
|
|
264
|
+
ctx.wcagViolations.add("2.2.1");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Detect color-only information
|
|
270
|
+
*/
|
|
271
|
+
async function detectColorOnlyInfo(ctx) {
|
|
272
|
+
const { page, persona, barriers } = ctx;
|
|
273
|
+
// Only check for color-blind personas
|
|
274
|
+
if (!persona.accessibilityTraits.colorBlindness)
|
|
275
|
+
return;
|
|
276
|
+
const colorOnlyElements = await page.$$eval('[class*="red"], [class*="green"], [class*="error"], [class*="success"], [class*="warning"]', (elements) => elements.map(el => {
|
|
277
|
+
const styles = window.getComputedStyle(el);
|
|
278
|
+
const hasIcon = el.querySelector('svg, i, [class*="icon"]') !== null;
|
|
279
|
+
const hasText = (el.textContent?.trim() || '').length > 0;
|
|
280
|
+
return {
|
|
281
|
+
selector: el.tagName.toLowerCase() + (el.className ? `.${el.className.split(' ')[0]}` : ''),
|
|
282
|
+
hasIcon,
|
|
283
|
+
hasText,
|
|
284
|
+
color: styles.color,
|
|
285
|
+
bgColor: styles.backgroundColor,
|
|
286
|
+
};
|
|
287
|
+
}).filter(el => !el.hasIcon && !el.hasText));
|
|
288
|
+
for (const el of colorOnlyElements.slice(0, 5)) {
|
|
289
|
+
barriers.push({
|
|
290
|
+
type: "sensory",
|
|
291
|
+
element: el.selector,
|
|
292
|
+
description: "Information conveyed by color alone may not be perceivable by color-blind users",
|
|
293
|
+
affectedPersonas: ["color-blind-deuteranopia"],
|
|
294
|
+
wcagCriteria: ["1.4.1"],
|
|
295
|
+
severity: "major",
|
|
296
|
+
remediation: "Add icons, patterns, or text labels in addition to color",
|
|
297
|
+
});
|
|
298
|
+
ctx.wcagViolations.add("1.4.1");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Detect missing alt text and captions
|
|
303
|
+
*/
|
|
304
|
+
async function detectMissingAltText(ctx) {
|
|
305
|
+
const { page, persona, barriers } = ctx;
|
|
306
|
+
// Check images
|
|
307
|
+
const imagesWithoutAlt = await page.$$eval('img:not([alt])', (elements) => elements.map(el => {
|
|
308
|
+
const imgEl = el;
|
|
309
|
+
return {
|
|
310
|
+
selector: `img[src="${imgEl.src.slice(0, 50)}..."]`,
|
|
311
|
+
isDecorative: imgEl.width < 20 || imgEl.height < 20,
|
|
312
|
+
};
|
|
313
|
+
}).filter(el => !el.isDecorative).slice(0, 10));
|
|
314
|
+
for (const img of imagesWithoutAlt) {
|
|
315
|
+
barriers.push({
|
|
316
|
+
type: "sensory",
|
|
317
|
+
element: img.selector,
|
|
318
|
+
description: "Image without alt text - screen reader users cannot understand the content",
|
|
319
|
+
affectedPersonas: ["deaf-user", "low-vision-magnified"],
|
|
320
|
+
wcagCriteria: ["1.1.1"],
|
|
321
|
+
severity: "major",
|
|
322
|
+
remediation: "Add descriptive alt text, or alt=\"\" if image is purely decorative",
|
|
323
|
+
});
|
|
324
|
+
ctx.wcagViolations.add("1.1.1");
|
|
325
|
+
}
|
|
326
|
+
// Check for videos without captions indicator
|
|
327
|
+
const videos = await page.$$eval('video, iframe[src*="youtube"], iframe[src*="vimeo"]', (elements) => elements.map(el => ({
|
|
328
|
+
selector: el.tagName.toLowerCase(),
|
|
329
|
+
hasCaptions: el.querySelector('track[kind="captions"]') !== null,
|
|
330
|
+
})).filter(el => !el.hasCaptions));
|
|
331
|
+
if (videos.length > 0 && persona.name === "deaf-user") {
|
|
332
|
+
barriers.push({
|
|
333
|
+
type: "sensory",
|
|
334
|
+
element: "video",
|
|
335
|
+
description: "Video content may not have captions - deaf users cannot access audio content",
|
|
336
|
+
affectedPersonas: ["deaf-user"],
|
|
337
|
+
wcagCriteria: ["1.2.2"],
|
|
338
|
+
severity: "critical",
|
|
339
|
+
remediation: "Add captions to all video content",
|
|
340
|
+
});
|
|
341
|
+
ctx.wcagViolations.add("1.2.2");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// Journey Simulation for Empathy
|
|
346
|
+
// ============================================================================
|
|
347
|
+
async function simulateAccessibilityJourney(page, url, goal, persona, maxSteps, maxTime) {
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
const ctx = {
|
|
350
|
+
page,
|
|
351
|
+
persona,
|
|
352
|
+
barriers: [],
|
|
353
|
+
frictionPoints: [],
|
|
354
|
+
wcagViolations: new Set(),
|
|
355
|
+
stepCount: 0,
|
|
356
|
+
};
|
|
357
|
+
let goalAchieved = false;
|
|
358
|
+
try {
|
|
359
|
+
// Navigate to URL
|
|
360
|
+
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
|
361
|
+
await page.waitForTimeout(2000);
|
|
362
|
+
// Run barrier detection
|
|
363
|
+
await detectSmallTouchTargets(ctx);
|
|
364
|
+
await detectLowContrast(ctx);
|
|
365
|
+
await detectCognitiveLoad(ctx);
|
|
366
|
+
await detectTimingIssues(ctx);
|
|
367
|
+
await detectColorOnlyInfo(ctx);
|
|
368
|
+
await detectMissingAltText(ctx);
|
|
369
|
+
// Simulate trying to achieve the goal
|
|
370
|
+
// (Simplified - in full implementation, would simulate actual interaction)
|
|
371
|
+
ctx.stepCount = Math.floor(Math.random() * maxSteps / 2) + 3;
|
|
372
|
+
// Check if barriers would likely prevent goal achievement
|
|
373
|
+
const criticalBarriers = ctx.barriers.filter(b => b.severity === "critical");
|
|
374
|
+
const majorBarriers = ctx.barriers.filter(b => b.severity === "major");
|
|
375
|
+
// Simulate friction based on persona traits
|
|
376
|
+
if (persona.accessibilityTraits.motorControl && persona.accessibilityTraits.motorControl < 0.5) {
|
|
377
|
+
const smallTargets = ctx.barriers.filter(b => b.type === "touch_target");
|
|
378
|
+
if (smallTargets.length > 3) {
|
|
379
|
+
ctx.frictionPoints.push({
|
|
380
|
+
step: 2,
|
|
381
|
+
type: "motor",
|
|
382
|
+
description: "Multiple small touch targets caused repeated mis-clicks",
|
|
383
|
+
impact: "high",
|
|
384
|
+
accessibilityContext: "Essential tremor makes precise clicking difficult",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (persona.accessibilityTraits.visionLevel && persona.accessibilityTraits.visionLevel < 0.5) {
|
|
389
|
+
const contrastIssues = ctx.barriers.filter(b => b.type === "contrast");
|
|
390
|
+
if (contrastIssues.length > 2) {
|
|
391
|
+
ctx.frictionPoints.push({
|
|
392
|
+
step: 1,
|
|
393
|
+
type: "visual",
|
|
394
|
+
description: "Low contrast text required zooming and squinting",
|
|
395
|
+
impact: "high",
|
|
396
|
+
accessibilityContext: "3x magnification still insufficient for gray text",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (persona.accessibilityTraits.attentionSpan && persona.accessibilityTraits.attentionSpan < 0.5) {
|
|
401
|
+
const cognitiveIssues = ctx.barriers.filter(b => b.type === "cognitive_load");
|
|
402
|
+
if (cognitiveIssues.length > 0) {
|
|
403
|
+
ctx.frictionPoints.push({
|
|
404
|
+
step: 3,
|
|
405
|
+
type: "cognitive",
|
|
406
|
+
description: "Lost focus due to complex form layout",
|
|
407
|
+
impact: "high",
|
|
408
|
+
accessibilityContext: "ADHD makes long forms particularly challenging",
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Determine if goal would be achieved
|
|
413
|
+
goalAchieved = criticalBarriers.length === 0 && majorBarriers.length < 3;
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
ctx.frictionPoints.push({
|
|
417
|
+
step: 0,
|
|
418
|
+
type: "error",
|
|
419
|
+
description: `Navigation error: ${e.message}`,
|
|
420
|
+
impact: "high",
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
// Calculate empathy score
|
|
424
|
+
const empathyScore = calculateEmpathyScore(ctx.barriers, ctx.frictionPoints, goalAchieved);
|
|
425
|
+
// Generate remediation priorities
|
|
426
|
+
const remediationPriority = generateRemediationPriority(ctx.barriers);
|
|
427
|
+
return {
|
|
428
|
+
url,
|
|
429
|
+
persona: persona.name,
|
|
430
|
+
disabilityType: getDisabilityType(persona),
|
|
431
|
+
goalAchieved,
|
|
432
|
+
barriers: ctx.barriers,
|
|
433
|
+
frictionPoints: ctx.frictionPoints,
|
|
434
|
+
wcagViolations: Array.from(ctx.wcagViolations),
|
|
435
|
+
remediationPriority,
|
|
436
|
+
empathyScore,
|
|
437
|
+
duration: Date.now() - startTime,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function getDisabilityType(persona) {
|
|
441
|
+
if (persona.accessibilityTraits.tremor)
|
|
442
|
+
return "Motor impairment (tremor)";
|
|
443
|
+
if (persona.accessibilityTraits.visionLevel && persona.accessibilityTraits.visionLevel < 0.5)
|
|
444
|
+
return "Low vision";
|
|
445
|
+
if (persona.accessibilityTraits.colorBlindness)
|
|
446
|
+
return `Color blindness (${persona.accessibilityTraits.colorBlindness})`;
|
|
447
|
+
if (persona.cognitiveTraits?.workingMemory && persona.cognitiveTraits.workingMemory < 0.5)
|
|
448
|
+
return "Cognitive (ADHD/Memory)";
|
|
449
|
+
if (persona.accessibilityTraits.processingSpeed && persona.accessibilityTraits.processingSpeed < 0.6)
|
|
450
|
+
return "Cognitive (Processing)";
|
|
451
|
+
return "General accessibility";
|
|
452
|
+
}
|
|
453
|
+
function calculateEmpathyScore(barriers, frictionPoints, goalAchieved) {
|
|
454
|
+
let score = 100;
|
|
455
|
+
// Deduct for barriers
|
|
456
|
+
for (const barrier of barriers) {
|
|
457
|
+
switch (barrier.severity) {
|
|
458
|
+
case "critical":
|
|
459
|
+
score -= 20;
|
|
460
|
+
break;
|
|
461
|
+
case "major":
|
|
462
|
+
score -= 10;
|
|
463
|
+
break;
|
|
464
|
+
case "minor":
|
|
465
|
+
score -= 3;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Deduct for friction points
|
|
470
|
+
for (const fp of frictionPoints) {
|
|
471
|
+
switch (fp.impact) {
|
|
472
|
+
case "high":
|
|
473
|
+
score -= 15;
|
|
474
|
+
break;
|
|
475
|
+
case "medium":
|
|
476
|
+
score -= 8;
|
|
477
|
+
break;
|
|
478
|
+
case "low":
|
|
479
|
+
score -= 3;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Bonus for goal achievement
|
|
484
|
+
if (!goalAchieved)
|
|
485
|
+
score -= 20;
|
|
486
|
+
return Math.max(0, Math.round(score));
|
|
487
|
+
}
|
|
488
|
+
function generateRemediationPriority(barriers) {
|
|
489
|
+
const items = [];
|
|
490
|
+
let priority = 1;
|
|
491
|
+
// Sort barriers by severity
|
|
492
|
+
const sorted = [...barriers].sort((a, b) => {
|
|
493
|
+
const severityOrder = {
|
|
494
|
+
critical: 0,
|
|
495
|
+
major: 1,
|
|
496
|
+
minor: 2,
|
|
497
|
+
};
|
|
498
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
499
|
+
});
|
|
500
|
+
// Group by type to avoid duplicates
|
|
501
|
+
const seen = new Set();
|
|
502
|
+
for (const barrier of sorted) {
|
|
503
|
+
const key = `${barrier.type}-${barrier.description.slice(0, 50)}`;
|
|
504
|
+
if (seen.has(key))
|
|
505
|
+
continue;
|
|
506
|
+
seen.add(key);
|
|
507
|
+
const effort = barrier.type === "contrast" ? "easy" :
|
|
508
|
+
barrier.type === "touch_target" ? "easy" :
|
|
509
|
+
barrier.type === "cognitive_load" ? "medium" :
|
|
510
|
+
barrier.type === "timing" ? "medium" :
|
|
511
|
+
"trivial";
|
|
512
|
+
items.push({
|
|
513
|
+
priority: priority++,
|
|
514
|
+
issue: barrier.description,
|
|
515
|
+
fix: barrier.remediation,
|
|
516
|
+
wcagCriteria: barrier.wcagCriteria,
|
|
517
|
+
effort,
|
|
518
|
+
});
|
|
519
|
+
if (items.length >= 10)
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
return items;
|
|
523
|
+
}
|
|
524
|
+
// ============================================================================
|
|
525
|
+
// Report Generation
|
|
526
|
+
// ============================================================================
|
|
527
|
+
/**
|
|
528
|
+
* Convert numeric empathy score to letter grade
|
|
529
|
+
*/
|
|
530
|
+
function getEmpathyGrade(score) {
|
|
531
|
+
if (score >= 80)
|
|
532
|
+
return "A";
|
|
533
|
+
if (score >= 65)
|
|
534
|
+
return "B";
|
|
535
|
+
if (score >= 50)
|
|
536
|
+
return "C";
|
|
537
|
+
if (score >= 35)
|
|
538
|
+
return "D";
|
|
539
|
+
return "F";
|
|
540
|
+
}
|
|
541
|
+
export function formatEmpathyAuditReport(result) {
|
|
542
|
+
const grade = getEmpathyGrade(result.overallScore);
|
|
543
|
+
let report = `
|
|
544
|
+
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
545
|
+
║ ACCESSIBILITY EMPATHY AUDIT ║
|
|
546
|
+
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
547
|
+
|
|
548
|
+
URL: ${result.url}
|
|
549
|
+
Goal: "${result.goal}"
|
|
550
|
+
Timestamp: ${result.timestamp}
|
|
551
|
+
Duration: ${(result.duration / 1000).toFixed(1)}s
|
|
552
|
+
|
|
553
|
+
⚠️ METHODOLOGY: Empathy scores are heuristic estimates based on barrier detection.*
|
|
554
|
+
This is NOT a substitute for testing with actual users who have disabilities.
|
|
555
|
+
*Based on WCAG criteria and persona simulation. See documentation for sources.
|
|
556
|
+
|
|
557
|
+
┌────────────────────────────────────────────────────────────────────────────┐
|
|
558
|
+
│ EMPATHY GRADE: ${grade} │
|
|
559
|
+
│ (Based on ${result.results.length} disability persona simulations) │
|
|
560
|
+
└────────────────────────────────────────────────────────────────────────────┘
|
|
561
|
+
|
|
562
|
+
`;
|
|
563
|
+
// Results by persona
|
|
564
|
+
for (const pr of result.results) {
|
|
565
|
+
const emoji = pr.goalAchieved ? '✓' : '✗';
|
|
566
|
+
const scoreColor = pr.empathyScore >= 70 ? '🟢' : pr.empathyScore >= 50 ? '🟠' : '🔴';
|
|
567
|
+
report += `
|
|
568
|
+
${pr.disabilityType}
|
|
569
|
+
${'─'.repeat(pr.disabilityType.length)}
|
|
570
|
+
Persona: ${pr.persona}
|
|
571
|
+
Score: ${scoreColor} ${pr.empathyScore}/100
|
|
572
|
+
Goal: ${emoji} ${pr.goalAchieved ? 'Achieved' : 'Not achieved'}
|
|
573
|
+
Barriers: ${pr.barriers.length} (${pr.barriers.filter(b => b.severity === 'critical').length} critical)
|
|
574
|
+
Friction points: ${pr.frictionPoints.length}
|
|
575
|
+
`;
|
|
576
|
+
if (pr.frictionPoints.length > 0) {
|
|
577
|
+
report += ` Experience issues:\n`;
|
|
578
|
+
for (const fp of pr.frictionPoints) {
|
|
579
|
+
report += ` • ${fp.description}${fp.accessibilityContext ? ` (${fp.accessibilityContext})` : ''}\n`;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Combined WCAG violations
|
|
584
|
+
report += `
|
|
585
|
+
WCAG VIOLATIONS
|
|
586
|
+
───────────────
|
|
587
|
+
`;
|
|
588
|
+
for (const violation of result.allWcagViolations) {
|
|
589
|
+
const criteria = WCAG_CRITERIA[violation];
|
|
590
|
+
if (criteria) {
|
|
591
|
+
report += ` ${violation} (Level ${criteria.level}): ${criteria.description}\n`;
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
report += ` ${violation}\n`;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Top remediations
|
|
598
|
+
report += `
|
|
599
|
+
TOP REMEDIATION PRIORITIES
|
|
600
|
+
──────────────────────────
|
|
601
|
+
`;
|
|
602
|
+
for (const rem of result.combinedRemediation.slice(0, 10)) {
|
|
603
|
+
report += `
|
|
604
|
+
${rem.priority}. ${rem.issue}
|
|
605
|
+
Fix: ${rem.fix}
|
|
606
|
+
WCAG: ${rem.wcagCriteria.join(', ')}
|
|
607
|
+
Effort: ${rem.effort}
|
|
608
|
+
`;
|
|
609
|
+
}
|
|
610
|
+
report += `
|
|
611
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
612
|
+
* Methodology and research sources: docs/METHODOLOGY.md
|
|
613
|
+
Key sources: WCAG 2.1, WebAIM Screen Reader Survey (2024), Baymard Institute
|
|
614
|
+
|
|
615
|
+
Generated by CBrowser v8.0.0 - Accessibility Empathy Audit
|
|
616
|
+
`;
|
|
617
|
+
return report;
|
|
618
|
+
}
|
|
619
|
+
export function generateEmpathyAuditHtmlReport(result) {
|
|
620
|
+
const grade = getEmpathyGrade(result.overallScore);
|
|
621
|
+
const gradeColor = grade === 'A' || grade === 'B' ? '#10b981' : grade === 'C' ? '#f59e0b' : '#ef4444';
|
|
622
|
+
const personaCards = result.results.map(pr => {
|
|
623
|
+
const personaGrade = getEmpathyGrade(pr.empathyScore);
|
|
624
|
+
const pGradeColor = personaGrade === 'A' || personaGrade === 'B' ? '#10b981' : personaGrade === 'C' ? '#f59e0b' : '#ef4444';
|
|
625
|
+
return `
|
|
626
|
+
<div class="persona-card ${pr.goalAchieved ? 'success' : 'failure'}">
|
|
627
|
+
<div class="persona-header">
|
|
628
|
+
<h3>${pr.disabilityType}</h3>
|
|
629
|
+
<span class="score" style="background: ${pGradeColor}33; color: ${pGradeColor}">
|
|
630
|
+
Grade ${personaGrade}
|
|
631
|
+
</span>
|
|
632
|
+
</div>
|
|
633
|
+
<p class="persona-name">${pr.persona}</p>
|
|
634
|
+
<div class="persona-stats">
|
|
635
|
+
<div class="stat">
|
|
636
|
+
<span class="label">Goal</span>
|
|
637
|
+
<span class="value">${pr.goalAchieved ? '✓ Achieved' : '✗ Failed'}</span>
|
|
638
|
+
</div>
|
|
639
|
+
<div class="stat">
|
|
640
|
+
<span class="label">Barriers</span>
|
|
641
|
+
<span class="value">${pr.barriers.length}</span>
|
|
642
|
+
</div>
|
|
643
|
+
<div class="stat">
|
|
644
|
+
<span class="label">Critical</span>
|
|
645
|
+
<span class="value">${pr.barriers.filter(b => b.severity === 'critical').length}</span>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
${pr.frictionPoints.length > 0 ? `
|
|
649
|
+
<div class="friction-points">
|
|
650
|
+
<h4>Experience Issues</h4>
|
|
651
|
+
<ul>
|
|
652
|
+
${pr.frictionPoints.map(fp => `
|
|
653
|
+
<li>
|
|
654
|
+
<strong>${fp.type}:</strong> ${fp.description}
|
|
655
|
+
${fp.accessibilityContext ? `<br><small>${fp.accessibilityContext}</small>` : ''}
|
|
656
|
+
</li>
|
|
657
|
+
`).join('')}
|
|
658
|
+
</ul>
|
|
659
|
+
</div>
|
|
660
|
+
` : ''}
|
|
661
|
+
</div>
|
|
662
|
+
`;
|
|
663
|
+
}).join('');
|
|
664
|
+
const wcagList = result.allWcagViolations.map(v => {
|
|
665
|
+
const criteria = WCAG_CRITERIA[v];
|
|
666
|
+
return `<li><strong>${v}</strong> (Level ${criteria?.level || '?'}): ${criteria?.description || 'Unknown'}</li>`;
|
|
667
|
+
}).join('');
|
|
668
|
+
const remediationRows = result.combinedRemediation.slice(0, 10).map(rem => `
|
|
669
|
+
<tr>
|
|
670
|
+
<td>${rem.priority}</td>
|
|
671
|
+
<td>${rem.issue}</td>
|
|
672
|
+
<td>${rem.fix}</td>
|
|
673
|
+
<td>${rem.wcagCriteria.join(', ')}</td>
|
|
674
|
+
<td><span class="badge badge-${rem.effort}">${rem.effort}</span></td>
|
|
675
|
+
</tr>
|
|
676
|
+
`).join('');
|
|
677
|
+
return `<!DOCTYPE html>
|
|
678
|
+
<html lang="en">
|
|
679
|
+
<head>
|
|
680
|
+
<meta charset="UTF-8">
|
|
681
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
682
|
+
<title>Accessibility Empathy Audit - ${result.url}</title>
|
|
683
|
+
<style>
|
|
684
|
+
* { box-sizing: border-box; }
|
|
685
|
+
body {
|
|
686
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
687
|
+
max-width: 1400px;
|
|
688
|
+
margin: 0 auto;
|
|
689
|
+
padding: 2rem;
|
|
690
|
+
background: #0f172a;
|
|
691
|
+
color: #e2e8f0;
|
|
692
|
+
}
|
|
693
|
+
h1 {
|
|
694
|
+
color: #f8fafc;
|
|
695
|
+
border-bottom: 3px solid #8b5cf6;
|
|
696
|
+
padding-bottom: 0.5rem;
|
|
697
|
+
}
|
|
698
|
+
h2 { color: #94a3b8; margin-top: 2rem; }
|
|
699
|
+
.meta {
|
|
700
|
+
background: #1e293b;
|
|
701
|
+
padding: 1rem;
|
|
702
|
+
border-radius: 8px;
|
|
703
|
+
margin-bottom: 1rem;
|
|
704
|
+
}
|
|
705
|
+
.meta p { margin: 0.25rem 0; }
|
|
706
|
+
.score-card {
|
|
707
|
+
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
|
708
|
+
border-radius: 16px;
|
|
709
|
+
padding: 2rem;
|
|
710
|
+
text-align: center;
|
|
711
|
+
margin: 2rem 0;
|
|
712
|
+
}
|
|
713
|
+
.score-value {
|
|
714
|
+
font-size: 4rem;
|
|
715
|
+
font-weight: bold;
|
|
716
|
+
color: ${gradeColor};
|
|
717
|
+
}
|
|
718
|
+
.score-label {
|
|
719
|
+
font-size: 1.25rem;
|
|
720
|
+
color: #94a3b8;
|
|
721
|
+
}
|
|
722
|
+
.persona-grid {
|
|
723
|
+
display: grid;
|
|
724
|
+
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
725
|
+
gap: 1rem;
|
|
726
|
+
margin: 2rem 0;
|
|
727
|
+
}
|
|
728
|
+
.persona-card {
|
|
729
|
+
background: #1e293b;
|
|
730
|
+
border-radius: 8px;
|
|
731
|
+
padding: 1rem;
|
|
732
|
+
border-left: 4px solid #3b82f6;
|
|
733
|
+
}
|
|
734
|
+
.persona-card.success { border-left-color: #10b981; }
|
|
735
|
+
.persona-card.failure { border-left-color: #ef4444; }
|
|
736
|
+
.persona-header {
|
|
737
|
+
display: flex;
|
|
738
|
+
justify-content: space-between;
|
|
739
|
+
align-items: center;
|
|
740
|
+
}
|
|
741
|
+
.persona-header h3 {
|
|
742
|
+
margin: 0;
|
|
743
|
+
color: #f8fafc;
|
|
744
|
+
}
|
|
745
|
+
.persona-header .score {
|
|
746
|
+
padding: 0.25rem 0.75rem;
|
|
747
|
+
border-radius: 4px;
|
|
748
|
+
font-weight: bold;
|
|
749
|
+
}
|
|
750
|
+
.persona-name {
|
|
751
|
+
color: #94a3b8;
|
|
752
|
+
font-size: 0.875rem;
|
|
753
|
+
margin: 0.5rem 0;
|
|
754
|
+
}
|
|
755
|
+
.persona-stats {
|
|
756
|
+
display: flex;
|
|
757
|
+
gap: 1rem;
|
|
758
|
+
margin: 1rem 0;
|
|
759
|
+
}
|
|
760
|
+
.persona-stats .stat {
|
|
761
|
+
flex: 1;
|
|
762
|
+
text-align: center;
|
|
763
|
+
background: #334155;
|
|
764
|
+
padding: 0.5rem;
|
|
765
|
+
border-radius: 4px;
|
|
766
|
+
}
|
|
767
|
+
.persona-stats .label {
|
|
768
|
+
display: block;
|
|
769
|
+
font-size: 0.75rem;
|
|
770
|
+
color: #94a3b8;
|
|
771
|
+
}
|
|
772
|
+
.persona-stats .value {
|
|
773
|
+
display: block;
|
|
774
|
+
font-weight: bold;
|
|
775
|
+
}
|
|
776
|
+
.friction-points {
|
|
777
|
+
margin-top: 1rem;
|
|
778
|
+
padding-top: 1rem;
|
|
779
|
+
border-top: 1px solid #334155;
|
|
780
|
+
}
|
|
781
|
+
.friction-points h4 {
|
|
782
|
+
margin: 0 0 0.5rem 0;
|
|
783
|
+
font-size: 0.875rem;
|
|
784
|
+
color: #94a3b8;
|
|
785
|
+
}
|
|
786
|
+
.friction-points ul {
|
|
787
|
+
margin: 0;
|
|
788
|
+
padding-left: 1.25rem;
|
|
789
|
+
}
|
|
790
|
+
.friction-points li {
|
|
791
|
+
margin: 0.5rem 0;
|
|
792
|
+
font-size: 0.875rem;
|
|
793
|
+
}
|
|
794
|
+
.friction-points small {
|
|
795
|
+
color: #94a3b8;
|
|
796
|
+
}
|
|
797
|
+
table {
|
|
798
|
+
width: 100%;
|
|
799
|
+
border-collapse: collapse;
|
|
800
|
+
background: #1e293b;
|
|
801
|
+
border-radius: 8px;
|
|
802
|
+
overflow: hidden;
|
|
803
|
+
margin: 1rem 0;
|
|
804
|
+
}
|
|
805
|
+
th, td {
|
|
806
|
+
padding: 0.75rem 1rem;
|
|
807
|
+
text-align: left;
|
|
808
|
+
border-bottom: 1px solid #334155;
|
|
809
|
+
}
|
|
810
|
+
th { background: #0f172a; color: #94a3b8; }
|
|
811
|
+
.badge {
|
|
812
|
+
padding: 0.25rem 0.5rem;
|
|
813
|
+
border-radius: 4px;
|
|
814
|
+
font-size: 0.75rem;
|
|
815
|
+
}
|
|
816
|
+
.badge-trivial { background: #16653433; color: #86efac; }
|
|
817
|
+
.badge-easy { background: #1e3a8a33; color: #93c5fd; }
|
|
818
|
+
.badge-medium { background: #78350f33; color: #fde047; }
|
|
819
|
+
.badge-hard { background: #7f1d1d33; color: #fca5a5; }
|
|
820
|
+
.wcag-list {
|
|
821
|
+
background: #1e293b;
|
|
822
|
+
padding: 1rem;
|
|
823
|
+
border-radius: 8px;
|
|
824
|
+
}
|
|
825
|
+
.wcag-list ul {
|
|
826
|
+
margin: 0;
|
|
827
|
+
padding-left: 1.5rem;
|
|
828
|
+
}
|
|
829
|
+
.wcag-list li {
|
|
830
|
+
margin: 0.5rem 0;
|
|
831
|
+
}
|
|
832
|
+
.disclaimer {
|
|
833
|
+
background: #1e3a5f;
|
|
834
|
+
border-left: 4px solid #8b5cf6;
|
|
835
|
+
padding: 1rem;
|
|
836
|
+
margin: 1rem 0;
|
|
837
|
+
border-radius: 0 8px 8px 0;
|
|
838
|
+
}
|
|
839
|
+
.disclaimer h4 {
|
|
840
|
+
margin: 0 0 0.5rem 0;
|
|
841
|
+
color: #a78bfa;
|
|
842
|
+
}
|
|
843
|
+
.disclaimer p {
|
|
844
|
+
margin: 0.25rem 0;
|
|
845
|
+
font-size: 0.875rem;
|
|
846
|
+
color: #94a3b8;
|
|
847
|
+
}
|
|
848
|
+
.footnote {
|
|
849
|
+
font-size: 0.75rem;
|
|
850
|
+
color: #64748b;
|
|
851
|
+
text-align: center;
|
|
852
|
+
margin-top: 1rem;
|
|
853
|
+
}
|
|
854
|
+
</style>
|
|
855
|
+
</head>
|
|
856
|
+
<body>
|
|
857
|
+
<h1>♿ Accessibility Empathy Audit</h1>
|
|
858
|
+
|
|
859
|
+
<div class="disclaimer">
|
|
860
|
+
<h4>⚠️ Important Methodology Note</h4>
|
|
861
|
+
<p>Empathy grades are <strong>heuristic estimates</strong> based on barrier detection and persona simulation.*</p>
|
|
862
|
+
<p>This is <strong>NOT a substitute</strong> for testing with actual users who have disabilities.</p>
|
|
863
|
+
<p style="font-size: 0.75rem; margin-top: 0.5rem;">*Based on WCAG 2.1 criteria and cognitive science research. Combine with automated WCAG checkers (axe, WAVE) and user testing for comprehensive accessibility validation.</p>
|
|
864
|
+
</div>
|
|
865
|
+
|
|
866
|
+
<div class="meta">
|
|
867
|
+
<p><strong>URL:</strong> ${result.url}</p>
|
|
868
|
+
<p><strong>Goal:</strong> "${result.goal}"</p>
|
|
869
|
+
<p><strong>Timestamp:</strong> ${result.timestamp}</p>
|
|
870
|
+
<p><strong>Duration:</strong> ${(result.duration / 1000).toFixed(1)}s</p>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
873
|
+
<div class="score-card">
|
|
874
|
+
<div class="score-value" style="color: ${gradeColor}">${grade}</div>
|
|
875
|
+
<div class="score-label">Overall Empathy Grade</div>
|
|
876
|
+
<p style="color: #94a3b8; margin-top: 0.5rem; font-size: 0.875rem;">Based on ${result.results.length} disability persona simulations</p>
|
|
877
|
+
</div>
|
|
878
|
+
|
|
879
|
+
<h2>Results by Disability Type</h2>
|
|
880
|
+
<div class="persona-grid">
|
|
881
|
+
${personaCards}
|
|
882
|
+
</div>
|
|
883
|
+
|
|
884
|
+
<h2>WCAG Violations (${result.allWcagViolations.length})</h2>
|
|
885
|
+
<div class="wcag-list">
|
|
886
|
+
<ul>
|
|
887
|
+
${wcagList}
|
|
888
|
+
</ul>
|
|
889
|
+
</div>
|
|
890
|
+
|
|
891
|
+
<h2>Remediation Priorities</h2>
|
|
892
|
+
<table>
|
|
893
|
+
<thead>
|
|
894
|
+
<tr>
|
|
895
|
+
<th>#</th>
|
|
896
|
+
<th>Issue</th>
|
|
897
|
+
<th>Fix</th>
|
|
898
|
+
<th>WCAG</th>
|
|
899
|
+
<th>Effort</th>
|
|
900
|
+
</tr>
|
|
901
|
+
</thead>
|
|
902
|
+
<tbody>
|
|
903
|
+
${remediationRows}
|
|
904
|
+
</tbody>
|
|
905
|
+
</table>
|
|
906
|
+
|
|
907
|
+
<div class="footnote">
|
|
908
|
+
<p>* Methodology and research sources: <a href="docs/METHODOLOGY.md" style="color: #60a5fa;">docs/METHODOLOGY.md</a></p>
|
|
909
|
+
<p>Key sources: WCAG 2.1, WebAIM Screen Reader Survey (2024), Baymard Institute</p>
|
|
910
|
+
<p style="margin-top: 0.5rem;"><strong>Important:</strong> This is NOT a substitute for testing with actual users who have disabilities.</p>
|
|
911
|
+
</div>
|
|
912
|
+
|
|
913
|
+
<p style="color: #64748b; text-align: center; margin-top: 2rem;">
|
|
914
|
+
Generated by CBrowser v8.0.0 - Accessibility Empathy Audit
|
|
915
|
+
</p>
|
|
916
|
+
</body>
|
|
917
|
+
</html>`;
|
|
918
|
+
}
|
|
919
|
+
// ============================================================================
|
|
920
|
+
// Main Empathy Audit Function
|
|
921
|
+
// ============================================================================
|
|
922
|
+
export async function runEmpathyAudit(url, options) {
|
|
923
|
+
const { goal, disabilities, wcagLevel = "AA", maxSteps = 20, maxTime = 120, headless = true, } = options;
|
|
924
|
+
const startTime = Date.now();
|
|
925
|
+
const results = [];
|
|
926
|
+
const allWcagViolations = new Set();
|
|
927
|
+
const allBarriers = [];
|
|
928
|
+
// Map disability names to personas
|
|
929
|
+
const personaMap = {
|
|
930
|
+
"motor-tremor": "motor-impairment-tremor",
|
|
931
|
+
"motor": "motor-impairment-tremor",
|
|
932
|
+
"tremor": "motor-impairment-tremor",
|
|
933
|
+
"low-vision": "low-vision-magnified",
|
|
934
|
+
"vision": "low-vision-magnified",
|
|
935
|
+
"magnified": "low-vision-magnified",
|
|
936
|
+
"adhd": "cognitive-adhd",
|
|
937
|
+
"cognitive": "cognitive-adhd",
|
|
938
|
+
"attention": "cognitive-adhd",
|
|
939
|
+
"dyslexia": "dyslexic-user",
|
|
940
|
+
"dyslexic": "dyslexic-user",
|
|
941
|
+
"reading": "dyslexic-user",
|
|
942
|
+
"deaf": "deaf-user",
|
|
943
|
+
"hearing": "deaf-user",
|
|
944
|
+
"elderly": "elderly-low-vision",
|
|
945
|
+
"elderly-low-vision": "elderly-low-vision",
|
|
946
|
+
"senior": "elderly-low-vision",
|
|
947
|
+
"color-blind": "color-blind-deuteranopia",
|
|
948
|
+
"colorblind": "color-blind-deuteranopia",
|
|
949
|
+
"deuteranopia": "color-blind-deuteranopia",
|
|
950
|
+
};
|
|
951
|
+
// Run audit for each disability type
|
|
952
|
+
for (const disability of disabilities) {
|
|
953
|
+
const personaName = personaMap[disability.toLowerCase()] || disability;
|
|
954
|
+
const persona = getAccessibilityPersona(personaName);
|
|
955
|
+
if (!persona) {
|
|
956
|
+
console.warn(`Unknown disability/persona: ${disability}, skipping`);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
let browser = null;
|
|
960
|
+
try {
|
|
961
|
+
browser = await chromium.launch({ headless });
|
|
962
|
+
const context = await browser.newContext({
|
|
963
|
+
viewport: { width: 1920, height: 1080 },
|
|
964
|
+
});
|
|
965
|
+
const page = await context.newPage();
|
|
966
|
+
const result = await simulateAccessibilityJourney(page, url, goal, persona, maxSteps, maxTime);
|
|
967
|
+
results.push(result);
|
|
968
|
+
// Collect WCAG violations and barriers
|
|
969
|
+
for (const v of result.wcagViolations) {
|
|
970
|
+
allWcagViolations.add(v);
|
|
971
|
+
}
|
|
972
|
+
allBarriers.push(...result.barriers);
|
|
973
|
+
}
|
|
974
|
+
finally {
|
|
975
|
+
if (browser) {
|
|
976
|
+
await browser.close();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
// Filter WCAG violations by level
|
|
981
|
+
const levelOrder = { A: 1, AA: 2, AAA: 3 };
|
|
982
|
+
const maxLevel = levelOrder[wcagLevel];
|
|
983
|
+
const filteredViolations = Array.from(allWcagViolations).filter(v => {
|
|
984
|
+
const criteria = WCAG_CRITERIA[v];
|
|
985
|
+
return !criteria || levelOrder[criteria.level] <= maxLevel;
|
|
986
|
+
});
|
|
987
|
+
// Generate combined remediation
|
|
988
|
+
const combinedRemediation = generateRemediationPriority(allBarriers);
|
|
989
|
+
// Calculate overall score
|
|
990
|
+
const overallScore = results.length > 0
|
|
991
|
+
? Math.round(results.reduce((sum, r) => sum + r.empathyScore, 0) / results.length)
|
|
992
|
+
: 0;
|
|
993
|
+
return {
|
|
994
|
+
url,
|
|
995
|
+
goal,
|
|
996
|
+
timestamp: new Date().toISOString(),
|
|
997
|
+
results,
|
|
998
|
+
allWcagViolations: filteredViolations,
|
|
999
|
+
allBarriers,
|
|
1000
|
+
combinedRemediation,
|
|
1001
|
+
overallScore,
|
|
1002
|
+
duration: Date.now() - startTime,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
//# sourceMappingURL=accessibility-empathy.js.map
|