cbrowser 18.82.2 → 19.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -3
- package/dist/analysis/accessibility-empathy.d.ts +68 -0
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +131 -6
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/agent-ready-audit.d.ts +44 -1
- package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
- package/dist/analysis/agent-ready-audit.js +100 -40
- package/dist/analysis/agent-ready-audit.js.map +1 -1
- package/dist/analysis/bug-hunter.d.ts +11 -1
- package/dist/analysis/bug-hunter.d.ts.map +1 -1
- package/dist/analysis/bug-hunter.js +44 -6
- package/dist/analysis/bug-hunter.js.map +1 -1
- package/dist/analysis/competitive-benchmark.d.ts +49 -1
- package/dist/analysis/competitive-benchmark.d.ts.map +1 -1
- package/dist/analysis/competitive-benchmark.js +138 -14
- package/dist/analysis/competitive-benchmark.js.map +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +5 -2
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +10 -10
- package/dist/cli.js.map +1 -1
- package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/analysis-tools.js +7 -1
- package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +10 -0
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +29 -1
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +25 -2
- package/dist/personas.js.map +1 -1
- package/dist/remediation/patch-generator.js +2 -2
- package/dist/remediation/patch-generator.js.map +1 -1
- package/dist/types.d.ts +93 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/visual/attention-quality.d.ts +32 -0
- package/dist/visual/attention-quality.d.ts.map +1 -1
- package/dist/visual/attention-quality.js +41 -1
- package/dist/visual/attention-quality.js.map +1 -1
- package/dist/visual/freeze-animations.d.ts +71 -0
- package/dist/visual/freeze-animations.d.ts.map +1 -0
- package/dist/visual/freeze-animations.js +111 -0
- package/dist/visual/freeze-animations.js.map +1 -0
- package/dist/visual/llm-relevance.d.ts +6 -0
- package/dist/visual/llm-relevance.d.ts.map +1 -1
- package/dist/visual/llm-relevance.js +42 -1
- package/dist/visual/llm-relevance.js.map +1 -1
- package/dist/visual/persona-context.d.ts +18 -0
- package/dist/visual/persona-context.d.ts.map +1 -1
- package/dist/visual/persona-context.js +19 -1
- package/dist/visual/persona-context.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ Five tools to make your site ready for the AI agent era:
|
|
|
128
128
|
|
|
129
129
|
| Tool | What It Does |
|
|
130
130
|
|------|-------------|
|
|
131
|
-
| `agent_ready_audit` | Score site on findability, stability,
|
|
131
|
+
| `agent_ready_audit` | Score site on findability, stability, agent-perceivability, semantics (A-F grade) |
|
|
132
132
|
| `ai_benchmark` | Compare AI-friendliness across competitor URLs |
|
|
133
133
|
| `webmcp_ready_audit` | Audit MCP server for WebMCP compatibility |
|
|
134
134
|
| `remediation_patches` | Generate actionable code fixes for audit findings |
|
|
@@ -299,9 +299,36 @@ npx cbrowser competitive-benchmark \
|
|
|
299
299
|
|
|
300
300
|
---
|
|
301
301
|
|
|
302
|
+
## Upgrading to 19.0.0
|
|
303
|
+
|
|
304
|
+
One breaking change: the agent-ready **accessibility** axis is now
|
|
305
|
+
**agentPerceivability**, and three deprecated aliases are removed.
|
|
306
|
+
|
|
307
|
+
| removed | use instead |
|
|
308
|
+
|---|---|
|
|
309
|
+
| `score.accessibility` | `score.agentPerceivability` |
|
|
310
|
+
| `summary.dynamicContentCount` | `summary.deferredLoadingPatterns` |
|
|
311
|
+
| `comparison.bestAccessibility` | `comparison.bestAgentPerceivability` |
|
|
312
|
+
| issue `category: "accessibility"` | `category: "agentPerceivability"` |
|
|
313
|
+
|
|
314
|
+
All three replacements have carried identical values since 18.82.0, so if you
|
|
315
|
+
upgraded within that range you can switch keys before upgrading and take this
|
|
316
|
+
release with no further change.
|
|
317
|
+
|
|
318
|
+
The rename is not cosmetic. That axis measures whether a *machine* can perceive
|
|
319
|
+
your elements — ARIA present, elements carry text. It is **not** a WCAG
|
|
320
|
+
conformance score, and it returned 100 on a page where `empathy_audit` found two
|
|
321
|
+
WCAG violations. Both numbers were right; calling one of them "accessibility" was
|
|
322
|
+
not. Run `empathy_audit` for conformance, and expect the two to differ.
|
|
323
|
+
|
|
324
|
+
On input, `category: "accessibility"` is still accepted and scores identically, so
|
|
325
|
+
tools that *send* issues need not change immediately. Tools that *read* scores do.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
302
329
|
## Geo Proxy
|
|
303
330
|
|
|
304
|
-
Test from
|
|
331
|
+
Test from 10 global regions via residential proxies:
|
|
305
332
|
|
|
306
333
|
```bash
|
|
307
334
|
npx cbrowser cognitive-journey \
|
|
@@ -311,7 +338,19 @@ npx cbrowser cognitive-journey \
|
|
|
311
338
|
--geo-region uk
|
|
312
339
|
```
|
|
313
340
|
|
|
314
|
-
**Regions:** `us-west`, `us-east`, `us-central`, `uk`, `germany`, `france`, `japan`, `australia`, `
|
|
341
|
+
**Regions:** `us-west`, `us-east`, `us-central`, `uk`, `germany`, `france`, `japan`, `australia`, `canada`, `brazil`
|
|
342
|
+
|
|
343
|
+
Geo routing needs your own residential-proxy credentials — the package ships the
|
|
344
|
+
mechanism and no keys. Set `IPROYAL_USERNAME` / `IPROYAL_PASSWORD`, or place a
|
|
345
|
+
`geo-proxy.json` at `~/.cbrowser/`. Without either, `--geo-region` fails loudly
|
|
346
|
+
rather than silently running from your own IP.
|
|
347
|
+
|
|
348
|
+
A `geo-proxy.json` may define additional regions; they merge over the ten above.
|
|
349
|
+
|
|
350
|
+
> **Corrected 2026-08-11.** This section previously advertised 12 regions
|
|
351
|
+
> including `india` and `singapore`. Those are not in the shipped defaults and
|
|
352
|
+
> resolve to nothing on a clean install — they existed only in a local config
|
|
353
|
+
> file on one machine. Advertising them was a claim the package could not keep.
|
|
315
354
|
|
|
316
355
|
---
|
|
317
356
|
|
|
@@ -4,7 +4,75 @@
|
|
|
4
4
|
* Learn more at https://cbrowser.ai - MIT License
|
|
5
5
|
*/
|
|
6
6
|
import type { EmpathyAuditResult, EmpathyAuditOptions } from "../types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Drop barriers outside the viewport when the audit is viewport-scoped.
|
|
9
|
+
*
|
|
10
|
+
* `viewportOnly` existed and had exactly ONE consumer:
|
|
11
|
+
* `detectSmallTouchTargets`. The other ten detectors scanned the whole document
|
|
12
|
+
* regardless of scope, so `scope: "viewport"` filtered touch targets and nothing
|
|
13
|
+
* else.
|
|
14
|
+
*
|
|
15
|
+
* Measured on /docs/home at 1280x800: six `sensory` barriers at y=5638, seven
|
|
16
|
+
* screens below the fold, with x up to 2029 -- 749px past the right edge. All
|
|
17
|
+
* six were counted in `affectedElements`, charged as `missing_alt: -2.2`, and
|
|
18
|
+
* folded into `barrierOnlyScore`. The response even stamped each one
|
|
19
|
+
* `outsideScreenshot: true`: the geometry was computed, published, and not
|
|
20
|
+
* acted on.
|
|
21
|
+
*
|
|
22
|
+
* So every viewport-scoped empathy score published to date may carry demand
|
|
23
|
+
* from content the persona never saw. It is the mirror of the bug
|
|
24
|
+
* `cognitive_effort`'s scopeNote documents -- that one measured too little and
|
|
25
|
+
* said so; this one measured too much and said nothing.
|
|
26
|
+
*
|
|
27
|
+
* Filtered HERE, once, after every detector and before scoring, rather than in
|
|
28
|
+
* each detector. Eleven copies of a rule is how it came to be applied in one
|
|
29
|
+
* place, and adding a twelfth detector must not be able to reintroduce this.
|
|
30
|
+
*
|
|
31
|
+
* A barrier with no rect is KEPT. Page-level findings -- navigation item count,
|
|
32
|
+
* animation presence, reading level -- have no coordinates and are properties of
|
|
33
|
+
* the page rather than of a location on it. Dropping the unlocatable would trade
|
|
34
|
+
* this bug for its opposite. (2026-08-05)
|
|
35
|
+
*/
|
|
36
|
+
export declare function filterBarriersToViewport<T extends {
|
|
37
|
+
rect?: {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
};
|
|
43
|
+
}>(barriers: T[], viewport: {
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
}): {
|
|
47
|
+
kept: T[];
|
|
48
|
+
dropped: number;
|
|
49
|
+
};
|
|
7
50
|
export declare function formatEmpathyAuditReport(result: EmpathyAuditResult): string;
|
|
8
51
|
export declare function generateEmpathyAuditHtmlReport(result: EmpathyAuditResult): string;
|
|
52
|
+
/**
|
|
53
|
+
* The published WCAG list, DERIVED from the barriers rather than accumulated
|
|
54
|
+
* alongside them.
|
|
55
|
+
*
|
|
56
|
+
* It used to be a Set that each detector appended to by hand, in parallel with
|
|
57
|
+
* the barrier it was pushing. Two lists maintained by hand drift, and this pair
|
|
58
|
+
* drifted in the direction that matters: a barrier declared criteria that the
|
|
59
|
+
* adjacent `.add()` line did not repeat, so the audit DETECTED a violation,
|
|
60
|
+
* displayed it on the barrier, and left it out of the conformance list.
|
|
61
|
+
* Measured on one fixture page: 1.3.1, 2.5.5, 2.5.7 and 3.3.4 all shown as
|
|
62
|
+
* barriers, none listed. 1.3.1 there was an unlabelled form input -- Level A,
|
|
63
|
+
* and among the most common real failures there is.
|
|
64
|
+
*
|
|
65
|
+
* Deriving also fixes the reported inverse (a criterion listed with no barrier
|
|
66
|
+
* explaining it), because barriers dropped by the viewport filter now take
|
|
67
|
+
* their criteria with them instead of leaving the string behind.
|
|
68
|
+
*
|
|
69
|
+
* Criteria no barrier ever claimed are still honoured -- see
|
|
70
|
+
* `criteriaEverOnABarrier`. The rule is "mirrors are derived", not "throw away
|
|
71
|
+
* anything the barriers do not know about". (2026-08-11)
|
|
72
|
+
*/
|
|
73
|
+
export declare function deriveWcagViolations(barriers: Array<{
|
|
74
|
+
wcagCriteria?: string[];
|
|
75
|
+
wcagAdvisoryCriteria?: string[];
|
|
76
|
+
}>, recordedByDetectors: Set<string>, criteriaEverOnABarrier: Set<string>, wcagLevel: "A" | "AA" | "AAA"): string[];
|
|
9
77
|
export declare function runEmpathyAudit(url: string, options: EmpathyAuditOptions): Promise<EmpathyAuditResult>;
|
|
10
78
|
//# sourceMappingURL=accessibility-empathy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessibility-empathy.d.ts","sourceRoot":"","sources":["../../src/analysis/accessibility-empathy.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAmCH,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;AAmCH,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EASpB,MAAM,aAAa,CAAC;AAkKrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EACnH,QAAQ,EAAE,CAAC,EAAE,EACb,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAWhC;AAw/DD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAmF3E;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAkTjF;AAqJD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,KAAK,CAAC;IAAE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,EAC7E,mBAAmB,EAAE,GAAG,CAAC,MAAM,CAAC,EAChC,sBAAsB,EAAE,GAAG,CAAC,MAAM,CAAC,EACnC,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,KAAK,GAC5B,MAAM,EAAE,CAkBV;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CA+Z7B"}
|
|
@@ -28,10 +28,12 @@ const WCAG_CRITERIA = {
|
|
|
28
28
|
"1.2.5": { level: "AA", description: "Audio Description (Prerecorded)" },
|
|
29
29
|
"1.3.1": { level: "A", description: "Info and Relationships" },
|
|
30
30
|
"1.4.1": { level: "A", description: "Use of Color" },
|
|
31
|
+
"1.4.2": { level: "A", description: "Audio Control" },
|
|
31
32
|
"1.4.3": { level: "AA", description: "Contrast (Minimum)" },
|
|
32
33
|
"1.4.4": { level: "AA", description: "Resize Text" },
|
|
33
34
|
"1.4.6": { level: "AAA", description: "Contrast (Enhanced)" },
|
|
34
35
|
"1.4.10": { level: "AA", description: "Reflow" },
|
|
36
|
+
"1.4.12": { level: "AA", description: "Text Spacing" },
|
|
35
37
|
"2.1.1": { level: "A", description: "Keyboard" },
|
|
36
38
|
"2.1.2": { level: "A", description: "No Keyboard Trap" },
|
|
37
39
|
"2.2.1": { level: "A", description: "Timing Adjustable" },
|
|
@@ -42,13 +44,16 @@ const WCAG_CRITERIA = {
|
|
|
42
44
|
"2.4.4": { level: "A", description: "Link Purpose" },
|
|
43
45
|
"2.4.6": { level: "AA", description: "Headings and Labels" },
|
|
44
46
|
"2.4.7": { level: "AA", description: "Focus Visible" },
|
|
45
|
-
"2.5.
|
|
47
|
+
"2.5.1": { level: "A", description: "Pointer Gestures" },
|
|
48
|
+
"2.5.5": { level: "AAA", description: "Target Size (Enhanced)" },
|
|
49
|
+
"2.5.7": { level: "AA", description: "Dragging Movements" },
|
|
46
50
|
"2.5.8": { level: "AA", description: "Target Size (Minimum)" },
|
|
47
51
|
"3.1.1": { level: "A", description: "Language of Page" },
|
|
48
52
|
"3.2.1": { level: "A", description: "On Focus" },
|
|
49
53
|
"3.2.2": { level: "A", description: "On Input" },
|
|
50
54
|
"3.3.1": { level: "A", description: "Error Identification" },
|
|
51
55
|
"3.3.2": { level: "A", description: "Labels or Instructions" },
|
|
56
|
+
"3.3.4": { level: "AA", description: "Error Prevention (Legal, Financial, Data)" },
|
|
52
57
|
"4.1.1": { level: "A", description: "Parsing" },
|
|
53
58
|
"4.1.2": { level: "A", description: "Name, Role, Value" },
|
|
54
59
|
};
|
|
@@ -106,6 +111,48 @@ function adjustSeverityForLevel(severity, barrierWcagLevel, auditLevel) {
|
|
|
106
111
|
}
|
|
107
112
|
return severity;
|
|
108
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Drop barriers outside the viewport when the audit is viewport-scoped.
|
|
116
|
+
*
|
|
117
|
+
* `viewportOnly` existed and had exactly ONE consumer:
|
|
118
|
+
* `detectSmallTouchTargets`. The other ten detectors scanned the whole document
|
|
119
|
+
* regardless of scope, so `scope: "viewport"` filtered touch targets and nothing
|
|
120
|
+
* else.
|
|
121
|
+
*
|
|
122
|
+
* Measured on /docs/home at 1280x800: six `sensory` barriers at y=5638, seven
|
|
123
|
+
* screens below the fold, with x up to 2029 -- 749px past the right edge. All
|
|
124
|
+
* six were counted in `affectedElements`, charged as `missing_alt: -2.2`, and
|
|
125
|
+
* folded into `barrierOnlyScore`. The response even stamped each one
|
|
126
|
+
* `outsideScreenshot: true`: the geometry was computed, published, and not
|
|
127
|
+
* acted on.
|
|
128
|
+
*
|
|
129
|
+
* So every viewport-scoped empathy score published to date may carry demand
|
|
130
|
+
* from content the persona never saw. It is the mirror of the bug
|
|
131
|
+
* `cognitive_effort`'s scopeNote documents -- that one measured too little and
|
|
132
|
+
* said so; this one measured too much and said nothing.
|
|
133
|
+
*
|
|
134
|
+
* Filtered HERE, once, after every detector and before scoring, rather than in
|
|
135
|
+
* each detector. Eleven copies of a rule is how it came to be applied in one
|
|
136
|
+
* place, and adding a twelfth detector must not be able to reintroduce this.
|
|
137
|
+
*
|
|
138
|
+
* A barrier with no rect is KEPT. Page-level findings -- navigation item count,
|
|
139
|
+
* animation presence, reading level -- have no coordinates and are properties of
|
|
140
|
+
* the page rather than of a location on it. Dropping the unlocatable would trade
|
|
141
|
+
* this bug for its opposite. (2026-08-05)
|
|
142
|
+
*/
|
|
143
|
+
export function filterBarriersToViewport(barriers, viewport) {
|
|
144
|
+
const kept = barriers.filter((b) => {
|
|
145
|
+
if (!b.rect)
|
|
146
|
+
return true; // page-level finding, not a located one
|
|
147
|
+
const { x, y, width, height } = b.rect;
|
|
148
|
+
// Document coordinates, and a viewport-scoped audit never scrolls, so the
|
|
149
|
+
// visible band is y 0..height and x 0..width.
|
|
150
|
+
const intersectsY = y < viewport.height && y + height > 0;
|
|
151
|
+
const intersectsX = x < viewport.width && x + width > 0;
|
|
152
|
+
return intersectsY && intersectsX;
|
|
153
|
+
});
|
|
154
|
+
return { kept, dropped: barriers.length - kept.length };
|
|
155
|
+
}
|
|
109
156
|
/**
|
|
110
157
|
* Detect small touch targets that are hard for motor-impaired users
|
|
111
158
|
* v10.10.0: Always detect touch target issues regardless of persona
|
|
@@ -435,6 +482,14 @@ async function detectCognitiveLoad(ctx) {
|
|
|
435
482
|
description: issue.description + (mapping.criteria.length === 0 ? ' (UX recommendation, not a WCAG violation)' : ''),
|
|
436
483
|
affectedPersonas: ["cognitive-adhd", "dyslexic-user"],
|
|
437
484
|
wcagCriteria: mapping.criteria,
|
|
485
|
+
// `violation: null` with a non-empty `criteria` means "relates to, does
|
|
486
|
+
// not violate" -- a text-wall is a readability recommendation under 1.3.1,
|
|
487
|
+
// not a 1.3.1 failure. Recording that on the barrier is what lets the
|
|
488
|
+
// published list be DERIVED from barriers without promoting
|
|
489
|
+
// recommendations into violations. (2026-08-11)
|
|
490
|
+
...(mapping.violation === null && mapping.criteria.length > 0
|
|
491
|
+
? { wcagAdvisoryCriteria: mapping.criteria }
|
|
492
|
+
: {}),
|
|
438
493
|
severity: issue.type === "long-form" ? "major" : "minor",
|
|
439
494
|
remediation: issue.type === "long-form"
|
|
440
495
|
? "Break form into multiple steps or sections"
|
|
@@ -1216,6 +1271,7 @@ async function simulateAccessibilityJourney(page, url, goal, persona, scope = "v
|
|
|
1216
1271
|
barriers: [],
|
|
1217
1272
|
frictionPoints: [],
|
|
1218
1273
|
wcagViolations: new Set(),
|
|
1274
|
+
criteriaEverOnABarrier: new Set(),
|
|
1219
1275
|
stepCount: 0,
|
|
1220
1276
|
viewportOnly: scope === "viewport",
|
|
1221
1277
|
wcagLevel,
|
|
@@ -1300,6 +1356,28 @@ async function simulateAccessibilityJourney(page, url, goal, persona, scope = "v
|
|
|
1300
1356
|
await detectVisionBarriers(ctx);
|
|
1301
1357
|
break;
|
|
1302
1358
|
}
|
|
1359
|
+
// Scope is enforced HERE, once, rather than in each detector -- see
|
|
1360
|
+
// filterBarriersToViewport. Ten of the eleven detectors above never honoured
|
|
1361
|
+
// it, so a viewport audit was scoring content seven screens below the fold.
|
|
1362
|
+
// Snapshotted BEFORE the filter runs, so the derivation below can tell a
|
|
1363
|
+
// criterion that no barrier ever claimed (a genuine page-level finding,
|
|
1364
|
+
// keep it) from one whose barriers were all filtered away (an orphan, drop
|
|
1365
|
+
// it). Without the distinction the two are indistinguishable afterwards.
|
|
1366
|
+
for (const b of ctx.barriers) {
|
|
1367
|
+
for (const c of b.wcagCriteria ?? [])
|
|
1368
|
+
ctx.criteriaEverOnABarrier.add(c);
|
|
1369
|
+
}
|
|
1370
|
+
if (ctx.viewportOnly) {
|
|
1371
|
+
const vp = await page.evaluate(() => ({
|
|
1372
|
+
width: window.innerWidth, height: window.innerHeight,
|
|
1373
|
+
})).catch(() => ({ width: 1280, height: 800 }));
|
|
1374
|
+
const { kept, dropped } = filterBarriersToViewport(ctx.barriers, vp);
|
|
1375
|
+
if (dropped > 0) {
|
|
1376
|
+
ctx.barriers.length = 0;
|
|
1377
|
+
ctx.barriers.push(...kept);
|
|
1378
|
+
ctx.outOfViewportDropped = dropped;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1303
1381
|
// Use cognitive journey for realistic step tracking if API key available
|
|
1304
1382
|
// v18.35.0: Add hard timeout to prevent MCP proxy disconnection (~60s limit on claude.ai)
|
|
1305
1383
|
const journeyTimeoutMs = Math.min(maxTime * 1000, 45000); // Hard cap at 45s to leave margin
|
|
@@ -1599,6 +1677,10 @@ async function simulateAccessibilityJourney(page, url, goal, persona, scope = "v
|
|
|
1599
1677
|
// Deprecated alias, one release only. See EmpathyPersonaResult.
|
|
1600
1678
|
goalAchieved: noBlockingBarriers,
|
|
1601
1679
|
barriers: ctx.barriers,
|
|
1680
|
+
// Surfaced, not kept internal: a filter whose effect is invisible cannot be
|
|
1681
|
+
// told apart from a filter that never ran, and that is exactly the defect
|
|
1682
|
+
// this closes.
|
|
1683
|
+
...(ctx.outOfViewportDropped ? { outOfViewportBarriersDropped: ctx.outOfViewportDropped } : {}),
|
|
1602
1684
|
frictionPoints: ctx.frictionPoints,
|
|
1603
1685
|
// Filtered to the audit's conformance level, exactly as the top-level
|
|
1604
1686
|
// allWcagViolations is (see the filter near the end of runEmpathyAudit).
|
|
@@ -1606,11 +1688,7 @@ async function simulateAccessibilityJourney(page, url, goal, persona, scope = "v
|
|
|
1606
1688
|
// had dropped at AA, so wcagViolationCount read 2 beside an
|
|
1607
1689
|
// allWcagViolations of ["2.2.2"] — the same finding counted under two
|
|
1608
1690
|
// different rules. (2026-07-29)
|
|
1609
|
-
wcagViolations:
|
|
1610
|
-
const criteria = WCAG_CRITERIA[v];
|
|
1611
|
-
const order = { A: 1, AA: 2, AAA: 3 };
|
|
1612
|
-
return !criteria || order[criteria.level] <= order[wcagLevel];
|
|
1613
|
-
}),
|
|
1691
|
+
wcagViolations: deriveWcagViolations(ctx.barriers, ctx.wcagViolations, ctx.criteriaEverOnABarrier, wcagLevel),
|
|
1614
1692
|
remediationPriority,
|
|
1615
1693
|
empathyScore,
|
|
1616
1694
|
scoreContext, // v18.22.0: Added score breakdown
|
|
@@ -2371,6 +2449,48 @@ function inferGoalType(goal) {
|
|
|
2371
2449
|
return "navigate_to";
|
|
2372
2450
|
return "complete_action";
|
|
2373
2451
|
}
|
|
2452
|
+
/**
|
|
2453
|
+
* The published WCAG list, DERIVED from the barriers rather than accumulated
|
|
2454
|
+
* alongside them.
|
|
2455
|
+
*
|
|
2456
|
+
* It used to be a Set that each detector appended to by hand, in parallel with
|
|
2457
|
+
* the barrier it was pushing. Two lists maintained by hand drift, and this pair
|
|
2458
|
+
* drifted in the direction that matters: a barrier declared criteria that the
|
|
2459
|
+
* adjacent `.add()` line did not repeat, so the audit DETECTED a violation,
|
|
2460
|
+
* displayed it on the barrier, and left it out of the conformance list.
|
|
2461
|
+
* Measured on one fixture page: 1.3.1, 2.5.5, 2.5.7 and 3.3.4 all shown as
|
|
2462
|
+
* barriers, none listed. 1.3.1 there was an unlabelled form input -- Level A,
|
|
2463
|
+
* and among the most common real failures there is.
|
|
2464
|
+
*
|
|
2465
|
+
* Deriving also fixes the reported inverse (a criterion listed with no barrier
|
|
2466
|
+
* explaining it), because barriers dropped by the viewport filter now take
|
|
2467
|
+
* their criteria with them instead of leaving the string behind.
|
|
2468
|
+
*
|
|
2469
|
+
* Criteria no barrier ever claimed are still honoured -- see
|
|
2470
|
+
* `criteriaEverOnABarrier`. The rule is "mirrors are derived", not "throw away
|
|
2471
|
+
* anything the barriers do not know about". (2026-08-11)
|
|
2472
|
+
*/
|
|
2473
|
+
export function deriveWcagViolations(barriers, recordedByDetectors, criteriaEverOnABarrier, wcagLevel) {
|
|
2474
|
+
const violated = new Set();
|
|
2475
|
+
for (const b of barriers) {
|
|
2476
|
+
const advisory = new Set(b.wcagAdvisoryCriteria ?? []);
|
|
2477
|
+
for (const c of b.wcagCriteria ?? []) {
|
|
2478
|
+
if (!advisory.has(c))
|
|
2479
|
+
violated.add(c);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
// Page-level findings: recorded by a detector, never attached to any barrier.
|
|
2483
|
+
// Kept. A criterion whose barriers existed and were filtered away is NOT kept.
|
|
2484
|
+
for (const c of recordedByDetectors) {
|
|
2485
|
+
if (!criteriaEverOnABarrier.has(c))
|
|
2486
|
+
violated.add(c);
|
|
2487
|
+
}
|
|
2488
|
+
const order = { A: 1, AA: 2, AAA: 3 };
|
|
2489
|
+
return Array.from(violated).filter((v) => {
|
|
2490
|
+
const criteria = WCAG_CRITERIA[v];
|
|
2491
|
+
return !criteria || order[criteria.level] <= order[wcagLevel];
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2374
2494
|
export async function runEmpathyAudit(url, options) {
|
|
2375
2495
|
const { goal, disabilities, wcagLevel = "AA", maxSteps = 20, maxTime = 120, headless = true, } = options;
|
|
2376
2496
|
const startTime = Date.now();
|
|
@@ -2737,6 +2857,11 @@ export async function runEmpathyAudit(url, options) {
|
|
|
2737
2857
|
topBarriers: deduplicatedBarriers, // v11.11.0: Deduplicated barriers grouped by type
|
|
2738
2858
|
combinedRemediation,
|
|
2739
2859
|
overallScore,
|
|
2860
|
+
// Summed from the per-persona audits: a filter whose effect is invisible is
|
|
2861
|
+
// indistinguishable from one that did not run.
|
|
2862
|
+
...(results.reduce((n, r) => n + (r.outOfViewportBarriersDropped ?? 0), 0) > 0
|
|
2863
|
+
? { outOfViewportBarriersDropped: results.reduce((n, r) => n + (r.outOfViewportBarriersDropped ?? 0), 0) }
|
|
2864
|
+
: {}),
|
|
2740
2865
|
duration: Date.now() - startTime,
|
|
2741
2866
|
};
|
|
2742
2867
|
}
|