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 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 83 tools, zero critical bugs, zero server crashes. [View Full Assessment →](docs/STRESS-TEST-v16.14.4.md)
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
  [![npm version](https://img.shields.io/npm/v/cbrowser.svg)](https://www.npmjs.com/package/cbrowser)
6
6
  [![Documentation](https://img.shields.io/badge/Docs-cbrowser.ai-blue.svg)](https://cbrowser.ai/docs)
7
7
  [![Grade A+](https://img.shields.io/badge/Stress%20Test-A+-brightgreen.svg)](docs/STRESS-TEST-v16.14.4.md)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
9
- [![MCP Ready](https://img.shields.io/badge/MCP-83%20Tools-blue)](https://modelcontextprotocol.io)
9
+ [![MCP Ready](https://img.shields.io/badge/MCP-90%20Tools-blue)](https://modelcontextprotocol.io)
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue.svg)](https://www.typescriptlang.org/)
11
11
  [![Node](https://img.shields.io/badge/Node-%3E%3D18-green.svg)](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
- **83 tools, 17 cognitive personas, 25 research-backed traits.** After rigorous stress testing across production sites including Airbnb and Hacker News:
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
- | **83 MCP Tools** | ✅ Production-ready | Full Claude integration—local and remote servers. |
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
- ### 83 MCP Tools
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 48 base tools + 8 marketing tools.
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;AA4yBrB,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"}
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 now run unconditionally regardless of persona
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 {