browser-pilot 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -617
- package/dist/actions.cjs +1438 -17
- package/dist/actions.d.cts +21 -3
- package/dist/actions.d.ts +21 -3
- package/dist/actions.mjs +1 -1
- package/dist/browser-MEWT75IB.mjs +11 -0
- package/dist/browser.cjs +1583 -22
- package/dist/browser.d.cts +12 -3
- package/dist/browser.d.ts +12 -3
- package/dist/browser.mjs +3 -3
- package/dist/cdp.cjs +36 -3
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +3 -1
- package/dist/{chunk-A2ZRAEO3.mjs → chunk-7YVCOL2W.mjs} +1428 -17
- package/dist/chunk-BVZALQT4.mjs +303 -0
- package/dist/chunk-DTVRFXKI.mjs +35 -0
- package/dist/{chunk-HP6R3W32.mjs → chunk-LCNFBXB5.mjs} +33 -6
- package/dist/chunk-LUGLEMVR.mjs +11 -0
- package/dist/chunk-USYSHCI3.mjs +8640 -0
- package/dist/chunk-WPNW23CE.mjs +466 -0
- package/dist/{chunk-VDAMDOS6.mjs → chunk-ZAXQ5OTV.mjs} +151 -7
- package/dist/cli.mjs +3225 -8434
- package/dist/{client-DRqxBdHv.d.ts → client-B5QBRgIy.d.cts} +10 -6
- package/dist/{client-DRqxBdHv.d.cts → client-B5QBRgIy.d.ts} +10 -6
- package/dist/client-JWWZWO6L.mjs +12 -0
- package/dist/index.cjs +1629 -24
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/page-XPS6IC6V.mjs +7 -0
- package/dist/transport-WHEBAZUP.mjs +83 -0
- package/dist/{types-CzgQjai9.d.ts → types-C9ySEdOX.d.cts} +78 -4
- package/dist/{types-BXMGFtnB.d.cts → types-Cvvf0oGu.d.ts} +78 -4
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -62,6 +72,230 @@ __export(src_exports, {
|
|
|
62
72
|
});
|
|
63
73
|
module.exports = __toCommonJS(src_exports);
|
|
64
74
|
|
|
75
|
+
// src/actions/executor.ts
|
|
76
|
+
var fs = __toESM(require("fs"), 1);
|
|
77
|
+
var import_node_path = require("path");
|
|
78
|
+
|
|
79
|
+
// src/recording/redaction.ts
|
|
80
|
+
var REDACTED_VALUE = "[REDACTED]";
|
|
81
|
+
var SENSITIVE_AUTOCOMPLETE_TOKENS = [
|
|
82
|
+
"current-password",
|
|
83
|
+
"new-password",
|
|
84
|
+
"one-time-code",
|
|
85
|
+
"cc-number",
|
|
86
|
+
"cc-csc",
|
|
87
|
+
"cc-exp",
|
|
88
|
+
"cc-exp-month",
|
|
89
|
+
"cc-exp-year"
|
|
90
|
+
];
|
|
91
|
+
function autocompleteTokens(autocomplete) {
|
|
92
|
+
if (!autocomplete) return [];
|
|
93
|
+
return autocomplete.toLowerCase().split(/\s+/).map((token) => token.trim()).filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
function isSensitiveFieldMetadata(metadata) {
|
|
96
|
+
if (!metadata) return false;
|
|
97
|
+
if (metadata.sensitiveValue) return true;
|
|
98
|
+
const inputType = metadata.inputType?.toLowerCase();
|
|
99
|
+
if (inputType === "password" || inputType === "hidden") {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
const sensitiveAutocompleteTokens = new Set(SENSITIVE_AUTOCOMPLETE_TOKENS);
|
|
103
|
+
return autocompleteTokens(metadata.autocomplete).some(
|
|
104
|
+
(token) => sensitiveAutocompleteTokens.has(token)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
function redactValueForRecording(value, metadata) {
|
|
108
|
+
if (value === void 0) return void 0;
|
|
109
|
+
return isSensitiveFieldMetadata(metadata) ? REDACTED_VALUE : value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/browser/action-highlight.ts
|
|
113
|
+
var HIGHLIGHT_STYLES = {
|
|
114
|
+
click: { outline: "3px solid rgba(229,57,53,0.8)", badge: "#e53935", marker: "crosshair" },
|
|
115
|
+
fill: { outline: "3px solid rgba(33,150,243,0.8)", badge: "#2196f3" },
|
|
116
|
+
type: { outline: "3px solid rgba(33,150,243,0.6)", badge: "#2196f3" },
|
|
117
|
+
select: { outline: "3px solid rgba(156,39,176,0.8)", badge: "#9c27b0" },
|
|
118
|
+
hover: { outline: "2px dashed rgba(158,158,158,0.5)", badge: "#9e9e9e" },
|
|
119
|
+
scroll: { outline: "none", badge: "#607d8b", marker: "arrow" },
|
|
120
|
+
navigate: { outline: "none", badge: "#4caf50" },
|
|
121
|
+
submit: { outline: "3px solid rgba(255,152,0,0.8)", badge: "#ff9800" },
|
|
122
|
+
"assert-pass": { outline: "3px solid rgba(76,175,80,0.8)", badge: "#4caf50", marker: "check" },
|
|
123
|
+
"assert-fail": { outline: "3px solid rgba(244,67,54,0.8)", badge: "#f44336", marker: "cross" },
|
|
124
|
+
evaluate: { outline: "none", badge: "#ffc107" },
|
|
125
|
+
focus: { outline: "3px dotted rgba(33,150,243,0.6)", badge: "#2196f3" }
|
|
126
|
+
};
|
|
127
|
+
function buildHighlightScript(options) {
|
|
128
|
+
const style = HIGHLIGHT_STYLES[options.kind];
|
|
129
|
+
const label = options.label ? options.label.slice(0, 80) : void 0;
|
|
130
|
+
const escapedLabel = label ? label.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n") : "";
|
|
131
|
+
return `(function() {
|
|
132
|
+
// Remove any existing highlight
|
|
133
|
+
var existing = document.getElementById('__bp-action-highlight');
|
|
134
|
+
if (existing) existing.remove();
|
|
135
|
+
|
|
136
|
+
var container = document.createElement('div');
|
|
137
|
+
container.id = '__bp-action-highlight';
|
|
138
|
+
container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
|
|
139
|
+
|
|
140
|
+
${options.bbox ? `
|
|
141
|
+
// Element outline
|
|
142
|
+
var outline = document.createElement('div');
|
|
143
|
+
outline.style.cssText = 'position:fixed;' +
|
|
144
|
+
'left:${options.bbox.x}px;top:${options.bbox.y}px;' +
|
|
145
|
+
'width:${options.bbox.width}px;height:${options.bbox.height}px;' +
|
|
146
|
+
'${style.outline !== "none" ? `outline:${style.outline};outline-offset:-1px;` : ""}' +
|
|
147
|
+
'pointer-events:none;box-sizing:border-box;';
|
|
148
|
+
container.appendChild(outline);
|
|
149
|
+
` : ""}
|
|
150
|
+
|
|
151
|
+
${options.point && style.marker === "crosshair" ? `
|
|
152
|
+
// Crosshair at click point
|
|
153
|
+
var hLine = document.createElement('div');
|
|
154
|
+
hLine.style.cssText = 'position:fixed;left:${options.point.x - 12}px;top:${options.point.y}px;' +
|
|
155
|
+
'width:24px;height:2px;background:${style.badge};pointer-events:none;';
|
|
156
|
+
var vLine = document.createElement('div');
|
|
157
|
+
vLine.style.cssText = 'position:fixed;left:${options.point.x}px;top:${options.point.y - 12}px;' +
|
|
158
|
+
'width:2px;height:24px;background:${style.badge};pointer-events:none;';
|
|
159
|
+
// Dot at center
|
|
160
|
+
var dot = document.createElement('div');
|
|
161
|
+
dot.style.cssText = 'position:fixed;left:${options.point.x - 4}px;top:${options.point.y - 4}px;' +
|
|
162
|
+
'width:8px;height:8px;border-radius:50%;background:${style.badge};pointer-events:none;';
|
|
163
|
+
container.appendChild(hLine);
|
|
164
|
+
container.appendChild(vLine);
|
|
165
|
+
container.appendChild(dot);
|
|
166
|
+
` : ""}
|
|
167
|
+
|
|
168
|
+
${label ? `
|
|
169
|
+
// Badge with label
|
|
170
|
+
var badge = document.createElement('div');
|
|
171
|
+
badge.style.cssText = 'position:fixed;' +
|
|
172
|
+
${options.bbox ? `'left:${options.bbox.x}px;top:${Math.max(0, options.bbox.y - 28)}px;'` : options.kind === "navigate" ? "'left:50%;top:8px;transform:translateX(-50%);'" : "'right:8px;top:8px;'"} +
|
|
173
|
+
'background:${style.badge};color:white;padding:4px 8px;' +
|
|
174
|
+
'font-family:monospace;font-size:12px;font-weight:bold;' +
|
|
175
|
+
'border-radius:3px;white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis;' +
|
|
176
|
+
'pointer-events:none;';
|
|
177
|
+
badge.textContent = '${escapedLabel}';
|
|
178
|
+
container.appendChild(badge);
|
|
179
|
+
` : ""}
|
|
180
|
+
|
|
181
|
+
${style.marker === "check" && options.bbox ? `
|
|
182
|
+
// Checkmark
|
|
183
|
+
var check = document.createElement('div');
|
|
184
|
+
check.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
|
|
185
|
+
'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
|
|
186
|
+
'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;';
|
|
187
|
+
check.textContent = '\\u2713';
|
|
188
|
+
container.appendChild(check);
|
|
189
|
+
` : ""}
|
|
190
|
+
|
|
191
|
+
${style.marker === "cross" && options.bbox ? `
|
|
192
|
+
// Cross mark
|
|
193
|
+
var cross = document.createElement('div');
|
|
194
|
+
cross.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
|
|
195
|
+
'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
|
|
196
|
+
'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;font-weight:bold;';
|
|
197
|
+
cross.textContent = '\\u2717';
|
|
198
|
+
container.appendChild(cross);
|
|
199
|
+
` : ""}
|
|
200
|
+
|
|
201
|
+
document.body.appendChild(container);
|
|
202
|
+
window.__bpRemoveActionHighlight = function() {
|
|
203
|
+
var el = document.getElementById('__bp-action-highlight');
|
|
204
|
+
if (el) el.remove();
|
|
205
|
+
delete window.__bpRemoveActionHighlight;
|
|
206
|
+
};
|
|
207
|
+
})();`;
|
|
208
|
+
}
|
|
209
|
+
async function injectActionHighlight(page, options) {
|
|
210
|
+
try {
|
|
211
|
+
await page.evaluate(buildHighlightScript(options));
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function removeActionHighlight(page) {
|
|
216
|
+
try {
|
|
217
|
+
await page.evaluate(`(function() {
|
|
218
|
+
if (window.__bpRemoveActionHighlight) {
|
|
219
|
+
window.__bpRemoveActionHighlight();
|
|
220
|
+
}
|
|
221
|
+
})()`);
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function stepToHighlightKind(step) {
|
|
226
|
+
switch (step.action) {
|
|
227
|
+
case "click":
|
|
228
|
+
return "click";
|
|
229
|
+
case "fill":
|
|
230
|
+
return "fill";
|
|
231
|
+
case "type":
|
|
232
|
+
return "type";
|
|
233
|
+
case "select":
|
|
234
|
+
return "select";
|
|
235
|
+
case "hover":
|
|
236
|
+
return "hover";
|
|
237
|
+
case "scroll":
|
|
238
|
+
return "scroll";
|
|
239
|
+
case "goto":
|
|
240
|
+
return "navigate";
|
|
241
|
+
case "submit":
|
|
242
|
+
return "submit";
|
|
243
|
+
case "focus":
|
|
244
|
+
return "focus";
|
|
245
|
+
case "evaluate":
|
|
246
|
+
case "press":
|
|
247
|
+
case "shortcut":
|
|
248
|
+
return "evaluate";
|
|
249
|
+
case "assertVisible":
|
|
250
|
+
case "assertExists":
|
|
251
|
+
case "assertText":
|
|
252
|
+
case "assertUrl":
|
|
253
|
+
case "assertValue":
|
|
254
|
+
return step.success ? "assert-pass" : "assert-fail";
|
|
255
|
+
// Observation-only actions — no highlight
|
|
256
|
+
case "wait":
|
|
257
|
+
case "snapshot":
|
|
258
|
+
case "forms":
|
|
259
|
+
case "text":
|
|
260
|
+
case "screenshot":
|
|
261
|
+
case "newTab":
|
|
262
|
+
case "closeTab":
|
|
263
|
+
case "switchFrame":
|
|
264
|
+
case "switchToMain":
|
|
265
|
+
return null;
|
|
266
|
+
default:
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function getHighlightLabel(step, result, targetMetadata) {
|
|
271
|
+
switch (step.action) {
|
|
272
|
+
case "fill":
|
|
273
|
+
case "type":
|
|
274
|
+
return typeof step.value === "string" ? `"${redactValueForRecording(step.value, targetMetadata)}"` : void 0;
|
|
275
|
+
case "select":
|
|
276
|
+
return redactValueForRecording(
|
|
277
|
+
typeof step.value === "string" ? step.value : void 0,
|
|
278
|
+
targetMetadata
|
|
279
|
+
);
|
|
280
|
+
case "goto":
|
|
281
|
+
return step.url;
|
|
282
|
+
case "evaluate":
|
|
283
|
+
return "JS";
|
|
284
|
+
case "press":
|
|
285
|
+
return step.key;
|
|
286
|
+
case "shortcut":
|
|
287
|
+
return step.combo;
|
|
288
|
+
case "assertText":
|
|
289
|
+
case "assertUrl":
|
|
290
|
+
case "assertValue":
|
|
291
|
+
case "assertVisible":
|
|
292
|
+
case "assertExists":
|
|
293
|
+
return result.success ? "\u2713" : "\u2717";
|
|
294
|
+
default:
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
65
299
|
// src/browser/actionability.ts
|
|
66
300
|
var ActionabilityError = class extends Error {
|
|
67
301
|
failureType;
|
|
@@ -686,8 +920,677 @@ var CDPError = class extends Error {
|
|
|
686
920
|
}
|
|
687
921
|
};
|
|
688
922
|
|
|
923
|
+
// src/trace/views.ts
|
|
924
|
+
function takeRecent(events, limit = 5) {
|
|
925
|
+
return events.slice(-limit).map((event) => ({
|
|
926
|
+
ts: event.ts,
|
|
927
|
+
event: event.event,
|
|
928
|
+
summary: event.summary,
|
|
929
|
+
severity: event.severity,
|
|
930
|
+
url: event.url
|
|
931
|
+
}));
|
|
932
|
+
}
|
|
933
|
+
function buildTraceSummaries(events) {
|
|
934
|
+
return {
|
|
935
|
+
ws: summarizeWs(events),
|
|
936
|
+
voice: summarizeVoice(events),
|
|
937
|
+
console: summarizeConsole(events),
|
|
938
|
+
permissions: summarizePermissions(events),
|
|
939
|
+
media: summarizeMedia(events),
|
|
940
|
+
ui: summarizeUi(events),
|
|
941
|
+
session: summarizeSession(events)
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function summarizeWs(events) {
|
|
945
|
+
const relevant = events.filter((event) => event.channel === "ws" || event.event.startsWith("ws."));
|
|
946
|
+
const connections = /* @__PURE__ */ new Map();
|
|
947
|
+
for (const event of relevant) {
|
|
948
|
+
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
949
|
+
let connection = connections.get(id);
|
|
950
|
+
if (!connection) {
|
|
951
|
+
connection = { id, sent: 0, received: 0, lastMessages: [] };
|
|
952
|
+
connections.set(id, connection);
|
|
953
|
+
}
|
|
954
|
+
connection.url = event.url ?? connection.url;
|
|
955
|
+
if (event.event === "ws.connection.created") {
|
|
956
|
+
connection.createdAt = event.ts;
|
|
957
|
+
}
|
|
958
|
+
if (event.event === "ws.connection.closed") {
|
|
959
|
+
connection.closedAt = event.ts;
|
|
960
|
+
}
|
|
961
|
+
if (event.event === "ws.frame.sent") {
|
|
962
|
+
connection.sent += 1;
|
|
963
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
964
|
+
if (payload) connection.lastMessages.push(`sent: ${payload}`);
|
|
965
|
+
}
|
|
966
|
+
if (event.event === "ws.frame.received") {
|
|
967
|
+
connection.received += 1;
|
|
968
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
969
|
+
if (payload) connection.lastMessages.push(`recv: ${payload}`);
|
|
970
|
+
}
|
|
971
|
+
connection.lastMessages = connection.lastMessages.slice(-3);
|
|
972
|
+
}
|
|
973
|
+
const values = [...connections.values()];
|
|
974
|
+
const reconnects = values.reduce((count, connection) => {
|
|
975
|
+
return connection.closedAt && !connection.createdAt ? count : count;
|
|
976
|
+
}, 0);
|
|
977
|
+
return {
|
|
978
|
+
view: "ws",
|
|
979
|
+
totalEvents: relevant.length,
|
|
980
|
+
connections: values.map((connection) => ({
|
|
981
|
+
id: connection.id,
|
|
982
|
+
url: connection.url ?? null,
|
|
983
|
+
createdAt: connection.createdAt ?? null,
|
|
984
|
+
closedAt: connection.closedAt ?? null,
|
|
985
|
+
sent: connection.sent,
|
|
986
|
+
received: connection.received,
|
|
987
|
+
lastMessages: connection.lastMessages,
|
|
988
|
+
connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
|
|
989
|
+
})),
|
|
990
|
+
reconnects,
|
|
991
|
+
recent: takeRecent(relevant)
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function summarizeConsole(events) {
|
|
995
|
+
const relevant = events.filter(
|
|
996
|
+
(event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
997
|
+
);
|
|
998
|
+
return {
|
|
999
|
+
view: "console",
|
|
1000
|
+
errors: relevant.filter(
|
|
1001
|
+
(event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
1002
|
+
).length,
|
|
1003
|
+
warnings: relevant.filter((event) => event.event === "console.warn").length,
|
|
1004
|
+
logs: relevant.filter((event) => event.event === "console.log").length,
|
|
1005
|
+
recent: takeRecent(relevant)
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function summarizePermissions(events) {
|
|
1009
|
+
const relevant = events.filter(
|
|
1010
|
+
(event) => event.channel === "permission" || event.event.startsWith("permission.")
|
|
1011
|
+
);
|
|
1012
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1013
|
+
for (const event of relevant) {
|
|
1014
|
+
const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
|
|
1015
|
+
const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
|
|
1016
|
+
if (name && state) {
|
|
1017
|
+
latest.set(name, state);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
view: "permissions",
|
|
1022
|
+
states: Object.fromEntries(latest),
|
|
1023
|
+
changes: relevant.filter((event) => event.event === "permission.changed").length,
|
|
1024
|
+
recent: takeRecent(relevant)
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function summarizeMedia(events) {
|
|
1028
|
+
const relevant = events.filter(
|
|
1029
|
+
(event) => event.channel === "media" || event.event.startsWith("media.")
|
|
1030
|
+
);
|
|
1031
|
+
const liveTracks = /* @__PURE__ */ new Map();
|
|
1032
|
+
for (const event of relevant) {
|
|
1033
|
+
const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
|
|
1034
|
+
const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
|
|
1035
|
+
const key = `${kind}:${label}`;
|
|
1036
|
+
if (event.event === "media.track.started") {
|
|
1037
|
+
liveTracks.set(key, kind);
|
|
1038
|
+
}
|
|
1039
|
+
if (event.event === "media.track.ended") {
|
|
1040
|
+
liveTracks.delete(key);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return {
|
|
1044
|
+
view: "media",
|
|
1045
|
+
tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
|
|
1046
|
+
tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
|
|
1047
|
+
playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
|
|
1048
|
+
playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
|
|
1049
|
+
liveTracks: [...liveTracks.values()],
|
|
1050
|
+
recent: takeRecent(relevant)
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function summarizeVoice(events) {
|
|
1054
|
+
const relevant = events.filter(
|
|
1055
|
+
(event) => event.channel === "voice" || event.event.startsWith("voice.")
|
|
1056
|
+
);
|
|
1057
|
+
return {
|
|
1058
|
+
view: "voice",
|
|
1059
|
+
ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
|
|
1060
|
+
notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
|
|
1061
|
+
captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
|
|
1062
|
+
captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
|
|
1063
|
+
detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
|
|
1064
|
+
recent: takeRecent(relevant)
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function summarizeUi(events) {
|
|
1068
|
+
const relevant = events.filter(
|
|
1069
|
+
(event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
|
|
1070
|
+
);
|
|
1071
|
+
return {
|
|
1072
|
+
view: "ui",
|
|
1073
|
+
actions: relevant.filter((event) => event.channel === "action").length,
|
|
1074
|
+
domChanges: relevant.filter((event) => event.channel === "dom").length,
|
|
1075
|
+
recent: takeRecent(relevant)
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function summarizeSession(events) {
|
|
1079
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
1080
|
+
const failedActions = events.filter((event) => event.event === "action.failed").length;
|
|
1081
|
+
for (const event of events) {
|
|
1082
|
+
byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
view: "session",
|
|
1086
|
+
totalEvents: events.length,
|
|
1087
|
+
byChannel: Object.fromEntries(byChannel),
|
|
1088
|
+
failedActions,
|
|
1089
|
+
recent: takeRecent(events)
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/recording/manifest.ts
|
|
1094
|
+
function isCanonicalRecordingManifest(value) {
|
|
1095
|
+
return Boolean(
|
|
1096
|
+
value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
function isLegacyRecordingManifest(value) {
|
|
1100
|
+
return Boolean(
|
|
1101
|
+
value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
function createRecordingManifest(input) {
|
|
1105
|
+
const actions = input.frames.map((frame) => {
|
|
1106
|
+
const actionId = frame.actionId ?? `action-${frame.seq}`;
|
|
1107
|
+
return {
|
|
1108
|
+
id: actionId,
|
|
1109
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1110
|
+
action: frame.action,
|
|
1111
|
+
selector: frame.selector,
|
|
1112
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1113
|
+
value: frame.value,
|
|
1114
|
+
url: frame.url,
|
|
1115
|
+
success: frame.success,
|
|
1116
|
+
durationMs: frame.durationMs,
|
|
1117
|
+
error: frame.error,
|
|
1118
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1119
|
+
pageUrl: frame.pageUrl,
|
|
1120
|
+
pageTitle: frame.pageTitle,
|
|
1121
|
+
coordinates: frame.coordinates,
|
|
1122
|
+
boundingBox: frame.boundingBox
|
|
1123
|
+
};
|
|
1124
|
+
});
|
|
1125
|
+
const screenshots = input.frames.map((frame) => ({
|
|
1126
|
+
id: `shot-${frame.seq}`,
|
|
1127
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1128
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1129
|
+
file: frame.screenshot,
|
|
1130
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1131
|
+
success: frame.success,
|
|
1132
|
+
pageUrl: frame.pageUrl,
|
|
1133
|
+
pageTitle: frame.pageTitle,
|
|
1134
|
+
coordinates: frame.coordinates,
|
|
1135
|
+
boundingBox: frame.boundingBox
|
|
1136
|
+
}));
|
|
1137
|
+
return {
|
|
1138
|
+
version: 2,
|
|
1139
|
+
recordedAt: input.recordedAt,
|
|
1140
|
+
session: {
|
|
1141
|
+
id: input.sessionId,
|
|
1142
|
+
startUrl: input.startUrl,
|
|
1143
|
+
endUrl: input.endUrl,
|
|
1144
|
+
targetId: input.targetId,
|
|
1145
|
+
profile: input.profile
|
|
1146
|
+
},
|
|
1147
|
+
recipe: {
|
|
1148
|
+
steps: input.steps
|
|
1149
|
+
},
|
|
1150
|
+
actions,
|
|
1151
|
+
screenshots,
|
|
1152
|
+
trace: {
|
|
1153
|
+
events: input.traceEvents,
|
|
1154
|
+
summaries: buildTraceSummaries(input.traceEvents)
|
|
1155
|
+
},
|
|
1156
|
+
assertions: input.assertions ?? [],
|
|
1157
|
+
notes: input.notes ?? [],
|
|
1158
|
+
artifacts: {
|
|
1159
|
+
recordingManifest: input.recordingManifest ?? "recording.json",
|
|
1160
|
+
screenshotDir: input.screenshotDir ?? "screenshots/"
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function canonicalizeRecordingArtifact(value) {
|
|
1165
|
+
if (isCanonicalRecordingManifest(value)) {
|
|
1166
|
+
return value;
|
|
1167
|
+
}
|
|
1168
|
+
if (!isLegacyRecordingManifest(value)) {
|
|
1169
|
+
throw new Error("Unsupported recording artifact");
|
|
1170
|
+
}
|
|
1171
|
+
const traceEvents = buildTraceEventsFromLegacy(value);
|
|
1172
|
+
const steps = value.frames.map((frame) => frameToStep(frame));
|
|
1173
|
+
return createRecordingManifest({
|
|
1174
|
+
recordedAt: value.recordedAt,
|
|
1175
|
+
sessionId: value.sessionId,
|
|
1176
|
+
startUrl: value.startUrl,
|
|
1177
|
+
endUrl: value.endUrl,
|
|
1178
|
+
steps,
|
|
1179
|
+
frames: value.frames,
|
|
1180
|
+
traceEvents,
|
|
1181
|
+
notes: ["Converted from legacy recording manifest"]
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
function buildTraceEventsFromLegacy(value) {
|
|
1185
|
+
const events = [];
|
|
1186
|
+
for (const frame of value.frames) {
|
|
1187
|
+
events.push({
|
|
1188
|
+
traceId: frame.actionId ?? `legacy-${frame.seq}`,
|
|
1189
|
+
sessionId: value.sessionId,
|
|
1190
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1191
|
+
elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
|
|
1192
|
+
channel: "action",
|
|
1193
|
+
event: frame.success ? "action.succeeded" : "action.failed",
|
|
1194
|
+
severity: frame.success ? "info" : "error",
|
|
1195
|
+
summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
|
|
1196
|
+
data: {
|
|
1197
|
+
action: frame.action,
|
|
1198
|
+
selector: frame.selector,
|
|
1199
|
+
value: frame.value ?? null,
|
|
1200
|
+
pageUrl: frame.pageUrl ?? null,
|
|
1201
|
+
pageTitle: frame.pageTitle ?? null,
|
|
1202
|
+
screenshot: frame.screenshot
|
|
1203
|
+
},
|
|
1204
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1205
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1206
|
+
selector: frame.selector,
|
|
1207
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1208
|
+
url: frame.pageUrl ?? frame.url
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
return events;
|
|
1212
|
+
}
|
|
1213
|
+
function frameToStep(frame) {
|
|
1214
|
+
switch (frame.action) {
|
|
1215
|
+
case "fill":
|
|
1216
|
+
return { action: "fill", selector: frame.selector, value: frame.value };
|
|
1217
|
+
case "submit":
|
|
1218
|
+
return { action: "submit", selector: frame.selector };
|
|
1219
|
+
case "goto":
|
|
1220
|
+
return { action: "goto", url: frame.url ?? frame.pageUrl };
|
|
1221
|
+
case "press":
|
|
1222
|
+
return { action: "press", key: frame.value ?? "Enter" };
|
|
1223
|
+
default:
|
|
1224
|
+
return { action: "click", selector: frame.selector };
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// src/trace/script.ts
|
|
1229
|
+
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
1230
|
+
var TRACE_SCRIPT = `
|
|
1231
|
+
(() => {
|
|
1232
|
+
if (window.__bpTraceInstalled) return;
|
|
1233
|
+
window.__bpTraceInstalled = true;
|
|
1234
|
+
|
|
1235
|
+
const binding = globalThis.${TRACE_BINDING_NAME};
|
|
1236
|
+
if (typeof binding !== 'function') return;
|
|
1237
|
+
|
|
1238
|
+
const emit = (event, data = {}, severity = 'info', summary) => {
|
|
1239
|
+
try {
|
|
1240
|
+
globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
|
|
1241
|
+
const payload = {
|
|
1242
|
+
event,
|
|
1243
|
+
severity,
|
|
1244
|
+
summary: summary || event,
|
|
1245
|
+
ts: Date.now(),
|
|
1246
|
+
data,
|
|
1247
|
+
};
|
|
1248
|
+
globalThis.__bpTraceRecentEvents.push(payload);
|
|
1249
|
+
if (globalThis.__bpTraceRecentEvents.length > 200) {
|
|
1250
|
+
globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
|
|
1251
|
+
}
|
|
1252
|
+
binding(JSON.stringify(payload));
|
|
1253
|
+
} catch {}
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
const patchWebSocket = () => {
|
|
1257
|
+
const NativeWebSocket = window.WebSocket;
|
|
1258
|
+
if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
|
|
1259
|
+
window.__bpTraceWebSocketInstalled = true;
|
|
1260
|
+
|
|
1261
|
+
const nextId = () => Math.random().toString(36).slice(2, 10);
|
|
1262
|
+
|
|
1263
|
+
const patchInstance = (socket, urlValue) => {
|
|
1264
|
+
if (!socket || socket.__bpTracePatched) return socket;
|
|
1265
|
+
socket.__bpTracePatched = true;
|
|
1266
|
+
socket.__bpTraceId = socket.__bpTraceId || nextId();
|
|
1267
|
+
socket.__bpTraceUrl = String(urlValue || socket.url || '');
|
|
1268
|
+
globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
|
|
1269
|
+
globalThis.__bpTrackedWebSockets.add(socket);
|
|
1270
|
+
|
|
1271
|
+
emit(
|
|
1272
|
+
'ws.connection.created',
|
|
1273
|
+
{ connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
|
|
1274
|
+
'info',
|
|
1275
|
+
'WebSocket opened ' + socket.__bpTraceUrl
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
const originalSend = socket.send;
|
|
1279
|
+
socket.send = function(data) {
|
|
1280
|
+
const payload =
|
|
1281
|
+
typeof data === 'string'
|
|
1282
|
+
? data
|
|
1283
|
+
: data && typeof data.toString === 'function'
|
|
1284
|
+
? data.toString()
|
|
1285
|
+
: '[binary]';
|
|
1286
|
+
emit(
|
|
1287
|
+
'ws.frame.sent',
|
|
1288
|
+
{
|
|
1289
|
+
connectionId: socket.__bpTraceId,
|
|
1290
|
+
url: socket.__bpTraceUrl,
|
|
1291
|
+
payload,
|
|
1292
|
+
length: payload.length,
|
|
1293
|
+
},
|
|
1294
|
+
'info',
|
|
1295
|
+
'WebSocket frame sent'
|
|
1296
|
+
);
|
|
1297
|
+
return originalSend.call(this, data);
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
socket.addEventListener('message', (event) => {
|
|
1301
|
+
if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const data = event && 'data' in event ? event.data : '';
|
|
1305
|
+
const payload =
|
|
1306
|
+
typeof data === 'string'
|
|
1307
|
+
? data
|
|
1308
|
+
: data && typeof data.toString === 'function'
|
|
1309
|
+
? data.toString()
|
|
1310
|
+
: '[binary]';
|
|
1311
|
+
emit(
|
|
1312
|
+
'ws.frame.received',
|
|
1313
|
+
{
|
|
1314
|
+
connectionId: socket.__bpTraceId,
|
|
1315
|
+
url: socket.__bpTraceUrl,
|
|
1316
|
+
payload,
|
|
1317
|
+
length: payload.length,
|
|
1318
|
+
},
|
|
1319
|
+
'info',
|
|
1320
|
+
'WebSocket frame received'
|
|
1321
|
+
);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
socket.addEventListener('close', (event) => {
|
|
1325
|
+
if (socket.__bpTraceClosed) {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
socket.__bpTraceClosed = true;
|
|
1329
|
+
try {
|
|
1330
|
+
globalThis.__bpTrackedWebSockets.delete(socket);
|
|
1331
|
+
} catch {}
|
|
1332
|
+
emit(
|
|
1333
|
+
'ws.connection.closed',
|
|
1334
|
+
{
|
|
1335
|
+
connectionId: socket.__bpTraceId,
|
|
1336
|
+
url: socket.__bpTraceUrl,
|
|
1337
|
+
code: event.code,
|
|
1338
|
+
reason: event.reason,
|
|
1339
|
+
},
|
|
1340
|
+
'warn',
|
|
1341
|
+
'WebSocket closed'
|
|
1342
|
+
);
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
return socket;
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
const TracedWebSocket = function(url, protocols) {
|
|
1349
|
+
return arguments.length > 1
|
|
1350
|
+
? patchInstance(new NativeWebSocket(url, protocols), url)
|
|
1351
|
+
: patchInstance(new NativeWebSocket(url), url);
|
|
1352
|
+
};
|
|
1353
|
+
TracedWebSocket.prototype = NativeWebSocket.prototype;
|
|
1354
|
+
Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
|
|
1355
|
+
window.WebSocket = TracedWebSocket;
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
window.addEventListener('error', (errorEvent) => {
|
|
1359
|
+
emit(
|
|
1360
|
+
'runtime.exception',
|
|
1361
|
+
{
|
|
1362
|
+
message: errorEvent.message,
|
|
1363
|
+
filename: errorEvent.filename,
|
|
1364
|
+
line: errorEvent.lineno,
|
|
1365
|
+
column: errorEvent.colno,
|
|
1366
|
+
},
|
|
1367
|
+
'error',
|
|
1368
|
+
errorEvent.message || 'Uncaught error'
|
|
1369
|
+
);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
1373
|
+
const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
|
|
1374
|
+
emit('runtime.unhandledRejection', { reason }, 'error', reason);
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
const patchPermissions = async () => {
|
|
1378
|
+
if (!navigator.permissions || !navigator.permissions.query) return;
|
|
1379
|
+
|
|
1380
|
+
const names = ['geolocation', 'microphone', 'camera', 'notifications'];
|
|
1381
|
+
for (const name of names) {
|
|
1382
|
+
try {
|
|
1383
|
+
const status = await navigator.permissions.query({ name });
|
|
1384
|
+
emit(
|
|
1385
|
+
'permission.state',
|
|
1386
|
+
{ name, state: status.state },
|
|
1387
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1388
|
+
name + ': ' + status.state
|
|
1389
|
+
);
|
|
1390
|
+
status.addEventListener('change', () => {
|
|
1391
|
+
emit(
|
|
1392
|
+
'permission.changed',
|
|
1393
|
+
{ name, state: status.state },
|
|
1394
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1395
|
+
name + ': ' + status.state
|
|
1396
|
+
);
|
|
1397
|
+
});
|
|
1398
|
+
} catch {}
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
const patchMediaElement = (element) => {
|
|
1403
|
+
if (!element || element.__bpTracePatched) return;
|
|
1404
|
+
element.__bpTracePatched = true;
|
|
1405
|
+
|
|
1406
|
+
element.addEventListener('play', () => {
|
|
1407
|
+
emit(
|
|
1408
|
+
'media.playback.started',
|
|
1409
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1410
|
+
'info',
|
|
1411
|
+
'Media playback started'
|
|
1412
|
+
);
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
const onStop = () => {
|
|
1416
|
+
emit(
|
|
1417
|
+
'media.playback.stopped',
|
|
1418
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1419
|
+
'warn',
|
|
1420
|
+
'Media playback stopped'
|
|
1421
|
+
);
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
element.addEventListener('pause', onStop);
|
|
1425
|
+
element.addEventListener('ended', onStop);
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
const patchMediaElements = () => {
|
|
1429
|
+
document.querySelectorAll('audio,video').forEach(patchMediaElement);
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
patchMediaElements();
|
|
1433
|
+
patchWebSocket();
|
|
1434
|
+
|
|
1435
|
+
if (document.documentElement) {
|
|
1436
|
+
const observer = new MutationObserver(() => {
|
|
1437
|
+
patchMediaElements();
|
|
1438
|
+
});
|
|
1439
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
1443
|
+
const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
|
1444
|
+
navigator.mediaDevices.getUserMedia = async (...args) => {
|
|
1445
|
+
emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
|
|
1446
|
+
try {
|
|
1447
|
+
const stream = await original(...args);
|
|
1448
|
+
const tracks = stream.getTracks();
|
|
1449
|
+
|
|
1450
|
+
for (const track of tracks) {
|
|
1451
|
+
emit(
|
|
1452
|
+
'media.track.started',
|
|
1453
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1454
|
+
'info',
|
|
1455
|
+
track.kind + ' track started'
|
|
1456
|
+
);
|
|
1457
|
+
track.addEventListener('ended', () => {
|
|
1458
|
+
emit(
|
|
1459
|
+
'media.track.ended',
|
|
1460
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1461
|
+
'warn',
|
|
1462
|
+
track.kind + ' track ended'
|
|
1463
|
+
);
|
|
1464
|
+
emit(
|
|
1465
|
+
'voice.capture.stopped',
|
|
1466
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1467
|
+
'warn',
|
|
1468
|
+
'Voice capture stopped'
|
|
1469
|
+
);
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
emit(
|
|
1474
|
+
'voice.capture.detectedAudio',
|
|
1475
|
+
{ trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
|
|
1476
|
+
'info',
|
|
1477
|
+
'Voice capture detected audio'
|
|
1478
|
+
);
|
|
1479
|
+
|
|
1480
|
+
return stream;
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
emit(
|
|
1483
|
+
'voice.pipeline.notReady',
|
|
1484
|
+
{ message: String(error && error.message ? error.message : error) },
|
|
1485
|
+
'error',
|
|
1486
|
+
String(error && error.message ? error.message : error)
|
|
1487
|
+
);
|
|
1488
|
+
throw error;
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
document.addEventListener('visibilitychange', () => {
|
|
1494
|
+
emit(
|
|
1495
|
+
'dom.state.changed',
|
|
1496
|
+
{ visibilityState: document.visibilityState },
|
|
1497
|
+
document.visibilityState === 'hidden' ? 'warn' : 'info',
|
|
1498
|
+
'Visibility ' + document.visibilityState
|
|
1499
|
+
);
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
patchPermissions();
|
|
1503
|
+
emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
|
|
1504
|
+
})();
|
|
1505
|
+
`;
|
|
1506
|
+
|
|
1507
|
+
// src/trace/model.ts
|
|
1508
|
+
function createTraceId(prefix = "evt") {
|
|
1509
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1510
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1511
|
+
}
|
|
1512
|
+
function normalizeTraceEvent(event) {
|
|
1513
|
+
return {
|
|
1514
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1515
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1516
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
1517
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
1518
|
+
data: event.data ?? {},
|
|
1519
|
+
...event
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function inferSeverity(eventName) {
|
|
1523
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1524
|
+
return "error";
|
|
1525
|
+
}
|
|
1526
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1527
|
+
return "warn";
|
|
1528
|
+
}
|
|
1529
|
+
return "info";
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// src/trace/live.ts
|
|
1533
|
+
function globToRegex(pattern) {
|
|
1534
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1535
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1536
|
+
return new RegExp(`^${withWildcards}$`);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
689
1539
|
// src/actions/executor.ts
|
|
690
1540
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1541
|
+
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
1542
|
+
"wait",
|
|
1543
|
+
"snapshot",
|
|
1544
|
+
"forms",
|
|
1545
|
+
"text",
|
|
1546
|
+
"screenshot"
|
|
1547
|
+
];
|
|
1548
|
+
function loadExistingRecording(manifestPath) {
|
|
1549
|
+
try {
|
|
1550
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1551
|
+
if (raw.version === 1) {
|
|
1552
|
+
const legacy = raw;
|
|
1553
|
+
return {
|
|
1554
|
+
frames: Array.isArray(legacy.frames) ? legacy.frames : [],
|
|
1555
|
+
traceEvents: [],
|
|
1556
|
+
recordedAt: legacy.recordedAt,
|
|
1557
|
+
startUrl: legacy.startUrl
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
const artifact = canonicalizeRecordingArtifact(raw);
|
|
1561
|
+
const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
|
|
1562
|
+
const frames = artifact.actions.map((action, index) => {
|
|
1563
|
+
const screenshot = screenshotsByAction.get(action.id);
|
|
1564
|
+
return {
|
|
1565
|
+
seq: index + 1,
|
|
1566
|
+
timestamp: Date.parse(action.ts),
|
|
1567
|
+
action: action.action,
|
|
1568
|
+
selector: action.selector,
|
|
1569
|
+
selectorUsed: action.selectorUsed,
|
|
1570
|
+
value: action.value,
|
|
1571
|
+
url: action.url,
|
|
1572
|
+
coordinates: action.coordinates,
|
|
1573
|
+
boundingBox: action.boundingBox,
|
|
1574
|
+
success: action.success,
|
|
1575
|
+
durationMs: action.durationMs,
|
|
1576
|
+
error: action.error,
|
|
1577
|
+
screenshot: screenshot?.file ?? "",
|
|
1578
|
+
pageUrl: action.pageUrl,
|
|
1579
|
+
pageTitle: action.pageTitle,
|
|
1580
|
+
stepIndex: action.stepIndex,
|
|
1581
|
+
actionId: action.id
|
|
1582
|
+
};
|
|
1583
|
+
});
|
|
1584
|
+
return {
|
|
1585
|
+
frames,
|
|
1586
|
+
traceEvents: artifact.trace.events,
|
|
1587
|
+
recordedAt: artifact.recordedAt,
|
|
1588
|
+
startUrl: artifact.session.startUrl
|
|
1589
|
+
};
|
|
1590
|
+
} catch {
|
|
1591
|
+
return { frames: [], traceEvents: [] };
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
691
1594
|
function classifyFailure(error) {
|
|
692
1595
|
if (error instanceof ElementNotFoundError) {
|
|
693
1596
|
return { reason: "missing" };
|
|
@@ -767,6 +1670,12 @@ var BatchExecutor = class {
|
|
|
767
1670
|
const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
|
|
768
1671
|
const results = [];
|
|
769
1672
|
const startTime = Date.now();
|
|
1673
|
+
const recording = options.record ? this.createRecordingContext(options.record) : null;
|
|
1674
|
+
if (steps.some((step) => step.action === "waitForWsMessage")) {
|
|
1675
|
+
await this.ensureTraceHooks();
|
|
1676
|
+
}
|
|
1677
|
+
const startUrl = recording ? await this.getPageUrlSafe() : "";
|
|
1678
|
+
let stoppedAtIndex;
|
|
770
1679
|
for (let i = 0; i < steps.length; i++) {
|
|
771
1680
|
const step = steps[i];
|
|
772
1681
|
const stepStart = Date.now();
|
|
@@ -774,13 +1683,34 @@ var BatchExecutor = class {
|
|
|
774
1683
|
const retryDelay = step.retryDelay ?? 500;
|
|
775
1684
|
let lastError;
|
|
776
1685
|
let succeeded = false;
|
|
1686
|
+
if (recording) {
|
|
1687
|
+
recording.traceEvents.push(
|
|
1688
|
+
normalizeTraceEvent({
|
|
1689
|
+
traceId: createTraceId("action"),
|
|
1690
|
+
elapsedMs: Date.now() - startTime,
|
|
1691
|
+
channel: "action",
|
|
1692
|
+
event: "action.started",
|
|
1693
|
+
summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
|
|
1694
|
+
data: {
|
|
1695
|
+
action: step.action,
|
|
1696
|
+
selector: step.selector ?? null,
|
|
1697
|
+
url: step.url ?? null
|
|
1698
|
+
},
|
|
1699
|
+
actionId: `action-${i + 1}`,
|
|
1700
|
+
stepIndex: i,
|
|
1701
|
+
selector: step.selector,
|
|
1702
|
+
url: step.url
|
|
1703
|
+
})
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
777
1706
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
778
1707
|
if (attempt > 0) {
|
|
779
1708
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
780
1709
|
}
|
|
781
1710
|
try {
|
|
1711
|
+
this.page.resetLastActionPosition();
|
|
782
1712
|
const result = await this.executeStep(step, timeout);
|
|
783
|
-
|
|
1713
|
+
const stepResult = {
|
|
784
1714
|
index: i,
|
|
785
1715
|
action: step.action,
|
|
786
1716
|
selector: step.selector,
|
|
@@ -788,8 +1718,37 @@ var BatchExecutor = class {
|
|
|
788
1718
|
success: true,
|
|
789
1719
|
durationMs: Date.now() - stepStart,
|
|
790
1720
|
result: result.value,
|
|
791
|
-
text: result.text
|
|
792
|
-
|
|
1721
|
+
text: result.text,
|
|
1722
|
+
timestamp: Date.now(),
|
|
1723
|
+
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
1724
|
+
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
1725
|
+
};
|
|
1726
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
1727
|
+
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1728
|
+
}
|
|
1729
|
+
if (recording) {
|
|
1730
|
+
recording.traceEvents.push(
|
|
1731
|
+
normalizeTraceEvent({
|
|
1732
|
+
traceId: createTraceId("action"),
|
|
1733
|
+
elapsedMs: Date.now() - startTime,
|
|
1734
|
+
channel: "action",
|
|
1735
|
+
event: "action.succeeded",
|
|
1736
|
+
summary: `${step.action} succeeded`,
|
|
1737
|
+
data: {
|
|
1738
|
+
action: step.action,
|
|
1739
|
+
selector: step.selector ?? null,
|
|
1740
|
+
selectorUsed: result.selectorUsed ?? null,
|
|
1741
|
+
durationMs: Date.now() - stepStart
|
|
1742
|
+
},
|
|
1743
|
+
actionId: `action-${i + 1}`,
|
|
1744
|
+
stepIndex: i,
|
|
1745
|
+
selector: step.selector,
|
|
1746
|
+
selectorUsed: result.selectorUsed,
|
|
1747
|
+
url: step.url
|
|
1748
|
+
})
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
results.push(stepResult);
|
|
793
1752
|
succeeded = true;
|
|
794
1753
|
break;
|
|
795
1754
|
} catch (error) {
|
|
@@ -810,7 +1769,7 @@ var BatchExecutor = class {
|
|
|
810
1769
|
} catch {
|
|
811
1770
|
}
|
|
812
1771
|
}
|
|
813
|
-
|
|
1772
|
+
const failedResult = {
|
|
814
1773
|
index: i,
|
|
815
1774
|
action: step.action,
|
|
816
1775
|
selector: step.selector,
|
|
@@ -820,25 +1779,183 @@ var BatchExecutor = class {
|
|
|
820
1779
|
hints,
|
|
821
1780
|
failureReason: reason,
|
|
822
1781
|
coveringElement,
|
|
823
|
-
suggestion: getSuggestion(reason)
|
|
824
|
-
|
|
1782
|
+
suggestion: getSuggestion(reason),
|
|
1783
|
+
timestamp: Date.now()
|
|
1784
|
+
};
|
|
1785
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
1786
|
+
await this.captureRecordingFrame(step, failedResult, recording);
|
|
1787
|
+
}
|
|
1788
|
+
if (recording) {
|
|
1789
|
+
recording.traceEvents.push(
|
|
1790
|
+
normalizeTraceEvent({
|
|
1791
|
+
traceId: createTraceId("action"),
|
|
1792
|
+
elapsedMs: Date.now() - startTime,
|
|
1793
|
+
channel: "action",
|
|
1794
|
+
event: "action.failed",
|
|
1795
|
+
severity: "error",
|
|
1796
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
1797
|
+
data: {
|
|
1798
|
+
action: step.action,
|
|
1799
|
+
selector: step.selector ?? null,
|
|
1800
|
+
error: errorMessage,
|
|
1801
|
+
reason
|
|
1802
|
+
},
|
|
1803
|
+
actionId: `action-${i + 1}`,
|
|
1804
|
+
stepIndex: i,
|
|
1805
|
+
selector: step.selector,
|
|
1806
|
+
url: step.url
|
|
1807
|
+
})
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
results.push(failedResult);
|
|
825
1811
|
if (onFail === "stop" && !step.optional) {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
stoppedAtIndex: i,
|
|
829
|
-
steps: results,
|
|
830
|
-
totalDurationMs: Date.now() - startTime
|
|
831
|
-
};
|
|
1812
|
+
stoppedAtIndex = i;
|
|
1813
|
+
break;
|
|
832
1814
|
}
|
|
833
1815
|
}
|
|
834
1816
|
}
|
|
835
|
-
const
|
|
1817
|
+
const totalDurationMs = Date.now() - startTime;
|
|
1818
|
+
const allSuccess = stoppedAtIndex === void 0 && results.every((result) => result.success || steps[result.index]?.optional);
|
|
1819
|
+
let recordingManifest;
|
|
1820
|
+
if (recording) {
|
|
1821
|
+
recordingManifest = await this.writeRecordingManifest(
|
|
1822
|
+
recording,
|
|
1823
|
+
startTime,
|
|
1824
|
+
startUrl,
|
|
1825
|
+
allSuccess,
|
|
1826
|
+
steps
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
836
1829
|
return {
|
|
837
1830
|
success: allSuccess,
|
|
1831
|
+
stoppedAtIndex,
|
|
838
1832
|
steps: results,
|
|
839
|
-
totalDurationMs
|
|
1833
|
+
totalDurationMs,
|
|
1834
|
+
recordingManifest
|
|
840
1835
|
};
|
|
841
1836
|
}
|
|
1837
|
+
createRecordingContext(record) {
|
|
1838
|
+
const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
|
|
1839
|
+
const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
|
|
1840
|
+
const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
|
|
1841
|
+
const existing = loadExistingRecording(manifestPath);
|
|
1842
|
+
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
1843
|
+
return {
|
|
1844
|
+
baseDir,
|
|
1845
|
+
screenshotDir,
|
|
1846
|
+
sessionId: record.sessionId ?? this.page.targetId,
|
|
1847
|
+
frames: existing.frames,
|
|
1848
|
+
traceEvents: existing.traceEvents,
|
|
1849
|
+
format: record.format ?? "webp",
|
|
1850
|
+
quality: Math.max(0, Math.min(100, record.quality ?? 40)),
|
|
1851
|
+
highlights: record.highlights !== false,
|
|
1852
|
+
skipActions: new Set(record.skipActions ?? DEFAULT_RECORDING_SKIP_ACTIONS)
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
async getPageUrlSafe() {
|
|
1856
|
+
try {
|
|
1857
|
+
return await this.page.url();
|
|
1858
|
+
} catch {
|
|
1859
|
+
return "";
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Capture a recording screenshot frame with optional highlight overlay
|
|
1864
|
+
*/
|
|
1865
|
+
async captureRecordingFrame(step, stepResult, recording) {
|
|
1866
|
+
const targetMetadata = this.page.getLastActionTargetMetadata();
|
|
1867
|
+
let highlightInjected = false;
|
|
1868
|
+
try {
|
|
1869
|
+
const ts = Date.now();
|
|
1870
|
+
const seq = String(recording.frames.length + 1).padStart(4, "0");
|
|
1871
|
+
const filename = `${seq}-${ts}-${stepResult.action}.${recording.format}`;
|
|
1872
|
+
const filepath = (0, import_node_path.join)(recording.screenshotDir, filename);
|
|
1873
|
+
if (recording.highlights) {
|
|
1874
|
+
const kind = stepToHighlightKind(stepResult);
|
|
1875
|
+
if (kind) {
|
|
1876
|
+
await injectActionHighlight(this.page, {
|
|
1877
|
+
kind,
|
|
1878
|
+
bbox: stepResult.boundingBox,
|
|
1879
|
+
point: stepResult.coordinates,
|
|
1880
|
+
label: getHighlightLabel(step, stepResult, targetMetadata)
|
|
1881
|
+
});
|
|
1882
|
+
highlightInjected = true;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const base64 = await this.page.screenshot({
|
|
1886
|
+
format: recording.format,
|
|
1887
|
+
quality: recording.quality
|
|
1888
|
+
});
|
|
1889
|
+
const buffer = Buffer.from(base64, "base64");
|
|
1890
|
+
fs.writeFileSync(filepath, buffer);
|
|
1891
|
+
stepResult.screenshotPath = filepath;
|
|
1892
|
+
let pageUrl;
|
|
1893
|
+
let pageTitle;
|
|
1894
|
+
try {
|
|
1895
|
+
pageUrl = await this.page.url();
|
|
1896
|
+
pageTitle = await this.page.title();
|
|
1897
|
+
} catch {
|
|
1898
|
+
}
|
|
1899
|
+
recording.frames.push({
|
|
1900
|
+
seq: recording.frames.length + 1,
|
|
1901
|
+
timestamp: ts,
|
|
1902
|
+
action: stepResult.action,
|
|
1903
|
+
selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
|
|
1904
|
+
selectorUsed: stepResult.selectorUsed,
|
|
1905
|
+
value: redactValueForRecording(
|
|
1906
|
+
typeof step.value === "string" ? step.value : void 0,
|
|
1907
|
+
targetMetadata
|
|
1908
|
+
),
|
|
1909
|
+
url: step.url,
|
|
1910
|
+
coordinates: stepResult.coordinates,
|
|
1911
|
+
boundingBox: stepResult.boundingBox,
|
|
1912
|
+
success: stepResult.success,
|
|
1913
|
+
durationMs: stepResult.durationMs,
|
|
1914
|
+
error: stepResult.error,
|
|
1915
|
+
screenshot: filename,
|
|
1916
|
+
pageUrl,
|
|
1917
|
+
pageTitle,
|
|
1918
|
+
stepIndex: stepResult.index,
|
|
1919
|
+
actionId: `action-${stepResult.index + 1}`
|
|
1920
|
+
});
|
|
1921
|
+
} catch {
|
|
1922
|
+
} finally {
|
|
1923
|
+
if (recording.highlights || highlightInjected) {
|
|
1924
|
+
await removeActionHighlight(this.page);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Write recording manifest to disk
|
|
1930
|
+
*/
|
|
1931
|
+
async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
|
|
1932
|
+
let endUrl = startUrl;
|
|
1933
|
+
try {
|
|
1934
|
+
endUrl = await this.page.url();
|
|
1935
|
+
} catch {
|
|
1936
|
+
}
|
|
1937
|
+
const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
|
|
1938
|
+
let recordedAt = new Date(startTime).toISOString();
|
|
1939
|
+
let originalStartUrl = startUrl;
|
|
1940
|
+
const existing = loadExistingRecording(manifestPath);
|
|
1941
|
+
if (existing.recordedAt) recordedAt = existing.recordedAt;
|
|
1942
|
+
if (existing.startUrl) originalStartUrl = existing.startUrl;
|
|
1943
|
+
const manifest = createRecordingManifest({
|
|
1944
|
+
recordedAt,
|
|
1945
|
+
sessionId: recording.sessionId,
|
|
1946
|
+
startUrl: originalStartUrl,
|
|
1947
|
+
endUrl,
|
|
1948
|
+
targetId: this.page.targetId,
|
|
1949
|
+
steps,
|
|
1950
|
+
frames: recording.frames,
|
|
1951
|
+
traceEvents: recording.traceEvents,
|
|
1952
|
+
notes: success ? [] : ["Replay ended with at least one failed action."],
|
|
1953
|
+
recordingManifest: "recording.json",
|
|
1954
|
+
screenshotDir: "screenshots/"
|
|
1955
|
+
});
|
|
1956
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1957
|
+
return manifestPath;
|
|
1958
|
+
}
|
|
842
1959
|
/**
|
|
843
1960
|
* Execute a single step
|
|
844
1961
|
*/
|
|
@@ -1118,6 +2235,39 @@ var BatchExecutor = class {
|
|
|
1118
2235
|
}
|
|
1119
2236
|
return { selectorUsed: usedSelector, value: actual };
|
|
1120
2237
|
}
|
|
2238
|
+
case "waitForWsMessage": {
|
|
2239
|
+
if (typeof step.match !== "string") {
|
|
2240
|
+
throw new Error("waitForWsMessage requires match");
|
|
2241
|
+
}
|
|
2242
|
+
const message = await this.waitForWsMessage(step.match, step.where, timeout);
|
|
2243
|
+
return { value: message };
|
|
2244
|
+
}
|
|
2245
|
+
case "assertNoConsoleErrors": {
|
|
2246
|
+
await this.assertNoConsoleErrors(step.windowMs ?? timeout);
|
|
2247
|
+
return {};
|
|
2248
|
+
}
|
|
2249
|
+
case "assertTextChanged": {
|
|
2250
|
+
const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
|
|
2251
|
+
if (typeof step.to !== "string") {
|
|
2252
|
+
throw new Error("assertTextChanged requires to");
|
|
2253
|
+
}
|
|
2254
|
+
const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
|
|
2255
|
+
return { selectorUsed: selector, text };
|
|
2256
|
+
}
|
|
2257
|
+
case "assertPermission": {
|
|
2258
|
+
if (!step.name || !step.state) {
|
|
2259
|
+
throw new Error("assertPermission requires name and state");
|
|
2260
|
+
}
|
|
2261
|
+
const permission = await this.assertPermission(step.name, step.state);
|
|
2262
|
+
return { value: permission };
|
|
2263
|
+
}
|
|
2264
|
+
case "assertMediaTrackLive": {
|
|
2265
|
+
if (!step.kind) {
|
|
2266
|
+
throw new Error("assertMediaTrackLive requires kind");
|
|
2267
|
+
}
|
|
2268
|
+
const media = await this.assertMediaTrackLive(step.kind);
|
|
2269
|
+
return { value: media };
|
|
2270
|
+
}
|
|
1121
2271
|
default: {
|
|
1122
2272
|
const action = step.action;
|
|
1123
2273
|
const aliases = {
|
|
@@ -1171,7 +2321,7 @@ var BatchExecutor = class {
|
|
|
1171
2321
|
};
|
|
1172
2322
|
const suggestion = aliases[action.toLowerCase()];
|
|
1173
2323
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
1174
|
-
const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, forms, screenshot, evaluate, text, newTab, closeTab, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
|
|
2324
|
+
const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, forms, screenshot, evaluate, text, newTab, closeTab, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue, waitForWsMessage, assertNoConsoleErrors, assertTextChanged, assertPermission, assertMediaTrackLive";
|
|
1175
2325
|
throw new Error(`Unknown action "${action}".${hint}
|
|
1176
2326
|
|
|
1177
2327
|
Valid actions: ${valid}`);
|
|
@@ -1187,6 +2337,233 @@ Valid actions: ${valid}`);
|
|
|
1187
2337
|
if (matched) return matched;
|
|
1188
2338
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
1189
2339
|
}
|
|
2340
|
+
async ensureTraceHooks() {
|
|
2341
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2342
|
+
await this.page.cdpClient.send("Page.enable");
|
|
2343
|
+
await this.page.cdpClient.send("Network.enable");
|
|
2344
|
+
try {
|
|
2345
|
+
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
2346
|
+
} catch {
|
|
2347
|
+
}
|
|
2348
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
|
|
2349
|
+
await this.page.cdpClient.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
|
|
2350
|
+
}
|
|
2351
|
+
async waitForWsMessage(match, where, timeout) {
|
|
2352
|
+
await this.ensureTraceHooks();
|
|
2353
|
+
const regex = globToRegex(match);
|
|
2354
|
+
const wsUrls = /* @__PURE__ */ new Map();
|
|
2355
|
+
const recentMatch = await this.findRecentWsMessage(regex, where);
|
|
2356
|
+
if (recentMatch) {
|
|
2357
|
+
return recentMatch;
|
|
2358
|
+
}
|
|
2359
|
+
return new Promise((resolve, reject) => {
|
|
2360
|
+
const cleanup = () => {
|
|
2361
|
+
this.page.cdpClient.off("Network.webSocketCreated", onCreated);
|
|
2362
|
+
this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
|
|
2363
|
+
this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
|
|
2364
|
+
clearTimeout(timer);
|
|
2365
|
+
};
|
|
2366
|
+
const onCreated = (params) => {
|
|
2367
|
+
wsUrls.set(String(params["requestId"] ?? ""), String(params["url"] ?? ""));
|
|
2368
|
+
};
|
|
2369
|
+
const onFrame = (params) => {
|
|
2370
|
+
const requestId = String(params["requestId"] ?? "");
|
|
2371
|
+
const response = params["response"] ?? {};
|
|
2372
|
+
const payload = String(response.payloadData ?? "");
|
|
2373
|
+
const url = wsUrls.get(requestId) ?? "";
|
|
2374
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
cleanup();
|
|
2381
|
+
resolve({ requestId, url, payload });
|
|
2382
|
+
};
|
|
2383
|
+
const onBinding = (params) => {
|
|
2384
|
+
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
try {
|
|
2388
|
+
const parsed = JSON.parse(String(params["payload"] ?? ""));
|
|
2389
|
+
if (parsed.event !== "ws.frame.received") {
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const data = parsed.data ?? {};
|
|
2393
|
+
const payload = String(data["payload"] ?? "");
|
|
2394
|
+
const url = String(data["url"] ?? "");
|
|
2395
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
cleanup();
|
|
2402
|
+
resolve({
|
|
2403
|
+
requestId: String(data["connectionId"] ?? ""),
|
|
2404
|
+
url,
|
|
2405
|
+
payload
|
|
2406
|
+
});
|
|
2407
|
+
} catch {
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
const timer = setTimeout(() => {
|
|
2411
|
+
cleanup();
|
|
2412
|
+
reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
|
|
2413
|
+
}, timeout);
|
|
2414
|
+
this.page.cdpClient.on("Network.webSocketCreated", onCreated);
|
|
2415
|
+
this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
|
|
2416
|
+
this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
payloadMatchesWhere(payload, where) {
|
|
2420
|
+
try {
|
|
2421
|
+
const parsed = JSON.parse(payload);
|
|
2422
|
+
return Object.entries(where).every(([key, expected]) => {
|
|
2423
|
+
const actual = key.split(".").reduce((current, part) => {
|
|
2424
|
+
if (!current || typeof current !== "object") {
|
|
2425
|
+
return void 0;
|
|
2426
|
+
}
|
|
2427
|
+
return current[part];
|
|
2428
|
+
}, parsed);
|
|
2429
|
+
return actual === expected;
|
|
2430
|
+
});
|
|
2431
|
+
} catch {
|
|
2432
|
+
return false;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
async findRecentWsMessage(regex, where) {
|
|
2436
|
+
const recent = await this.page.evaluate(
|
|
2437
|
+
"(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
|
|
2438
|
+
);
|
|
2439
|
+
if (!Array.isArray(recent)) {
|
|
2440
|
+
return null;
|
|
2441
|
+
}
|
|
2442
|
+
for (let i = recent.length - 1; i >= 0; i--) {
|
|
2443
|
+
const entry = recent[i];
|
|
2444
|
+
if (!entry || typeof entry !== "object") {
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
const event = String(entry["event"] ?? "");
|
|
2448
|
+
if (event !== "ws.frame.received") {
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
const data = entry["data"] ?? {};
|
|
2452
|
+
const payload = String(data["payload"] ?? "");
|
|
2453
|
+
const url = String(data["url"] ?? "");
|
|
2454
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
return {
|
|
2461
|
+
requestId: String(data["connectionId"] ?? ""),
|
|
2462
|
+
url,
|
|
2463
|
+
payload
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
async assertNoConsoleErrors(windowMs) {
|
|
2469
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2470
|
+
return new Promise((resolve, reject) => {
|
|
2471
|
+
const errors = [];
|
|
2472
|
+
const cleanup = () => {
|
|
2473
|
+
this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
|
|
2474
|
+
this.page.cdpClient.off("Runtime.exceptionThrown", onException);
|
|
2475
|
+
clearTimeout(timer);
|
|
2476
|
+
};
|
|
2477
|
+
const onConsole = (params) => {
|
|
2478
|
+
if (params["type"] !== "error") {
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2482
|
+
errors.push(
|
|
2483
|
+
args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
|
|
2484
|
+
);
|
|
2485
|
+
};
|
|
2486
|
+
const onException = (params) => {
|
|
2487
|
+
const details = params["exceptionDetails"] ?? {};
|
|
2488
|
+
errors.push(String(details["text"] ?? "Runtime exception"));
|
|
2489
|
+
};
|
|
2490
|
+
const timer = setTimeout(() => {
|
|
2491
|
+
cleanup();
|
|
2492
|
+
if (errors.length > 0) {
|
|
2493
|
+
reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
resolve();
|
|
2497
|
+
}, windowMs);
|
|
2498
|
+
this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
|
|
2499
|
+
this.page.cdpClient.on("Runtime.exceptionThrown", onException);
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
async assertTextChanged(selector, from, to, timeout) {
|
|
2503
|
+
const initialText = from ?? await this.page.text(selector);
|
|
2504
|
+
const deadline = Date.now() + timeout;
|
|
2505
|
+
while (Date.now() < deadline) {
|
|
2506
|
+
const text = await this.page.text(selector);
|
|
2507
|
+
if (text !== initialText && text.includes(to)) {
|
|
2508
|
+
return text;
|
|
2509
|
+
}
|
|
2510
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2511
|
+
}
|
|
2512
|
+
throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
|
|
2513
|
+
}
|
|
2514
|
+
async assertPermission(name, state) {
|
|
2515
|
+
const result = await this.page.evaluate(
|
|
2516
|
+
`(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
|
|
2517
|
+
);
|
|
2518
|
+
if (!result || typeof result !== "object" || result.state !== state) {
|
|
2519
|
+
throw new Error(`Permission ${name} is not ${state}`);
|
|
2520
|
+
}
|
|
2521
|
+
return result;
|
|
2522
|
+
}
|
|
2523
|
+
async assertMediaTrackLive(kind) {
|
|
2524
|
+
const result = await this.page.evaluate(
|
|
2525
|
+
`(() => {
|
|
2526
|
+
const requestedKind = ${JSON.stringify(kind)};
|
|
2527
|
+
const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
|
|
2528
|
+
const tracks = [];
|
|
2529
|
+
if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
|
|
2530
|
+
tracks.push(...el.srcObject.getTracks());
|
|
2531
|
+
}
|
|
2532
|
+
return {
|
|
2533
|
+
tag: el.tagName.toLowerCase(),
|
|
2534
|
+
paused: !!el.paused,
|
|
2535
|
+
tracks: tracks.map((track) => ({
|
|
2536
|
+
kind: track.kind,
|
|
2537
|
+
readyState: track.readyState,
|
|
2538
|
+
enabled: track.enabled,
|
|
2539
|
+
label: track.label,
|
|
2540
|
+
})),
|
|
2541
|
+
};
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
const globalTracks =
|
|
2545
|
+
window.__bpStream && typeof window.__bpStream.getTracks === 'function'
|
|
2546
|
+
? window.__bpStream.getTracks().map((track) => ({
|
|
2547
|
+
kind: track.kind,
|
|
2548
|
+
readyState: track.readyState,
|
|
2549
|
+
enabled: track.enabled,
|
|
2550
|
+
label: track.label,
|
|
2551
|
+
}))
|
|
2552
|
+
: [];
|
|
2553
|
+
|
|
2554
|
+
const liveTracks = mediaElements
|
|
2555
|
+
.flatMap((entry) => entry.tracks)
|
|
2556
|
+
.concat(globalTracks)
|
|
2557
|
+
.filter((track) => track.kind === requestedKind && track.readyState === 'live');
|
|
2558
|
+
|
|
2559
|
+
return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
|
|
2560
|
+
})()`
|
|
2561
|
+
);
|
|
2562
|
+
if (!result || typeof result !== "object" || !result.live) {
|
|
2563
|
+
throw new Error(`No live ${kind} media track detected`);
|
|
2564
|
+
}
|
|
2565
|
+
return result;
|
|
2566
|
+
}
|
|
1190
2567
|
};
|
|
1191
2568
|
function addBatchToPage(page) {
|
|
1192
2569
|
const executor = new BatchExecutor(page);
|
|
@@ -1317,7 +2694,7 @@ var ACTION_RULES = {
|
|
|
1317
2694
|
value: { type: "string|string[]" },
|
|
1318
2695
|
trigger: { type: "string|string[]" },
|
|
1319
2696
|
option: { type: "string|string[]" },
|
|
1320
|
-
match: { type: "string"
|
|
2697
|
+
match: { type: "string" }
|
|
1321
2698
|
}
|
|
1322
2699
|
},
|
|
1323
2700
|
check: {
|
|
@@ -1448,6 +2825,38 @@ var ACTION_RULES = {
|
|
|
1448
2825
|
expect: { type: "string" },
|
|
1449
2826
|
value: { type: "string" }
|
|
1450
2827
|
}
|
|
2828
|
+
},
|
|
2829
|
+
waitForWsMessage: {
|
|
2830
|
+
required: { match: { type: "string" } },
|
|
2831
|
+
optional: {
|
|
2832
|
+
where: { type: "object" }
|
|
2833
|
+
}
|
|
2834
|
+
},
|
|
2835
|
+
assertNoConsoleErrors: {
|
|
2836
|
+
required: {},
|
|
2837
|
+
optional: {
|
|
2838
|
+
windowMs: { type: "number" }
|
|
2839
|
+
}
|
|
2840
|
+
},
|
|
2841
|
+
assertTextChanged: {
|
|
2842
|
+
required: { to: { type: "string" } },
|
|
2843
|
+
optional: {
|
|
2844
|
+
selector: { type: "string|string[]" },
|
|
2845
|
+
from: { type: "string" }
|
|
2846
|
+
}
|
|
2847
|
+
},
|
|
2848
|
+
assertPermission: {
|
|
2849
|
+
required: {
|
|
2850
|
+
name: { type: "string" },
|
|
2851
|
+
state: { type: "string" }
|
|
2852
|
+
},
|
|
2853
|
+
optional: {}
|
|
2854
|
+
},
|
|
2855
|
+
assertMediaTrackLive: {
|
|
2856
|
+
required: {
|
|
2857
|
+
kind: { type: "string", enum: ["audio", "video"] }
|
|
2858
|
+
},
|
|
2859
|
+
optional: {}
|
|
1451
2860
|
}
|
|
1452
2861
|
};
|
|
1453
2862
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -1471,6 +2880,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1471
2880
|
"trigger",
|
|
1472
2881
|
"option",
|
|
1473
2882
|
"match",
|
|
2883
|
+
"where",
|
|
1474
2884
|
"x",
|
|
1475
2885
|
"y",
|
|
1476
2886
|
"direction",
|
|
@@ -1480,7 +2890,13 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1480
2890
|
"fullPage",
|
|
1481
2891
|
"expect",
|
|
1482
2892
|
"retry",
|
|
1483
|
-
"retryDelay"
|
|
2893
|
+
"retryDelay",
|
|
2894
|
+
"from",
|
|
2895
|
+
"to",
|
|
2896
|
+
"name",
|
|
2897
|
+
"state",
|
|
2898
|
+
"kind",
|
|
2899
|
+
"windowMs"
|
|
1484
2900
|
]);
|
|
1485
2901
|
function resolveAction(name) {
|
|
1486
2902
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -1553,6 +2969,11 @@ function checkFieldType(value, rule) {
|
|
|
1553
2969
|
return `expected boolean or "auto", got ${typeof value}`;
|
|
1554
2970
|
}
|
|
1555
2971
|
return null;
|
|
2972
|
+
case "object":
|
|
2973
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2974
|
+
return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
|
|
2975
|
+
}
|
|
2976
|
+
return null;
|
|
1556
2977
|
default: {
|
|
1557
2978
|
const _exhaustive = rule.type;
|
|
1558
2979
|
return `unknown type: ${_exhaustive}`;
|
|
@@ -1916,6 +3337,10 @@ async function grantAudioPermissions(cdp, origin) {
|
|
|
1916
3337
|
await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
1917
3338
|
source: PERMISSIONS_OVERRIDE_SCRIPT
|
|
1918
3339
|
});
|
|
3340
|
+
await cdp.send("Runtime.evaluate", {
|
|
3341
|
+
expression: PERMISSIONS_OVERRIDE_SCRIPT,
|
|
3342
|
+
awaitPromise: false
|
|
3343
|
+
});
|
|
1919
3344
|
}
|
|
1920
3345
|
var PERMISSIONS_OVERRIDE_SCRIPT = `
|
|
1921
3346
|
(function() {
|
|
@@ -3189,6 +4614,24 @@ Content-Type: ${contentType}\r
|
|
|
3189
4614
|
parts.push(data);
|
|
3190
4615
|
}
|
|
3191
4616
|
|
|
4617
|
+
// src/utils/json.ts
|
|
4618
|
+
function isRecord(value) {
|
|
4619
|
+
return typeof value === "object" && value !== null;
|
|
4620
|
+
}
|
|
4621
|
+
function stringifyUnknown(value) {
|
|
4622
|
+
if (typeof value === "string") return value;
|
|
4623
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
4624
|
+
return String(value);
|
|
4625
|
+
}
|
|
4626
|
+
if (value === null) return "null";
|
|
4627
|
+
if (value === void 0) return "undefined";
|
|
4628
|
+
try {
|
|
4629
|
+
return JSON.stringify(value);
|
|
4630
|
+
} catch {
|
|
4631
|
+
return Object.prototype.toString.call(value);
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
|
|
3192
4635
|
// src/cdp/transport.ts
|
|
3193
4636
|
function createTransport(wsUrl, options = {}) {
|
|
3194
4637
|
const { timeout = 3e4 } = options;
|
|
@@ -3292,8 +4735,12 @@ function getReadyStateString(state) {
|
|
|
3292
4735
|
|
|
3293
4736
|
// src/cdp/client.ts
|
|
3294
4737
|
async function createCDPClient(wsUrl, options = {}) {
|
|
3295
|
-
const {
|
|
4738
|
+
const { timeout = 3e4 } = options;
|
|
3296
4739
|
const transport = await createTransport(wsUrl, { timeout });
|
|
4740
|
+
return buildCDPClient(transport, options);
|
|
4741
|
+
}
|
|
4742
|
+
function buildCDPClient(transport, options = {}) {
|
|
4743
|
+
const { debug = false, timeout = 3e4 } = options;
|
|
3297
4744
|
let messageId = 0;
|
|
3298
4745
|
let currentSessionId;
|
|
3299
4746
|
let connected = true;
|
|
@@ -3303,7 +4750,19 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
3303
4750
|
transport.onMessage((raw) => {
|
|
3304
4751
|
let msg;
|
|
3305
4752
|
try {
|
|
3306
|
-
|
|
4753
|
+
const parsed = JSON.parse(raw);
|
|
4754
|
+
if (!isRecord(parsed)) {
|
|
4755
|
+
if (debug) console.error("[CDP] Ignoring non-object message:", raw);
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
if ("id" in parsed && typeof parsed["id"] === "number") {
|
|
4759
|
+
msg = parsed;
|
|
4760
|
+
} else if ("method" in parsed && typeof parsed["method"] === "string") {
|
|
4761
|
+
msg = parsed;
|
|
4762
|
+
} else {
|
|
4763
|
+
if (debug) console.error("[CDP] Ignoring invalid message shape:", raw);
|
|
4764
|
+
return;
|
|
4765
|
+
}
|
|
3307
4766
|
} catch {
|
|
3308
4767
|
if (debug) console.error("[CDP] Failed to parse message:", raw);
|
|
3309
4768
|
return;
|
|
@@ -3318,7 +4777,8 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
3318
4777
|
pending.delete(response.id);
|
|
3319
4778
|
clearTimeout(request.timer);
|
|
3320
4779
|
if (response.error) {
|
|
3321
|
-
|
|
4780
|
+
const error = typeof response.error === "string" ? { code: -32e3, message: response.error } : response.error;
|
|
4781
|
+
request.reject(new CDPError(error));
|
|
3322
4782
|
} else {
|
|
3323
4783
|
request.resolve(response.result);
|
|
3324
4784
|
}
|
|
@@ -3416,6 +4876,9 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
3416
4876
|
onAny(handler) {
|
|
3417
4877
|
anyEventHandlers.add(handler);
|
|
3418
4878
|
},
|
|
4879
|
+
offAny(handler) {
|
|
4880
|
+
anyEventHandlers.delete(handler);
|
|
4881
|
+
},
|
|
3419
4882
|
async close() {
|
|
3420
4883
|
connected = false;
|
|
3421
4884
|
await transport.close();
|
|
@@ -3431,6 +4894,9 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
3431
4894
|
get sessionId() {
|
|
3432
4895
|
return currentSessionId;
|
|
3433
4896
|
},
|
|
4897
|
+
setSessionId(sessionId) {
|
|
4898
|
+
currentSessionId = sessionId;
|
|
4899
|
+
},
|
|
3434
4900
|
get isConnected() {
|
|
3435
4901
|
return connected;
|
|
3436
4902
|
}
|
|
@@ -4666,6 +6132,9 @@ var Page = class {
|
|
|
4666
6132
|
brokenFrame = null;
|
|
4667
6133
|
/** Last matched selector from findElement (for selectorUsed tracking) */
|
|
4668
6134
|
_lastMatchedSelector;
|
|
6135
|
+
_lastActionCoordinates = null;
|
|
6136
|
+
_lastActionBoundingBox = null;
|
|
6137
|
+
_lastActionTargetMetadata = null;
|
|
4669
6138
|
/** Last snapshot for stale ref recovery */
|
|
4670
6139
|
lastSnapshot;
|
|
4671
6140
|
/** Audio input controller (lazy-initialized) */
|
|
@@ -4697,6 +6166,76 @@ var Page = class {
|
|
|
4697
6166
|
getLastMatchedSelector() {
|
|
4698
6167
|
return this._lastMatchedSelector;
|
|
4699
6168
|
}
|
|
6169
|
+
async getActionTargetMetadata(identifiers) {
|
|
6170
|
+
try {
|
|
6171
|
+
const objectId = identifiers.objectId ?? (identifiers.nodeId ? await this.resolveObjectId(identifiers.nodeId) : void 0);
|
|
6172
|
+
if (!objectId) return null;
|
|
6173
|
+
const response = await this.cdp.send("Runtime.callFunctionOn", {
|
|
6174
|
+
objectId,
|
|
6175
|
+
functionDeclaration: `function() {
|
|
6176
|
+
const tagName = this.tagName?.toLowerCase?.() || '';
|
|
6177
|
+
const inputType =
|
|
6178
|
+
tagName === 'input' && typeof this.type === 'string' ? this.type.toLowerCase() : '';
|
|
6179
|
+
const autocomplete =
|
|
6180
|
+
typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '';
|
|
6181
|
+
return { tagName, inputType, autocomplete };
|
|
6182
|
+
}`,
|
|
6183
|
+
returnByValue: true
|
|
6184
|
+
});
|
|
6185
|
+
return response.result.value ?? null;
|
|
6186
|
+
} catch {
|
|
6187
|
+
return null;
|
|
6188
|
+
}
|
|
6189
|
+
}
|
|
6190
|
+
async getElementPosition(identifiers) {
|
|
6191
|
+
try {
|
|
6192
|
+
const { quads } = await this.cdp.send(
|
|
6193
|
+
"DOM.getContentQuads",
|
|
6194
|
+
identifiers
|
|
6195
|
+
);
|
|
6196
|
+
if (quads?.length > 0) {
|
|
6197
|
+
const q = quads[0];
|
|
6198
|
+
const minX = Math.min(q[0], q[2], q[4], q[6]);
|
|
6199
|
+
const maxX = Math.max(q[0], q[2], q[4], q[6]);
|
|
6200
|
+
const minY = Math.min(q[1], q[3], q[5], q[7]);
|
|
6201
|
+
const maxY = Math.max(q[1], q[3], q[5], q[7]);
|
|
6202
|
+
return {
|
|
6203
|
+
center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
|
|
6204
|
+
bbox: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
6205
|
+
};
|
|
6206
|
+
}
|
|
6207
|
+
} catch {
|
|
6208
|
+
}
|
|
6209
|
+
if (identifiers.nodeId) {
|
|
6210
|
+
const box = await this.getBoxModel(identifiers.nodeId);
|
|
6211
|
+
if (box) {
|
|
6212
|
+
return {
|
|
6213
|
+
center: { x: box.content[0] + box.width / 2, y: box.content[1] + box.height / 2 },
|
|
6214
|
+
bbox: { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
|
|
6215
|
+
};
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
return null;
|
|
6219
|
+
}
|
|
6220
|
+
setLastActionPosition(coords, bbox) {
|
|
6221
|
+
this._lastActionCoordinates = coords;
|
|
6222
|
+
this._lastActionBoundingBox = bbox;
|
|
6223
|
+
}
|
|
6224
|
+
getLastActionCoordinates() {
|
|
6225
|
+
return this._lastActionCoordinates;
|
|
6226
|
+
}
|
|
6227
|
+
getLastActionBoundingBox() {
|
|
6228
|
+
return this._lastActionBoundingBox;
|
|
6229
|
+
}
|
|
6230
|
+
getLastActionTargetMetadata() {
|
|
6231
|
+
return this._lastActionTargetMetadata;
|
|
6232
|
+
}
|
|
6233
|
+
/** Reset position tracking (call before each executor step) */
|
|
6234
|
+
resetLastActionPosition() {
|
|
6235
|
+
this._lastActionCoordinates = null;
|
|
6236
|
+
this._lastActionBoundingBox = null;
|
|
6237
|
+
this._lastActionTargetMetadata = null;
|
|
6238
|
+
}
|
|
4700
6239
|
/**
|
|
4701
6240
|
* Initialize the page (enable required CDP domains)
|
|
4702
6241
|
*/
|
|
@@ -4864,6 +6403,14 @@ var Page = class {
|
|
|
4864
6403
|
const quad = quads[0];
|
|
4865
6404
|
clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
4866
6405
|
clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
6406
|
+
const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
|
6407
|
+
const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
|
|
6408
|
+
const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
|
6409
|
+
const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
|
|
6410
|
+
this.setLastActionPosition(
|
|
6411
|
+
{ x: clickX, y: clickY },
|
|
6412
|
+
{ x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
6413
|
+
);
|
|
4867
6414
|
} else {
|
|
4868
6415
|
throw new Error("No quads");
|
|
4869
6416
|
}
|
|
@@ -4872,6 +6419,10 @@ var Page = class {
|
|
|
4872
6419
|
if (!box) throw new Error("Could not get element position");
|
|
4873
6420
|
clickX = box.content[0] + box.width / 2;
|
|
4874
6421
|
clickY = box.content[1] + box.height / 2;
|
|
6422
|
+
this.setLastActionPosition(
|
|
6423
|
+
{ x: clickX, y: clickY },
|
|
6424
|
+
{ x: box.content[0], y: box.content[1], width: box.width, height: box.height }
|
|
6425
|
+
);
|
|
4875
6426
|
}
|
|
4876
6427
|
const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
|
|
4877
6428
|
const HIT_TARGET_RETRIES = 3;
|
|
@@ -4922,13 +6473,20 @@ var Page = class {
|
|
|
4922
6473
|
if (options.optional) return false;
|
|
4923
6474
|
throw e;
|
|
4924
6475
|
}
|
|
6476
|
+
const fillPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
6477
|
+
if (fillPos) this.setLastActionPosition(fillPos.center, fillPos.bbox);
|
|
4925
6478
|
const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4926
6479
|
objectId,
|
|
4927
6480
|
functionDeclaration: `function() {
|
|
4928
|
-
return {
|
|
6481
|
+
return {
|
|
6482
|
+
tagName: this.tagName?.toLowerCase() || '',
|
|
6483
|
+
inputType: (this.type || '').toLowerCase(),
|
|
6484
|
+
autocomplete: typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '',
|
|
6485
|
+
};
|
|
4929
6486
|
}`,
|
|
4930
6487
|
returnByValue: true
|
|
4931
6488
|
});
|
|
6489
|
+
this._lastActionTargetMetadata = tagInfo.result.value;
|
|
4932
6490
|
const { tagName, inputType } = tagInfo.result.value;
|
|
4933
6491
|
const specialInputTypes = /* @__PURE__ */ new Set([
|
|
4934
6492
|
"date",
|
|
@@ -5010,6 +6568,9 @@ var Page = class {
|
|
|
5010
6568
|
if (options.optional) return false;
|
|
5011
6569
|
throw e;
|
|
5012
6570
|
}
|
|
6571
|
+
const typePos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
6572
|
+
if (typePos) this.setLastActionPosition(typePos.center, typePos.bbox);
|
|
6573
|
+
this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
|
|
5013
6574
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
5014
6575
|
for (const char of text) {
|
|
5015
6576
|
const def = US_KEYBOARD[char];
|
|
@@ -5089,6 +6650,9 @@ var Page = class {
|
|
|
5089
6650
|
if (options.optional) return false;
|
|
5090
6651
|
throw e;
|
|
5091
6652
|
}
|
|
6653
|
+
const selectPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
6654
|
+
if (selectPos) this.setLastActionPosition(selectPos.center, selectPos.bbox);
|
|
6655
|
+
this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
|
|
5092
6656
|
const metadata = await this.getNativeSelectMetadata(objectId, values);
|
|
5093
6657
|
if (!metadata.isSelect) {
|
|
5094
6658
|
throw new Error("select() target must be a native <select> element");
|
|
@@ -5225,6 +6789,8 @@ var Page = class {
|
|
|
5225
6789
|
if (options.optional) return false;
|
|
5226
6790
|
throw e;
|
|
5227
6791
|
}
|
|
6792
|
+
const checkPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
6793
|
+
if (checkPos) this.setLastActionPosition(checkPos.center, checkPos.bbox);
|
|
5228
6794
|
const before = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5229
6795
|
objectId: object.objectId,
|
|
5230
6796
|
functionDeclaration: "function() { return !!this.checked; }",
|
|
@@ -5273,6 +6839,8 @@ var Page = class {
|
|
|
5273
6839
|
if (options.optional) return false;
|
|
5274
6840
|
throw e;
|
|
5275
6841
|
}
|
|
6842
|
+
const uncheckPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
6843
|
+
if (uncheckPos) this.setLastActionPosition(uncheckPos.center, uncheckPos.bbox);
|
|
5276
6844
|
const isRadio = await this.cdp.send(
|
|
5277
6845
|
"Runtime.callFunctionOn",
|
|
5278
6846
|
{
|
|
@@ -5328,6 +6896,8 @@ var Page = class {
|
|
|
5328
6896
|
throw new ElementNotFoundError(selector, hints);
|
|
5329
6897
|
}
|
|
5330
6898
|
const objectId = await this.resolveObjectId(element.nodeId);
|
|
6899
|
+
const submitPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
6900
|
+
if (submitPos) this.setLastActionPosition(submitPos.center, submitPos.bbox);
|
|
5331
6901
|
const isFormElement = await this.cdp.send(
|
|
5332
6902
|
"Runtime.callFunctionOn",
|
|
5333
6903
|
{
|
|
@@ -5424,6 +6994,8 @@ var Page = class {
|
|
|
5424
6994
|
const hints = await generateHints(this, selectorList, "focus");
|
|
5425
6995
|
throw new ElementNotFoundError(selector, hints);
|
|
5426
6996
|
}
|
|
6997
|
+
const focusPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
6998
|
+
if (focusPos) this.setLastActionPosition(focusPos.center, focusPos.bbox);
|
|
5427
6999
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
5428
7000
|
return true;
|
|
5429
7001
|
}
|
|
@@ -5459,6 +7031,14 @@ var Page = class {
|
|
|
5459
7031
|
const quad = quads[0];
|
|
5460
7032
|
x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
5461
7033
|
y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
7034
|
+
const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
|
7035
|
+
const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
|
|
7036
|
+
const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
|
7037
|
+
const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
|
|
7038
|
+
this.setLastActionPosition(
|
|
7039
|
+
{ x, y },
|
|
7040
|
+
{ x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
7041
|
+
);
|
|
5462
7042
|
} else {
|
|
5463
7043
|
throw new Error("No quads");
|
|
5464
7044
|
}
|
|
@@ -5470,6 +7050,10 @@ var Page = class {
|
|
|
5470
7050
|
}
|
|
5471
7051
|
x = box.content[0] + box.width / 2;
|
|
5472
7052
|
y = box.content[1] + box.height / 2;
|
|
7053
|
+
this.setLastActionPosition(
|
|
7054
|
+
{ x, y },
|
|
7055
|
+
{ x: box.content[0], y: box.content[1], width: box.width, height: box.height }
|
|
7056
|
+
);
|
|
5473
7057
|
}
|
|
5474
7058
|
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
5475
7059
|
type: "mouseMoved",
|
|
@@ -5495,6 +7079,8 @@ var Page = class {
|
|
|
5495
7079
|
if (options.optional) return false;
|
|
5496
7080
|
throw new ElementNotFoundError(selector);
|
|
5497
7081
|
}
|
|
7082
|
+
const scrollPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
7083
|
+
if (scrollPos) this.setLastActionPosition(scrollPos.center, scrollPos.bbox);
|
|
5498
7084
|
await this.scrollIntoView(element.nodeId);
|
|
5499
7085
|
return true;
|
|
5500
7086
|
}
|
|
@@ -6256,7 +7842,7 @@ var Page = class {
|
|
|
6256
7842
|
return {
|
|
6257
7843
|
role,
|
|
6258
7844
|
name,
|
|
6259
|
-
value: value !== void 0 ?
|
|
7845
|
+
value: value !== void 0 ? stringifyUnknown(value) : void 0,
|
|
6260
7846
|
ref,
|
|
6261
7847
|
children: children.length > 0 ? children : void 0,
|
|
6262
7848
|
disabled,
|
|
@@ -6318,7 +7904,7 @@ var Page = class {
|
|
|
6318
7904
|
selector,
|
|
6319
7905
|
disabled,
|
|
6320
7906
|
checked,
|
|
6321
|
-
value: value !== void 0 ?
|
|
7907
|
+
value: value !== void 0 ? stringifyUnknown(value) : void 0
|
|
6322
7908
|
});
|
|
6323
7909
|
}
|
|
6324
7910
|
}
|
|
@@ -6788,7 +8374,7 @@ var Page = class {
|
|
|
6788
8374
|
*/
|
|
6789
8375
|
formatConsoleArgs(args) {
|
|
6790
8376
|
return args.map((arg) => {
|
|
6791
|
-
if (arg.value !== void 0) return
|
|
8377
|
+
if (arg.value !== void 0) return stringifyUnknown(arg.value);
|
|
6792
8378
|
if (arg.description) return arg.description;
|
|
6793
8379
|
return "[object]";
|
|
6794
8380
|
}).join(" ");
|
|
@@ -7577,6 +9163,25 @@ var Browser = class _Browser {
|
|
|
7577
9163
|
this.cdp = cdp;
|
|
7578
9164
|
this.providerSession = providerSession;
|
|
7579
9165
|
}
|
|
9166
|
+
/**
|
|
9167
|
+
* Create a Browser from an existing CDPClient (used by daemon fast-path).
|
|
9168
|
+
* The caller is responsible for the CDP connection lifecycle.
|
|
9169
|
+
*/
|
|
9170
|
+
static fromCDP(cdp, sessionInfo) {
|
|
9171
|
+
const providerSession = {
|
|
9172
|
+
wsUrl: sessionInfo.wsUrl,
|
|
9173
|
+
sessionId: sessionInfo.sessionId,
|
|
9174
|
+
async close() {
|
|
9175
|
+
}
|
|
9176
|
+
};
|
|
9177
|
+
const provider = {
|
|
9178
|
+
name: sessionInfo.provider ?? "daemon",
|
|
9179
|
+
async createSession() {
|
|
9180
|
+
return providerSession;
|
|
9181
|
+
}
|
|
9182
|
+
};
|
|
9183
|
+
return new _Browser(cdp, provider, providerSession, { provider: "generic" });
|
|
9184
|
+
}
|
|
7580
9185
|
/**
|
|
7581
9186
|
* Connect to a browser instance
|
|
7582
9187
|
*/
|