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.
Files changed (35) hide show
  1. package/README.md +121 -0
  2. package/dist/analysis/accessibility-empathy.d.ts +32 -0
  3. package/dist/analysis/accessibility-empathy.d.ts.map +1 -0
  4. package/dist/analysis/accessibility-empathy.js +1005 -0
  5. package/dist/analysis/accessibility-empathy.js.map +1 -0
  6. package/dist/analysis/agent-ready-audit.d.ts +11 -0
  7. package/dist/analysis/agent-ready-audit.d.ts.map +1 -0
  8. package/dist/analysis/agent-ready-audit.js +900 -0
  9. package/dist/analysis/agent-ready-audit.js.map +1 -0
  10. package/dist/analysis/competitive-benchmark.d.ts +11 -0
  11. package/dist/analysis/competitive-benchmark.d.ts.map +1 -0
  12. package/dist/analysis/competitive-benchmark.js +1122 -0
  13. package/dist/analysis/competitive-benchmark.js.map +1 -0
  14. package/dist/analysis/index.d.ts +5 -1
  15. package/dist/analysis/index.d.ts.map +1 -1
  16. package/dist/analysis/index.js +5 -1
  17. package/dist/analysis/index.js.map +1 -1
  18. package/dist/cli.js +191 -2
  19. package/dist/cli.js.map +1 -1
  20. package/dist/cognitive/focus-hierarchies.d.ts +113 -0
  21. package/dist/cognitive/focus-hierarchies.d.ts.map +1 -0
  22. package/dist/cognitive/focus-hierarchies.js +500 -0
  23. package/dist/cognitive/focus-hierarchies.js.map +1 -0
  24. package/dist/mcp-server.d.ts.map +1 -1
  25. package/dist/mcp-server.js +100 -1
  26. package/dist/mcp-server.js.map +1 -1
  27. package/dist/personas.d.ts +14 -0
  28. package/dist/personas.d.ts.map +1 -1
  29. package/dist/personas.js +393 -0
  30. package/dist/personas.js.map +1 -1
  31. package/dist/types.d.ts +429 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js.map +1 -1
  34. package/docs/METHODOLOGY.md +245 -0
  35. 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