@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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `page exec <code> | --file <path>` — run arbitrary JavaScript in the tab.
|
|
3
|
+
*
|
|
4
|
+
* A bare expression evaluates directly; a statement body may use top-level
|
|
5
|
+
* `return`/`await` (wrapped in an async IIFE by `buildExecExpression`);
|
|
6
|
+
* leading static imports bundle the forked vault libs before any CDP
|
|
7
|
+
* connection is opened (fail fast). The JSON-serialized return value is
|
|
8
|
+
* rendered inline in the `<exec-result>` block under a generous cap; a
|
|
9
|
+
* result exceeding the cap is additionally written WHOLE to a private
|
|
10
|
+
* artifact file — the active session's `page/` dir when a session exists,
|
|
11
|
+
* else a one-shot artifact dir (`createOneshotSession('page')`), never a
|
|
12
|
+
* loose `/tmp` file — and the block carries that file's absolute path.
|
|
13
|
+
* `--json` mirrors the block with the value at full fidelity. During a live
|
|
14
|
+
* composed recording the CDP routes through the recorder socket unmarked
|
|
15
|
+
* (`Runtime.evaluate` is never a landmark).
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { type ParsedArgs } from '../../types.js';
|
|
20
|
+
import { withPageAction, type SettleFacts } from '../../connection.js';
|
|
21
|
+
import { CaptureError, captureError } from '../../../errors.js';
|
|
22
|
+
import { buildExecExpression } from '../../exec-expression.js';
|
|
23
|
+
import { getActiveSession } from '../../../session-context.js';
|
|
24
|
+
import { createOneshotSession } from '../../../session/commands.js';
|
|
25
|
+
import { writePrivateFile } from '../../../session/artifacts.js';
|
|
26
|
+
import { withScopeSerialization } from '../../scope-lock.js';
|
|
27
|
+
import { isRecorderHeldClient } from '../../recorder-client.js';
|
|
28
|
+
import { hasImports, bundleExec } from '../../../vault/bundle.js';
|
|
29
|
+
import {
|
|
30
|
+
capped,
|
|
31
|
+
data,
|
|
32
|
+
emitResult,
|
|
33
|
+
fact,
|
|
34
|
+
line,
|
|
35
|
+
text,
|
|
36
|
+
type FactLine,
|
|
37
|
+
} from '../../../output/render.js';
|
|
38
|
+
|
|
39
|
+
/** Generous inline cap for the serialized result value: big enough for a
|
|
40
|
+
* typical scrape/query result, still bounded in the prose block. `--json`
|
|
41
|
+
* carries the value uncapped (full fidelity); a result larger than this is
|
|
42
|
+
* also spilled whole to an artifact file. */
|
|
43
|
+
const GENEROUS_RESULT_CAP = 4000;
|
|
44
|
+
|
|
45
|
+
const DEFAULT_SETTLE_MS = 3000;
|
|
46
|
+
|
|
47
|
+
const USAGE = `capture page exec <code> | --file <path> — run arbitrary JavaScript in the tab: a bare expression evaluates directly; a statement body may use top-level return/await (wrapped in an async IIFE); leading static imports bundle the forked vault libs (dev checkout only)
|
|
48
|
+
|
|
49
|
+
input:
|
|
50
|
+
<code> JS source as one argument (exactly one of <code> / --file)
|
|
51
|
+
--file <path> read the JS source from a file instead of inline
|
|
52
|
+
--settle <ms> network-settle window after execution so an active session's HAR captures the requests the code triggers (default: ${DEFAULT_SETTLE_MS}; 0 disables); the block reports the measured requested/waited settle
|
|
53
|
+
--target <id> target a tab explicitly (default: the active session tab; with no active session, --target or --url is required)
|
|
54
|
+
--url <pattern> target the first tab whose URL matches <pattern>
|
|
55
|
+
output:
|
|
56
|
+
<exec-result result-chars=…> — the JSON-serialized return value inline, escaped and capped at ${GENEROUS_RESULT_CAP} chars; a larger result is also written whole to a private artifact file (the active session's page/ dir, else a one-shot artifact dir) whose absolute path appears in the block. --json mirrors the same block with the value at full fidelity.
|
|
57
|
+
effects:
|
|
58
|
+
runs the code in the page with full DOM/JS access — whatever the code mutates, it mutates; emulates focus on the tab for the duration of the call (disabled again before the command returns); writes the oversize-result artifact file when the inline cap is exceeded`;
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Test-injectable dependency seam (repo CDP-stub pattern — see
|
|
62
|
+
// `test/page-exec.test.ts`).
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export interface PageExecDeps {
|
|
66
|
+
withPageAction: typeof withPageAction;
|
|
67
|
+
getActiveSession: typeof getActiveSession;
|
|
68
|
+
createOneshotSession: typeof createOneshotSession;
|
|
69
|
+
isRecorderHeldClient: typeof isRecorderHeldClient;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let deps: PageExecDeps = { withPageAction, getActiveSession, createOneshotSession, isRecorderHeldClient };
|
|
73
|
+
|
|
74
|
+
/** Swap the connection/session seams for the CDP-stub tests. */
|
|
75
|
+
export function __setPageExecDepsForTest(overrides: Partial<PageExecDeps>): () => void {
|
|
76
|
+
const previous = deps;
|
|
77
|
+
deps = { ...deps, ...overrides };
|
|
78
|
+
return () => { deps = previous; };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function emitExecError(parsed: ParsedArgs, code: string, summary: FactLine, followUp?: FactLine): void {
|
|
82
|
+
emitResult(
|
|
83
|
+
{ tag: 'error', attrs: { command: 'page exec', code }, summary, followUp },
|
|
84
|
+
{ json: parsed.json },
|
|
85
|
+
);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Writes an over-cap result whole to a private (0600) artifact file: into
|
|
91
|
+
* the active session's `page/` dir when a session exists, else into a fresh
|
|
92
|
+
* one-shot artifact dir. Returns the absolute file path.
|
|
93
|
+
*/
|
|
94
|
+
function spillWholeResult(payload: string): string {
|
|
95
|
+
const name = `exec-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`;
|
|
96
|
+
const session = deps.getActiveSession();
|
|
97
|
+
const filePath = session
|
|
98
|
+
? path.join(session.dir, 'page', name)
|
|
99
|
+
: path.join(deps.createOneshotSession('page').artifactsDir, name);
|
|
100
|
+
writePrivateFile(filePath, payload);
|
|
101
|
+
return filePath;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface EvalResult {
|
|
105
|
+
result?: { value?: unknown };
|
|
106
|
+
exceptionDetails?: { exception?: { description?: string } };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The structural client surface the focus/evaluation scope drives — the
|
|
110
|
+
* subset of `CDPClient` both a direct connection and the recorder-held
|
|
111
|
+
* adapter satisfy. */
|
|
112
|
+
interface ExecClient {
|
|
113
|
+
send(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The one focus-emulation state transaction: focus true → evaluate → focus
|
|
118
|
+
* false. A lost enable response does not prove Chrome rejected the override,
|
|
119
|
+
* so restoration ownership is claimed BEFORE the enable is awaited and the
|
|
120
|
+
* matching disable is always sent exactly once in `finally`. A disable
|
|
121
|
+
* failure prevents success: alone it throws a typed cleanup failure; paired
|
|
122
|
+
* with a primary failure it throws an `AggregateError` preserving both.
|
|
123
|
+
*/
|
|
124
|
+
async function focusScopedEvaluate(client: ExecClient, expression: string): Promise<EvalResult> {
|
|
125
|
+
let ownsFocusOverride = false;
|
|
126
|
+
let primaryFailed = false;
|
|
127
|
+
let primaryError: unknown;
|
|
128
|
+
try {
|
|
129
|
+
// Emulate focus so network requests the code triggers aren't deferred
|
|
130
|
+
// by the browser, without foregrounding the tab.
|
|
131
|
+
ownsFocusOverride = true;
|
|
132
|
+
await client.send('Emulation.setFocusEmulationEnabled', { enabled: true });
|
|
133
|
+
return (await client.send('Runtime.evaluate', {
|
|
134
|
+
expression,
|
|
135
|
+
awaitPromise: true,
|
|
136
|
+
returnByValue: true,
|
|
137
|
+
})) as EvalResult;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
primaryFailed = true;
|
|
140
|
+
primaryError = error;
|
|
141
|
+
throw error;
|
|
142
|
+
} finally {
|
|
143
|
+
if (ownsFocusOverride) {
|
|
144
|
+
try {
|
|
145
|
+
await client.send('Emulation.setFocusEmulationEnabled', { enabled: false });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const cleanupError = captureError(
|
|
148
|
+
'cleanup',
|
|
149
|
+
'focus_cleanup_failed',
|
|
150
|
+
'Emulation.setFocusEmulationEnabled(false) failed after page exec — the tab may retain a focus-emulation override.',
|
|
151
|
+
error,
|
|
152
|
+
);
|
|
153
|
+
if (primaryFailed) {
|
|
154
|
+
throw new AggregateError(
|
|
155
|
+
[primaryError, cleanupError],
|
|
156
|
+
'page exec failed and focus-emulation cleanup also failed.',
|
|
157
|
+
{ cause: primaryError },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
throw cleanupError;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Runs the focus/evaluation scope, serialized when the connection is the
|
|
168
|
+
* recorder-held one. The recorder holds ONE persistent tab connection whose
|
|
169
|
+
* focus-emulation state outlives any single command, so two concurrent
|
|
170
|
+
* routed execs could otherwise clear one another's live override; the
|
|
171
|
+
* entire three-request scope therefore runs under the owning session's
|
|
172
|
+
* `.focus-scope.lock` via the shared scope-serialization authority
|
|
173
|
+
* (`../../scope-lock.js`), passing this module's injectable seams so the
|
|
174
|
+
* test-controlled deps govern the held/session branch. A direct connection
|
|
175
|
+
* needs no lock — its emulation state is scoped to its own websocket
|
|
176
|
+
* session, which closes with the command.
|
|
177
|
+
*/
|
|
178
|
+
async function evaluateWithFocusEmulation(client: ExecClient, expression: string): Promise<EvalResult> {
|
|
179
|
+
return withScopeSerialization(
|
|
180
|
+
client,
|
|
181
|
+
'focus',
|
|
182
|
+
'page exec',
|
|
183
|
+
() => focusScopedEvaluate(client, expression),
|
|
184
|
+
{ isRecorderHeldClient: deps.isRecorderHeldClient, getActiveSession: deps.getActiveSession },
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function cmdPageExec(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
189
|
+
if (parsed.help) {
|
|
190
|
+
console.log(USAGE);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let code: string;
|
|
195
|
+
if (parsed.file !== undefined) {
|
|
196
|
+
if (parsed.positional.length > 0) {
|
|
197
|
+
return emitExecError(
|
|
198
|
+
parsed,
|
|
199
|
+
'invalid_input',
|
|
200
|
+
text`received: both inline <code> and --file; expected exactly one source of JS to run.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
code = fs.readFileSync(parsed.file, 'utf-8');
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return emitExecError(
|
|
207
|
+
parsed,
|
|
208
|
+
'file_unreadable',
|
|
209
|
+
fact`received: --file ${parsed.file} which could not be read (${err instanceof Error ? err.message : String(err)}); expected: a readable JS source file.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
if (parsed.positional.length !== 1 || !parsed.positional[0]) {
|
|
214
|
+
return emitExecError(
|
|
215
|
+
parsed,
|
|
216
|
+
'invalid_input',
|
|
217
|
+
fact`received: ${parsed.positional.length} positional arguments; expected exactly one <code> argument (or --file <path>).`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
code = parsed.positional[0];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Import-driven exec: leading static imports bundle the forked vault libs
|
|
224
|
+
// on the fly (esbuild) BEFORE opening a tab — fail fast, no wasted CDP
|
|
225
|
+
// connection. Plain exec (no imports) skips this entirely.
|
|
226
|
+
let prebuilt: string | undefined;
|
|
227
|
+
if (hasImports(code)) {
|
|
228
|
+
try {
|
|
229
|
+
prebuilt = await bundleExec(code);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return emitExecError(
|
|
232
|
+
parsed,
|
|
233
|
+
'bundle_failed',
|
|
234
|
+
fact`vault import bundling failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const settle = parsed.settle ?? DEFAULT_SETTLE_MS;
|
|
240
|
+
|
|
241
|
+
// A prebuilt bundle is already a complete IIFE returning the user's
|
|
242
|
+
// promise; otherwise buildExecExpression wraps statement bodies. Pure
|
|
243
|
+
// string work — built before any connection is opened.
|
|
244
|
+
const expression = buildExecExpression(code, prebuilt);
|
|
245
|
+
|
|
246
|
+
let evalResult: EvalResult;
|
|
247
|
+
let settleFacts: SettleFacts;
|
|
248
|
+
try {
|
|
249
|
+
// connection.ts derives its per-invocation label from parsed.command,
|
|
250
|
+
// which the router leaves as the branch token 'page' — restore the verb
|
|
251
|
+
// so a routed exec is labeled `exec:…`. Runtime.evaluate is never a
|
|
252
|
+
// markable method, so the routed call stays unmarked in events.jsonl.
|
|
253
|
+
const outcome = await deps.withPageAction(
|
|
254
|
+
{ ...parsed, command: 'exec' },
|
|
255
|
+
{ settleMs: settle },
|
|
256
|
+
(client) => evaluateWithFocusEmulation(client, expression),
|
|
257
|
+
);
|
|
258
|
+
evalResult = outcome.result;
|
|
259
|
+
settleFacts = outcome.settle;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
// A typed failure (e.g. `recorder_unavailable` from strict A2 routing,
|
|
262
|
+
// `har_missing`, or a lone `focus_cleanup_failed`) crosses the boundary
|
|
263
|
+
// unrelabeled — only a genuinely untyped error is the exec-specific
|
|
264
|
+
// `exec_failed`.
|
|
265
|
+
if (err instanceof CaptureError) throw err;
|
|
266
|
+
if (err instanceof AggregateError && err.errors.length === 2) {
|
|
267
|
+
// The primary+disable dual failure — preserve both facts distinctly.
|
|
268
|
+
const message = (value: unknown) => (value instanceof Error ? value.message : String(value));
|
|
269
|
+
return emitExecError(
|
|
270
|
+
parsed,
|
|
271
|
+
'exec_failed',
|
|
272
|
+
fact`execution failed (${message(err.errors[0])}) and focus-emulation cleanup also failed (${message(err.errors[1])})`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return emitExecError(
|
|
276
|
+
parsed,
|
|
277
|
+
'exec_failed',
|
|
278
|
+
fact`execution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
279
|
+
text`Check a CDP-enabled browser is running and a tab is targetable (probe: capture tab list).`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (evalResult.exceptionDetails) {
|
|
284
|
+
const description = evalResult.exceptionDetails.exception?.description ?? 'Unknown error';
|
|
285
|
+
return emitExecError(
|
|
286
|
+
parsed,
|
|
287
|
+
'exec_exception',
|
|
288
|
+
line(text`the page threw during execution: `, data(capped(description, GENEROUS_RESULT_CAP))),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const payload = JSON.stringify(evalResult.result?.value) ?? 'undefined';
|
|
293
|
+
|
|
294
|
+
let spillPath: string | undefined;
|
|
295
|
+
if (payload.length > GENEROUS_RESULT_CAP) {
|
|
296
|
+
spillPath = spillWholeResult(payload);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const inlineCap = parsed.json ? payload.length : GENEROUS_RESULT_CAP;
|
|
300
|
+
const sections: FactLine[] = [line(text`result: `, data(capped(payload, inlineCap)))];
|
|
301
|
+
if (spillPath) {
|
|
302
|
+
sections.push(fact`full result (${payload.length} chars) written whole to ${spillPath}`);
|
|
303
|
+
}
|
|
304
|
+
sections.push(fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`);
|
|
305
|
+
|
|
306
|
+
emitResult(
|
|
307
|
+
{
|
|
308
|
+
tag: 'exec-result',
|
|
309
|
+
attrs: {
|
|
310
|
+
'result-chars': payload.length,
|
|
311
|
+
'result-file': spillPath,
|
|
312
|
+
},
|
|
313
|
+
sections,
|
|
314
|
+
},
|
|
315
|
+
{ json: parsed.json },
|
|
316
|
+
);
|
|
317
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `capture page` branch router — every verb against the live session tab:
|
|
3
|
+
* driving (click, type, scroll, navigate, exec) and looking (shot,
|
|
4
|
+
* elements).
|
|
5
|
+
*
|
|
6
|
+
* Shifts the consumed leaf token out of `parsed.positional` before handing
|
|
7
|
+
* off, so every leaf command still finds its own primary target (a target
|
|
8
|
+
* selector, a URL, code, ...) at `positional[0]` exactly as every other
|
|
9
|
+
* capture command does — leaf commands never need to know they're nested
|
|
10
|
+
* under a branch.
|
|
11
|
+
*/
|
|
12
|
+
import { invalidInput } from '../../../errors.js';
|
|
13
|
+
import { type ParsedArgs } from '../../types.js';
|
|
14
|
+
import { cmdPageClick } from './click.js';
|
|
15
|
+
import { cmdPageType } from './type.js';
|
|
16
|
+
import { cmdPageScroll } from './scroll.js';
|
|
17
|
+
import { cmdPageNavigate } from './navigate.js';
|
|
18
|
+
import { cmdPageExec } from './exec.js';
|
|
19
|
+
import { cmdPageShot } from './shot.js';
|
|
20
|
+
import { cmdPageElements } from './elements.js';
|
|
21
|
+
|
|
22
|
+
/** Root-help representation of this branch, assembled by `src/capture.ts`. */
|
|
23
|
+
export const COMMAND_BLOCK = `<command name="page">
|
|
24
|
+
verbs against the live session tab — act (click, type, scroll, navigate, exec) and look (shot, elements)
|
|
25
|
+
use when driving or inspecting the page a session opened; all verbs auto-target the active session tab, --target/--url override
|
|
26
|
+
click · type · scroll · navigate · exec · shot · elements — \`capture page -h\`
|
|
27
|
+
</command>`;
|
|
28
|
+
|
|
29
|
+
export const PAGE_USAGE = `capture page — verbs against the live session tab: driving, looking, targeting.
|
|
30
|
+
|
|
31
|
+
All verbs auto-target the active session tab; --target/--url override. The
|
|
32
|
+
driving verbs (click, type, scroll) resolve exactly one element via the
|
|
33
|
+
unified target grammar — bare CSS selector, ax:<name>, axid:<id>,
|
|
34
|
+
backend:<id> — and reject an ambiguous target with the candidate list.
|
|
35
|
+
During a live composed recording every verb routes through the recorder.
|
|
36
|
+
|
|
37
|
+
<subcommand name="click" args="<target> [--no-screenshot] [--settle <ms>]" whenToUse="dispatch a real click on one resolved element"/>
|
|
38
|
+
<subcommand name="type" args="<text> [--into <target>] [--no-screenshot] [--settle <ms>]" whenToUse="type text into the focused element or one resolved field"/>
|
|
39
|
+
<subcommand name="scroll" args="<target> --to <top|bottom|px> [--no-screenshot]" whenToUse="scroll one resolved container to a position"/>
|
|
40
|
+
<subcommand name="navigate" args="<url> [--settle <ms>]" whenToUse="navigate the tab and wait for load + settle"/>
|
|
41
|
+
<subcommand name="exec" args="<code> | --file <path>" whenToUse="run arbitrary JS in the tab (expressions, return, await)"/>
|
|
42
|
+
<subcommand name="shot" args="[--viewport <WxH>] [--full-page] [--out <path>]" whenToUse="look at the page right now without acting"/>
|
|
43
|
+
<subcommand name="elements" args="[--all] [--limit <n>]" whenToUse="list what can be acted on — role, name, backend:<id> per element"/>
|
|
44
|
+
|
|
45
|
+
capture page <leaf> -h Per-leaf usage`;
|
|
46
|
+
|
|
47
|
+
export async function pageMain(parsed: ParsedArgs, args: string[]): Promise<void> {
|
|
48
|
+
const leaf = parsed.positional[0];
|
|
49
|
+
const rest: ParsedArgs = { ...parsed, positional: parsed.positional.slice(1) };
|
|
50
|
+
|
|
51
|
+
switch (leaf) {
|
|
52
|
+
case 'click':
|
|
53
|
+
return cmdPageClick(rest, args);
|
|
54
|
+
case 'type':
|
|
55
|
+
return cmdPageType(rest, args);
|
|
56
|
+
case 'scroll':
|
|
57
|
+
return cmdPageScroll(rest, args);
|
|
58
|
+
case 'navigate':
|
|
59
|
+
return cmdPageNavigate(rest, args);
|
|
60
|
+
case 'exec':
|
|
61
|
+
return cmdPageExec(rest, args);
|
|
62
|
+
case 'shot':
|
|
63
|
+
return cmdPageShot(rest, args);
|
|
64
|
+
case 'elements':
|
|
65
|
+
return cmdPageElements(rest, args);
|
|
66
|
+
case undefined:
|
|
67
|
+
console.log(PAGE_USAGE);
|
|
68
|
+
return;
|
|
69
|
+
default:
|
|
70
|
+
throw invalidInput(`Unknown page leaf: ${leaf}.`, 'unknown_command');
|
|
71
|
+
}
|
|
72
|
+
}
|