@crouton-kit/capture 1.3.11 → 1.3.13
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 +57 -20
- package/package.json +1 -1
- package/src/cdp/record.ts +40 -14
- package/src/interact.ts +25 -2
- package/src/session/commands.ts +16 -3
package/bin/capture
CHANGED
|
@@ -8391,19 +8391,42 @@ async function navigateAndWait(port, url, options = {}) {
|
|
|
8391
8391
|
throw new Error("New tab has no WebSocket debugger URL");
|
|
8392
8392
|
}
|
|
8393
8393
|
const client = new CDPClient(tab.webSocketDebuggerUrl);
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8394
|
+
try {
|
|
8395
|
+
await client.waitReady();
|
|
8396
|
+
await client.send("Page.enable");
|
|
8397
|
+
let timer;
|
|
8398
|
+
let loadHandler;
|
|
8399
|
+
let loadTimeoutError;
|
|
8400
|
+
let resolveLoad;
|
|
8401
|
+
const loadPromise = new Promise((resolve11) => {
|
|
8402
|
+
resolveLoad = resolve11;
|
|
8403
|
+
loadHandler = () => {
|
|
8404
|
+
if (timer) clearTimeout(timer);
|
|
8405
|
+
resolveLoad();
|
|
8406
|
+
};
|
|
8407
|
+
client.on("Page.loadEventFired", loadHandler);
|
|
8408
|
+
timer = setTimeout(() => {
|
|
8409
|
+
loadTimeoutError = new Error(`Page load timeout: ${url}`);
|
|
8410
|
+
resolveLoad();
|
|
8411
|
+
}, timeout);
|
|
8404
8412
|
});
|
|
8405
|
-
|
|
8406
|
-
|
|
8413
|
+
try {
|
|
8414
|
+
const result = await client.send("Runtime.evaluate", {
|
|
8415
|
+
expression: "document.readyState",
|
|
8416
|
+
returnByValue: true
|
|
8417
|
+
});
|
|
8418
|
+
const readyState = result?.result?.value;
|
|
8419
|
+
if (readyState !== "complete") {
|
|
8420
|
+
await loadPromise;
|
|
8421
|
+
if (loadTimeoutError) throw loadTimeoutError;
|
|
8422
|
+
}
|
|
8423
|
+
} finally {
|
|
8424
|
+
if (timer) clearTimeout(timer);
|
|
8425
|
+
if (loadHandler) client.off("Page.loadEventFired", loadHandler);
|
|
8426
|
+
}
|
|
8427
|
+
} finally {
|
|
8428
|
+
client.close();
|
|
8429
|
+
}
|
|
8407
8430
|
const targets = await listTargets(port);
|
|
8408
8431
|
const refreshed = targets.find((t) => t.id === tab.id);
|
|
8409
8432
|
return refreshed ?? tab;
|
|
@@ -9799,7 +9822,7 @@ function validateHarAppendBatch(value, source) {
|
|
|
9799
9822
|
return { entries, incompleteLifecycles };
|
|
9800
9823
|
}
|
|
9801
9824
|
function captureVersion() {
|
|
9802
|
-
const version = "1.3.
|
|
9825
|
+
const version = "1.3.13";
|
|
9803
9826
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
9804
9827
|
}
|
|
9805
9828
|
function token() {
|
|
@@ -11558,7 +11581,7 @@ function wallTime(value) {
|
|
|
11558
11581
|
}
|
|
11559
11582
|
}
|
|
11560
11583
|
function captureVersion2() {
|
|
11561
|
-
const version = "1.3.
|
|
11584
|
+
const version = "1.3.13";
|
|
11562
11585
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
11563
11586
|
}
|
|
11564
11587
|
function deepFreeze(value) {
|
|
@@ -13819,9 +13842,9 @@ function readSession(id) {
|
|
|
13819
13842
|
function generateId() {
|
|
13820
13843
|
return `cap-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
13821
13844
|
}
|
|
13822
|
-
async function waitForPageLoad(client, timeoutMs) {
|
|
13845
|
+
async function waitForPageLoad(client, timeoutMs, attachTimeoutMs = SESSION_TAB_ATTACH_TIMEOUT_MS) {
|
|
13823
13846
|
await client.waitReady();
|
|
13824
|
-
await client.send("Page.enable");
|
|
13847
|
+
await client.send("Page.enable", {}, attachTimeoutMs);
|
|
13825
13848
|
return await new Promise((resolve11) => {
|
|
13826
13849
|
const timer = setTimeout(() => resolve11(true), timeoutMs);
|
|
13827
13850
|
const done = (timedOut) => {
|
|
@@ -14558,7 +14581,7 @@ async function sessionMain(parsed, _args) {
|
|
|
14558
14581
|
return;
|
|
14559
14582
|
}
|
|
14560
14583
|
}
|
|
14561
|
-
var fs9, path12, VIEW_FILTERS, SECTION_LABELS, COMMAND_BLOCK, START_USAGE, STOP_USAGE, LIST_USAGE, VIEW_USAGE, HAR_USAGE, LOG_USAGE, productionStartWorld, startWorld;
|
|
14584
|
+
var fs9, path12, VIEW_FILTERS, SECTION_LABELS, SESSION_TAB_ATTACH_TIMEOUT_MS, COMMAND_BLOCK, START_USAGE, STOP_USAGE, LIST_USAGE, VIEW_USAGE, HAR_USAGE, LOG_USAGE, productionStartWorld, startWorld;
|
|
14562
14585
|
var init_commands = __esm({
|
|
14563
14586
|
"src/session/commands.ts"() {
|
|
14564
14587
|
fs9 = __toESM(require("fs"));
|
|
@@ -14589,6 +14612,7 @@ var init_commands = __esm({
|
|
|
14589
14612
|
recs: "motion",
|
|
14590
14613
|
other: "other"
|
|
14591
14614
|
};
|
|
14615
|
+
SESSION_TAB_ATTACH_TIMEOUT_MS = 5e3;
|
|
14592
14616
|
COMMAND_BLOCK = `<command name="session">
|
|
14593
14617
|
the artifact container \u2014 a session opens a tab, records HAR, and bundles every artifact; while active, every command auto-targets its tab
|
|
14594
14618
|
use when starting scoped work against a page: start first, then every other capture command needs no --target/--port threading
|
|
@@ -14728,7 +14752,14 @@ effects:
|
|
|
14728
14752
|
} catch (err) {
|
|
14729
14753
|
throw worldFailure(`capture session start could not attach to ${url}: ${err instanceof Error ? err.message : err}. Reuse an existing tab with --target.`, err);
|
|
14730
14754
|
}
|
|
14731
|
-
|
|
14755
|
+
try {
|
|
14756
|
+
return await waitForPageLoad(client, 1e4);
|
|
14757
|
+
} catch (err) {
|
|
14758
|
+
throw worldFailure(
|
|
14759
|
+
`capture session start could not attach to ${url}: Page.enable failed during its ${SESSION_TAB_ATTACH_TIMEOUT_MS}ms attachment window (${err instanceof Error ? err.message : err}). The CDP endpoint created the tab but cannot drive it; choose another endpoint from \`capture tab list\` and retry with \`--port\`.`,
|
|
14760
|
+
err
|
|
14761
|
+
);
|
|
14762
|
+
}
|
|
14732
14763
|
} finally {
|
|
14733
14764
|
client.close();
|
|
14734
14765
|
}
|
|
@@ -15075,7 +15106,13 @@ async function resolveLiveTarget(client, input) {
|
|
|
15075
15106
|
return resolveLiveCss(client, input, parsed.value);
|
|
15076
15107
|
}
|
|
15077
15108
|
}
|
|
15078
|
-
function
|
|
15109
|
+
function preferActionable(matches) {
|
|
15110
|
+
if (matches.length <= 1) return matches;
|
|
15111
|
+
const interactive = matches.filter((m) => m.role?.value !== void 0 && INTERACTIVE_ROLES.has(m.role.value));
|
|
15112
|
+
return interactive.length === 1 ? interactive : matches;
|
|
15113
|
+
}
|
|
15114
|
+
function settleAxMatches(kind, input, rawMatches) {
|
|
15115
|
+
const matches = preferActionable(rawMatches);
|
|
15079
15116
|
if (matches.length === 1) {
|
|
15080
15117
|
const m = matches[0];
|
|
15081
15118
|
return {
|
|
@@ -241019,7 +241056,7 @@ CDP_PORT / CDP_TARGET pin the browser + tab for orchestrators \u2014 precedence:
|
|
|
241019
241056
|
explicit flag > active session > env.`;
|
|
241020
241057
|
}
|
|
241021
241058
|
function printVersion() {
|
|
241022
|
-
const declared = "1.3.
|
|
241059
|
+
const declared = "1.3.13";
|
|
241023
241060
|
if (declared) {
|
|
241024
241061
|
console.log(declared);
|
|
241025
241062
|
return;
|
package/package.json
CHANGED
package/src/cdp/record.ts
CHANGED
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
findTab,
|
|
4
4
|
listTargets,
|
|
5
5
|
openTab,
|
|
6
|
-
type CDPTarget,
|
|
7
6
|
} from './targets.js';
|
|
7
|
+
import { type CDPTarget } from './types.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Opens (or reuses) a tab for `url` and waits for its load event — `tab open`'s
|
|
@@ -32,23 +32,49 @@ export async function navigateAndWait(
|
|
|
32
32
|
throw new Error('New tab has no WebSocket debugger URL');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Wait for page load
|
|
35
|
+
// Wait for page load. The client owns a page WebSocket, so close it even
|
|
36
|
+
// when connection, readiness, timeout, or target-refresh work fails.
|
|
36
37
|
const client = new CDPClient(tab.webSocketDebuggerUrl);
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
try {
|
|
39
|
+
await client.waitReady();
|
|
40
|
+
await client.send('Page.enable');
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
let timer: NodeJS.Timeout | undefined;
|
|
43
|
+
let loadHandler: ((params: unknown) => void) | undefined;
|
|
44
|
+
let loadTimeoutError: Error | undefined;
|
|
45
|
+
let resolveLoad!: () => void;
|
|
46
|
+
const loadPromise = new Promise<void>((resolve) => {
|
|
47
|
+
resolveLoad = resolve;
|
|
48
|
+
loadHandler = () => {
|
|
49
|
+
if (timer) clearTimeout(timer);
|
|
50
|
+
resolveLoad();
|
|
51
|
+
};
|
|
52
|
+
// Arm the event before checking readyState: the load event may fire
|
|
53
|
+
// while the readiness query is in flight.
|
|
54
|
+
client.on('Page.loadEventFired', loadHandler);
|
|
55
|
+
timer = setTimeout(() => {
|
|
56
|
+
loadTimeoutError = new Error(`Page load timeout: ${url}`);
|
|
57
|
+
resolveLoad();
|
|
58
|
+
}, timeout);
|
|
48
59
|
});
|
|
49
|
-
});
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
try {
|
|
62
|
+
const result = await client.send('Runtime.evaluate', {
|
|
63
|
+
expression: 'document.readyState',
|
|
64
|
+
returnByValue: true,
|
|
65
|
+
});
|
|
66
|
+
const readyState = (result as { result?: { value?: unknown } } | undefined)?.result?.value;
|
|
67
|
+
if (readyState !== 'complete') {
|
|
68
|
+
await loadPromise;
|
|
69
|
+
if (loadTimeoutError) throw loadTimeoutError;
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
if (timer) clearTimeout(timer);
|
|
73
|
+
if (loadHandler) client.off('Page.loadEventFired', loadHandler);
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
client.close();
|
|
77
|
+
}
|
|
52
78
|
|
|
53
79
|
// Refresh target info — find by ID to avoid returning a different tab with same URL
|
|
54
80
|
const targets = await listTargets(port);
|
package/src/interact.ts
CHANGED
|
@@ -18,11 +18,19 @@
|
|
|
18
18
|
* nothing — command leaves own all output; returned facts carry the resolved
|
|
19
19
|
* identity (`backendNodeId`, role, name) and the dispatched coordinates.
|
|
20
20
|
*
|
|
21
|
+
* `ax:`/`axid:` tie-break: a same-name match set that mixes an
|
|
22
|
+
* {@link INTERACTIVE_ROLES} role (e.g. `button`) with non-interactive ones
|
|
23
|
+
* (e.g. `StaticText` — the common shape of a label rendering inside its own
|
|
24
|
+
* button) narrows to the interactive match when that leaves exactly one
|
|
25
|
+
* candidate, because that is the only element the caller can actually drive.
|
|
26
|
+
* A tie among multiple interactive roles (or zero) stays genuinely
|
|
27
|
+
* ambiguous — see {@link preferActionable}.
|
|
28
|
+
*
|
|
21
29
|
* Uses getFullAXTree instead of queryAXTree because queryAXTree hangs on
|
|
22
30
|
* complex pages (7000+ AX nodes) when given the DOM root nodeId.
|
|
23
31
|
*/
|
|
24
32
|
|
|
25
|
-
import { readFullAXTree, type FullAXNode } from './cdp/a11y.js';
|
|
33
|
+
import { INTERACTIVE_ROLES, readFullAXTree, type FullAXNode } from './cdp/a11y.js';
|
|
26
34
|
import { isScrollDestination } from './cdp/leaf-grammar.js';
|
|
27
35
|
import { captureError } from './errors.js';
|
|
28
36
|
import { parseSelectorInput, type SelectorInputKind } from './output/selector.js';
|
|
@@ -163,11 +171,26 @@ export async function resolveLiveTarget(
|
|
|
163
171
|
}
|
|
164
172
|
}
|
|
165
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Narrows a same-name match set to its lone {@link INTERACTIVE_ROLES} member
|
|
176
|
+
* when the set mixes interactive and non-interactive roles — e.g. a `button`
|
|
177
|
+
* plus the `StaticText` label rendered inside it, both carrying the same
|
|
178
|
+
* accessible name. Leaves `matches` untouched when zero or more than one
|
|
179
|
+
* interactive role is present, so a genuine tie (two buttons, or no
|
|
180
|
+
* interactive role at all) still surfaces every candidate.
|
|
181
|
+
*/
|
|
182
|
+
function preferActionable(matches: FullAXNode[]): FullAXNode[] {
|
|
183
|
+
if (matches.length <= 1) return matches;
|
|
184
|
+
const interactive = matches.filter((m) => m.role?.value !== undefined && INTERACTIVE_ROLES.has(m.role.value));
|
|
185
|
+
return interactive.length === 1 ? interactive : matches;
|
|
186
|
+
}
|
|
187
|
+
|
|
166
188
|
function settleAxMatches(
|
|
167
189
|
kind: LiveTargetKind,
|
|
168
190
|
input: string,
|
|
169
|
-
|
|
191
|
+
rawMatches: FullAXNode[],
|
|
170
192
|
): ResolvedTarget | ResolutionFailure {
|
|
193
|
+
const matches = preferActionable(rawMatches);
|
|
171
194
|
if (matches.length === 1) {
|
|
172
195
|
const m = matches[0];
|
|
173
196
|
return {
|
package/src/session/commands.ts
CHANGED
|
@@ -286,12 +286,18 @@ function generateId(): string {
|
|
|
286
286
|
return `cap-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/** A fresh target must acknowledge Page.enable promptly. Target.createTarget
|
|
290
|
+
* already succeeded; waiting a full CDP request timeout here only postpones a
|
|
291
|
+
* failed session start before it can publish or roll back its target. */
|
|
292
|
+
export const SESSION_TAB_ATTACH_TIMEOUT_MS = 5_000;
|
|
293
|
+
|
|
289
294
|
export async function waitForPageLoad(
|
|
290
|
-
client: { waitReady(): Promise<void>; send(method: string, params?: Record<string, unknown
|
|
295
|
+
client: { waitReady(): Promise<void>; send(method: string, params?: Record<string, unknown>, timeout?: number): Promise<unknown>; on(event: string, handler: (params: unknown) => void): void; },
|
|
291
296
|
timeoutMs: number,
|
|
297
|
+
attachTimeoutMs = SESSION_TAB_ATTACH_TIMEOUT_MS,
|
|
292
298
|
): Promise<boolean> {
|
|
293
299
|
await client.waitReady();
|
|
294
|
-
await client.send('Page.enable');
|
|
300
|
+
await client.send('Page.enable', {}, attachTimeoutMs);
|
|
295
301
|
|
|
296
302
|
return await new Promise<boolean>((resolve) => {
|
|
297
303
|
const timer = setTimeout(() => resolve(true), timeoutMs);
|
|
@@ -502,7 +508,14 @@ const productionStartWorld: SessionStartWorld = {
|
|
|
502
508
|
} catch (err) {
|
|
503
509
|
throw worldFailure(`capture session start could not attach to ${url}: ${err instanceof Error ? err.message : err}. Reuse an existing tab with --target.`, err);
|
|
504
510
|
}
|
|
505
|
-
|
|
511
|
+
try {
|
|
512
|
+
return await waitForPageLoad(client, 10_000);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
throw worldFailure(
|
|
515
|
+
`capture session start could not attach to ${url}: Page.enable failed during its ${SESSION_TAB_ATTACH_TIMEOUT_MS}ms attachment window (${err instanceof Error ? err.message : err}). The CDP endpoint created the tab but cannot drive it; choose another endpoint from \`capture tab list\` and retry with \`--port\`.`,
|
|
516
|
+
err,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
506
519
|
} finally {
|
|
507
520
|
client.close();
|
|
508
521
|
}
|