@crouton-kit/capture 1.3.8 → 1.3.10
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/bin/capture +29841 -8539
- package/package.json +6 -3
- package/src/capture.ts +86 -173
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -205
- package/src/cdp/bridge/protocol.ts +93 -6
- package/src/cdp/bridge/server.ts +118 -53
- package/src/cdp/bridge/spawn.ts +53 -6
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +20 -14
- package/src/cdp/commands/cdp.ts +259 -90
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +76 -20
- package/src/cdp/commands/measure/check.ts +100 -26
- package/src/cdp/commands/measure/diff.ts +109 -21
- package/src/cdp/commands/measure/explain.ts +121 -19
- package/src/cdp/commands/measure/index.ts +31 -39
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +41 -13
- package/src/cdp/commands/measure/map-layers.ts +49 -15
- package/src/cdp/commands/measure/map-scroll.ts +85 -15
- package/src/cdp/commands/measure/snap.ts +405 -25
- package/src/cdp/commands/measure/sweep.ts +217 -20
- package/src/cdp/commands/motion/index.ts +18 -17
- package/src/cdp/commands/motion/jank.ts +119 -9
- package/src/cdp/commands/motion/mask.ts +81 -12
- package/src/cdp/commands/motion/rec.ts +520 -19
- package/src/cdp/commands/motion/response.ts +65 -15
- package/src/cdp/commands/motion/timeline.ts +139 -14
- package/src/cdp/commands/page/click.ts +194 -0
- package/src/cdp/commands/page/elements.ts +154 -0
- package/src/cdp/commands/page/exec.ts +317 -0
- package/src/cdp/commands/page/index.ts +72 -0
- package/src/cdp/commands/page/navigate.ts +330 -0
- package/src/cdp/commands/page/scroll.ts +107 -0
- package/src/cdp/commands/page/shot.ts +208 -0
- package/src/cdp/commands/page/type.ts +104 -0
- package/src/cdp/commands/tab/index.ts +59 -0
- package/src/cdp/commands/tab/list.ts +167 -0
- package/src/cdp/commands/tab/network.ts +162 -0
- package/src/cdp/commands/tab/open.ts +90 -0
- package/src/cdp/commands/tab/reset.ts +160 -0
- package/src/cdp/connection.ts +368 -37
- package/src/cdp/detect.ts +42 -13
- package/src/cdp/dispatch.ts +74 -28
- package/src/cdp/har-recorder.ts +648 -115
- package/src/cdp/leaf-grammar.ts +99 -0
- package/src/cdp/measure/census.ts +205 -0
- package/src/cdp/measure/check.ts +318 -0
- package/src/cdp/measure/collectors/animation.ts +385 -0
- package/src/cdp/measure/collectors/ax.ts +278 -0
- package/src/cdp/measure/collectors/focus.ts +1041 -0
- package/src/cdp/measure/collectors/forms.ts +905 -0
- package/src/cdp/measure/collectors/geometry.ts +1038 -0
- package/src/cdp/measure/collectors/hittest.ts +835 -0
- package/src/cdp/measure/collectors/layers.ts +697 -0
- package/src/cdp/measure/collectors/media.ts +522 -0
- package/src/cdp/measure/collectors/pixels.ts +1584 -0
- package/src/cdp/measure/collectors/queries.ts +464 -0
- package/src/cdp/measure/collectors/scroll.ts +599 -0
- package/src/cdp/measure/collectors/states.ts +992 -0
- package/src/cdp/measure/collectors/style-provenance.ts +643 -0
- package/src/cdp/measure/collectors/styles.ts +355 -0
- package/src/cdp/measure/collectors/text.ts +713 -0
- package/src/cdp/measure/diff.ts +470 -0
- package/src/cdp/measure/explain.ts +498 -0
- package/src/cdp/measure/map-ax.ts +633 -0
- package/src/cdp/measure/map-focus.ts +188 -0
- package/src/cdp/measure/map-layers.ts +177 -0
- package/src/cdp/measure/map-scroll.ts +317 -0
- package/src/cdp/measure/redaction.ts +51 -0
- package/src/cdp/measure/settle.ts +1051 -0
- package/src/cdp/measure/snapshot.ts +390 -0
- package/src/cdp/measure/sweep.ts +330 -0
- package/src/cdp/measure/types.ts +220 -0
- package/src/cdp/measure/v2/collectors.ts +19 -0
- package/src/cdp/measure/v2/orchestrator.ts +46 -0
- package/src/cdp/measure/v2/snap-selection.ts +33 -0
- package/src/cdp/measure/v2/target-lock.ts +15 -0
- package/src/cdp/measure/v2/target.ts +16 -0
- package/src/cdp/motion/jank.ts +433 -0
- package/src/cdp/motion/mask.ts +421 -0
- package/src/cdp/motion/recorder.ts +764 -0
- package/src/cdp/motion/response.ts +356 -0
- package/src/cdp/motion/timeline.ts +215 -0
- package/src/cdp/record.ts +5 -212
- package/src/cdp/recorder-bridge.ts +1478 -105
- package/src/cdp/recorder-client.ts +326 -0
- package/src/cdp/scope-lock.ts +73 -0
- package/src/cdp/screenshot.ts +199 -66
- package/src/cdp/targets.ts +21 -1
- package/src/cdp/timing.ts +15 -10
- package/src/cdp/types.ts +22 -7
- package/src/cdp/viewport.ts +21 -0
- package/src/cdp.ts +12 -58
- package/src/command-help.ts +29 -0
- package/src/command-resolution.ts +22 -0
- package/src/command-walker.ts +35 -0
- package/src/contracts/a11y.ts +33 -0
- package/src/contracts/cursor.ts +35 -0
- package/src/contracts/index.ts +15 -0
- package/src/contracts/motion.ts +19 -0
- package/src/contracts/neutral.ts +56 -0
- package/src/contracts/primitives.ts +205 -0
- package/src/contracts/release.ts +17 -0
- package/src/contracts/results.ts +208 -0
- package/src/contracts/routes.ts +140 -0
- package/src/contracts/snapshot.ts +92 -0
- package/src/cursor/index.ts +13 -0
- package/src/cursor/service.ts +310 -0
- package/src/errors.ts +71 -0
- package/src/har-manager.ts +992 -41
- package/src/interact.ts +410 -98
- package/src/output/artifact-lifecycle.ts +371 -0
- package/src/output/artifact.ts +6 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +396 -2
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +452 -200
- package/src/session/commands.ts +1162 -397
- package/src/session/coordinator.ts +251 -0
- package/src/session/har-filter.ts +57 -0
- package/src/session/log-tailer.ts +653 -0
- package/src/session-context.ts +279 -51
- package/src/vault/bundle.ts +32 -17
- package/src/cdp/commands/exec.ts +0 -105
- package/src/cdp/commands/har.ts +0 -128
- package/src/cdp/commands/tabs.ts +0 -183
- package/src/cdp/commands/traffic.ts +0 -121
- package/src/cdp/commands/ui.ts +0 -172
- package/src/cdp/execute.ts +0 -113
- package/src/cdp/har-output.ts +0 -47
- package/src/cdp/usage.ts +0 -79
- package/src/tab-session.ts +0 -91
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouton-kit/capture",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.10",
|
|
4
4
|
"description": "Browser automation and UI validation via CDP — session-based screenshots, HAR, a11y, click/type, and JS exec",
|
|
5
5
|
"bin": {
|
|
6
6
|
"capture": "./bin/capture"
|
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
"src"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "
|
|
13
|
+
"build": "node scripts/build.mjs",
|
|
14
|
+
"check:bin": "node scripts/build.mjs --check",
|
|
14
15
|
"dev": "tsx src/capture.ts",
|
|
15
|
-
"test": "node
|
|
16
|
+
"test": "node scripts/test.mjs",
|
|
17
|
+
"test:live": "node scripts/test.mjs --live",
|
|
18
|
+
"test:install-chrome": "npx --yes @puppeteer/browsers install chrome-headless-shell@143.0.7499.40"
|
|
16
19
|
},
|
|
17
20
|
"repository": {
|
|
18
21
|
"type": "git",
|
package/src/capture.ts
CHANGED
|
@@ -1,190 +1,103 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* capture — browser automation and UI measurement over CDP.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* CDP commands (top-level):
|
|
12
|
-
* capture detect Detect CDP port
|
|
13
|
-
* capture exec <code> Execute JS in a tab
|
|
14
|
-
* capture list List browser tabs
|
|
15
|
-
* capture open <url> Open URL in browser
|
|
16
|
-
* capture screenshot Capture screenshot
|
|
17
|
-
* capture a11y Get accessibility tree
|
|
18
|
-
* capture record Passive HAR recording
|
|
19
|
-
* capture navigate <url> Navigate + record HAR
|
|
20
|
-
* capture har create|read|delete Manage HAR recordings
|
|
4
|
+
* Root router: seven visible roots (session, page, tab, measure, motion,
|
|
5
|
+
* cdp, lib) plus the hidden `__bridge-serve` and `__log-tail-serve`
|
|
6
|
+
* internals, all dispatched below. Root help is assembled from each branch's exported
|
|
7
|
+
* `COMMAND_BLOCK` — the parent walks its children, it never hardcodes a
|
|
8
|
+
* child's description. An unknown first token is a structured
|
|
9
|
+
* `<error code="unknown_command">` (exit 1), never help text.
|
|
21
10
|
*/
|
|
22
11
|
|
|
23
|
-
import * as fs from
|
|
24
|
-
import * as path from
|
|
25
|
-
import { cdpMain } from
|
|
26
|
-
import {
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { cdpMain } from './cdp.js';
|
|
15
|
+
import { captureError, failureResult } from './errors.js';
|
|
16
|
+
import { emitResult } from './output/render.js';
|
|
17
|
+
import { COMMAND_BLOCK as SESSION_BLOCK } from './session/commands.js';
|
|
18
|
+
import { COMMAND_BLOCK as PAGE_BLOCK } from './cdp/commands/page/index.js';
|
|
19
|
+
import { COMMAND_BLOCK as TAB_BLOCK } from './cdp/commands/tab/index.js';
|
|
20
|
+
import { COMMAND_BLOCK as MEASURE_BLOCK } from './cdp/commands/measure/index.js';
|
|
21
|
+
import { COMMAND_BLOCK as MOTION_BLOCK } from './cdp/commands/motion/index.js';
|
|
22
|
+
import { COMMAND_BLOCK as CDP_BLOCK } from './cdp/commands/cdp.js';
|
|
23
|
+
import { COMMAND_BLOCK as LIB_BLOCK } from './cdp/commands/lib.js';
|
|
24
|
+
|
|
25
|
+
/** The seven visible root children, in help order. */
|
|
26
|
+
const ROOTS = ['session', 'page', 'tab', 'measure', 'motion', 'cdp', 'lib'] as const;
|
|
27
|
+
|
|
28
|
+
const ROOT_BLOCKS = [
|
|
29
|
+
SESSION_BLOCK,
|
|
30
|
+
PAGE_BLOCK,
|
|
31
|
+
TAB_BLOCK,
|
|
32
|
+
MEASURE_BLOCK,
|
|
33
|
+
MOTION_BLOCK,
|
|
34
|
+
CDP_BLOCK,
|
|
35
|
+
LIB_BLOCK,
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
function rootHelp(): string {
|
|
39
|
+
return `capture — browser automation and UI measurement over CDP.
|
|
40
|
+
|
|
41
|
+
${ROOT_BLOCKS.join('\n\n')}
|
|
42
|
+
|
|
43
|
+
I/O contract: flags and positionals on input; one rendered prose block on
|
|
44
|
+
stdout. --json mirrors the same result as JSON, but the rendered block is
|
|
45
|
+
the contract. stderr carries in-flight diagnostics only.
|
|
46
|
+
|
|
47
|
+
A CDP-enabled browser must be running; \`capture tab list\` is the probe.
|
|
48
|
+
CDP_PORT / CDP_TARGET pin the browser + tab for orchestrators — precedence:
|
|
49
|
+
explicit flag > active session > env.`;
|
|
50
|
+
}
|
|
27
51
|
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
//
|
|
52
|
+
function printVersion(): void {
|
|
53
|
+
// Version is injected at build time via esbuild's --define flag.
|
|
54
|
+
// Falls back to reading package.json at runtime if not injected.
|
|
55
|
+
const declared = (globalThis as { __CAPTURE_VERSION__?: string }).__CAPTURE_VERSION__;
|
|
56
|
+
if (declared) {
|
|
57
|
+
console.log(declared);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const pkgPath = path.resolve(__dirname, '..', 'package.json');
|
|
62
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string };
|
|
63
|
+
console.log(pkg.version);
|
|
64
|
+
} catch {
|
|
65
|
+
console.log('unknown');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
31
68
|
|
|
32
69
|
async function main(): Promise<void> {
|
|
33
|
-
const [command
|
|
70
|
+
const [command] = process.argv.slice(2);
|
|
34
71
|
|
|
35
|
-
if (command ===
|
|
36
|
-
|
|
37
|
-
// Falls back to reading package.json at runtime if not injected.
|
|
38
|
-
const declared = (globalThis as { __CAPTURE_VERSION__?: string }).__CAPTURE_VERSION__;
|
|
39
|
-
if (declared) {
|
|
40
|
-
console.log(declared);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
const pkgPath = path.resolve(__dirname, "..", "package.json");
|
|
45
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { version: string };
|
|
46
|
-
console.log(pkg.version);
|
|
47
|
-
} catch {
|
|
48
|
-
console.log("unknown");
|
|
49
|
-
}
|
|
72
|
+
if (command === '--version') {
|
|
73
|
+
printVersion();
|
|
50
74
|
return;
|
|
51
75
|
}
|
|
52
76
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
case "log":
|
|
58
|
-
return logCommand(args);
|
|
59
|
-
|
|
60
|
-
// CDP commands — delegate to cdp.ts
|
|
61
|
-
case "detect":
|
|
62
|
-
case "exec":
|
|
63
|
-
case "open":
|
|
64
|
-
case "reset-tab":
|
|
65
|
-
case "screenshot":
|
|
66
|
-
case "click":
|
|
67
|
-
case "type":
|
|
68
|
-
case "a11y":
|
|
69
|
-
case "record":
|
|
70
|
-
case "navigate":
|
|
71
|
-
case "har":
|
|
72
|
-
case "lib":
|
|
73
|
-
case "network":
|
|
74
|
-
case "list":
|
|
75
|
-
case "cdp":
|
|
76
|
-
case "measure":
|
|
77
|
-
case "motion":
|
|
78
|
-
case "__bridge-serve":
|
|
79
|
-
return cdpMain();
|
|
80
|
-
|
|
81
|
-
default:
|
|
82
|
-
console.log(`Capture — browser automation over CDP for validating UI features.
|
|
83
|
-
|
|
84
|
-
Most tasks start with a session. Once a session is active, every command
|
|
85
|
-
auto-fills --target (the tab) and --har (the recording) — don't thread
|
|
86
|
-
those flags yourself.
|
|
87
|
-
|
|
88
|
-
TYPICAL WORKFLOW
|
|
89
|
-
|
|
90
|
-
1. Start a session (opens tab, starts HAR, marks it active):
|
|
91
|
-
capture session start --url http://localhost:3000
|
|
92
|
-
|
|
93
|
-
2. Interact — no --target / --har needed:
|
|
94
|
-
capture a11y --interactive See what's on the page
|
|
95
|
-
capture click "Sign in" Click by accessible name (auto-screenshots)
|
|
96
|
-
capture type "hi@me.com" --into "Email"
|
|
97
|
-
capture screenshot Save current state
|
|
98
|
-
capture navigate https://... Navigate within the session
|
|
99
|
-
capture exec "document.title" Run JS (expressions, return, await)
|
|
100
|
-
capture har read --filter-url /api Inspect recorded traffic
|
|
101
|
-
|
|
102
|
-
3. Bundle and inspect:
|
|
103
|
-
capture session stop <session-id>
|
|
104
|
-
capture session view <session-id>
|
|
105
|
-
|
|
106
|
-
SESSION COMMANDS
|
|
107
|
-
|
|
108
|
-
session start [--url <url>] Start a session
|
|
109
|
-
session stop <session-id> Finalize and bundle artifacts
|
|
110
|
-
session list List active and stopped sessions
|
|
111
|
-
session view <id> [--filter section] section = screenshots|har|a11y|logs
|
|
112
|
-
log <path> [--name label] Tail a log file into the active session
|
|
113
|
-
|
|
114
|
-
INTERACTION COMMANDS (work inside or outside a session)
|
|
115
|
-
|
|
116
|
-
a11y [--interactive] [--json] Accessibility tree (use this first to see elements)
|
|
117
|
-
click "name" [--role <role>] Click by accessible name
|
|
118
|
-
type "text" [--into "Field"] Type into focused element or named field
|
|
119
|
-
screenshot [--out <path>] [--full-page] Screenshot (viewport: desktop|desktop-wide|tablet|mobile)
|
|
120
|
-
exec <code> | exec --file <path> Evaluate JS; expressions, return, and await are supported
|
|
121
|
-
navigate <url> [--settle <ms>] Navigate the current tab + record HAR
|
|
122
|
-
|
|
123
|
-
MEASUREMENT & MOTION
|
|
124
|
-
|
|
125
|
-
measure snap|check|diff|census|explain|sweep|map Enriched snapshot substrate + read-only queries
|
|
126
|
-
motion rec|mask|timeline|jank|response Recorder lifecycle + read-only queries over a finalized recording
|
|
127
|
-
|
|
128
|
-
capture measure --help Full leaf reference (input constraints per leaf)
|
|
129
|
-
capture motion --help Full leaf reference (input constraints per leaf)
|
|
130
|
-
|
|
131
|
-
DIAGNOSTICS & ONE-OFFS (no session needed)
|
|
132
|
-
|
|
133
|
-
detect Find running CDP endpoints
|
|
134
|
-
list List open tabs across endpoints
|
|
135
|
-
open <url> [--new] Open a URL, return its tab id
|
|
136
|
-
reset-tab <url> Abandon a stuck tab, open fresh (updates session)
|
|
137
|
-
record [--duration <secs>] Passive HAR recording; parallel-safe capture
|
|
138
|
-
network <offline|online> Toggle connectivity for a tab
|
|
139
|
-
har create | read [id] | delete <id> Manage standalone HAR recordings
|
|
140
|
-
|
|
141
|
-
RAW CDP ESCAPE HATCH
|
|
142
|
-
|
|
143
|
-
cdp <Domain.method> [--params json] Send any CDP command capture doesn't wrap
|
|
144
|
-
[--browser] Target the browser connection (Browser.*, ServiceWorker.*)
|
|
145
|
-
[--wait-event name] Wait for a CDP event, e.g. after ServiceWorker.enable
|
|
146
|
-
|
|
147
|
-
Browser-level state (permission grants, ServiceWorker enablement) reverts
|
|
148
|
-
the instant its connection closes. session start --hold keeps ONE browser
|
|
149
|
-
connection open across commands so that state survives between them:
|
|
150
|
-
|
|
151
|
-
capture session start --url http://localhost:3000 --hold
|
|
152
|
-
capture cdp Browser.grantPermissions --browser --params '{"origin":"...","permissions":["notifications"]}'
|
|
153
|
-
capture cdp ServiceWorker.enable --browser --target <pageTabId>
|
|
154
|
-
capture cdp ServiceWorker.deliverPushMessage --browser --target <pageTabId> --params '{...}'
|
|
155
|
-
capture session stop <session-id>
|
|
156
|
-
|
|
157
|
-
capture cdp --help Full reference
|
|
158
|
-
|
|
159
|
-
LIBRARY (vault libs — dev checkout only)
|
|
160
|
-
|
|
161
|
-
lib list List available libs
|
|
162
|
-
lib search "<query>" [--limit N] Fuzzy-search functions across libs
|
|
163
|
-
lib show <name> Lib + function summaries
|
|
164
|
-
lib read <name> [fn…] Full input/output schemas (+ .ts source path)
|
|
165
|
-
exec "import {fn} from 'libs/<name>'; return await fn({…})" Run a lib in the active tab
|
|
166
|
-
|
|
167
|
-
TARGETING (only when NOT in a session, or picking a parallel tab)
|
|
168
|
-
|
|
169
|
-
CDP_PORT / CDP_TARGET Orchestrators can pin the active browser + tab
|
|
170
|
-
--target <tabId> Exact id; a prefix of 8 chars is enough (preferred, parallel-safe)
|
|
171
|
-
--url <pattern> Fuzzy URL match against open tabs
|
|
172
|
-
NOTE: on \`session start\`, --url is a URL to OPEN, not a pattern.
|
|
173
|
-
|
|
174
|
-
HELP
|
|
77
|
+
if (command === undefined || command === '-h') {
|
|
78
|
+
console.log(rootHelp());
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
175
81
|
|
|
176
|
-
|
|
177
|
-
|
|
82
|
+
if (command === '__log-tail-serve') {
|
|
83
|
+
// Internal self-spawn target for `session log`'s detached tailer worker.
|
|
84
|
+
// Not a visible root: absent from ROOTS, help, and every COMMAND_BLOCK.
|
|
85
|
+
const { runLogTailer } = await import('./session/log-tailer.js');
|
|
86
|
+
return runLogTailer(process.argv.slice(3));
|
|
87
|
+
}
|
|
178
88
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
Chrome / Chromium --remote-debugging-port=9222
|
|
182
|
-
Electron apps CDP exposed automatically
|
|
183
|
-
Verify with: capture detect`);
|
|
89
|
+
if ((ROOTS as readonly string[]).includes(command) || command === '__bridge-serve') {
|
|
90
|
+
return cdpMain();
|
|
184
91
|
}
|
|
92
|
+
|
|
93
|
+
throw captureError(
|
|
94
|
+
'invocation',
|
|
95
|
+
'unknown_command',
|
|
96
|
+
`Unknown command ${command}; expected one of the seven roots: session, page, tab, measure, motion, cdp, lib.`,
|
|
97
|
+
);
|
|
185
98
|
}
|
|
186
99
|
|
|
187
|
-
main().catch((
|
|
188
|
-
|
|
189
|
-
process.
|
|
100
|
+
main().catch((error) => {
|
|
101
|
+
emitResult(failureResult(error), { json: process.argv.includes('--json') });
|
|
102
|
+
process.exitCode = 1;
|
|
190
103
|
});
|
package/src/cdp/a11y.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { captureError } from '../errors.js';
|
|
2
|
+
import { withScopeSerialization } from './scope-lock.js';
|
|
2
3
|
|
|
3
4
|
export const INTERACTIVE_ROLES = new Set([
|
|
4
5
|
'button',
|
|
@@ -49,12 +50,18 @@ export function flattenA11yTree(
|
|
|
49
50
|
return lines;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
interface
|
|
53
|
+
export interface AccessibilityClient {
|
|
54
|
+
send(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The fields consumed from a node returned by `Accessibility.getFullAXTree`. */
|
|
58
|
+
export interface FullAXNode {
|
|
53
59
|
nodeId: string;
|
|
60
|
+
backendDOMNodeId?: number;
|
|
61
|
+
ignored?: boolean;
|
|
54
62
|
role?: { value: string };
|
|
55
63
|
name?: { value: string };
|
|
56
64
|
childIds?: string[];
|
|
57
|
-
properties?: Array<{ name: string; value: { value: unknown } }>;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
export interface A11yNode {
|
|
@@ -63,23 +70,115 @@ export interface A11yNode {
|
|
|
63
70
|
children: A11yNode[];
|
|
64
71
|
}
|
|
65
72
|
|
|
73
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
74
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isAxStringValue(value: unknown): value is { value: string } {
|
|
78
|
+
return isRecord(value) && typeof value.value === 'string';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isFullAXNode(value: unknown): value is FullAXNode {
|
|
82
|
+
if (!isRecord(value) || typeof value.nodeId !== 'string') return false;
|
|
83
|
+
if (
|
|
84
|
+
value.backendDOMNodeId !== undefined &&
|
|
85
|
+
(!Number.isSafeInteger(value.backendDOMNodeId) || (value.backendDOMNodeId as number) < 0)
|
|
86
|
+
) return false;
|
|
87
|
+
if (value.ignored !== undefined && typeof value.ignored !== 'boolean') return false;
|
|
88
|
+
if (value.role !== undefined && !isAxStringValue(value.role)) return false;
|
|
89
|
+
if (value.name !== undefined && !isAxStringValue(value.name)) return false;
|
|
90
|
+
if (
|
|
91
|
+
value.childIds !== undefined &&
|
|
92
|
+
(!Array.isArray(value.childIds) || !value.childIds.every((id) => typeof id === 'string'))
|
|
93
|
+
) return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reads one complete AX tree with call-scoped Accessibility ownership.
|
|
99
|
+
*
|
|
100
|
+
* The Accessibility domain's enabled state lives on the connection: a direct
|
|
101
|
+
* connection's state dies with its own websocket session, but the
|
|
102
|
+
* recorder-held connection is shared across commands (its bridge handles
|
|
103
|
+
* each CDP request independently — it serializes nothing), so the entire
|
|
104
|
+
* enable→read→disable scope serializes under the owning session's
|
|
105
|
+
* `.ax-scope.lock` (`withScopeSerialization`) when the client is the
|
|
106
|
+
* recorder-held adapter — otherwise a concurrent routed caller could
|
|
107
|
+
* disable this read's domain mid-flight.
|
|
108
|
+
*/
|
|
109
|
+
export async function readFullAXTree(client: AccessibilityClient): Promise<FullAXNode[]> {
|
|
110
|
+
return withScopeSerialization(client, 'ax', 'full AX tree read', () => axScopedRead(client));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The one Accessibility state transaction: enable → read → disable.
|
|
115
|
+
* Claiming ownership before awaiting `Accessibility.enable` matters because a
|
|
116
|
+
* lost response does not prove the browser rejected the enable request. The
|
|
117
|
+
* matching disable therefore always runs, without touching the DOM domain.
|
|
118
|
+
* A disable failure prevents success — alone it throws a typed cleanup
|
|
119
|
+
* failure; paired with a primary failure it throws an `AggregateError`
|
|
120
|
+
* preserving both facts.
|
|
121
|
+
*/
|
|
122
|
+
async function axScopedRead(client: AccessibilityClient): Promise<FullAXNode[]> {
|
|
123
|
+
let primaryFailed = false;
|
|
124
|
+
let primaryError: unknown;
|
|
125
|
+
let ownsAccessibility = false;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
ownsAccessibility = true;
|
|
129
|
+
await client.send('Accessibility.enable');
|
|
130
|
+
const response = await client.send('Accessibility.getFullAXTree');
|
|
131
|
+
const nodes = isRecord(response) ? response.nodes : undefined;
|
|
132
|
+
if (!Array.isArray(nodes) || !nodes.every(isFullAXNode)) {
|
|
133
|
+
throw captureError(
|
|
134
|
+
'world',
|
|
135
|
+
'malformed_protocol',
|
|
136
|
+
'Accessibility.getFullAXTree returned a malformed nodes payload.',
|
|
137
|
+
{ method: 'Accessibility.getFullAXTree', response },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return nodes;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
primaryFailed = true;
|
|
143
|
+
primaryError = error;
|
|
144
|
+
throw error;
|
|
145
|
+
} finally {
|
|
146
|
+
if (ownsAccessibility) {
|
|
147
|
+
try {
|
|
148
|
+
await client.send('Accessibility.disable');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const cleanupError = captureError(
|
|
151
|
+
'cleanup',
|
|
152
|
+
'accessibility_cleanup_failed',
|
|
153
|
+
'Accessibility.disable failed after a full AX tree read.',
|
|
154
|
+
error,
|
|
155
|
+
);
|
|
156
|
+
if (primaryFailed) {
|
|
157
|
+
throw new AggregateError(
|
|
158
|
+
[primaryError, cleanupError],
|
|
159
|
+
'The full AX tree read failed and Accessibility cleanup also failed.',
|
|
160
|
+
{ cause: primaryError },
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
throw cleanupError;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
66
169
|
export async function getAccessibilityTree(
|
|
67
|
-
client:
|
|
170
|
+
client: AccessibilityClient,
|
|
68
171
|
): Promise<A11yNode[]> {
|
|
69
|
-
await client
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await client.send('Accessibility.disable');
|
|
74
|
-
|
|
75
|
-
const nodeMap = new Map<string, AXNode>();
|
|
76
|
-
for (const node of result.nodes) {
|
|
172
|
+
const nodes = await readFullAXTree(client);
|
|
173
|
+
|
|
174
|
+
const nodeMap = new Map<string, FullAXNode>();
|
|
175
|
+
for (const node of nodes) {
|
|
77
176
|
nodeMap.set(node.nodeId, node);
|
|
78
177
|
}
|
|
79
178
|
|
|
80
179
|
const SKIP_ROLES = new Set(['none', 'generic', 'InlineTextBox']);
|
|
81
180
|
|
|
82
|
-
function buildNode(node:
|
|
181
|
+
function buildNode(node: FullAXNode): A11yNode[] {
|
|
83
182
|
const role = node.role?.value ?? '';
|
|
84
183
|
const childNodes = (node.childIds ?? []).flatMap((id) => {
|
|
85
184
|
const child = nodeMap.get(id);
|
|
@@ -99,6 +198,6 @@ export async function getAccessibilityTree(
|
|
|
99
198
|
];
|
|
100
199
|
}
|
|
101
200
|
|
|
102
|
-
const root =
|
|
201
|
+
const root = nodes[0];
|
|
103
202
|
return root ? buildNode(root) : [];
|
|
104
203
|
}
|