@crouton-kit/capture 1.3.20 → 1.3.21
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
CHANGED
|
@@ -10231,7 +10231,7 @@ function validateHarAppendBatch(value, source) {
|
|
|
10231
10231
|
return { entries, incompleteLifecycles };
|
|
10232
10232
|
}
|
|
10233
10233
|
function captureVersion() {
|
|
10234
|
-
const version = "1.3.
|
|
10234
|
+
const version = "1.3.21";
|
|
10235
10235
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
10236
10236
|
}
|
|
10237
10237
|
function token() {
|
|
@@ -11990,7 +11990,7 @@ function wallTime(value) {
|
|
|
11990
11990
|
}
|
|
11991
11991
|
}
|
|
11992
11992
|
function captureVersion2() {
|
|
11993
|
-
const version = "1.3.
|
|
11993
|
+
const version = "1.3.21";
|
|
11994
11994
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
11995
11995
|
}
|
|
11996
11996
|
function deepFreeze(value) {
|
|
@@ -15943,6 +15943,21 @@ var init_interact = __esm({
|
|
|
15943
15943
|
function pageInputDeps() {
|
|
15944
15944
|
return deps2;
|
|
15945
15945
|
}
|
|
15946
|
+
async function capturePageInputScreenshot(client, action, label2, noScreenshot) {
|
|
15947
|
+
try {
|
|
15948
|
+
return {
|
|
15949
|
+
screenshot: await deps2.autoScreenshot(client, action, label2, noScreenshot),
|
|
15950
|
+
screenshotWarning: null
|
|
15951
|
+
};
|
|
15952
|
+
} catch (error) {
|
|
15953
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15954
|
+
if (!/^CDP request timeout \(\d+ms\): Page\.captureScreenshot$/.test(message)) throw error;
|
|
15955
|
+
return {
|
|
15956
|
+
screenshot: null,
|
|
15957
|
+
screenshotWarning: `automatic screenshot timed out after the page action completed; no screenshot was saved (${message})`
|
|
15958
|
+
};
|
|
15959
|
+
}
|
|
15960
|
+
}
|
|
15946
15961
|
function effectiveSettle(parsed, defaults) {
|
|
15947
15962
|
if (parsed.settle !== void 0) return parsed.settle;
|
|
15948
15963
|
return deps2.getActiveSession() !== null ? defaults.session : defaults.standalone;
|
|
@@ -16024,19 +16039,20 @@ async function cmdPageClick(parsed, _args) {
|
|
|
16024
16039
|
const resolved = await resolveLiveTarget(live, target);
|
|
16025
16040
|
if (!resolved.ok) return { failure: resolved };
|
|
16026
16041
|
const dispatch2 = await clickResolved(live, resolved);
|
|
16027
|
-
const
|
|
16028
|
-
return { dispatch: dispatch2,
|
|
16042
|
+
const screenshotResult = await capturePageInputScreenshot(client, "click", target, parsed.noScreenshot);
|
|
16043
|
+
return { dispatch: dispatch2, ...screenshotResult };
|
|
16029
16044
|
}
|
|
16030
16045
|
);
|
|
16031
16046
|
if ("failure" in outcome) {
|
|
16032
16047
|
return emitResolutionError(parsed, "page click", outcome.failure);
|
|
16033
16048
|
}
|
|
16034
|
-
const { dispatch, screenshot } = outcome;
|
|
16049
|
+
const { dispatch, screenshot, screenshotWarning } = outcome;
|
|
16035
16050
|
const rows = [
|
|
16036
16051
|
fact`clicked ${dispatch.role ?? "unknown"} "${dispatch.name ?? ""}" (backend:${dispatch.backendNodeId}) at x=${dispatch.x} y=${dispatch.y}`,
|
|
16037
16052
|
fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`
|
|
16038
16053
|
];
|
|
16039
16054
|
if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
|
|
16055
|
+
if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
|
|
16040
16056
|
emitResult(
|
|
16041
16057
|
{
|
|
16042
16058
|
tag: "clicked",
|
|
@@ -16066,7 +16082,7 @@ input:
|
|
|
16066
16082
|
--settle <ms> network-settle window applied after the click (default: 1000; 2500 with an active session; 0 disables)
|
|
16067
16083
|
--no-screenshot skip the auto-screenshot
|
|
16068
16084
|
output:
|
|
16069
|
-
<clicked backend-node-id=\u2026 role=\u2026 name=\u2026> \u2014 resolved identity, dispatched coordinates, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
|
|
16085
|
+
<clicked backend-node-id=\u2026 role=\u2026 name=\u2026> \u2014 resolved identity, dispatched coordinates, the measured settle (requested/waited), and either the screenshot artifact path or a warning when only that follow-up capture timed out; --json mirrors the same fields
|
|
16070
16086
|
effects:
|
|
16071
16087
|
scrolls the target into view, then dispatches a real mouse press/release at its center; writes one screenshot into the active session's shots/ sequence unless --no-screenshot`;
|
|
16072
16088
|
}
|
|
@@ -16101,19 +16117,20 @@ async function cmdPageType(parsed, _args) {
|
|
|
16101
16117
|
} else {
|
|
16102
16118
|
await typeText(live, textArg);
|
|
16103
16119
|
}
|
|
16104
|
-
const
|
|
16105
|
-
return { dispatch: dispatch2,
|
|
16120
|
+
const screenshotResult = await capturePageInputScreenshot(client, "type", parsed.into ?? "focused element", parsed.noScreenshot);
|
|
16121
|
+
return { dispatch: dispatch2, ...screenshotResult };
|
|
16106
16122
|
}
|
|
16107
16123
|
);
|
|
16108
16124
|
if ("failure" in outcome) {
|
|
16109
16125
|
return emitResolutionError(parsed, "page type", outcome.failure);
|
|
16110
16126
|
}
|
|
16111
|
-
const { dispatch, screenshot } = outcome;
|
|
16127
|
+
const { dispatch, screenshot, screenshotWarning } = outcome;
|
|
16112
16128
|
const rows = [
|
|
16113
16129
|
dispatch ? fact`typed "${textArg}" into ${dispatch.role ?? "unknown"} "${dispatch.name ?? ""}" (backend:${dispatch.backendNodeId}), focus click at x=${dispatch.x} y=${dispatch.y}` : fact`typed "${textArg}" into the focused element`,
|
|
16114
16130
|
fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`
|
|
16115
16131
|
];
|
|
16116
16132
|
if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
|
|
16133
|
+
if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
|
|
16117
16134
|
emitResult(
|
|
16118
16135
|
{
|
|
16119
16136
|
tag: "typed",
|
|
@@ -16141,7 +16158,7 @@ input:
|
|
|
16141
16158
|
--settle <ms> network-settle window applied after typing (default: 500; 1500 with an active session; 0 disables)
|
|
16142
16159
|
--no-screenshot skip the auto-screenshot
|
|
16143
16160
|
output:
|
|
16144
|
-
<typed> \u2014 the typed text, the resolved field identity (backend-node-id, role, name) and focus-click coordinates when --into was given, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
|
|
16161
|
+
<typed> \u2014 the typed text, the resolved field identity (backend-node-id, role, name) and focus-click coordinates when --into was given, the measured settle (requested/waited), and either the screenshot artifact path or a warning when only that follow-up capture timed out; --json mirrors the same fields
|
|
16145
16162
|
effects:
|
|
16146
16163
|
dispatches a real focus click (when --into) followed by real text insertion; writes one screenshot into the active session's shots/ sequence unless --no-screenshot`;
|
|
16147
16164
|
}
|
|
@@ -16186,19 +16203,20 @@ async function cmdPageScroll(parsed, _args) {
|
|
|
16186
16203
|
const resolved = await resolveLiveTarget(live, target);
|
|
16187
16204
|
if (!resolved.ok) return { failure: resolved };
|
|
16188
16205
|
const dispatch2 = await scrollResolved(live, resolved, to, { mark: `scroll:${target},to=${to}` });
|
|
16189
|
-
const
|
|
16190
|
-
return { dispatch: dispatch2,
|
|
16206
|
+
const screenshotResult = await capturePageInputScreenshot(client, "scroll", target, parsed.noScreenshot);
|
|
16207
|
+
return { dispatch: dispatch2, ...screenshotResult };
|
|
16191
16208
|
}
|
|
16192
16209
|
);
|
|
16193
16210
|
if ("failure" in outcome) {
|
|
16194
16211
|
return emitResolutionError(parsed, "page scroll", outcome.failure);
|
|
16195
16212
|
}
|
|
16196
|
-
const { dispatch, screenshot } = outcome;
|
|
16213
|
+
const { dispatch, screenshot, screenshotWarning } = outcome;
|
|
16197
16214
|
const rows = [
|
|
16198
16215
|
fact`scrolled ${dispatch.role ?? "unknown"} "${dispatch.name ?? ""}" (backend:${dispatch.backendNodeId}) to ${dispatch.to} — scrollTop now ${dispatch.scrollTop}`,
|
|
16199
16216
|
fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`
|
|
16200
16217
|
];
|
|
16201
16218
|
if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
|
|
16219
|
+
if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
|
|
16202
16220
|
emitResult(
|
|
16203
16221
|
{
|
|
16204
16222
|
tag: "scrolled",
|
|
@@ -16229,7 +16247,7 @@ input:
|
|
|
16229
16247
|
--settle <ms> network-settle window applied after the scroll (default: 1000; 2500 with an active session; 0 disables)
|
|
16230
16248
|
--no-screenshot skip the auto-screenshot
|
|
16231
16249
|
output:
|
|
16232
|
-
<scrolled backend-node-id=\u2026 role=\u2026 name=\u2026> \u2014 resolved identity, destination, the container's resulting scrollTop, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
|
|
16250
|
+
<scrolled backend-node-id=\u2026 role=\u2026 name=\u2026> \u2014 resolved identity, destination, the container's resulting scrollTop, the measured settle (requested/waited), and either the screenshot artifact path or a warning when only that follow-up capture timed out; --json mirrors the same fields
|
|
16233
16251
|
effects:
|
|
16234
16252
|
assigns the container's scrollTop in-page (may trigger lazy-load network); writes one screenshot into the active session's shots/ sequence unless --no-screenshot`;
|
|
16235
16253
|
}
|
|
@@ -230926,17 +230944,23 @@ function normalizeComputed(raw) {
|
|
|
230926
230944
|
function resolvedIdentity3(backendNodeId2) {
|
|
230927
230945
|
return backendNodeId2 === void 0 ? { backendNodeId: null, identityUnresolved: true } : { backendNodeId: backendNodeId2 };
|
|
230928
230946
|
}
|
|
230929
|
-
async function mapWithConcurrency(values, limit, fn) {
|
|
230930
|
-
const result =
|
|
230947
|
+
async function mapWithConcurrency(values, limit, budgetMs, fn) {
|
|
230948
|
+
const result = Array(values.length).fill(void 0);
|
|
230949
|
+
const deadline = Date.now() + budgetMs;
|
|
230931
230950
|
let next = 0;
|
|
230951
|
+
let truncated = false;
|
|
230932
230952
|
const worker = async () => {
|
|
230933
230953
|
while (next < values.length) {
|
|
230954
|
+
if (Date.now() >= deadline) {
|
|
230955
|
+
truncated = true;
|
|
230956
|
+
return;
|
|
230957
|
+
}
|
|
230934
230958
|
const index = next++;
|
|
230935
230959
|
result[index] = await fn(values[index], index);
|
|
230936
230960
|
}
|
|
230937
230961
|
};
|
|
230938
230962
|
await Promise.all(Array.from({ length: Math.min(limit, values.length) }, worker));
|
|
230939
|
-
return result;
|
|
230963
|
+
return { values: result, truncated };
|
|
230940
230964
|
}
|
|
230941
230965
|
async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
|
|
230942
230966
|
const { client } = ctx;
|
|
@@ -230986,7 +231010,7 @@ async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
|
|
|
230986
231010
|
resolved = facts.map(() => void 0);
|
|
230987
231011
|
}
|
|
230988
231012
|
const sourceCache = /* @__PURE__ */ new Map();
|
|
230989
|
-
const
|
|
231013
|
+
const provenance = await mapWithConcurrency(facts, PROVENANCE_CONCURRENCY, PROVENANCE_BUDGET_MS, async (fact4, index) => {
|
|
230990
231014
|
const ref = resolved[index];
|
|
230991
231015
|
const computed2 = normalizeComputed(fact4.computed);
|
|
230992
231016
|
let winningDeclarations;
|
|
@@ -231021,6 +231045,7 @@ async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
|
|
|
231021
231045
|
provenanceUnavailable
|
|
231022
231046
|
};
|
|
231023
231047
|
});
|
|
231048
|
+
const elements = provenance.values.filter((element) => element !== void 0);
|
|
231024
231049
|
ctx.write.json("styles.json", {
|
|
231025
231050
|
elements,
|
|
231026
231051
|
coverage: {
|
|
@@ -231029,7 +231054,9 @@ async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
|
|
|
231029
231054
|
shadowRootsNotWalked: inventory.shadowRootsNotWalked,
|
|
231030
231055
|
totalCandidateElements: inventory.total,
|
|
231031
231056
|
keptElements: elements.length,
|
|
231032
|
-
elementsTruncated: inventory.total >
|
|
231057
|
+
elementsTruncated: inventory.total > facts.length,
|
|
231058
|
+
provenanceTruncated: provenance.truncated,
|
|
231059
|
+
...provenance.truncated ? { provenanceTruncationReason: "collector-time-budget" } : {}
|
|
231033
231060
|
},
|
|
231034
231061
|
styleSheetHeaders,
|
|
231035
231062
|
identity: identity3,
|
|
@@ -231037,13 +231064,14 @@ async function collectStylesInner(ctx, styleSheetUrls, styleSheetHeaders) {
|
|
|
231037
231064
|
...unavailableReason ? { unavailableReason } : {}
|
|
231038
231065
|
});
|
|
231039
231066
|
}
|
|
231040
|
-
var STYLES_MAX_ELEMENTS, PROVENANCE_CONCURRENCY, COMPUTED_PROPERTIES, PROVENANCE_PROPERTIES, EXCLUDED_TAGS, STYLES_SCRIPT, collectStyles;
|
|
231067
|
+
var STYLES_MAX_ELEMENTS, PROVENANCE_CONCURRENCY, PROVENANCE_BUDGET_MS, COMPUTED_PROPERTIES, PROVENANCE_PROPERTIES, EXCLUDED_TAGS, STYLES_SCRIPT, collectStyles;
|
|
231041
231068
|
var init_styles = __esm({
|
|
231042
231069
|
"src/cdp/measure/collectors/styles.ts"() {
|
|
231043
231070
|
init_redaction();
|
|
231044
231071
|
init_style_provenance();
|
|
231045
231072
|
STYLES_MAX_ELEMENTS = 150;
|
|
231046
231073
|
PROVENANCE_CONCURRENCY = 8;
|
|
231074
|
+
PROVENANCE_BUDGET_MS = 5e3;
|
|
231047
231075
|
COMPUTED_PROPERTIES = [
|
|
231048
231076
|
"display",
|
|
231049
231077
|
"position",
|
|
@@ -235936,22 +235964,24 @@ function boundedClient(client, phase, partialPath, onTimeout) {
|
|
|
235936
235964
|
const timeoutMs = Math.min(timeout, SNAPSHOT_REQUEST_TIMEOUT_MS);
|
|
235937
235965
|
let timer;
|
|
235938
235966
|
let timedOut = false;
|
|
235939
|
-
const markTimedOut = () => {
|
|
235967
|
+
const markTimedOut = (error) => {
|
|
235940
235968
|
if (timedOut) return;
|
|
235941
235969
|
timedOut = true;
|
|
235942
|
-
onTimeout?.();
|
|
235970
|
+
onTimeout?.(error);
|
|
235943
235971
|
};
|
|
235944
235972
|
const deadline = new Promise((_, reject) => {
|
|
235945
235973
|
timer = setTimeout(() => {
|
|
235946
|
-
|
|
235947
|
-
|
|
235974
|
+
const error = new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
|
|
235975
|
+
markTimedOut(error);
|
|
235976
|
+
reject(error);
|
|
235948
235977
|
}, timeoutMs);
|
|
235949
235978
|
});
|
|
235950
235979
|
const request = Promise.resolve().then(() => client.send(method, params, timeoutMs, sessionId)).catch((error) => {
|
|
235951
235980
|
if (error instanceof SnapshotCaptureTimeout) throw error;
|
|
235952
235981
|
if (error instanceof Error && error.message.startsWith("CDP request timeout")) {
|
|
235953
|
-
|
|
235954
|
-
|
|
235982
|
+
const timeoutError = new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
|
|
235983
|
+
markTimedOut(timeoutError);
|
|
235984
|
+
throw timeoutError;
|
|
235955
235985
|
}
|
|
235956
235986
|
throw error;
|
|
235957
235987
|
});
|
|
@@ -236055,21 +236085,25 @@ async function captureSnapshotSubstrate(options) {
|
|
|
236055
236085
|
if (skipCollectors.has(collector.name)) return { name: collector.name, status: "skipped" };
|
|
236056
236086
|
const startedAt = Date.now();
|
|
236057
236087
|
let requestTimeouts = 0;
|
|
236088
|
+
let timeoutError;
|
|
236058
236089
|
const collectorWriter = makeCollectorWriter(ctx.write);
|
|
236059
236090
|
try {
|
|
236060
236091
|
await collector.fn({
|
|
236061
236092
|
...ctx,
|
|
236062
|
-
client: boundedClient(client, `collector:${collector.name}`, dir, () => {
|
|
236093
|
+
client: boundedClient(client, `collector:${collector.name}`, dir, (error) => {
|
|
236063
236094
|
requestTimeouts += 1;
|
|
236095
|
+
timeoutError ??= error;
|
|
236064
236096
|
}),
|
|
236065
236097
|
write: collectorWriter.writer
|
|
236066
236098
|
});
|
|
236099
|
+
if (collector.name === "styles" && timeoutError) throw timeoutError;
|
|
236067
236100
|
const ms = Date.now() - startedAt;
|
|
236068
236101
|
collectorWriter.commit();
|
|
236069
236102
|
return requestTimeouts > 0 ? { name: collector.name, status: "degraded", ms, requestTimeouts } : { name: collector.name, status: "ok", ms };
|
|
236070
|
-
} catch {
|
|
236103
|
+
} catch (error) {
|
|
236071
236104
|
const ms = Date.now() - startedAt;
|
|
236072
236105
|
collectorWriter.discard();
|
|
236106
|
+
if (collector.name === "styles" && error instanceof SnapshotCaptureTimeout) throw error;
|
|
236073
236107
|
return { name: collector.name, status: "error", ms, ...requestTimeouts > 0 ? { requestTimeouts } : {} };
|
|
236074
236108
|
}
|
|
236075
236109
|
};
|
|
@@ -242178,7 +242212,7 @@ CDP_PORT / CDP_TARGET pin the browser + tab for orchestrators \u2014 precedence:
|
|
|
242178
242212
|
explicit flag > active session > env.`;
|
|
242179
242213
|
}
|
|
242180
242214
|
function printVersion() {
|
|
242181
|
-
const declared = "1.3.
|
|
242215
|
+
const declared = "1.3.21";
|
|
242182
242216
|
if (declared) {
|
|
242183
242217
|
console.log(declared);
|
|
242184
242218
|
return;
|
package/package.json
CHANGED
|
@@ -52,6 +52,33 @@ export function pageInputDeps(): PageInputDeps {
|
|
|
52
52
|
return deps;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface PageInputScreenshot {
|
|
56
|
+
readonly screenshot: string | null;
|
|
57
|
+
readonly screenshotWarning: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A dispatched page action stays successful when only its follow-up screenshot times out. */
|
|
61
|
+
export async function capturePageInputScreenshot(
|
|
62
|
+
client: Parameters<typeof autoScreenshot>[0],
|
|
63
|
+
action: string,
|
|
64
|
+
label: string,
|
|
65
|
+
noScreenshot?: boolean,
|
|
66
|
+
): Promise<PageInputScreenshot> {
|
|
67
|
+
try {
|
|
68
|
+
return {
|
|
69
|
+
screenshot: await deps.autoScreenshot(client, action, label, noScreenshot),
|
|
70
|
+
screenshotWarning: null,
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
if (!/^CDP request timeout \(\d+ms\): Page\.captureScreenshot$/.test(message)) throw error;
|
|
75
|
+
return {
|
|
76
|
+
screenshot: null,
|
|
77
|
+
screenshotWarning: `automatic screenshot timed out after the page action completed; no screenshot was saved (${message})`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
55
82
|
/**
|
|
56
83
|
* The verb's settle window: `--settle <ms>` (including 0) wins outright;
|
|
57
84
|
* otherwise the default is keyed off active-session presence (the in-session
|
|
@@ -147,7 +174,7 @@ input:
|
|
|
147
174
|
--settle <ms> network-settle window applied after the click (default: 1000; 2500 with an active session; 0 disables)
|
|
148
175
|
--no-screenshot skip the auto-screenshot
|
|
149
176
|
output:
|
|
150
|
-
<clicked backend-node-id=… role=… name=…> — resolved identity, dispatched coordinates, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
|
|
177
|
+
<clicked backend-node-id=… role=… name=…> — resolved identity, dispatched coordinates, the measured settle (requested/waited), and either the screenshot artifact path or a warning when only that follow-up capture timed out; --json mirrors the same fields
|
|
151
178
|
effects:
|
|
152
179
|
scrolls the target into view, then dispatches a real mouse press/release at its center; writes one screenshot into the active session's shots/ sequence unless --no-screenshot`;
|
|
153
180
|
|
|
@@ -177,8 +204,8 @@ export async function cmdPageClick(parsed: ParsedArgs, _args: string[]): Promise
|
|
|
177
204
|
const resolved = await resolveLiveTarget(live, target);
|
|
178
205
|
if (!resolved.ok) return { failure: resolved } as const;
|
|
179
206
|
const dispatch = await clickResolved(live, resolved);
|
|
180
|
-
const
|
|
181
|
-
return { dispatch,
|
|
207
|
+
const screenshotResult = await capturePageInputScreenshot(client, 'click', target, parsed.noScreenshot);
|
|
208
|
+
return { dispatch, ...screenshotResult } as const;
|
|
182
209
|
},
|
|
183
210
|
);
|
|
184
211
|
|
|
@@ -186,12 +213,13 @@ export async function cmdPageClick(parsed: ParsedArgs, _args: string[]): Promise
|
|
|
186
213
|
return emitResolutionError(parsed, 'page click', outcome.failure);
|
|
187
214
|
}
|
|
188
215
|
|
|
189
|
-
const { dispatch, screenshot } = outcome;
|
|
216
|
+
const { dispatch, screenshot, screenshotWarning } = outcome;
|
|
190
217
|
const rows: FactLine[] = [
|
|
191
218
|
fact`clicked ${dispatch.role ?? 'unknown'} "${dispatch.name ?? ''}" (backend:${dispatch.backendNodeId}) at x=${dispatch.x} y=${dispatch.y}`,
|
|
192
219
|
fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`,
|
|
193
220
|
];
|
|
194
221
|
if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
|
|
222
|
+
if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
|
|
195
223
|
|
|
196
224
|
emitResult(
|
|
197
225
|
{
|
|
@@ -13,6 +13,7 @@ import { resolveLiveTarget, scrollResolved, type LiveClient } from '../../../int
|
|
|
13
13
|
import { emitResult, fact, lineList, type FactLine } from '../../../output/render.js';
|
|
14
14
|
import {
|
|
15
15
|
pageInputDeps,
|
|
16
|
+
capturePageInputScreenshot,
|
|
16
17
|
effectiveSettle,
|
|
17
18
|
emitInvalidInput,
|
|
18
19
|
emitResolutionError,
|
|
@@ -26,7 +27,7 @@ input:
|
|
|
26
27
|
--settle <ms> network-settle window applied after the scroll (default: 1000; 2500 with an active session; 0 disables)
|
|
27
28
|
--no-screenshot skip the auto-screenshot
|
|
28
29
|
output:
|
|
29
|
-
<scrolled backend-node-id=… role=… name=…> — resolved identity, destination, the container's resulting scrollTop, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
|
|
30
|
+
<scrolled backend-node-id=… role=… name=…> — resolved identity, destination, the container's resulting scrollTop, the measured settle (requested/waited), and either the screenshot artifact path or a warning when only that follow-up capture timed out; --json mirrors the same fields
|
|
30
31
|
effects:
|
|
31
32
|
assigns the container's scrollTop in-page (may trigger lazy-load network); writes one screenshot into the active session's shots/ sequence unless --no-screenshot`;
|
|
32
33
|
|
|
@@ -74,8 +75,8 @@ export async function cmdPageScroll(parsed: ParsedArgs, _args: string[]): Promis
|
|
|
74
75
|
// Same landmark shape `motion rec --do scroll:` records; carried by
|
|
75
76
|
// the one mutating call when the transport records landmarks.
|
|
76
77
|
const dispatch = await scrollResolved(live, resolved, to, { mark: `scroll:${target},to=${to}` });
|
|
77
|
-
const
|
|
78
|
-
return { dispatch,
|
|
78
|
+
const screenshotResult = await capturePageInputScreenshot(client, 'scroll', target, parsed.noScreenshot);
|
|
79
|
+
return { dispatch, ...screenshotResult } as const;
|
|
79
80
|
},
|
|
80
81
|
);
|
|
81
82
|
|
|
@@ -83,12 +84,13 @@ export async function cmdPageScroll(parsed: ParsedArgs, _args: string[]): Promis
|
|
|
83
84
|
return emitResolutionError(parsed, 'page scroll', outcome.failure);
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
const { dispatch, screenshot } = outcome;
|
|
87
|
+
const { dispatch, screenshot, screenshotWarning } = outcome;
|
|
87
88
|
const rows: FactLine[] = [
|
|
88
89
|
fact`scrolled ${dispatch.role ?? 'unknown'} "${dispatch.name ?? ''}" (backend:${dispatch.backendNodeId}) to ${dispatch.to} — scrollTop now ${dispatch.scrollTop}`,
|
|
89
90
|
fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`,
|
|
90
91
|
];
|
|
91
92
|
if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
|
|
93
|
+
if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
|
|
92
94
|
|
|
93
95
|
emitResult(
|
|
94
96
|
{
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { emitResult, fact, lineList, type FactLine } from '../../../output/render.js';
|
|
19
19
|
import {
|
|
20
20
|
pageInputDeps,
|
|
21
|
+
capturePageInputScreenshot,
|
|
21
22
|
effectiveSettle,
|
|
22
23
|
emitInvalidInput,
|
|
23
24
|
emitResolutionError,
|
|
@@ -31,7 +32,7 @@ input:
|
|
|
31
32
|
--settle <ms> network-settle window applied after typing (default: 500; 1500 with an active session; 0 disables)
|
|
32
33
|
--no-screenshot skip the auto-screenshot
|
|
33
34
|
output:
|
|
34
|
-
<typed> — the typed text, the resolved field identity (backend-node-id, role, name) and focus-click coordinates when --into was given, the measured settle (requested/waited), screenshot artifact path; --json mirrors the same fields
|
|
35
|
+
<typed> — the typed text, the resolved field identity (backend-node-id, role, name) and focus-click coordinates when --into was given, the measured settle (requested/waited), and either the screenshot artifact path or a warning when only that follow-up capture timed out; --json mirrors the same fields
|
|
35
36
|
effects:
|
|
36
37
|
dispatches a real focus click (when --into) followed by real text insertion; writes one screenshot into the active session's shots/ sequence unless --no-screenshot`;
|
|
37
38
|
|
|
@@ -69,8 +70,8 @@ export async function cmdPageType(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
69
70
|
await typeText(live, textArg);
|
|
70
71
|
}
|
|
71
72
|
// Screenshot label identifies the field, never the typed content.
|
|
72
|
-
const
|
|
73
|
-
return { dispatch,
|
|
73
|
+
const screenshotResult = await capturePageInputScreenshot(client, 'type', parsed.into ?? 'focused element', parsed.noScreenshot);
|
|
74
|
+
return { dispatch, ...screenshotResult } as const;
|
|
74
75
|
},
|
|
75
76
|
);
|
|
76
77
|
|
|
@@ -78,7 +79,7 @@ export async function cmdPageType(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
78
79
|
return emitResolutionError(parsed, 'page type', outcome.failure);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
const { dispatch, screenshot } = outcome;
|
|
82
|
+
const { dispatch, screenshot, screenshotWarning } = outcome;
|
|
82
83
|
const rows: FactLine[] = [
|
|
83
84
|
dispatch
|
|
84
85
|
? fact`typed "${textArg}" into ${dispatch.role ?? 'unknown'} "${dispatch.name ?? ''}" (backend:${dispatch.backendNodeId}), focus click at x=${dispatch.x} y=${dispatch.y}`
|
|
@@ -86,6 +87,7 @@ export async function cmdPageType(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
86
87
|
fact`settle: requested ${settleFacts.requestedMs}ms, waited ${settleFacts.waitedMs}ms`,
|
|
87
88
|
];
|
|
88
89
|
if (screenshot) rows.push(fact`screenshot: ${screenshot}`);
|
|
90
|
+
if (screenshotWarning) rows.push(fact`screenshot-warning: ${screenshotWarning}`);
|
|
89
91
|
|
|
90
92
|
emitResult(
|
|
91
93
|
{
|
|
@@ -35,6 +35,8 @@ export type { Specificity, CSSRange, WinningDeclaration } from './style-provenan
|
|
|
35
35
|
|
|
36
36
|
const STYLES_MAX_ELEMENTS = 150;
|
|
37
37
|
const PROVENANCE_CONCURRENCY = 8;
|
|
38
|
+
/** Bounds queued provenance work without abandoning in-flight CDP requests on the shared snap connection. Matches snapshot.ts's per-request bound but remains local to avoid a collector/orchestrator import cycle. */
|
|
39
|
+
const PROVENANCE_BUDGET_MS = 5_000;
|
|
38
40
|
|
|
39
41
|
/** Full computed-style snapshot tracked per element — broad, cheap (no CDP round trip; read via `getComputedStyle` in one `Runtime.evaluate`). */
|
|
40
42
|
const COMPUTED_PROPERTIES = [
|
|
@@ -95,6 +97,10 @@ export interface StylesCoverage {
|
|
|
95
97
|
keptElements: number;
|
|
96
98
|
/** `true` when {@link STYLES_MAX_ELEMENTS} dropped one or more candidate elements from enumeration — an explicit fact (I-5), never a silent cap. */
|
|
97
99
|
elementsTruncated: boolean;
|
|
100
|
+
/** `true` when the bounded provenance pass stopped starting element reads before every enumerated element was inspected; completed in-flight reads are still included. */
|
|
101
|
+
provenanceTruncated: boolean;
|
|
102
|
+
/** Present only when {@link provenanceTruncated} is true; names the factual bound that stopped additional provenance requests. */
|
|
103
|
+
provenanceTruncationReason?: 'collector-time-budget';
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
export interface StylesReport {
|
|
@@ -205,18 +211,33 @@ function resolvedIdentity(backendNodeId: number | undefined): { backendNodeId: n
|
|
|
205
211
|
return backendNodeId === undefined ? { backendNodeId: null, identityUnresolved: true } : { backendNodeId };
|
|
206
212
|
}
|
|
207
213
|
|
|
208
|
-
/**
|
|
209
|
-
|
|
210
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Limits expensive per-element CDP provenance reads without changing document
|
|
216
|
+
* order. Once its budget expires it starts no more reads, but awaits the
|
|
217
|
+
* current worker batch so no CDP work is left running under the next collector.
|
|
218
|
+
*/
|
|
219
|
+
async function mapWithConcurrency<T, R>(
|
|
220
|
+
values: readonly T[],
|
|
221
|
+
limit: number,
|
|
222
|
+
budgetMs: number,
|
|
223
|
+
fn: (value: T, index: number) => Promise<R>,
|
|
224
|
+
): Promise<{ values: Array<R | undefined>; truncated: boolean }> {
|
|
225
|
+
const result = Array<R | undefined>(values.length).fill(undefined);
|
|
226
|
+
const deadline = Date.now() + budgetMs;
|
|
211
227
|
let next = 0;
|
|
228
|
+
let truncated = false;
|
|
212
229
|
const worker = async (): Promise<void> => {
|
|
213
230
|
while (next < values.length) {
|
|
231
|
+
if (Date.now() >= deadline) {
|
|
232
|
+
truncated = true;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
214
235
|
const index = next++;
|
|
215
236
|
result[index] = await fn(values[index]!, index);
|
|
216
237
|
}
|
|
217
238
|
};
|
|
218
239
|
await Promise.all(Array.from({ length: Math.min(limit, values.length) }, worker));
|
|
219
|
-
return result;
|
|
240
|
+
return { values: result, truncated };
|
|
220
241
|
}
|
|
221
242
|
|
|
222
243
|
// ============================================================================
|
|
@@ -305,48 +326,49 @@ async function collectStylesInner(
|
|
|
305
326
|
|
|
306
327
|
const sourceCache = new Map<string, Promise<ResolvedSourceLocation>>();
|
|
307
328
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
winningDeclarations = [];
|
|
334
|
-
provenanceUnavailable = true;
|
|
335
|
-
}
|
|
336
|
-
} else {
|
|
329
|
+
const provenance = await mapWithConcurrency(facts, PROVENANCE_CONCURRENCY, PROVENANCE_BUDGET_MS, async (fact, index): Promise<StylesElementRecord> => {
|
|
330
|
+
const ref = resolved[index];
|
|
331
|
+
const computed = normalizeComputed(fact.computed);
|
|
332
|
+
|
|
333
|
+
// A `noDeclarationRecord` is reserved for a SUCCESSFUL matched-styles response that genuinely had
|
|
334
|
+
// no candidate for a property — an honest "nothing declares this" fact. When provenance itself
|
|
335
|
+
// couldn't be inspected (no CDP nodeId, or `CSS.getMatchedStylesForNode` failed), that's a different
|
|
336
|
+
// fact — "unknown", not "none" — so `winningDeclarations` is left empty and `provenanceUnavailable`
|
|
337
|
+
// is set, rather than conflating the two into misleading no-declaration records.
|
|
338
|
+
let winningDeclarations: WinningDeclaration[];
|
|
339
|
+
let provenanceUnavailable: boolean | undefined;
|
|
340
|
+
if (ref?.nodeId !== undefined) {
|
|
341
|
+
try {
|
|
342
|
+
const matched = (await client.send('CSS.getMatchedStylesForNode', {
|
|
343
|
+
nodeId: ref.nodeId,
|
|
344
|
+
})) as CDPMatchedStylesResponse;
|
|
345
|
+
winningDeclarations = await buildWinningDeclarations(
|
|
346
|
+
client,
|
|
347
|
+
matched,
|
|
348
|
+
computed,
|
|
349
|
+
sourceCache,
|
|
350
|
+
styleSheetUrls,
|
|
351
|
+
PROVENANCE_PROPERTIES,
|
|
352
|
+
);
|
|
353
|
+
} catch {
|
|
337
354
|
winningDeclarations = [];
|
|
338
355
|
provenanceUnavailable = true;
|
|
339
356
|
}
|
|
357
|
+
} else {
|
|
358
|
+
winningDeclarations = [];
|
|
359
|
+
provenanceUnavailable = true;
|
|
360
|
+
}
|
|
340
361
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
362
|
+
return {
|
|
363
|
+
id: `s-${index}`,
|
|
364
|
+
selector: sanitizeString(fact.cssPath),
|
|
365
|
+
...resolvedIdentity(ref?.backendNodeId),
|
|
366
|
+
computed,
|
|
367
|
+
winningDeclarations,
|
|
368
|
+
provenanceUnavailable,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
const elements = provenance.values.filter((element): element is StylesElementRecord => element !== undefined);
|
|
350
372
|
|
|
351
373
|
ctx.write.json('styles.json', {
|
|
352
374
|
elements,
|
|
@@ -356,7 +378,9 @@ async function collectStylesInner(
|
|
|
356
378
|
shadowRootsNotWalked: inventory.shadowRootsNotWalked,
|
|
357
379
|
totalCandidateElements: inventory.total,
|
|
358
380
|
keptElements: elements.length,
|
|
359
|
-
elementsTruncated: inventory.total >
|
|
381
|
+
elementsTruncated: inventory.total > facts.length,
|
|
382
|
+
provenanceTruncated: provenance.truncated,
|
|
383
|
+
...(provenance.truncated ? { provenanceTruncationReason: 'collector-time-budget' as const } : {}),
|
|
360
384
|
},
|
|
361
385
|
styleSheetHeaders: styleSheetHeaders,
|
|
362
386
|
identity,
|
|
@@ -199,28 +199,30 @@ export class SnapshotCaptureTimeout extends Error {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/** Bounds one snapshot CDP request and labels the phase that owns it. */
|
|
202
|
-
function boundedClient(client: CDPClient, phase: string, partialPath: string, onTimeout?: () => void): CDPClient {
|
|
202
|
+
function boundedClient(client: CDPClient, phase: string, partialPath: string, onTimeout?: (error: SnapshotCaptureTimeout) => void): CDPClient {
|
|
203
203
|
const bounded = Object.create(client) as CDPClient;
|
|
204
204
|
bounded.send = (method: string, params: Record<string, unknown> = {}, timeout = 60_000, sessionId?: string): Promise<unknown> => {
|
|
205
205
|
const timeoutMs = Math.min(timeout, SNAPSHOT_REQUEST_TIMEOUT_MS);
|
|
206
206
|
let timer: NodeJS.Timeout | undefined;
|
|
207
207
|
let timedOut = false;
|
|
208
|
-
const markTimedOut = (): void => {
|
|
208
|
+
const markTimedOut = (error: SnapshotCaptureTimeout): void => {
|
|
209
209
|
if (timedOut) return;
|
|
210
210
|
timedOut = true;
|
|
211
|
-
onTimeout?.();
|
|
211
|
+
onTimeout?.(error);
|
|
212
212
|
};
|
|
213
213
|
const deadline = new Promise<never>((_, reject) => {
|
|
214
214
|
timer = setTimeout(() => {
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
const error = new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
|
|
216
|
+
markTimedOut(error);
|
|
217
|
+
reject(error);
|
|
217
218
|
}, timeoutMs);
|
|
218
219
|
});
|
|
219
220
|
const request = Promise.resolve().then(() => client.send(method, params, timeoutMs, sessionId)).catch((error: unknown) => {
|
|
220
221
|
if (error instanceof SnapshotCaptureTimeout) throw error;
|
|
221
222
|
if (error instanceof Error && error.message.startsWith('CDP request timeout')) {
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
const timeoutError = new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
|
|
224
|
+
markTimedOut(timeoutError);
|
|
225
|
+
throw timeoutError;
|
|
224
226
|
}
|
|
225
227
|
throw error;
|
|
226
228
|
});
|
|
@@ -387,25 +389,34 @@ export async function captureSnapshotSubstrate(options: CaptureSnapshotOptions):
|
|
|
387
389
|
|
|
388
390
|
const startedAt = Date.now();
|
|
389
391
|
let requestTimeouts = 0;
|
|
392
|
+
let timeoutError: SnapshotCaptureTimeout | undefined;
|
|
390
393
|
const collectorWriter = makeCollectorWriter(ctx.write);
|
|
391
394
|
try {
|
|
392
|
-
// Await the collector itself, not a wall-clock race around it.
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
// connection collector starts.
|
|
395
|
+
// Await the collector itself, not a wall-clock race around it. The
|
|
396
|
+
// collector owns its cleanup, and timeout handling below decides whether
|
|
397
|
+
// this capture can safely move on to another shared-connection collector.
|
|
396
398
|
await collector.fn({
|
|
397
399
|
...ctx,
|
|
398
|
-
client: boundedClient(client, `collector:${collector.name}`, dir, () => {
|
|
400
|
+
client: boundedClient(client, `collector:${collector.name}`, dir, (error) => {
|
|
401
|
+
requestTimeouts += 1;
|
|
402
|
+
timeoutError ??= error;
|
|
403
|
+
}),
|
|
399
404
|
write: collectorWriter.writer,
|
|
400
405
|
});
|
|
406
|
+
// A styles timeout may be caught per-record by the provenance collector,
|
|
407
|
+
// but its browser operation can still belong to this shared connection.
|
|
408
|
+
// End this capture instead of letting another collector issue commands
|
|
409
|
+
// under that outstanding operation.
|
|
410
|
+
if (collector.name === 'styles' && timeoutError) throw timeoutError;
|
|
401
411
|
const ms = Date.now() - startedAt;
|
|
402
412
|
collectorWriter.commit();
|
|
403
413
|
return requestTimeouts > 0
|
|
404
414
|
? { name: collector.name, status: 'degraded', ms, requestTimeouts }
|
|
405
415
|
: { name: collector.name, status: 'ok', ms };
|
|
406
|
-
} catch {
|
|
416
|
+
} catch (error) {
|
|
407
417
|
const ms = Date.now() - startedAt;
|
|
408
418
|
collectorWriter.discard();
|
|
419
|
+
if (collector.name === 'styles' && error instanceof SnapshotCaptureTimeout) throw error;
|
|
409
420
|
return { name: collector.name, status: 'error', ms, ...(requestTimeouts > 0 ? { requestTimeouts } : {}) };
|
|
410
421
|
}
|
|
411
422
|
};
|