cbrowser 18.14.0 → 18.15.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 +45 -6
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +408 -1
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/agent-ready-audit.d.ts +1 -1
- package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
- package/dist/analysis/agent-ready-audit.js +84 -64
- package/dist/analysis/agent-ready-audit.js.map +1 -1
- package/dist/analysis/bug-hunter.d.ts +2 -1
- package/dist/analysis/bug-hunter.d.ts.map +1 -1
- package/dist/analysis/bug-hunter.js +137 -2
- package/dist/analysis/bug-hunter.js.map +1 -1
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
- package/dist/analysis/competitive-benchmark.js +243 -113
- package/dist/analysis/competitive-benchmark.js.map +1 -1
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +103 -40
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/remediation/llms-txt.d.ts.map +1 -1
- package/dist/remediation/llms-txt.js +90 -2
- package/dist/remediation/llms-txt.js.map +1 -1
- package/dist/remediation/structured-data.d.ts.map +1 -1
- package/dist/remediation/structured-data.js +3 -1
- package/dist/remediation/structured-data.js.map +1 -1
- package/dist/types.d.ts +33 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/hunt-bugs-coverage.md +103 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# CBrowser — Cognitive Browser Automation
|
|
2
2
|
|
|
3
|
-
> **The browser automation that thinks.** Achieved **Grade A+** in comprehensive stress testing—100% pass rate across
|
|
3
|
+
> **The browser automation that thinks.** Achieved **Grade A+** in comprehensive stress testing—100% pass rate across 90 tools, zero critical bugs, zero server crashes. [View Full Assessment →](docs/STRESS-TEST-v16.14.4.md)
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/cbrowser)
|
|
6
6
|
[](https://cbrowser.ai/docs)
|
|
7
7
|
[](docs/STRESS-TEST-v16.14.4.md)
|
|
8
8
|
[](LICENSE)
|
|
9
|
-
[](https://modelcontextprotocol.io)
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
11
|
[](https://nodejs.org/)
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ Sites that pass CBrowser's cognitive tests are easier for both humans **and** AI
|
|
|
18
18
|
|
|
19
19
|
## What Makes CBrowser Different
|
|
20
20
|
|
|
21
|
-
**
|
|
21
|
+
**90 tools, 17 cognitive personas, 25 research-backed traits.** After rigorous stress testing across production sites including Airbnb and Hacker News:
|
|
22
22
|
|
|
23
23
|
| Capability | Status | Why It Matters |
|
|
24
24
|
|------------|--------|----------------|
|
|
@@ -27,7 +27,7 @@ Sites that pass CBrowser's cognitive tests are easier for both humans **and** AI
|
|
|
27
27
|
| **Empathy Accessibility Audits** | 🔬 Novel | Simulate users with tremors, low vision, ADHD. No competitor offers this. |
|
|
28
28
|
| **Self-Healing Selectors** | ✅ Production-ready | ARIA-first with 0.8+ confidence gating. Handles DOM changes automatically. |
|
|
29
29
|
| **Constitutional AI Safety** | 🔬 Novel | Risk-classified actions prevent autonomous agents from doing damage. |
|
|
30
|
-
| **
|
|
30
|
+
| **90 MCP Tools** | ✅ Production-ready | Full Claude integration—local and remote servers. |
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
@@ -315,7 +315,7 @@ Deploy your own: see [Remote MCP Server Guide](https://cbrowser.ai/docs/Remote-M
|
|
|
315
315
|
}
|
|
316
316
|
```
|
|
317
317
|
|
|
318
|
-
###
|
|
318
|
+
### 90 MCP Tools
|
|
319
319
|
|
|
320
320
|
| Category | Tools |
|
|
321
321
|
|----------|-------|
|
|
@@ -421,6 +421,45 @@ await browser.close();
|
|
|
421
421
|
npx cbrowser config set-api-key
|
|
422
422
|
```
|
|
423
423
|
|
|
424
|
+
### Token Cost & Selective Loading
|
|
425
|
+
|
|
426
|
+
CBrowser's 90 MCP tools consume approximately **~38,000 tokens** when loaded into an LLM context. For cost-sensitive applications, use selective tool loading:
|
|
427
|
+
|
|
428
|
+
**Tool Categories (for programmatic use):**
|
|
429
|
+
|
|
430
|
+
| Category | Tools | Use Case |
|
|
431
|
+
|----------|-------|----------|
|
|
432
|
+
| `navigation` | navigate, screenshot, scroll | Basic browsing |
|
|
433
|
+
| `interaction` | click, fill, smart_click | Form automation |
|
|
434
|
+
| `extraction` | extract, analyze_page | Data scraping |
|
|
435
|
+
| `assertion` | assert | Testing validation |
|
|
436
|
+
| `accessibility` | empathy_audit, hunt_bugs | A11y testing |
|
|
437
|
+
| `cognitive` | cognitive_journey_* | User simulation |
|
|
438
|
+
| `visual` | visual_baseline, visual_regression | Visual testing |
|
|
439
|
+
| `performance` | perf_baseline, perf_regression | Performance monitoring |
|
|
440
|
+
| `session` | save_session, load_session | State management |
|
|
441
|
+
|
|
442
|
+
**Programmatic selective loading:**
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
446
|
+
import {
|
|
447
|
+
registerNavigationTools,
|
|
448
|
+
registerInteractionTools,
|
|
449
|
+
registerExtractionTools,
|
|
450
|
+
} from "cbrowser/mcp-tools";
|
|
451
|
+
|
|
452
|
+
const server = new McpServer({ name: "my-app", version: "1.0.0" });
|
|
453
|
+
const context = { getBrowser: () => browser };
|
|
454
|
+
|
|
455
|
+
// Only load what you need (~5,000 tokens instead of ~38,000)
|
|
456
|
+
registerNavigationTools(server, context);
|
|
457
|
+
registerInteractionTools(server, context);
|
|
458
|
+
registerExtractionTools(server, context);
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**Full category list:** `navigation`, `interaction`, `extraction`, `assertion`, `analysis`, `session`, `healing`, `visualTesting`, `testing`, `bugAnalysis`, `personaComparison`, `cognitive`, `values`, `performance`, `audit`, `browserManagement`, `security`, `marketing`, `remediation`, `llmsTxt`.
|
|
462
|
+
|
|
424
463
|
---
|
|
425
464
|
|
|
426
465
|
## Examples
|
|
@@ -447,7 +486,7 @@ npx cbrowser config set-api-key
|
|
|
447
486
|
| **Lever Analysis** | Which psychological persuasion patterns work for each persona |
|
|
448
487
|
| **Constitutional Stealth** | Full stealth measures for authorized penetration testing |
|
|
449
488
|
|
|
450
|
-
**MCP Server:** Enterprise MCP includes all
|
|
489
|
+
**MCP Server:** Enterprise MCP includes all 64 base tools + marketing tools (4 active + 4 planned).
|
|
451
490
|
|
|
452
491
|
```bash
|
|
453
492
|
# Start Enterprise MCP server
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessibility-empathy.d.ts","sourceRoot":"","sources":["../../src/analysis/accessibility-empathy.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgCH,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EASpB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"accessibility-empathy.d.ts","sourceRoot":"","sources":["../../src/analysis/accessibility-empathy.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgCH,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EASpB,MAAM,aAAa,CAAC;AAkyCrB,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAoF3E;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAkTjF;AAsFD,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAmH7B"}
|
|
@@ -427,6 +427,393 @@ async function detectMissingFormLabels(ctx) {
|
|
|
427
427
|
}
|
|
428
428
|
}
|
|
429
429
|
// ============================================================================
|
|
430
|
+
// Persona-Specific Detectors (v18.15.0)
|
|
431
|
+
// ============================================================================
|
|
432
|
+
/**
|
|
433
|
+
* Get the persona category for routing to specialized detectors.
|
|
434
|
+
* @since v18.15.0
|
|
435
|
+
*/
|
|
436
|
+
function getPersonaCategory(personaName) {
|
|
437
|
+
const name = personaName.toLowerCase();
|
|
438
|
+
// Motor personas
|
|
439
|
+
if (name.includes("motor") ||
|
|
440
|
+
name.includes("tremor") ||
|
|
441
|
+
name.includes("limited-mobility") ||
|
|
442
|
+
name.includes("mobility")) {
|
|
443
|
+
return "motor";
|
|
444
|
+
}
|
|
445
|
+
// Cognitive personas
|
|
446
|
+
if (name.includes("adhd") ||
|
|
447
|
+
name.includes("dyslexia") ||
|
|
448
|
+
name.includes("dyslexic") ||
|
|
449
|
+
name.includes("memory") ||
|
|
450
|
+
name.includes("cognitive")) {
|
|
451
|
+
return "cognitive";
|
|
452
|
+
}
|
|
453
|
+
// Vision personas
|
|
454
|
+
if (name.includes("vision") ||
|
|
455
|
+
name.includes("low-vision") ||
|
|
456
|
+
name.includes("color-blind") ||
|
|
457
|
+
name.includes("deuteranopia") ||
|
|
458
|
+
name.includes("elderly")) {
|
|
459
|
+
return "vision";
|
|
460
|
+
}
|
|
461
|
+
return "general";
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Detect barriers specifically relevant to motor-impaired personas.
|
|
465
|
+
*
|
|
466
|
+
* Key checks:
|
|
467
|
+
* - Target size violations (< 44x44px critical for tremor users)
|
|
468
|
+
* - Hover-dependent interactions (impossible with tremor)
|
|
469
|
+
* - Drag-and-drop without keyboard alternative
|
|
470
|
+
* - Time-limited interactions
|
|
471
|
+
*
|
|
472
|
+
* @since v18.15.0
|
|
473
|
+
*/
|
|
474
|
+
async function detectMotorBarriers(ctx) {
|
|
475
|
+
const { page, barriers } = ctx;
|
|
476
|
+
// Check for hover-dependent interactions (no click alternative)
|
|
477
|
+
const hoverOnlyElements = await page.$$eval('[class*="hover"], [class*="dropdown"], [class*="menu"], [class*="tooltip"]', (elements) => {
|
|
478
|
+
const results = [];
|
|
479
|
+
for (const el of elements.slice(0, 20)) {
|
|
480
|
+
// Check if element or children have click handlers
|
|
481
|
+
const hasClick = el.hasAttribute('onclick') ||
|
|
482
|
+
el.querySelector('[onclick]') !== null ||
|
|
483
|
+
el.querySelector('a, button') !== null;
|
|
484
|
+
results.push({
|
|
485
|
+
selector: el.tagName.toLowerCase() + (el.className ? `.${el.className.split(' ')[0]}` : ''),
|
|
486
|
+
hasClickAlternative: hasClick,
|
|
487
|
+
text: el.textContent?.trim().slice(0, 30) || '',
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return results.filter(r => !r.hasClickAlternative);
|
|
491
|
+
});
|
|
492
|
+
for (const el of hoverOnlyElements.slice(0, 5)) {
|
|
493
|
+
barriers.push({
|
|
494
|
+
type: "motor_precision",
|
|
495
|
+
element: el.selector,
|
|
496
|
+
description: `Hover-dependent interaction without click alternative may be inaccessible for users with tremors`,
|
|
497
|
+
affectedPersonas: ["motor-impairment-tremor", "motor-impairment-limited-mobility"],
|
|
498
|
+
wcagCriteria: ["2.1.1", "2.5.1"],
|
|
499
|
+
severity: "major",
|
|
500
|
+
remediation: "Add click/tap alternative to hover interactions, or make hover content accessible via keyboard focus",
|
|
501
|
+
});
|
|
502
|
+
ctx.wcagViolations.add("2.1.1");
|
|
503
|
+
}
|
|
504
|
+
// Check for drag-and-drop without keyboard alternative
|
|
505
|
+
const dragDropElements = await page.$$eval('[draggable="true"], [class*="drag"], [class*="sortable"], [class*="reorder"]', (elements) => elements.map(el => ({
|
|
506
|
+
selector: el.tagName.toLowerCase() + (el.id ? `#${el.id}` : ''),
|
|
507
|
+
hasAriaGrabbed: el.hasAttribute('aria-grabbed'),
|
|
508
|
+
hasKeyboardHandler: el.hasAttribute('onkeydown') || el.hasAttribute('onkeyup'),
|
|
509
|
+
})));
|
|
510
|
+
for (const el of dragDropElements.slice(0, 3)) {
|
|
511
|
+
if (!el.hasKeyboardHandler) {
|
|
512
|
+
barriers.push({
|
|
513
|
+
type: "motor_precision",
|
|
514
|
+
element: el.selector,
|
|
515
|
+
description: `Drag-and-drop element lacks keyboard alternative`,
|
|
516
|
+
affectedPersonas: ["motor-impairment-tremor", "motor-impairment-limited-mobility"],
|
|
517
|
+
wcagCriteria: ["2.1.1", "2.5.7"],
|
|
518
|
+
severity: "critical",
|
|
519
|
+
remediation: "Provide keyboard-accessible alternative for drag-and-drop (arrow keys, or explicit move buttons)",
|
|
520
|
+
});
|
|
521
|
+
ctx.wcagViolations.add("2.1.1");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Check for very small spacing between interactive elements (motor precision issue)
|
|
525
|
+
const closeElements = await page.$$eval('button, a, input[type="checkbox"], input[type="radio"]', (elements) => {
|
|
526
|
+
const closeGroups = [];
|
|
527
|
+
const elArray = Array.from(elements);
|
|
528
|
+
for (let i = 0; i < Math.min(elArray.length, 30); i++) {
|
|
529
|
+
const rect1 = elArray[i].getBoundingClientRect();
|
|
530
|
+
if (rect1.width === 0)
|
|
531
|
+
continue;
|
|
532
|
+
for (let j = i + 1; j < Math.min(elArray.length, 30); j++) {
|
|
533
|
+
const rect2 = elArray[j].getBoundingClientRect();
|
|
534
|
+
if (rect2.width === 0)
|
|
535
|
+
continue;
|
|
536
|
+
// Calculate distance between elements
|
|
537
|
+
const dx = Math.max(0, Math.max(rect1.left, rect2.left) - Math.min(rect1.right, rect2.right));
|
|
538
|
+
const dy = Math.max(0, Math.max(rect1.top, rect2.top) - Math.min(rect1.bottom, rect2.bottom));
|
|
539
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
540
|
+
// Flag elements less than 8px apart
|
|
541
|
+
if (distance < 8 && distance >= 0) {
|
|
542
|
+
closeGroups.push({
|
|
543
|
+
selectors: [
|
|
544
|
+
elArray[i].tagName.toLowerCase() + (elArray[i].id ? `#${elArray[i].id}` : ''),
|
|
545
|
+
elArray[j].tagName.toLowerCase() + (elArray[j].id ? `#${elArray[j].id}` : '')
|
|
546
|
+
],
|
|
547
|
+
spacing: Math.round(distance),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return closeGroups.slice(0, 5);
|
|
553
|
+
});
|
|
554
|
+
if (closeElements.length > 0) {
|
|
555
|
+
barriers.push({
|
|
556
|
+
type: "motor_precision",
|
|
557
|
+
element: `${closeElements.length} element groups`,
|
|
558
|
+
description: `${closeElements.length} groups of interactive elements are very close together (< 8px spacing), making them difficult to target for users with tremors`,
|
|
559
|
+
affectedPersonas: ["motor-impairment-tremor"],
|
|
560
|
+
wcagCriteria: ["2.5.5"],
|
|
561
|
+
severity: "major",
|
|
562
|
+
remediation: "Increase spacing between interactive elements to at least 8-12px",
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Detect barriers specifically relevant to cognitive personas (ADHD, dyslexia, memory impairment).
|
|
568
|
+
*
|
|
569
|
+
* Key checks:
|
|
570
|
+
* - Form complexity (field count, required fields)
|
|
571
|
+
* - Distraction count (animations, auto-playing media)
|
|
572
|
+
* - Reading level (sentence complexity)
|
|
573
|
+
* - Memory burden (multi-step processes without progress indicators)
|
|
574
|
+
*
|
|
575
|
+
* @since v18.15.0
|
|
576
|
+
*/
|
|
577
|
+
async function detectCognitiveBarriers(ctx) {
|
|
578
|
+
const { page, barriers } = ctx;
|
|
579
|
+
// Check for auto-playing media (distraction for ADHD)
|
|
580
|
+
const autoPlayMedia = await page.$$eval('video[autoplay], audio[autoplay], [class*="autoplay"]', (elements) => elements.map(el => ({
|
|
581
|
+
selector: el.tagName.toLowerCase(),
|
|
582
|
+
hasControls: el.hasAttribute('controls'),
|
|
583
|
+
hasMuted: el.hasAttribute('muted'),
|
|
584
|
+
})));
|
|
585
|
+
for (const media of autoPlayMedia) {
|
|
586
|
+
if (!media.hasMuted) {
|
|
587
|
+
barriers.push({
|
|
588
|
+
type: "cognitive_load",
|
|
589
|
+
element: media.selector,
|
|
590
|
+
description: `Auto-playing ${media.selector} with sound can be highly distracting for users with ADHD`,
|
|
591
|
+
affectedPersonas: ["cognitive-adhd"],
|
|
592
|
+
wcagCriteria: ["1.4.2", "2.2.2"],
|
|
593
|
+
severity: "critical",
|
|
594
|
+
remediation: "Add muted attribute to autoplay media, or provide user controls to pause/stop",
|
|
595
|
+
});
|
|
596
|
+
ctx.wcagViolations.add("1.4.2");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// Check for multi-step forms without progress indicator
|
|
600
|
+
const multiStepForms = await page.$$eval('form', (forms) => {
|
|
601
|
+
const results = [];
|
|
602
|
+
for (const form of forms) {
|
|
603
|
+
const inputs = form.querySelectorAll('input:not([type="hidden"]):not([type="submit"]), textarea, select');
|
|
604
|
+
const hasProgress = form.querySelector('[class*="progress"], [class*="stepper"], [role="progressbar"]') !== null ||
|
|
605
|
+
document.querySelector('[class*="step-indicator"], [class*="wizard"]') !== null;
|
|
606
|
+
results.push({
|
|
607
|
+
selector: 'form' + (form.id ? `#${form.id}` : ''),
|
|
608
|
+
fieldCount: inputs.length,
|
|
609
|
+
hasProgress,
|
|
610
|
+
hasStepIndicator: hasProgress,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return results;
|
|
614
|
+
});
|
|
615
|
+
for (const form of multiStepForms) {
|
|
616
|
+
if (form.fieldCount > 5 && !form.hasProgress) {
|
|
617
|
+
barriers.push({
|
|
618
|
+
type: "cognitive_load",
|
|
619
|
+
element: form.selector,
|
|
620
|
+
description: `Form with ${form.fieldCount} fields lacks progress indicator - users with memory impairment may lose track of progress`,
|
|
621
|
+
affectedPersonas: ["cognitive-adhd", "cognitive-memory-impairment"],
|
|
622
|
+
wcagCriteria: ["3.3.4"],
|
|
623
|
+
severity: "major",
|
|
624
|
+
remediation: "Add progress indicator showing steps completed and remaining, or break form into clearly numbered sections",
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Check for dense text blocks (dyslexia barrier)
|
|
629
|
+
const denseTextBlocks = await page.$$eval('p, article, section', (elements) => {
|
|
630
|
+
const results = [];
|
|
631
|
+
for (const el of elements.slice(0, 20)) {
|
|
632
|
+
const text = el.textContent || '';
|
|
633
|
+
const wordCount = text.split(/\s+/).filter(w => w.length > 0).length;
|
|
634
|
+
const styles = window.getComputedStyle(el);
|
|
635
|
+
// Flag blocks with 200+ words AND tight line spacing
|
|
636
|
+
if (wordCount > 200) {
|
|
637
|
+
results.push({
|
|
638
|
+
selector: el.tagName.toLowerCase() + (el.className ? `.${el.className.split(' ')[0]}` : ''),
|
|
639
|
+
wordCount,
|
|
640
|
+
lineHeight: styles.lineHeight,
|
|
641
|
+
fontSize: styles.fontSize,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return results;
|
|
646
|
+
});
|
|
647
|
+
for (const block of denseTextBlocks.slice(0, 3)) {
|
|
648
|
+
const lineHeightNum = parseFloat(block.lineHeight);
|
|
649
|
+
const fontSizeNum = parseFloat(block.fontSize);
|
|
650
|
+
const lineHeightRatio = lineHeightNum / fontSizeNum;
|
|
651
|
+
// Line height < 1.5 is hard for dyslexic users
|
|
652
|
+
if (lineHeightRatio < 1.5) {
|
|
653
|
+
barriers.push({
|
|
654
|
+
type: "cognitive_load",
|
|
655
|
+
element: block.selector,
|
|
656
|
+
description: `Dense text block (${block.wordCount} words) with tight line spacing (${lineHeightRatio.toFixed(1)}x) is difficult for dyslexic users`,
|
|
657
|
+
affectedPersonas: ["dyslexic-user"],
|
|
658
|
+
wcagCriteria: ["1.4.12"],
|
|
659
|
+
severity: "major",
|
|
660
|
+
remediation: "Increase line-height to at least 1.5x font size, and consider breaking text into shorter paragraphs with headings",
|
|
661
|
+
});
|
|
662
|
+
ctx.wcagViolations.add("1.4.12");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Check for justified text (dyslexia barrier)
|
|
666
|
+
const justifiedText = await page.$$eval('p, article, div', (elements) => {
|
|
667
|
+
const results = [];
|
|
668
|
+
for (const el of elements.slice(0, 50)) {
|
|
669
|
+
const styles = window.getComputedStyle(el);
|
|
670
|
+
if (styles.textAlign === 'justify') {
|
|
671
|
+
results.push(el.tagName.toLowerCase() + (el.className ? `.${el.className.split(' ')[0]}` : ''));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return results.slice(0, 5);
|
|
675
|
+
});
|
|
676
|
+
if (justifiedText.length > 0) {
|
|
677
|
+
barriers.push({
|
|
678
|
+
type: "cognitive_load",
|
|
679
|
+
element: `${justifiedText.length} elements`,
|
|
680
|
+
description: `Justified text creates uneven word spacing that makes reading difficult for dyslexic users`,
|
|
681
|
+
affectedPersonas: ["dyslexic-user"],
|
|
682
|
+
wcagCriteria: ["1.4.12"],
|
|
683
|
+
severity: "minor",
|
|
684
|
+
remediation: "Use left-aligned text instead of justified for better readability",
|
|
685
|
+
});
|
|
686
|
+
ctx.wcagViolations.add("1.4.12");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Detect barriers specifically relevant to vision personas (low vision, color blindness, elderly).
|
|
691
|
+
*
|
|
692
|
+
* Key checks:
|
|
693
|
+
* - Contrast ratios below WCAG thresholds
|
|
694
|
+
* - Text scaling behavior
|
|
695
|
+
* - Color-only information
|
|
696
|
+
* - Small font sizes (< 16px)
|
|
697
|
+
*
|
|
698
|
+
* @since v18.15.0
|
|
699
|
+
*/
|
|
700
|
+
async function detectVisionBarriers(ctx) {
|
|
701
|
+
const { page, barriers } = ctx;
|
|
702
|
+
// Check for small base font sizes (vision impairment)
|
|
703
|
+
const smallFontElements = await page.$$eval('body, p, span, div, li, td', (elements) => {
|
|
704
|
+
const results = [];
|
|
705
|
+
for (const el of elements.slice(0, 100)) {
|
|
706
|
+
const styles = window.getComputedStyle(el);
|
|
707
|
+
const fontSize = parseFloat(styles.fontSize);
|
|
708
|
+
// Flag fonts smaller than 14px as problematic for low vision
|
|
709
|
+
if (fontSize > 0 && fontSize < 14) {
|
|
710
|
+
results.push({
|
|
711
|
+
selector: el.tagName.toLowerCase() + (el.className ? `.${el.className.split(' ')[0]}` : ''),
|
|
712
|
+
fontSize: styles.fontSize,
|
|
713
|
+
fontSizeNum: fontSize,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return results.slice(0, 10);
|
|
718
|
+
});
|
|
719
|
+
if (smallFontElements.length > 0) {
|
|
720
|
+
const avgSize = smallFontElements.reduce((sum, el) => sum + el.fontSizeNum, 0) / smallFontElements.length;
|
|
721
|
+
barriers.push({
|
|
722
|
+
type: "visual_clarity",
|
|
723
|
+
element: `${smallFontElements.length} elements`,
|
|
724
|
+
description: `${smallFontElements.length} text elements use small font sizes (avg ${avgSize.toFixed(0)}px) that may be difficult for low-vision users`,
|
|
725
|
+
affectedPersonas: ["low-vision-magnified", "elderly-low-vision"],
|
|
726
|
+
wcagCriteria: ["1.4.4"],
|
|
727
|
+
severity: avgSize < 12 ? "critical" : "major",
|
|
728
|
+
remediation: "Use minimum 16px base font size, and ensure text can be resized to 200% without loss of content",
|
|
729
|
+
});
|
|
730
|
+
ctx.wcagViolations.add("1.4.4");
|
|
731
|
+
}
|
|
732
|
+
// Check for thin fonts (hard for low vision)
|
|
733
|
+
const thinFontElements = await page.$$eval('body, h1, h2, h3, p, span', (elements) => {
|
|
734
|
+
const results = [];
|
|
735
|
+
for (const el of elements.slice(0, 50)) {
|
|
736
|
+
const styles = window.getComputedStyle(el);
|
|
737
|
+
const fontWeight = parseInt(styles.fontWeight, 10) || 400;
|
|
738
|
+
// Font weight < 400 is thin and harder to read
|
|
739
|
+
if (fontWeight < 400 && el.textContent && el.textContent.trim().length > 0) {
|
|
740
|
+
results.push(el.tagName.toLowerCase());
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return results;
|
|
744
|
+
});
|
|
745
|
+
if (thinFontElements.length > 3) {
|
|
746
|
+
barriers.push({
|
|
747
|
+
type: "visual_clarity",
|
|
748
|
+
element: `${thinFontElements.length} text elements`,
|
|
749
|
+
description: `Multiple elements use thin font weights (< 400) which are harder to read for low-vision users`,
|
|
750
|
+
affectedPersonas: ["low-vision-magnified", "elderly-low-vision"],
|
|
751
|
+
wcagCriteria: ["1.4.12"],
|
|
752
|
+
severity: "minor",
|
|
753
|
+
remediation: "Use font-weight 400 or higher for body text to improve readability",
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
// Check for links distinguished only by color (color blindness)
|
|
757
|
+
const colorOnlyLinks = await page.$$eval('a', (links) => {
|
|
758
|
+
const results = [];
|
|
759
|
+
for (const link of links.slice(0, 30)) {
|
|
760
|
+
const styles = window.getComputedStyle(link);
|
|
761
|
+
const hasUnderline = styles.textDecoration.includes('underline');
|
|
762
|
+
const hasIcon = link.querySelector('svg, i, [class*="icon"]') !== null;
|
|
763
|
+
if (!hasUnderline && !hasIcon) {
|
|
764
|
+
results.push({
|
|
765
|
+
selector: link.textContent?.trim().slice(0, 20) || 'link',
|
|
766
|
+
hasUnderline,
|
|
767
|
+
hasIcon,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return results;
|
|
772
|
+
});
|
|
773
|
+
if (colorOnlyLinks.length > 2) {
|
|
774
|
+
barriers.push({
|
|
775
|
+
type: "sensory",
|
|
776
|
+
element: `${colorOnlyLinks.length} links`,
|
|
777
|
+
description: `${colorOnlyLinks.length} links are distinguished only by color, without underline or icon - color-blind users may not identify them as links`,
|
|
778
|
+
affectedPersonas: ["color-blind-deuteranopia"],
|
|
779
|
+
wcagCriteria: ["1.4.1"],
|
|
780
|
+
severity: "major",
|
|
781
|
+
remediation: "Add underline to links on hover/focus at minimum, or use a non-color visual indicator",
|
|
782
|
+
});
|
|
783
|
+
ctx.wcagViolations.add("1.4.1");
|
|
784
|
+
}
|
|
785
|
+
// Check for status indicators using only red/green (color blindness)
|
|
786
|
+
const redGreenIndicators = await page.$$eval('[class*="status"], [class*="indicator"], [class*="badge"], [class*="alert"]', (elements) => {
|
|
787
|
+
const problematic = [];
|
|
788
|
+
for (const el of elements.slice(0, 20)) {
|
|
789
|
+
const styles = window.getComputedStyle(el);
|
|
790
|
+
const bgColor = styles.backgroundColor;
|
|
791
|
+
const color = styles.color;
|
|
792
|
+
// Simplified red/green detection
|
|
793
|
+
const hasRedGreen = (bgColor.includes('255') && bgColor.includes('0')) || // Pure red or green
|
|
794
|
+
(color.includes('255') && color.includes('0'));
|
|
795
|
+
const hasIcon = el.querySelector('svg, i, [class*="icon"]') !== null;
|
|
796
|
+
const hasText = (el.textContent?.trim() || '').length > 1;
|
|
797
|
+
if (hasRedGreen && !hasIcon && !hasText) {
|
|
798
|
+
problematic.push(el.className?.split(' ')[0] || 'indicator');
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return problematic;
|
|
802
|
+
});
|
|
803
|
+
if (redGreenIndicators.length > 0) {
|
|
804
|
+
barriers.push({
|
|
805
|
+
type: "sensory",
|
|
806
|
+
element: `${redGreenIndicators.length} status indicators`,
|
|
807
|
+
description: `Status indicators using red/green without additional cues may be indistinguishable for color-blind users`,
|
|
808
|
+
affectedPersonas: ["color-blind-deuteranopia"],
|
|
809
|
+
wcagCriteria: ["1.4.1"],
|
|
810
|
+
severity: "major",
|
|
811
|
+
remediation: "Add icons, patterns, or text labels to status indicators in addition to color",
|
|
812
|
+
});
|
|
813
|
+
ctx.wcagViolations.add("1.4.1");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// ============================================================================
|
|
430
817
|
// Journey Simulation for Empathy
|
|
431
818
|
// ============================================================================
|
|
432
819
|
async function simulateAccessibilityJourney(page, url, goal, persona, maxSteps, maxTime) {
|
|
@@ -448,7 +835,7 @@ async function simulateAccessibilityJourney(page, url, goal, persona, maxSteps,
|
|
|
448
835
|
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
|
|
449
836
|
await page.waitForTimeout(2000);
|
|
450
837
|
// Run barrier detection
|
|
451
|
-
// v10.10.0: All detectors
|
|
838
|
+
// v10.10.0: All general detectors run unconditionally regardless of persona
|
|
452
839
|
await detectSmallTouchTargets(ctx);
|
|
453
840
|
await detectLowContrast(ctx);
|
|
454
841
|
await detectCognitiveLoad(ctx);
|
|
@@ -456,6 +843,26 @@ async function simulateAccessibilityJourney(page, url, goal, persona, maxSteps,
|
|
|
456
843
|
await detectColorOnlyInfo(ctx);
|
|
457
844
|
await detectMissingAltText(ctx);
|
|
458
845
|
await detectMissingFormLabels(ctx);
|
|
846
|
+
// v18.15.0: Run persona-specific detectors based on persona category
|
|
847
|
+
// This ensures each persona type gets specialized barrier detection
|
|
848
|
+
const personaCategory = getPersonaCategory(persona.name);
|
|
849
|
+
switch (personaCategory) {
|
|
850
|
+
case "motor":
|
|
851
|
+
await detectMotorBarriers(ctx);
|
|
852
|
+
break;
|
|
853
|
+
case "cognitive":
|
|
854
|
+
await detectCognitiveBarriers(ctx);
|
|
855
|
+
break;
|
|
856
|
+
case "vision":
|
|
857
|
+
await detectVisionBarriers(ctx);
|
|
858
|
+
break;
|
|
859
|
+
case "general":
|
|
860
|
+
// Run all category-specific detectors for general personas
|
|
861
|
+
await detectMotorBarriers(ctx);
|
|
862
|
+
await detectCognitiveBarriers(ctx);
|
|
863
|
+
await detectVisionBarriers(ctx);
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
459
866
|
// Use cognitive journey for realistic step tracking if API key available
|
|
460
867
|
if (isApiKeyConfigured()) {
|
|
461
868
|
try {
|