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/browser.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/browser/index.ts
|
|
@@ -29,6 +39,24 @@ __export(browser_exports, {
|
|
|
29
39
|
});
|
|
30
40
|
module.exports = __toCommonJS(browser_exports);
|
|
31
41
|
|
|
42
|
+
// src/utils/json.ts
|
|
43
|
+
function isRecord(value) {
|
|
44
|
+
return typeof value === "object" && value !== null;
|
|
45
|
+
}
|
|
46
|
+
function stringifyUnknown(value) {
|
|
47
|
+
if (typeof value === "string") return value;
|
|
48
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
if (value === null) return "null";
|
|
52
|
+
if (value === void 0) return "undefined";
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
} catch {
|
|
56
|
+
return Object.prototype.toString.call(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
32
60
|
// src/cdp/protocol.ts
|
|
33
61
|
var CDPError = class extends Error {
|
|
34
62
|
code;
|
|
@@ -144,8 +172,12 @@ function getReadyStateString(state) {
|
|
|
144
172
|
|
|
145
173
|
// src/cdp/client.ts
|
|
146
174
|
async function createCDPClient(wsUrl, options = {}) {
|
|
147
|
-
const {
|
|
175
|
+
const { timeout = 3e4 } = options;
|
|
148
176
|
const transport = await createTransport(wsUrl, { timeout });
|
|
177
|
+
return buildCDPClient(transport, options);
|
|
178
|
+
}
|
|
179
|
+
function buildCDPClient(transport, options = {}) {
|
|
180
|
+
const { debug = false, timeout = 3e4 } = options;
|
|
149
181
|
let messageId = 0;
|
|
150
182
|
let currentSessionId;
|
|
151
183
|
let connected = true;
|
|
@@ -155,7 +187,19 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
155
187
|
transport.onMessage((raw) => {
|
|
156
188
|
let msg;
|
|
157
189
|
try {
|
|
158
|
-
|
|
190
|
+
const parsed = JSON.parse(raw);
|
|
191
|
+
if (!isRecord(parsed)) {
|
|
192
|
+
if (debug) console.error("[CDP] Ignoring non-object message:", raw);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if ("id" in parsed && typeof parsed["id"] === "number") {
|
|
196
|
+
msg = parsed;
|
|
197
|
+
} else if ("method" in parsed && typeof parsed["method"] === "string") {
|
|
198
|
+
msg = parsed;
|
|
199
|
+
} else {
|
|
200
|
+
if (debug) console.error("[CDP] Ignoring invalid message shape:", raw);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
159
203
|
} catch {
|
|
160
204
|
if (debug) console.error("[CDP] Failed to parse message:", raw);
|
|
161
205
|
return;
|
|
@@ -170,7 +214,8 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
170
214
|
pending.delete(response.id);
|
|
171
215
|
clearTimeout(request.timer);
|
|
172
216
|
if (response.error) {
|
|
173
|
-
|
|
217
|
+
const error = typeof response.error === "string" ? { code: -32e3, message: response.error } : response.error;
|
|
218
|
+
request.reject(new CDPError(error));
|
|
174
219
|
} else {
|
|
175
220
|
request.resolve(response.result);
|
|
176
221
|
}
|
|
@@ -268,6 +313,9 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
268
313
|
onAny(handler) {
|
|
269
314
|
anyEventHandlers.add(handler);
|
|
270
315
|
},
|
|
316
|
+
offAny(handler) {
|
|
317
|
+
anyEventHandlers.delete(handler);
|
|
318
|
+
},
|
|
271
319
|
async close() {
|
|
272
320
|
connected = false;
|
|
273
321
|
await transport.close();
|
|
@@ -283,6 +331,9 @@ async function createCDPClient(wsUrl, options = {}) {
|
|
|
283
331
|
get sessionId() {
|
|
284
332
|
return currentSessionId;
|
|
285
333
|
},
|
|
334
|
+
setSessionId(sessionId) {
|
|
335
|
+
currentSessionId = sessionId;
|
|
336
|
+
},
|
|
286
337
|
get isConnected() {
|
|
287
338
|
return connected;
|
|
288
339
|
}
|
|
@@ -472,6 +523,230 @@ function createProvider(options) {
|
|
|
472
523
|
}
|
|
473
524
|
}
|
|
474
525
|
|
|
526
|
+
// src/actions/executor.ts
|
|
527
|
+
var fs = __toESM(require("fs"), 1);
|
|
528
|
+
var import_node_path = require("path");
|
|
529
|
+
|
|
530
|
+
// src/recording/redaction.ts
|
|
531
|
+
var REDACTED_VALUE = "[REDACTED]";
|
|
532
|
+
var SENSITIVE_AUTOCOMPLETE_TOKENS = [
|
|
533
|
+
"current-password",
|
|
534
|
+
"new-password",
|
|
535
|
+
"one-time-code",
|
|
536
|
+
"cc-number",
|
|
537
|
+
"cc-csc",
|
|
538
|
+
"cc-exp",
|
|
539
|
+
"cc-exp-month",
|
|
540
|
+
"cc-exp-year"
|
|
541
|
+
];
|
|
542
|
+
function autocompleteTokens(autocomplete) {
|
|
543
|
+
if (!autocomplete) return [];
|
|
544
|
+
return autocomplete.toLowerCase().split(/\s+/).map((token) => token.trim()).filter(Boolean);
|
|
545
|
+
}
|
|
546
|
+
function isSensitiveFieldMetadata(metadata) {
|
|
547
|
+
if (!metadata) return false;
|
|
548
|
+
if (metadata.sensitiveValue) return true;
|
|
549
|
+
const inputType = metadata.inputType?.toLowerCase();
|
|
550
|
+
if (inputType === "password" || inputType === "hidden") {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
const sensitiveAutocompleteTokens = new Set(SENSITIVE_AUTOCOMPLETE_TOKENS);
|
|
554
|
+
return autocompleteTokens(metadata.autocomplete).some(
|
|
555
|
+
(token) => sensitiveAutocompleteTokens.has(token)
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
function redactValueForRecording(value, metadata) {
|
|
559
|
+
if (value === void 0) return void 0;
|
|
560
|
+
return isSensitiveFieldMetadata(metadata) ? REDACTED_VALUE : value;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/browser/action-highlight.ts
|
|
564
|
+
var HIGHLIGHT_STYLES = {
|
|
565
|
+
click: { outline: "3px solid rgba(229,57,53,0.8)", badge: "#e53935", marker: "crosshair" },
|
|
566
|
+
fill: { outline: "3px solid rgba(33,150,243,0.8)", badge: "#2196f3" },
|
|
567
|
+
type: { outline: "3px solid rgba(33,150,243,0.6)", badge: "#2196f3" },
|
|
568
|
+
select: { outline: "3px solid rgba(156,39,176,0.8)", badge: "#9c27b0" },
|
|
569
|
+
hover: { outline: "2px dashed rgba(158,158,158,0.5)", badge: "#9e9e9e" },
|
|
570
|
+
scroll: { outline: "none", badge: "#607d8b", marker: "arrow" },
|
|
571
|
+
navigate: { outline: "none", badge: "#4caf50" },
|
|
572
|
+
submit: { outline: "3px solid rgba(255,152,0,0.8)", badge: "#ff9800" },
|
|
573
|
+
"assert-pass": { outline: "3px solid rgba(76,175,80,0.8)", badge: "#4caf50", marker: "check" },
|
|
574
|
+
"assert-fail": { outline: "3px solid rgba(244,67,54,0.8)", badge: "#f44336", marker: "cross" },
|
|
575
|
+
evaluate: { outline: "none", badge: "#ffc107" },
|
|
576
|
+
focus: { outline: "3px dotted rgba(33,150,243,0.6)", badge: "#2196f3" }
|
|
577
|
+
};
|
|
578
|
+
function buildHighlightScript(options) {
|
|
579
|
+
const style = HIGHLIGHT_STYLES[options.kind];
|
|
580
|
+
const label = options.label ? options.label.slice(0, 80) : void 0;
|
|
581
|
+
const escapedLabel = label ? label.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n") : "";
|
|
582
|
+
return `(function() {
|
|
583
|
+
// Remove any existing highlight
|
|
584
|
+
var existing = document.getElementById('__bp-action-highlight');
|
|
585
|
+
if (existing) existing.remove();
|
|
586
|
+
|
|
587
|
+
var container = document.createElement('div');
|
|
588
|
+
container.id = '__bp-action-highlight';
|
|
589
|
+
container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
|
|
590
|
+
|
|
591
|
+
${options.bbox ? `
|
|
592
|
+
// Element outline
|
|
593
|
+
var outline = document.createElement('div');
|
|
594
|
+
outline.style.cssText = 'position:fixed;' +
|
|
595
|
+
'left:${options.bbox.x}px;top:${options.bbox.y}px;' +
|
|
596
|
+
'width:${options.bbox.width}px;height:${options.bbox.height}px;' +
|
|
597
|
+
'${style.outline !== "none" ? `outline:${style.outline};outline-offset:-1px;` : ""}' +
|
|
598
|
+
'pointer-events:none;box-sizing:border-box;';
|
|
599
|
+
container.appendChild(outline);
|
|
600
|
+
` : ""}
|
|
601
|
+
|
|
602
|
+
${options.point && style.marker === "crosshair" ? `
|
|
603
|
+
// Crosshair at click point
|
|
604
|
+
var hLine = document.createElement('div');
|
|
605
|
+
hLine.style.cssText = 'position:fixed;left:${options.point.x - 12}px;top:${options.point.y}px;' +
|
|
606
|
+
'width:24px;height:2px;background:${style.badge};pointer-events:none;';
|
|
607
|
+
var vLine = document.createElement('div');
|
|
608
|
+
vLine.style.cssText = 'position:fixed;left:${options.point.x}px;top:${options.point.y - 12}px;' +
|
|
609
|
+
'width:2px;height:24px;background:${style.badge};pointer-events:none;';
|
|
610
|
+
// Dot at center
|
|
611
|
+
var dot = document.createElement('div');
|
|
612
|
+
dot.style.cssText = 'position:fixed;left:${options.point.x - 4}px;top:${options.point.y - 4}px;' +
|
|
613
|
+
'width:8px;height:8px;border-radius:50%;background:${style.badge};pointer-events:none;';
|
|
614
|
+
container.appendChild(hLine);
|
|
615
|
+
container.appendChild(vLine);
|
|
616
|
+
container.appendChild(dot);
|
|
617
|
+
` : ""}
|
|
618
|
+
|
|
619
|
+
${label ? `
|
|
620
|
+
// Badge with label
|
|
621
|
+
var badge = document.createElement('div');
|
|
622
|
+
badge.style.cssText = 'position:fixed;' +
|
|
623
|
+
${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;'"} +
|
|
624
|
+
'background:${style.badge};color:white;padding:4px 8px;' +
|
|
625
|
+
'font-family:monospace;font-size:12px;font-weight:bold;' +
|
|
626
|
+
'border-radius:3px;white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis;' +
|
|
627
|
+
'pointer-events:none;';
|
|
628
|
+
badge.textContent = '${escapedLabel}';
|
|
629
|
+
container.appendChild(badge);
|
|
630
|
+
` : ""}
|
|
631
|
+
|
|
632
|
+
${style.marker === "check" && options.bbox ? `
|
|
633
|
+
// Checkmark
|
|
634
|
+
var check = document.createElement('div');
|
|
635
|
+
check.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
|
|
636
|
+
'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
|
|
637
|
+
'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;';
|
|
638
|
+
check.textContent = '\\u2713';
|
|
639
|
+
container.appendChild(check);
|
|
640
|
+
` : ""}
|
|
641
|
+
|
|
642
|
+
${style.marker === "cross" && options.bbox ? `
|
|
643
|
+
// Cross mark
|
|
644
|
+
var cross = document.createElement('div');
|
|
645
|
+
cross.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
|
|
646
|
+
'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
|
|
647
|
+
'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;font-weight:bold;';
|
|
648
|
+
cross.textContent = '\\u2717';
|
|
649
|
+
container.appendChild(cross);
|
|
650
|
+
` : ""}
|
|
651
|
+
|
|
652
|
+
document.body.appendChild(container);
|
|
653
|
+
window.__bpRemoveActionHighlight = function() {
|
|
654
|
+
var el = document.getElementById('__bp-action-highlight');
|
|
655
|
+
if (el) el.remove();
|
|
656
|
+
delete window.__bpRemoveActionHighlight;
|
|
657
|
+
};
|
|
658
|
+
})();`;
|
|
659
|
+
}
|
|
660
|
+
async function injectActionHighlight(page, options) {
|
|
661
|
+
try {
|
|
662
|
+
await page.evaluate(buildHighlightScript(options));
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function removeActionHighlight(page) {
|
|
667
|
+
try {
|
|
668
|
+
await page.evaluate(`(function() {
|
|
669
|
+
if (window.__bpRemoveActionHighlight) {
|
|
670
|
+
window.__bpRemoveActionHighlight();
|
|
671
|
+
}
|
|
672
|
+
})()`);
|
|
673
|
+
} catch {
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function stepToHighlightKind(step) {
|
|
677
|
+
switch (step.action) {
|
|
678
|
+
case "click":
|
|
679
|
+
return "click";
|
|
680
|
+
case "fill":
|
|
681
|
+
return "fill";
|
|
682
|
+
case "type":
|
|
683
|
+
return "type";
|
|
684
|
+
case "select":
|
|
685
|
+
return "select";
|
|
686
|
+
case "hover":
|
|
687
|
+
return "hover";
|
|
688
|
+
case "scroll":
|
|
689
|
+
return "scroll";
|
|
690
|
+
case "goto":
|
|
691
|
+
return "navigate";
|
|
692
|
+
case "submit":
|
|
693
|
+
return "submit";
|
|
694
|
+
case "focus":
|
|
695
|
+
return "focus";
|
|
696
|
+
case "evaluate":
|
|
697
|
+
case "press":
|
|
698
|
+
case "shortcut":
|
|
699
|
+
return "evaluate";
|
|
700
|
+
case "assertVisible":
|
|
701
|
+
case "assertExists":
|
|
702
|
+
case "assertText":
|
|
703
|
+
case "assertUrl":
|
|
704
|
+
case "assertValue":
|
|
705
|
+
return step.success ? "assert-pass" : "assert-fail";
|
|
706
|
+
// Observation-only actions — no highlight
|
|
707
|
+
case "wait":
|
|
708
|
+
case "snapshot":
|
|
709
|
+
case "forms":
|
|
710
|
+
case "text":
|
|
711
|
+
case "screenshot":
|
|
712
|
+
case "newTab":
|
|
713
|
+
case "closeTab":
|
|
714
|
+
case "switchFrame":
|
|
715
|
+
case "switchToMain":
|
|
716
|
+
return null;
|
|
717
|
+
default:
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function getHighlightLabel(step, result, targetMetadata) {
|
|
722
|
+
switch (step.action) {
|
|
723
|
+
case "fill":
|
|
724
|
+
case "type":
|
|
725
|
+
return typeof step.value === "string" ? `"${redactValueForRecording(step.value, targetMetadata)}"` : void 0;
|
|
726
|
+
case "select":
|
|
727
|
+
return redactValueForRecording(
|
|
728
|
+
typeof step.value === "string" ? step.value : void 0,
|
|
729
|
+
targetMetadata
|
|
730
|
+
);
|
|
731
|
+
case "goto":
|
|
732
|
+
return step.url;
|
|
733
|
+
case "evaluate":
|
|
734
|
+
return "JS";
|
|
735
|
+
case "press":
|
|
736
|
+
return step.key;
|
|
737
|
+
case "shortcut":
|
|
738
|
+
return step.combo;
|
|
739
|
+
case "assertText":
|
|
740
|
+
case "assertUrl":
|
|
741
|
+
case "assertValue":
|
|
742
|
+
case "assertVisible":
|
|
743
|
+
case "assertExists":
|
|
744
|
+
return result.success ? "\u2713" : "\u2717";
|
|
745
|
+
default:
|
|
746
|
+
return void 0;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
475
750
|
// src/browser/actionability.ts
|
|
476
751
|
var ActionabilityError = class extends Error {
|
|
477
752
|
failureType;
|
|
@@ -1084,8 +1359,677 @@ var NavigationError = class extends Error {
|
|
|
1084
1359
|
}
|
|
1085
1360
|
};
|
|
1086
1361
|
|
|
1362
|
+
// src/trace/views.ts
|
|
1363
|
+
function takeRecent(events, limit = 5) {
|
|
1364
|
+
return events.slice(-limit).map((event) => ({
|
|
1365
|
+
ts: event.ts,
|
|
1366
|
+
event: event.event,
|
|
1367
|
+
summary: event.summary,
|
|
1368
|
+
severity: event.severity,
|
|
1369
|
+
url: event.url
|
|
1370
|
+
}));
|
|
1371
|
+
}
|
|
1372
|
+
function buildTraceSummaries(events) {
|
|
1373
|
+
return {
|
|
1374
|
+
ws: summarizeWs(events),
|
|
1375
|
+
voice: summarizeVoice(events),
|
|
1376
|
+
console: summarizeConsole(events),
|
|
1377
|
+
permissions: summarizePermissions(events),
|
|
1378
|
+
media: summarizeMedia(events),
|
|
1379
|
+
ui: summarizeUi(events),
|
|
1380
|
+
session: summarizeSession(events)
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function summarizeWs(events) {
|
|
1384
|
+
const relevant = events.filter((event) => event.channel === "ws" || event.event.startsWith("ws."));
|
|
1385
|
+
const connections = /* @__PURE__ */ new Map();
|
|
1386
|
+
for (const event of relevant) {
|
|
1387
|
+
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
1388
|
+
let connection = connections.get(id);
|
|
1389
|
+
if (!connection) {
|
|
1390
|
+
connection = { id, sent: 0, received: 0, lastMessages: [] };
|
|
1391
|
+
connections.set(id, connection);
|
|
1392
|
+
}
|
|
1393
|
+
connection.url = event.url ?? connection.url;
|
|
1394
|
+
if (event.event === "ws.connection.created") {
|
|
1395
|
+
connection.createdAt = event.ts;
|
|
1396
|
+
}
|
|
1397
|
+
if (event.event === "ws.connection.closed") {
|
|
1398
|
+
connection.closedAt = event.ts;
|
|
1399
|
+
}
|
|
1400
|
+
if (event.event === "ws.frame.sent") {
|
|
1401
|
+
connection.sent += 1;
|
|
1402
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
1403
|
+
if (payload) connection.lastMessages.push(`sent: ${payload}`);
|
|
1404
|
+
}
|
|
1405
|
+
if (event.event === "ws.frame.received") {
|
|
1406
|
+
connection.received += 1;
|
|
1407
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
1408
|
+
if (payload) connection.lastMessages.push(`recv: ${payload}`);
|
|
1409
|
+
}
|
|
1410
|
+
connection.lastMessages = connection.lastMessages.slice(-3);
|
|
1411
|
+
}
|
|
1412
|
+
const values = [...connections.values()];
|
|
1413
|
+
const reconnects = values.reduce((count, connection) => {
|
|
1414
|
+
return connection.closedAt && !connection.createdAt ? count : count;
|
|
1415
|
+
}, 0);
|
|
1416
|
+
return {
|
|
1417
|
+
view: "ws",
|
|
1418
|
+
totalEvents: relevant.length,
|
|
1419
|
+
connections: values.map((connection) => ({
|
|
1420
|
+
id: connection.id,
|
|
1421
|
+
url: connection.url ?? null,
|
|
1422
|
+
createdAt: connection.createdAt ?? null,
|
|
1423
|
+
closedAt: connection.closedAt ?? null,
|
|
1424
|
+
sent: connection.sent,
|
|
1425
|
+
received: connection.received,
|
|
1426
|
+
lastMessages: connection.lastMessages,
|
|
1427
|
+
connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
|
|
1428
|
+
})),
|
|
1429
|
+
reconnects,
|
|
1430
|
+
recent: takeRecent(relevant)
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
function summarizeConsole(events) {
|
|
1434
|
+
const relevant = events.filter(
|
|
1435
|
+
(event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
1436
|
+
);
|
|
1437
|
+
return {
|
|
1438
|
+
view: "console",
|
|
1439
|
+
errors: relevant.filter(
|
|
1440
|
+
(event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
1441
|
+
).length,
|
|
1442
|
+
warnings: relevant.filter((event) => event.event === "console.warn").length,
|
|
1443
|
+
logs: relevant.filter((event) => event.event === "console.log").length,
|
|
1444
|
+
recent: takeRecent(relevant)
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
function summarizePermissions(events) {
|
|
1448
|
+
const relevant = events.filter(
|
|
1449
|
+
(event) => event.channel === "permission" || event.event.startsWith("permission.")
|
|
1450
|
+
);
|
|
1451
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1452
|
+
for (const event of relevant) {
|
|
1453
|
+
const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
|
|
1454
|
+
const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
|
|
1455
|
+
if (name && state) {
|
|
1456
|
+
latest.set(name, state);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
view: "permissions",
|
|
1461
|
+
states: Object.fromEntries(latest),
|
|
1462
|
+
changes: relevant.filter((event) => event.event === "permission.changed").length,
|
|
1463
|
+
recent: takeRecent(relevant)
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function summarizeMedia(events) {
|
|
1467
|
+
const relevant = events.filter(
|
|
1468
|
+
(event) => event.channel === "media" || event.event.startsWith("media.")
|
|
1469
|
+
);
|
|
1470
|
+
const liveTracks = /* @__PURE__ */ new Map();
|
|
1471
|
+
for (const event of relevant) {
|
|
1472
|
+
const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
|
|
1473
|
+
const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
|
|
1474
|
+
const key = `${kind}:${label}`;
|
|
1475
|
+
if (event.event === "media.track.started") {
|
|
1476
|
+
liveTracks.set(key, kind);
|
|
1477
|
+
}
|
|
1478
|
+
if (event.event === "media.track.ended") {
|
|
1479
|
+
liveTracks.delete(key);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
view: "media",
|
|
1484
|
+
tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
|
|
1485
|
+
tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
|
|
1486
|
+
playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
|
|
1487
|
+
playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
|
|
1488
|
+
liveTracks: [...liveTracks.values()],
|
|
1489
|
+
recent: takeRecent(relevant)
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
function summarizeVoice(events) {
|
|
1493
|
+
const relevant = events.filter(
|
|
1494
|
+
(event) => event.channel === "voice" || event.event.startsWith("voice.")
|
|
1495
|
+
);
|
|
1496
|
+
return {
|
|
1497
|
+
view: "voice",
|
|
1498
|
+
ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
|
|
1499
|
+
notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
|
|
1500
|
+
captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
|
|
1501
|
+
captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
|
|
1502
|
+
detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
|
|
1503
|
+
recent: takeRecent(relevant)
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function summarizeUi(events) {
|
|
1507
|
+
const relevant = events.filter(
|
|
1508
|
+
(event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
|
|
1509
|
+
);
|
|
1510
|
+
return {
|
|
1511
|
+
view: "ui",
|
|
1512
|
+
actions: relevant.filter((event) => event.channel === "action").length,
|
|
1513
|
+
domChanges: relevant.filter((event) => event.channel === "dom").length,
|
|
1514
|
+
recent: takeRecent(relevant)
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function summarizeSession(events) {
|
|
1518
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
1519
|
+
const failedActions = events.filter((event) => event.event === "action.failed").length;
|
|
1520
|
+
for (const event of events) {
|
|
1521
|
+
byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
|
|
1522
|
+
}
|
|
1523
|
+
return {
|
|
1524
|
+
view: "session",
|
|
1525
|
+
totalEvents: events.length,
|
|
1526
|
+
byChannel: Object.fromEntries(byChannel),
|
|
1527
|
+
failedActions,
|
|
1528
|
+
recent: takeRecent(events)
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// src/recording/manifest.ts
|
|
1533
|
+
function isCanonicalRecordingManifest(value) {
|
|
1534
|
+
return Boolean(
|
|
1535
|
+
value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
function isLegacyRecordingManifest(value) {
|
|
1539
|
+
return Boolean(
|
|
1540
|
+
value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
function createRecordingManifest(input) {
|
|
1544
|
+
const actions = input.frames.map((frame) => {
|
|
1545
|
+
const actionId = frame.actionId ?? `action-${frame.seq}`;
|
|
1546
|
+
return {
|
|
1547
|
+
id: actionId,
|
|
1548
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1549
|
+
action: frame.action,
|
|
1550
|
+
selector: frame.selector,
|
|
1551
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1552
|
+
value: frame.value,
|
|
1553
|
+
url: frame.url,
|
|
1554
|
+
success: frame.success,
|
|
1555
|
+
durationMs: frame.durationMs,
|
|
1556
|
+
error: frame.error,
|
|
1557
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1558
|
+
pageUrl: frame.pageUrl,
|
|
1559
|
+
pageTitle: frame.pageTitle,
|
|
1560
|
+
coordinates: frame.coordinates,
|
|
1561
|
+
boundingBox: frame.boundingBox
|
|
1562
|
+
};
|
|
1563
|
+
});
|
|
1564
|
+
const screenshots = input.frames.map((frame) => ({
|
|
1565
|
+
id: `shot-${frame.seq}`,
|
|
1566
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1567
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1568
|
+
file: frame.screenshot,
|
|
1569
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1570
|
+
success: frame.success,
|
|
1571
|
+
pageUrl: frame.pageUrl,
|
|
1572
|
+
pageTitle: frame.pageTitle,
|
|
1573
|
+
coordinates: frame.coordinates,
|
|
1574
|
+
boundingBox: frame.boundingBox
|
|
1575
|
+
}));
|
|
1576
|
+
return {
|
|
1577
|
+
version: 2,
|
|
1578
|
+
recordedAt: input.recordedAt,
|
|
1579
|
+
session: {
|
|
1580
|
+
id: input.sessionId,
|
|
1581
|
+
startUrl: input.startUrl,
|
|
1582
|
+
endUrl: input.endUrl,
|
|
1583
|
+
targetId: input.targetId,
|
|
1584
|
+
profile: input.profile
|
|
1585
|
+
},
|
|
1586
|
+
recipe: {
|
|
1587
|
+
steps: input.steps
|
|
1588
|
+
},
|
|
1589
|
+
actions,
|
|
1590
|
+
screenshots,
|
|
1591
|
+
trace: {
|
|
1592
|
+
events: input.traceEvents,
|
|
1593
|
+
summaries: buildTraceSummaries(input.traceEvents)
|
|
1594
|
+
},
|
|
1595
|
+
assertions: input.assertions ?? [],
|
|
1596
|
+
notes: input.notes ?? [],
|
|
1597
|
+
artifacts: {
|
|
1598
|
+
recordingManifest: input.recordingManifest ?? "recording.json",
|
|
1599
|
+
screenshotDir: input.screenshotDir ?? "screenshots/"
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
function canonicalizeRecordingArtifact(value) {
|
|
1604
|
+
if (isCanonicalRecordingManifest(value)) {
|
|
1605
|
+
return value;
|
|
1606
|
+
}
|
|
1607
|
+
if (!isLegacyRecordingManifest(value)) {
|
|
1608
|
+
throw new Error("Unsupported recording artifact");
|
|
1609
|
+
}
|
|
1610
|
+
const traceEvents = buildTraceEventsFromLegacy(value);
|
|
1611
|
+
const steps = value.frames.map((frame) => frameToStep(frame));
|
|
1612
|
+
return createRecordingManifest({
|
|
1613
|
+
recordedAt: value.recordedAt,
|
|
1614
|
+
sessionId: value.sessionId,
|
|
1615
|
+
startUrl: value.startUrl,
|
|
1616
|
+
endUrl: value.endUrl,
|
|
1617
|
+
steps,
|
|
1618
|
+
frames: value.frames,
|
|
1619
|
+
traceEvents,
|
|
1620
|
+
notes: ["Converted from legacy recording manifest"]
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
function buildTraceEventsFromLegacy(value) {
|
|
1624
|
+
const events = [];
|
|
1625
|
+
for (const frame of value.frames) {
|
|
1626
|
+
events.push({
|
|
1627
|
+
traceId: frame.actionId ?? `legacy-${frame.seq}`,
|
|
1628
|
+
sessionId: value.sessionId,
|
|
1629
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1630
|
+
elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
|
|
1631
|
+
channel: "action",
|
|
1632
|
+
event: frame.success ? "action.succeeded" : "action.failed",
|
|
1633
|
+
severity: frame.success ? "info" : "error",
|
|
1634
|
+
summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
|
|
1635
|
+
data: {
|
|
1636
|
+
action: frame.action,
|
|
1637
|
+
selector: frame.selector,
|
|
1638
|
+
value: frame.value ?? null,
|
|
1639
|
+
pageUrl: frame.pageUrl ?? null,
|
|
1640
|
+
pageTitle: frame.pageTitle ?? null,
|
|
1641
|
+
screenshot: frame.screenshot
|
|
1642
|
+
},
|
|
1643
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1644
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1645
|
+
selector: frame.selector,
|
|
1646
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1647
|
+
url: frame.pageUrl ?? frame.url
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
return events;
|
|
1651
|
+
}
|
|
1652
|
+
function frameToStep(frame) {
|
|
1653
|
+
switch (frame.action) {
|
|
1654
|
+
case "fill":
|
|
1655
|
+
return { action: "fill", selector: frame.selector, value: frame.value };
|
|
1656
|
+
case "submit":
|
|
1657
|
+
return { action: "submit", selector: frame.selector };
|
|
1658
|
+
case "goto":
|
|
1659
|
+
return { action: "goto", url: frame.url ?? frame.pageUrl };
|
|
1660
|
+
case "press":
|
|
1661
|
+
return { action: "press", key: frame.value ?? "Enter" };
|
|
1662
|
+
default:
|
|
1663
|
+
return { action: "click", selector: frame.selector };
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/trace/script.ts
|
|
1668
|
+
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
1669
|
+
var TRACE_SCRIPT = `
|
|
1670
|
+
(() => {
|
|
1671
|
+
if (window.__bpTraceInstalled) return;
|
|
1672
|
+
window.__bpTraceInstalled = true;
|
|
1673
|
+
|
|
1674
|
+
const binding = globalThis.${TRACE_BINDING_NAME};
|
|
1675
|
+
if (typeof binding !== 'function') return;
|
|
1676
|
+
|
|
1677
|
+
const emit = (event, data = {}, severity = 'info', summary) => {
|
|
1678
|
+
try {
|
|
1679
|
+
globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
|
|
1680
|
+
const payload = {
|
|
1681
|
+
event,
|
|
1682
|
+
severity,
|
|
1683
|
+
summary: summary || event,
|
|
1684
|
+
ts: Date.now(),
|
|
1685
|
+
data,
|
|
1686
|
+
};
|
|
1687
|
+
globalThis.__bpTraceRecentEvents.push(payload);
|
|
1688
|
+
if (globalThis.__bpTraceRecentEvents.length > 200) {
|
|
1689
|
+
globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
|
|
1690
|
+
}
|
|
1691
|
+
binding(JSON.stringify(payload));
|
|
1692
|
+
} catch {}
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
const patchWebSocket = () => {
|
|
1696
|
+
const NativeWebSocket = window.WebSocket;
|
|
1697
|
+
if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
|
|
1698
|
+
window.__bpTraceWebSocketInstalled = true;
|
|
1699
|
+
|
|
1700
|
+
const nextId = () => Math.random().toString(36).slice(2, 10);
|
|
1701
|
+
|
|
1702
|
+
const patchInstance = (socket, urlValue) => {
|
|
1703
|
+
if (!socket || socket.__bpTracePatched) return socket;
|
|
1704
|
+
socket.__bpTracePatched = true;
|
|
1705
|
+
socket.__bpTraceId = socket.__bpTraceId || nextId();
|
|
1706
|
+
socket.__bpTraceUrl = String(urlValue || socket.url || '');
|
|
1707
|
+
globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
|
|
1708
|
+
globalThis.__bpTrackedWebSockets.add(socket);
|
|
1709
|
+
|
|
1710
|
+
emit(
|
|
1711
|
+
'ws.connection.created',
|
|
1712
|
+
{ connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
|
|
1713
|
+
'info',
|
|
1714
|
+
'WebSocket opened ' + socket.__bpTraceUrl
|
|
1715
|
+
);
|
|
1716
|
+
|
|
1717
|
+
const originalSend = socket.send;
|
|
1718
|
+
socket.send = function(data) {
|
|
1719
|
+
const payload =
|
|
1720
|
+
typeof data === 'string'
|
|
1721
|
+
? data
|
|
1722
|
+
: data && typeof data.toString === 'function'
|
|
1723
|
+
? data.toString()
|
|
1724
|
+
: '[binary]';
|
|
1725
|
+
emit(
|
|
1726
|
+
'ws.frame.sent',
|
|
1727
|
+
{
|
|
1728
|
+
connectionId: socket.__bpTraceId,
|
|
1729
|
+
url: socket.__bpTraceUrl,
|
|
1730
|
+
payload,
|
|
1731
|
+
length: payload.length,
|
|
1732
|
+
},
|
|
1733
|
+
'info',
|
|
1734
|
+
'WebSocket frame sent'
|
|
1735
|
+
);
|
|
1736
|
+
return originalSend.call(this, data);
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
socket.addEventListener('message', (event) => {
|
|
1740
|
+
if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
const data = event && 'data' in event ? event.data : '';
|
|
1744
|
+
const payload =
|
|
1745
|
+
typeof data === 'string'
|
|
1746
|
+
? data
|
|
1747
|
+
: data && typeof data.toString === 'function'
|
|
1748
|
+
? data.toString()
|
|
1749
|
+
: '[binary]';
|
|
1750
|
+
emit(
|
|
1751
|
+
'ws.frame.received',
|
|
1752
|
+
{
|
|
1753
|
+
connectionId: socket.__bpTraceId,
|
|
1754
|
+
url: socket.__bpTraceUrl,
|
|
1755
|
+
payload,
|
|
1756
|
+
length: payload.length,
|
|
1757
|
+
},
|
|
1758
|
+
'info',
|
|
1759
|
+
'WebSocket frame received'
|
|
1760
|
+
);
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
socket.addEventListener('close', (event) => {
|
|
1764
|
+
if (socket.__bpTraceClosed) {
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
socket.__bpTraceClosed = true;
|
|
1768
|
+
try {
|
|
1769
|
+
globalThis.__bpTrackedWebSockets.delete(socket);
|
|
1770
|
+
} catch {}
|
|
1771
|
+
emit(
|
|
1772
|
+
'ws.connection.closed',
|
|
1773
|
+
{
|
|
1774
|
+
connectionId: socket.__bpTraceId,
|
|
1775
|
+
url: socket.__bpTraceUrl,
|
|
1776
|
+
code: event.code,
|
|
1777
|
+
reason: event.reason,
|
|
1778
|
+
},
|
|
1779
|
+
'warn',
|
|
1780
|
+
'WebSocket closed'
|
|
1781
|
+
);
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
return socket;
|
|
1785
|
+
};
|
|
1786
|
+
|
|
1787
|
+
const TracedWebSocket = function(url, protocols) {
|
|
1788
|
+
return arguments.length > 1
|
|
1789
|
+
? patchInstance(new NativeWebSocket(url, protocols), url)
|
|
1790
|
+
: patchInstance(new NativeWebSocket(url), url);
|
|
1791
|
+
};
|
|
1792
|
+
TracedWebSocket.prototype = NativeWebSocket.prototype;
|
|
1793
|
+
Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
|
|
1794
|
+
window.WebSocket = TracedWebSocket;
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
window.addEventListener('error', (errorEvent) => {
|
|
1798
|
+
emit(
|
|
1799
|
+
'runtime.exception',
|
|
1800
|
+
{
|
|
1801
|
+
message: errorEvent.message,
|
|
1802
|
+
filename: errorEvent.filename,
|
|
1803
|
+
line: errorEvent.lineno,
|
|
1804
|
+
column: errorEvent.colno,
|
|
1805
|
+
},
|
|
1806
|
+
'error',
|
|
1807
|
+
errorEvent.message || 'Uncaught error'
|
|
1808
|
+
);
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
1812
|
+
const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
|
|
1813
|
+
emit('runtime.unhandledRejection', { reason }, 'error', reason);
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
const patchPermissions = async () => {
|
|
1817
|
+
if (!navigator.permissions || !navigator.permissions.query) return;
|
|
1818
|
+
|
|
1819
|
+
const names = ['geolocation', 'microphone', 'camera', 'notifications'];
|
|
1820
|
+
for (const name of names) {
|
|
1821
|
+
try {
|
|
1822
|
+
const status = await navigator.permissions.query({ name });
|
|
1823
|
+
emit(
|
|
1824
|
+
'permission.state',
|
|
1825
|
+
{ name, state: status.state },
|
|
1826
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1827
|
+
name + ': ' + status.state
|
|
1828
|
+
);
|
|
1829
|
+
status.addEventListener('change', () => {
|
|
1830
|
+
emit(
|
|
1831
|
+
'permission.changed',
|
|
1832
|
+
{ name, state: status.state },
|
|
1833
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1834
|
+
name + ': ' + status.state
|
|
1835
|
+
);
|
|
1836
|
+
});
|
|
1837
|
+
} catch {}
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1841
|
+
const patchMediaElement = (element) => {
|
|
1842
|
+
if (!element || element.__bpTracePatched) return;
|
|
1843
|
+
element.__bpTracePatched = true;
|
|
1844
|
+
|
|
1845
|
+
element.addEventListener('play', () => {
|
|
1846
|
+
emit(
|
|
1847
|
+
'media.playback.started',
|
|
1848
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1849
|
+
'info',
|
|
1850
|
+
'Media playback started'
|
|
1851
|
+
);
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
const onStop = () => {
|
|
1855
|
+
emit(
|
|
1856
|
+
'media.playback.stopped',
|
|
1857
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1858
|
+
'warn',
|
|
1859
|
+
'Media playback stopped'
|
|
1860
|
+
);
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
element.addEventListener('pause', onStop);
|
|
1864
|
+
element.addEventListener('ended', onStop);
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1867
|
+
const patchMediaElements = () => {
|
|
1868
|
+
document.querySelectorAll('audio,video').forEach(patchMediaElement);
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
patchMediaElements();
|
|
1872
|
+
patchWebSocket();
|
|
1873
|
+
|
|
1874
|
+
if (document.documentElement) {
|
|
1875
|
+
const observer = new MutationObserver(() => {
|
|
1876
|
+
patchMediaElements();
|
|
1877
|
+
});
|
|
1878
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
1882
|
+
const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
|
1883
|
+
navigator.mediaDevices.getUserMedia = async (...args) => {
|
|
1884
|
+
emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
|
|
1885
|
+
try {
|
|
1886
|
+
const stream = await original(...args);
|
|
1887
|
+
const tracks = stream.getTracks();
|
|
1888
|
+
|
|
1889
|
+
for (const track of tracks) {
|
|
1890
|
+
emit(
|
|
1891
|
+
'media.track.started',
|
|
1892
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1893
|
+
'info',
|
|
1894
|
+
track.kind + ' track started'
|
|
1895
|
+
);
|
|
1896
|
+
track.addEventListener('ended', () => {
|
|
1897
|
+
emit(
|
|
1898
|
+
'media.track.ended',
|
|
1899
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1900
|
+
'warn',
|
|
1901
|
+
track.kind + ' track ended'
|
|
1902
|
+
);
|
|
1903
|
+
emit(
|
|
1904
|
+
'voice.capture.stopped',
|
|
1905
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1906
|
+
'warn',
|
|
1907
|
+
'Voice capture stopped'
|
|
1908
|
+
);
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
emit(
|
|
1913
|
+
'voice.capture.detectedAudio',
|
|
1914
|
+
{ trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
|
|
1915
|
+
'info',
|
|
1916
|
+
'Voice capture detected audio'
|
|
1917
|
+
);
|
|
1918
|
+
|
|
1919
|
+
return stream;
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
emit(
|
|
1922
|
+
'voice.pipeline.notReady',
|
|
1923
|
+
{ message: String(error && error.message ? error.message : error) },
|
|
1924
|
+
'error',
|
|
1925
|
+
String(error && error.message ? error.message : error)
|
|
1926
|
+
);
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
document.addEventListener('visibilitychange', () => {
|
|
1933
|
+
emit(
|
|
1934
|
+
'dom.state.changed',
|
|
1935
|
+
{ visibilityState: document.visibilityState },
|
|
1936
|
+
document.visibilityState === 'hidden' ? 'warn' : 'info',
|
|
1937
|
+
'Visibility ' + document.visibilityState
|
|
1938
|
+
);
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
patchPermissions();
|
|
1942
|
+
emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
|
|
1943
|
+
})();
|
|
1944
|
+
`;
|
|
1945
|
+
|
|
1946
|
+
// src/trace/model.ts
|
|
1947
|
+
function createTraceId(prefix = "evt") {
|
|
1948
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1949
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1950
|
+
}
|
|
1951
|
+
function normalizeTraceEvent(event) {
|
|
1952
|
+
return {
|
|
1953
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1954
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1955
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
1956
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
1957
|
+
data: event.data ?? {},
|
|
1958
|
+
...event
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function inferSeverity(eventName) {
|
|
1962
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1963
|
+
return "error";
|
|
1964
|
+
}
|
|
1965
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1966
|
+
return "warn";
|
|
1967
|
+
}
|
|
1968
|
+
return "info";
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// src/trace/live.ts
|
|
1972
|
+
function globToRegex(pattern) {
|
|
1973
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1974
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1975
|
+
return new RegExp(`^${withWildcards}$`);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1087
1978
|
// src/actions/executor.ts
|
|
1088
1979
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1980
|
+
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
1981
|
+
"wait",
|
|
1982
|
+
"snapshot",
|
|
1983
|
+
"forms",
|
|
1984
|
+
"text",
|
|
1985
|
+
"screenshot"
|
|
1986
|
+
];
|
|
1987
|
+
function loadExistingRecording(manifestPath) {
|
|
1988
|
+
try {
|
|
1989
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1990
|
+
if (raw.version === 1) {
|
|
1991
|
+
const legacy = raw;
|
|
1992
|
+
return {
|
|
1993
|
+
frames: Array.isArray(legacy.frames) ? legacy.frames : [],
|
|
1994
|
+
traceEvents: [],
|
|
1995
|
+
recordedAt: legacy.recordedAt,
|
|
1996
|
+
startUrl: legacy.startUrl
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
const artifact = canonicalizeRecordingArtifact(raw);
|
|
2000
|
+
const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
|
|
2001
|
+
const frames = artifact.actions.map((action, index) => {
|
|
2002
|
+
const screenshot = screenshotsByAction.get(action.id);
|
|
2003
|
+
return {
|
|
2004
|
+
seq: index + 1,
|
|
2005
|
+
timestamp: Date.parse(action.ts),
|
|
2006
|
+
action: action.action,
|
|
2007
|
+
selector: action.selector,
|
|
2008
|
+
selectorUsed: action.selectorUsed,
|
|
2009
|
+
value: action.value,
|
|
2010
|
+
url: action.url,
|
|
2011
|
+
coordinates: action.coordinates,
|
|
2012
|
+
boundingBox: action.boundingBox,
|
|
2013
|
+
success: action.success,
|
|
2014
|
+
durationMs: action.durationMs,
|
|
2015
|
+
error: action.error,
|
|
2016
|
+
screenshot: screenshot?.file ?? "",
|
|
2017
|
+
pageUrl: action.pageUrl,
|
|
2018
|
+
pageTitle: action.pageTitle,
|
|
2019
|
+
stepIndex: action.stepIndex,
|
|
2020
|
+
actionId: action.id
|
|
2021
|
+
};
|
|
2022
|
+
});
|
|
2023
|
+
return {
|
|
2024
|
+
frames,
|
|
2025
|
+
traceEvents: artifact.trace.events,
|
|
2026
|
+
recordedAt: artifact.recordedAt,
|
|
2027
|
+
startUrl: artifact.session.startUrl
|
|
2028
|
+
};
|
|
2029
|
+
} catch {
|
|
2030
|
+
return { frames: [], traceEvents: [] };
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
1089
2033
|
function classifyFailure(error) {
|
|
1090
2034
|
if (error instanceof ElementNotFoundError) {
|
|
1091
2035
|
return { reason: "missing" };
|
|
@@ -1165,6 +2109,12 @@ var BatchExecutor = class {
|
|
|
1165
2109
|
const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
|
|
1166
2110
|
const results = [];
|
|
1167
2111
|
const startTime = Date.now();
|
|
2112
|
+
const recording = options.record ? this.createRecordingContext(options.record) : null;
|
|
2113
|
+
if (steps.some((step) => step.action === "waitForWsMessage")) {
|
|
2114
|
+
await this.ensureTraceHooks();
|
|
2115
|
+
}
|
|
2116
|
+
const startUrl = recording ? await this.getPageUrlSafe() : "";
|
|
2117
|
+
let stoppedAtIndex;
|
|
1168
2118
|
for (let i = 0; i < steps.length; i++) {
|
|
1169
2119
|
const step = steps[i];
|
|
1170
2120
|
const stepStart = Date.now();
|
|
@@ -1172,13 +2122,34 @@ var BatchExecutor = class {
|
|
|
1172
2122
|
const retryDelay = step.retryDelay ?? 500;
|
|
1173
2123
|
let lastError;
|
|
1174
2124
|
let succeeded = false;
|
|
2125
|
+
if (recording) {
|
|
2126
|
+
recording.traceEvents.push(
|
|
2127
|
+
normalizeTraceEvent({
|
|
2128
|
+
traceId: createTraceId("action"),
|
|
2129
|
+
elapsedMs: Date.now() - startTime,
|
|
2130
|
+
channel: "action",
|
|
2131
|
+
event: "action.started",
|
|
2132
|
+
summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
|
|
2133
|
+
data: {
|
|
2134
|
+
action: step.action,
|
|
2135
|
+
selector: step.selector ?? null,
|
|
2136
|
+
url: step.url ?? null
|
|
2137
|
+
},
|
|
2138
|
+
actionId: `action-${i + 1}`,
|
|
2139
|
+
stepIndex: i,
|
|
2140
|
+
selector: step.selector,
|
|
2141
|
+
url: step.url
|
|
2142
|
+
})
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
1175
2145
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1176
2146
|
if (attempt > 0) {
|
|
1177
2147
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1178
2148
|
}
|
|
1179
2149
|
try {
|
|
2150
|
+
this.page.resetLastActionPosition();
|
|
1180
2151
|
const result = await this.executeStep(step, timeout);
|
|
1181
|
-
|
|
2152
|
+
const stepResult = {
|
|
1182
2153
|
index: i,
|
|
1183
2154
|
action: step.action,
|
|
1184
2155
|
selector: step.selector,
|
|
@@ -1186,8 +2157,37 @@ var BatchExecutor = class {
|
|
|
1186
2157
|
success: true,
|
|
1187
2158
|
durationMs: Date.now() - stepStart,
|
|
1188
2159
|
result: result.value,
|
|
1189
|
-
text: result.text
|
|
1190
|
-
|
|
2160
|
+
text: result.text,
|
|
2161
|
+
timestamp: Date.now(),
|
|
2162
|
+
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
2163
|
+
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
2164
|
+
};
|
|
2165
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
2166
|
+
await this.captureRecordingFrame(step, stepResult, recording);
|
|
2167
|
+
}
|
|
2168
|
+
if (recording) {
|
|
2169
|
+
recording.traceEvents.push(
|
|
2170
|
+
normalizeTraceEvent({
|
|
2171
|
+
traceId: createTraceId("action"),
|
|
2172
|
+
elapsedMs: Date.now() - startTime,
|
|
2173
|
+
channel: "action",
|
|
2174
|
+
event: "action.succeeded",
|
|
2175
|
+
summary: `${step.action} succeeded`,
|
|
2176
|
+
data: {
|
|
2177
|
+
action: step.action,
|
|
2178
|
+
selector: step.selector ?? null,
|
|
2179
|
+
selectorUsed: result.selectorUsed ?? null,
|
|
2180
|
+
durationMs: Date.now() - stepStart
|
|
2181
|
+
},
|
|
2182
|
+
actionId: `action-${i + 1}`,
|
|
2183
|
+
stepIndex: i,
|
|
2184
|
+
selector: step.selector,
|
|
2185
|
+
selectorUsed: result.selectorUsed,
|
|
2186
|
+
url: step.url
|
|
2187
|
+
})
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
results.push(stepResult);
|
|
1191
2191
|
succeeded = true;
|
|
1192
2192
|
break;
|
|
1193
2193
|
} catch (error) {
|
|
@@ -1208,7 +2208,7 @@ var BatchExecutor = class {
|
|
|
1208
2208
|
} catch {
|
|
1209
2209
|
}
|
|
1210
2210
|
}
|
|
1211
|
-
|
|
2211
|
+
const failedResult = {
|
|
1212
2212
|
index: i,
|
|
1213
2213
|
action: step.action,
|
|
1214
2214
|
selector: step.selector,
|
|
@@ -1218,25 +2218,183 @@ var BatchExecutor = class {
|
|
|
1218
2218
|
hints,
|
|
1219
2219
|
failureReason: reason,
|
|
1220
2220
|
coveringElement,
|
|
1221
|
-
suggestion: getSuggestion(reason)
|
|
1222
|
-
|
|
2221
|
+
suggestion: getSuggestion(reason),
|
|
2222
|
+
timestamp: Date.now()
|
|
2223
|
+
};
|
|
2224
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
2225
|
+
await this.captureRecordingFrame(step, failedResult, recording);
|
|
2226
|
+
}
|
|
2227
|
+
if (recording) {
|
|
2228
|
+
recording.traceEvents.push(
|
|
2229
|
+
normalizeTraceEvent({
|
|
2230
|
+
traceId: createTraceId("action"),
|
|
2231
|
+
elapsedMs: Date.now() - startTime,
|
|
2232
|
+
channel: "action",
|
|
2233
|
+
event: "action.failed",
|
|
2234
|
+
severity: "error",
|
|
2235
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
2236
|
+
data: {
|
|
2237
|
+
action: step.action,
|
|
2238
|
+
selector: step.selector ?? null,
|
|
2239
|
+
error: errorMessage,
|
|
2240
|
+
reason
|
|
2241
|
+
},
|
|
2242
|
+
actionId: `action-${i + 1}`,
|
|
2243
|
+
stepIndex: i,
|
|
2244
|
+
selector: step.selector,
|
|
2245
|
+
url: step.url
|
|
2246
|
+
})
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
results.push(failedResult);
|
|
1223
2250
|
if (onFail === "stop" && !step.optional) {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
stoppedAtIndex: i,
|
|
1227
|
-
steps: results,
|
|
1228
|
-
totalDurationMs: Date.now() - startTime
|
|
1229
|
-
};
|
|
2251
|
+
stoppedAtIndex = i;
|
|
2252
|
+
break;
|
|
1230
2253
|
}
|
|
1231
2254
|
}
|
|
1232
2255
|
}
|
|
1233
|
-
const
|
|
2256
|
+
const totalDurationMs = Date.now() - startTime;
|
|
2257
|
+
const allSuccess = stoppedAtIndex === void 0 && results.every((result) => result.success || steps[result.index]?.optional);
|
|
2258
|
+
let recordingManifest;
|
|
2259
|
+
if (recording) {
|
|
2260
|
+
recordingManifest = await this.writeRecordingManifest(
|
|
2261
|
+
recording,
|
|
2262
|
+
startTime,
|
|
2263
|
+
startUrl,
|
|
2264
|
+
allSuccess,
|
|
2265
|
+
steps
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
1234
2268
|
return {
|
|
1235
2269
|
success: allSuccess,
|
|
2270
|
+
stoppedAtIndex,
|
|
1236
2271
|
steps: results,
|
|
1237
|
-
totalDurationMs
|
|
2272
|
+
totalDurationMs,
|
|
2273
|
+
recordingManifest
|
|
1238
2274
|
};
|
|
1239
2275
|
}
|
|
2276
|
+
createRecordingContext(record) {
|
|
2277
|
+
const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
|
|
2278
|
+
const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
|
|
2279
|
+
const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
|
|
2280
|
+
const existing = loadExistingRecording(manifestPath);
|
|
2281
|
+
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
2282
|
+
return {
|
|
2283
|
+
baseDir,
|
|
2284
|
+
screenshotDir,
|
|
2285
|
+
sessionId: record.sessionId ?? this.page.targetId,
|
|
2286
|
+
frames: existing.frames,
|
|
2287
|
+
traceEvents: existing.traceEvents,
|
|
2288
|
+
format: record.format ?? "webp",
|
|
2289
|
+
quality: Math.max(0, Math.min(100, record.quality ?? 40)),
|
|
2290
|
+
highlights: record.highlights !== false,
|
|
2291
|
+
skipActions: new Set(record.skipActions ?? DEFAULT_RECORDING_SKIP_ACTIONS)
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
async getPageUrlSafe() {
|
|
2295
|
+
try {
|
|
2296
|
+
return await this.page.url();
|
|
2297
|
+
} catch {
|
|
2298
|
+
return "";
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Capture a recording screenshot frame with optional highlight overlay
|
|
2303
|
+
*/
|
|
2304
|
+
async captureRecordingFrame(step, stepResult, recording) {
|
|
2305
|
+
const targetMetadata = this.page.getLastActionTargetMetadata();
|
|
2306
|
+
let highlightInjected = false;
|
|
2307
|
+
try {
|
|
2308
|
+
const ts = Date.now();
|
|
2309
|
+
const seq = String(recording.frames.length + 1).padStart(4, "0");
|
|
2310
|
+
const filename = `${seq}-${ts}-${stepResult.action}.${recording.format}`;
|
|
2311
|
+
const filepath = (0, import_node_path.join)(recording.screenshotDir, filename);
|
|
2312
|
+
if (recording.highlights) {
|
|
2313
|
+
const kind = stepToHighlightKind(stepResult);
|
|
2314
|
+
if (kind) {
|
|
2315
|
+
await injectActionHighlight(this.page, {
|
|
2316
|
+
kind,
|
|
2317
|
+
bbox: stepResult.boundingBox,
|
|
2318
|
+
point: stepResult.coordinates,
|
|
2319
|
+
label: getHighlightLabel(step, stepResult, targetMetadata)
|
|
2320
|
+
});
|
|
2321
|
+
highlightInjected = true;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
const base64 = await this.page.screenshot({
|
|
2325
|
+
format: recording.format,
|
|
2326
|
+
quality: recording.quality
|
|
2327
|
+
});
|
|
2328
|
+
const buffer = Buffer.from(base64, "base64");
|
|
2329
|
+
fs.writeFileSync(filepath, buffer);
|
|
2330
|
+
stepResult.screenshotPath = filepath;
|
|
2331
|
+
let pageUrl;
|
|
2332
|
+
let pageTitle;
|
|
2333
|
+
try {
|
|
2334
|
+
pageUrl = await this.page.url();
|
|
2335
|
+
pageTitle = await this.page.title();
|
|
2336
|
+
} catch {
|
|
2337
|
+
}
|
|
2338
|
+
recording.frames.push({
|
|
2339
|
+
seq: recording.frames.length + 1,
|
|
2340
|
+
timestamp: ts,
|
|
2341
|
+
action: stepResult.action,
|
|
2342
|
+
selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
|
|
2343
|
+
selectorUsed: stepResult.selectorUsed,
|
|
2344
|
+
value: redactValueForRecording(
|
|
2345
|
+
typeof step.value === "string" ? step.value : void 0,
|
|
2346
|
+
targetMetadata
|
|
2347
|
+
),
|
|
2348
|
+
url: step.url,
|
|
2349
|
+
coordinates: stepResult.coordinates,
|
|
2350
|
+
boundingBox: stepResult.boundingBox,
|
|
2351
|
+
success: stepResult.success,
|
|
2352
|
+
durationMs: stepResult.durationMs,
|
|
2353
|
+
error: stepResult.error,
|
|
2354
|
+
screenshot: filename,
|
|
2355
|
+
pageUrl,
|
|
2356
|
+
pageTitle,
|
|
2357
|
+
stepIndex: stepResult.index,
|
|
2358
|
+
actionId: `action-${stepResult.index + 1}`
|
|
2359
|
+
});
|
|
2360
|
+
} catch {
|
|
2361
|
+
} finally {
|
|
2362
|
+
if (recording.highlights || highlightInjected) {
|
|
2363
|
+
await removeActionHighlight(this.page);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Write recording manifest to disk
|
|
2369
|
+
*/
|
|
2370
|
+
async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
|
|
2371
|
+
let endUrl = startUrl;
|
|
2372
|
+
try {
|
|
2373
|
+
endUrl = await this.page.url();
|
|
2374
|
+
} catch {
|
|
2375
|
+
}
|
|
2376
|
+
const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
|
|
2377
|
+
let recordedAt = new Date(startTime).toISOString();
|
|
2378
|
+
let originalStartUrl = startUrl;
|
|
2379
|
+
const existing = loadExistingRecording(manifestPath);
|
|
2380
|
+
if (existing.recordedAt) recordedAt = existing.recordedAt;
|
|
2381
|
+
if (existing.startUrl) originalStartUrl = existing.startUrl;
|
|
2382
|
+
const manifest = createRecordingManifest({
|
|
2383
|
+
recordedAt,
|
|
2384
|
+
sessionId: recording.sessionId,
|
|
2385
|
+
startUrl: originalStartUrl,
|
|
2386
|
+
endUrl,
|
|
2387
|
+
targetId: this.page.targetId,
|
|
2388
|
+
steps,
|
|
2389
|
+
frames: recording.frames,
|
|
2390
|
+
traceEvents: recording.traceEvents,
|
|
2391
|
+
notes: success ? [] : ["Replay ended with at least one failed action."],
|
|
2392
|
+
recordingManifest: "recording.json",
|
|
2393
|
+
screenshotDir: "screenshots/"
|
|
2394
|
+
});
|
|
2395
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2396
|
+
return manifestPath;
|
|
2397
|
+
}
|
|
1240
2398
|
/**
|
|
1241
2399
|
* Execute a single step
|
|
1242
2400
|
*/
|
|
@@ -1516,6 +2674,39 @@ var BatchExecutor = class {
|
|
|
1516
2674
|
}
|
|
1517
2675
|
return { selectorUsed: usedSelector, value: actual };
|
|
1518
2676
|
}
|
|
2677
|
+
case "waitForWsMessage": {
|
|
2678
|
+
if (typeof step.match !== "string") {
|
|
2679
|
+
throw new Error("waitForWsMessage requires match");
|
|
2680
|
+
}
|
|
2681
|
+
const message = await this.waitForWsMessage(step.match, step.where, timeout);
|
|
2682
|
+
return { value: message };
|
|
2683
|
+
}
|
|
2684
|
+
case "assertNoConsoleErrors": {
|
|
2685
|
+
await this.assertNoConsoleErrors(step.windowMs ?? timeout);
|
|
2686
|
+
return {};
|
|
2687
|
+
}
|
|
2688
|
+
case "assertTextChanged": {
|
|
2689
|
+
const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
|
|
2690
|
+
if (typeof step.to !== "string") {
|
|
2691
|
+
throw new Error("assertTextChanged requires to");
|
|
2692
|
+
}
|
|
2693
|
+
const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
|
|
2694
|
+
return { selectorUsed: selector, text };
|
|
2695
|
+
}
|
|
2696
|
+
case "assertPermission": {
|
|
2697
|
+
if (!step.name || !step.state) {
|
|
2698
|
+
throw new Error("assertPermission requires name and state");
|
|
2699
|
+
}
|
|
2700
|
+
const permission = await this.assertPermission(step.name, step.state);
|
|
2701
|
+
return { value: permission };
|
|
2702
|
+
}
|
|
2703
|
+
case "assertMediaTrackLive": {
|
|
2704
|
+
if (!step.kind) {
|
|
2705
|
+
throw new Error("assertMediaTrackLive requires kind");
|
|
2706
|
+
}
|
|
2707
|
+
const media = await this.assertMediaTrackLive(step.kind);
|
|
2708
|
+
return { value: media };
|
|
2709
|
+
}
|
|
1519
2710
|
default: {
|
|
1520
2711
|
const action = step.action;
|
|
1521
2712
|
const aliases = {
|
|
@@ -1569,7 +2760,7 @@ var BatchExecutor = class {
|
|
|
1569
2760
|
};
|
|
1570
2761
|
const suggestion = aliases[action.toLowerCase()];
|
|
1571
2762
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
1572
|
-
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";
|
|
2763
|
+
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";
|
|
1573
2764
|
throw new Error(`Unknown action "${action}".${hint}
|
|
1574
2765
|
|
|
1575
2766
|
Valid actions: ${valid}`);
|
|
@@ -1585,6 +2776,233 @@ Valid actions: ${valid}`);
|
|
|
1585
2776
|
if (matched) return matched;
|
|
1586
2777
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
1587
2778
|
}
|
|
2779
|
+
async ensureTraceHooks() {
|
|
2780
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2781
|
+
await this.page.cdpClient.send("Page.enable");
|
|
2782
|
+
await this.page.cdpClient.send("Network.enable");
|
|
2783
|
+
try {
|
|
2784
|
+
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
2785
|
+
} catch {
|
|
2786
|
+
}
|
|
2787
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
|
|
2788
|
+
await this.page.cdpClient.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
|
|
2789
|
+
}
|
|
2790
|
+
async waitForWsMessage(match, where, timeout) {
|
|
2791
|
+
await this.ensureTraceHooks();
|
|
2792
|
+
const regex = globToRegex(match);
|
|
2793
|
+
const wsUrls = /* @__PURE__ */ new Map();
|
|
2794
|
+
const recentMatch = await this.findRecentWsMessage(regex, where);
|
|
2795
|
+
if (recentMatch) {
|
|
2796
|
+
return recentMatch;
|
|
2797
|
+
}
|
|
2798
|
+
return new Promise((resolve, reject) => {
|
|
2799
|
+
const cleanup = () => {
|
|
2800
|
+
this.page.cdpClient.off("Network.webSocketCreated", onCreated);
|
|
2801
|
+
this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
|
|
2802
|
+
this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
|
|
2803
|
+
clearTimeout(timer);
|
|
2804
|
+
};
|
|
2805
|
+
const onCreated = (params) => {
|
|
2806
|
+
wsUrls.set(String(params["requestId"] ?? ""), String(params["url"] ?? ""));
|
|
2807
|
+
};
|
|
2808
|
+
const onFrame = (params) => {
|
|
2809
|
+
const requestId = String(params["requestId"] ?? "");
|
|
2810
|
+
const response = params["response"] ?? {};
|
|
2811
|
+
const payload = String(response.payloadData ?? "");
|
|
2812
|
+
const url = wsUrls.get(requestId) ?? "";
|
|
2813
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
cleanup();
|
|
2820
|
+
resolve({ requestId, url, payload });
|
|
2821
|
+
};
|
|
2822
|
+
const onBinding = (params) => {
|
|
2823
|
+
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
try {
|
|
2827
|
+
const parsed = JSON.parse(String(params["payload"] ?? ""));
|
|
2828
|
+
if (parsed.event !== "ws.frame.received") {
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
const data = parsed.data ?? {};
|
|
2832
|
+
const payload = String(data["payload"] ?? "");
|
|
2833
|
+
const url = String(data["url"] ?? "");
|
|
2834
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
cleanup();
|
|
2841
|
+
resolve({
|
|
2842
|
+
requestId: String(data["connectionId"] ?? ""),
|
|
2843
|
+
url,
|
|
2844
|
+
payload
|
|
2845
|
+
});
|
|
2846
|
+
} catch {
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
const timer = setTimeout(() => {
|
|
2850
|
+
cleanup();
|
|
2851
|
+
reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
|
|
2852
|
+
}, timeout);
|
|
2853
|
+
this.page.cdpClient.on("Network.webSocketCreated", onCreated);
|
|
2854
|
+
this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
|
|
2855
|
+
this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
payloadMatchesWhere(payload, where) {
|
|
2859
|
+
try {
|
|
2860
|
+
const parsed = JSON.parse(payload);
|
|
2861
|
+
return Object.entries(where).every(([key, expected]) => {
|
|
2862
|
+
const actual = key.split(".").reduce((current, part) => {
|
|
2863
|
+
if (!current || typeof current !== "object") {
|
|
2864
|
+
return void 0;
|
|
2865
|
+
}
|
|
2866
|
+
return current[part];
|
|
2867
|
+
}, parsed);
|
|
2868
|
+
return actual === expected;
|
|
2869
|
+
});
|
|
2870
|
+
} catch {
|
|
2871
|
+
return false;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
async findRecentWsMessage(regex, where) {
|
|
2875
|
+
const recent = await this.page.evaluate(
|
|
2876
|
+
"(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
|
|
2877
|
+
);
|
|
2878
|
+
if (!Array.isArray(recent)) {
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
for (let i = recent.length - 1; i >= 0; i--) {
|
|
2882
|
+
const entry = recent[i];
|
|
2883
|
+
if (!entry || typeof entry !== "object") {
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
const event = String(entry["event"] ?? "");
|
|
2887
|
+
if (event !== "ws.frame.received") {
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2890
|
+
const data = entry["data"] ?? {};
|
|
2891
|
+
const payload = String(data["payload"] ?? "");
|
|
2892
|
+
const url = String(data["url"] ?? "");
|
|
2893
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
return {
|
|
2900
|
+
requestId: String(data["connectionId"] ?? ""),
|
|
2901
|
+
url,
|
|
2902
|
+
payload
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
return null;
|
|
2906
|
+
}
|
|
2907
|
+
async assertNoConsoleErrors(windowMs) {
|
|
2908
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2909
|
+
return new Promise((resolve, reject) => {
|
|
2910
|
+
const errors = [];
|
|
2911
|
+
const cleanup = () => {
|
|
2912
|
+
this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
|
|
2913
|
+
this.page.cdpClient.off("Runtime.exceptionThrown", onException);
|
|
2914
|
+
clearTimeout(timer);
|
|
2915
|
+
};
|
|
2916
|
+
const onConsole = (params) => {
|
|
2917
|
+
if (params["type"] !== "error") {
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2921
|
+
errors.push(
|
|
2922
|
+
args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
|
|
2923
|
+
);
|
|
2924
|
+
};
|
|
2925
|
+
const onException = (params) => {
|
|
2926
|
+
const details = params["exceptionDetails"] ?? {};
|
|
2927
|
+
errors.push(String(details["text"] ?? "Runtime exception"));
|
|
2928
|
+
};
|
|
2929
|
+
const timer = setTimeout(() => {
|
|
2930
|
+
cleanup();
|
|
2931
|
+
if (errors.length > 0) {
|
|
2932
|
+
reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
resolve();
|
|
2936
|
+
}, windowMs);
|
|
2937
|
+
this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
|
|
2938
|
+
this.page.cdpClient.on("Runtime.exceptionThrown", onException);
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
async assertTextChanged(selector, from, to, timeout) {
|
|
2942
|
+
const initialText = from ?? await this.page.text(selector);
|
|
2943
|
+
const deadline = Date.now() + timeout;
|
|
2944
|
+
while (Date.now() < deadline) {
|
|
2945
|
+
const text = await this.page.text(selector);
|
|
2946
|
+
if (text !== initialText && text.includes(to)) {
|
|
2947
|
+
return text;
|
|
2948
|
+
}
|
|
2949
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2950
|
+
}
|
|
2951
|
+
throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
|
|
2952
|
+
}
|
|
2953
|
+
async assertPermission(name, state) {
|
|
2954
|
+
const result = await this.page.evaluate(
|
|
2955
|
+
`(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
|
|
2956
|
+
);
|
|
2957
|
+
if (!result || typeof result !== "object" || result.state !== state) {
|
|
2958
|
+
throw new Error(`Permission ${name} is not ${state}`);
|
|
2959
|
+
}
|
|
2960
|
+
return result;
|
|
2961
|
+
}
|
|
2962
|
+
async assertMediaTrackLive(kind) {
|
|
2963
|
+
const result = await this.page.evaluate(
|
|
2964
|
+
`(() => {
|
|
2965
|
+
const requestedKind = ${JSON.stringify(kind)};
|
|
2966
|
+
const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
|
|
2967
|
+
const tracks = [];
|
|
2968
|
+
if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
|
|
2969
|
+
tracks.push(...el.srcObject.getTracks());
|
|
2970
|
+
}
|
|
2971
|
+
return {
|
|
2972
|
+
tag: el.tagName.toLowerCase(),
|
|
2973
|
+
paused: !!el.paused,
|
|
2974
|
+
tracks: tracks.map((track) => ({
|
|
2975
|
+
kind: track.kind,
|
|
2976
|
+
readyState: track.readyState,
|
|
2977
|
+
enabled: track.enabled,
|
|
2978
|
+
label: track.label,
|
|
2979
|
+
})),
|
|
2980
|
+
};
|
|
2981
|
+
});
|
|
2982
|
+
|
|
2983
|
+
const globalTracks =
|
|
2984
|
+
window.__bpStream && typeof window.__bpStream.getTracks === 'function'
|
|
2985
|
+
? window.__bpStream.getTracks().map((track) => ({
|
|
2986
|
+
kind: track.kind,
|
|
2987
|
+
readyState: track.readyState,
|
|
2988
|
+
enabled: track.enabled,
|
|
2989
|
+
label: track.label,
|
|
2990
|
+
}))
|
|
2991
|
+
: [];
|
|
2992
|
+
|
|
2993
|
+
const liveTracks = mediaElements
|
|
2994
|
+
.flatMap((entry) => entry.tracks)
|
|
2995
|
+
.concat(globalTracks)
|
|
2996
|
+
.filter((track) => track.kind === requestedKind && track.readyState === 'live');
|
|
2997
|
+
|
|
2998
|
+
return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
|
|
2999
|
+
})()`
|
|
3000
|
+
);
|
|
3001
|
+
if (!result || typeof result !== "object" || !result.live) {
|
|
3002
|
+
throw new Error(`No live ${kind} media track detected`);
|
|
3003
|
+
}
|
|
3004
|
+
return result;
|
|
3005
|
+
}
|
|
1588
3006
|
};
|
|
1589
3007
|
|
|
1590
3008
|
// src/audio/encoding.ts
|
|
@@ -1622,6 +3040,10 @@ async function grantAudioPermissions(cdp, origin) {
|
|
|
1622
3040
|
await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
1623
3041
|
source: PERMISSIONS_OVERRIDE_SCRIPT
|
|
1624
3042
|
});
|
|
3043
|
+
await cdp.send("Runtime.evaluate", {
|
|
3044
|
+
expression: PERMISSIONS_OVERRIDE_SCRIPT,
|
|
3045
|
+
awaitPromise: false
|
|
3046
|
+
});
|
|
1625
3047
|
}
|
|
1626
3048
|
var PERMISSIONS_OVERRIDE_SCRIPT = `
|
|
1627
3049
|
(function() {
|
|
@@ -3766,6 +5188,9 @@ var Page = class {
|
|
|
3766
5188
|
brokenFrame = null;
|
|
3767
5189
|
/** Last matched selector from findElement (for selectorUsed tracking) */
|
|
3768
5190
|
_lastMatchedSelector;
|
|
5191
|
+
_lastActionCoordinates = null;
|
|
5192
|
+
_lastActionBoundingBox = null;
|
|
5193
|
+
_lastActionTargetMetadata = null;
|
|
3769
5194
|
/** Last snapshot for stale ref recovery */
|
|
3770
5195
|
lastSnapshot;
|
|
3771
5196
|
/** Audio input controller (lazy-initialized) */
|
|
@@ -3797,6 +5222,76 @@ var Page = class {
|
|
|
3797
5222
|
getLastMatchedSelector() {
|
|
3798
5223
|
return this._lastMatchedSelector;
|
|
3799
5224
|
}
|
|
5225
|
+
async getActionTargetMetadata(identifiers) {
|
|
5226
|
+
try {
|
|
5227
|
+
const objectId = identifiers.objectId ?? (identifiers.nodeId ? await this.resolveObjectId(identifiers.nodeId) : void 0);
|
|
5228
|
+
if (!objectId) return null;
|
|
5229
|
+
const response = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5230
|
+
objectId,
|
|
5231
|
+
functionDeclaration: `function() {
|
|
5232
|
+
const tagName = this.tagName?.toLowerCase?.() || '';
|
|
5233
|
+
const inputType =
|
|
5234
|
+
tagName === 'input' && typeof this.type === 'string' ? this.type.toLowerCase() : '';
|
|
5235
|
+
const autocomplete =
|
|
5236
|
+
typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '';
|
|
5237
|
+
return { tagName, inputType, autocomplete };
|
|
5238
|
+
}`,
|
|
5239
|
+
returnByValue: true
|
|
5240
|
+
});
|
|
5241
|
+
return response.result.value ?? null;
|
|
5242
|
+
} catch {
|
|
5243
|
+
return null;
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
async getElementPosition(identifiers) {
|
|
5247
|
+
try {
|
|
5248
|
+
const { quads } = await this.cdp.send(
|
|
5249
|
+
"DOM.getContentQuads",
|
|
5250
|
+
identifiers
|
|
5251
|
+
);
|
|
5252
|
+
if (quads?.length > 0) {
|
|
5253
|
+
const q = quads[0];
|
|
5254
|
+
const minX = Math.min(q[0], q[2], q[4], q[6]);
|
|
5255
|
+
const maxX = Math.max(q[0], q[2], q[4], q[6]);
|
|
5256
|
+
const minY = Math.min(q[1], q[3], q[5], q[7]);
|
|
5257
|
+
const maxY = Math.max(q[1], q[3], q[5], q[7]);
|
|
5258
|
+
return {
|
|
5259
|
+
center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
|
|
5260
|
+
bbox: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
5261
|
+
};
|
|
5262
|
+
}
|
|
5263
|
+
} catch {
|
|
5264
|
+
}
|
|
5265
|
+
if (identifiers.nodeId) {
|
|
5266
|
+
const box = await this.getBoxModel(identifiers.nodeId);
|
|
5267
|
+
if (box) {
|
|
5268
|
+
return {
|
|
5269
|
+
center: { x: box.content[0] + box.width / 2, y: box.content[1] + box.height / 2 },
|
|
5270
|
+
bbox: { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
|
|
5271
|
+
};
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
return null;
|
|
5275
|
+
}
|
|
5276
|
+
setLastActionPosition(coords, bbox) {
|
|
5277
|
+
this._lastActionCoordinates = coords;
|
|
5278
|
+
this._lastActionBoundingBox = bbox;
|
|
5279
|
+
}
|
|
5280
|
+
getLastActionCoordinates() {
|
|
5281
|
+
return this._lastActionCoordinates;
|
|
5282
|
+
}
|
|
5283
|
+
getLastActionBoundingBox() {
|
|
5284
|
+
return this._lastActionBoundingBox;
|
|
5285
|
+
}
|
|
5286
|
+
getLastActionTargetMetadata() {
|
|
5287
|
+
return this._lastActionTargetMetadata;
|
|
5288
|
+
}
|
|
5289
|
+
/** Reset position tracking (call before each executor step) */
|
|
5290
|
+
resetLastActionPosition() {
|
|
5291
|
+
this._lastActionCoordinates = null;
|
|
5292
|
+
this._lastActionBoundingBox = null;
|
|
5293
|
+
this._lastActionTargetMetadata = null;
|
|
5294
|
+
}
|
|
3800
5295
|
/**
|
|
3801
5296
|
* Initialize the page (enable required CDP domains)
|
|
3802
5297
|
*/
|
|
@@ -3964,6 +5459,14 @@ var Page = class {
|
|
|
3964
5459
|
const quad = quads[0];
|
|
3965
5460
|
clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
3966
5461
|
clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
5462
|
+
const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
|
5463
|
+
const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
|
|
5464
|
+
const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
|
5465
|
+
const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
|
|
5466
|
+
this.setLastActionPosition(
|
|
5467
|
+
{ x: clickX, y: clickY },
|
|
5468
|
+
{ x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
5469
|
+
);
|
|
3967
5470
|
} else {
|
|
3968
5471
|
throw new Error("No quads");
|
|
3969
5472
|
}
|
|
@@ -3972,6 +5475,10 @@ var Page = class {
|
|
|
3972
5475
|
if (!box) throw new Error("Could not get element position");
|
|
3973
5476
|
clickX = box.content[0] + box.width / 2;
|
|
3974
5477
|
clickY = box.content[1] + box.height / 2;
|
|
5478
|
+
this.setLastActionPosition(
|
|
5479
|
+
{ x: clickX, y: clickY },
|
|
5480
|
+
{ x: box.content[0], y: box.content[1], width: box.width, height: box.height }
|
|
5481
|
+
);
|
|
3975
5482
|
}
|
|
3976
5483
|
const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
|
|
3977
5484
|
const HIT_TARGET_RETRIES = 3;
|
|
@@ -4022,13 +5529,20 @@ var Page = class {
|
|
|
4022
5529
|
if (options.optional) return false;
|
|
4023
5530
|
throw e;
|
|
4024
5531
|
}
|
|
5532
|
+
const fillPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
5533
|
+
if (fillPos) this.setLastActionPosition(fillPos.center, fillPos.bbox);
|
|
4025
5534
|
const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4026
5535
|
objectId,
|
|
4027
5536
|
functionDeclaration: `function() {
|
|
4028
|
-
return {
|
|
5537
|
+
return {
|
|
5538
|
+
tagName: this.tagName?.toLowerCase() || '',
|
|
5539
|
+
inputType: (this.type || '').toLowerCase(),
|
|
5540
|
+
autocomplete: typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '',
|
|
5541
|
+
};
|
|
4029
5542
|
}`,
|
|
4030
5543
|
returnByValue: true
|
|
4031
5544
|
});
|
|
5545
|
+
this._lastActionTargetMetadata = tagInfo.result.value;
|
|
4032
5546
|
const { tagName, inputType } = tagInfo.result.value;
|
|
4033
5547
|
const specialInputTypes = /* @__PURE__ */ new Set([
|
|
4034
5548
|
"date",
|
|
@@ -4110,6 +5624,9 @@ var Page = class {
|
|
|
4110
5624
|
if (options.optional) return false;
|
|
4111
5625
|
throw e;
|
|
4112
5626
|
}
|
|
5627
|
+
const typePos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
5628
|
+
if (typePos) this.setLastActionPosition(typePos.center, typePos.bbox);
|
|
5629
|
+
this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
|
|
4113
5630
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
4114
5631
|
for (const char of text) {
|
|
4115
5632
|
const def = US_KEYBOARD[char];
|
|
@@ -4189,6 +5706,9 @@ var Page = class {
|
|
|
4189
5706
|
if (options.optional) return false;
|
|
4190
5707
|
throw e;
|
|
4191
5708
|
}
|
|
5709
|
+
const selectPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
5710
|
+
if (selectPos) this.setLastActionPosition(selectPos.center, selectPos.bbox);
|
|
5711
|
+
this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
|
|
4192
5712
|
const metadata = await this.getNativeSelectMetadata(objectId, values);
|
|
4193
5713
|
if (!metadata.isSelect) {
|
|
4194
5714
|
throw new Error("select() target must be a native <select> element");
|
|
@@ -4325,6 +5845,8 @@ var Page = class {
|
|
|
4325
5845
|
if (options.optional) return false;
|
|
4326
5846
|
throw e;
|
|
4327
5847
|
}
|
|
5848
|
+
const checkPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
5849
|
+
if (checkPos) this.setLastActionPosition(checkPos.center, checkPos.bbox);
|
|
4328
5850
|
const before = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4329
5851
|
objectId: object.objectId,
|
|
4330
5852
|
functionDeclaration: "function() { return !!this.checked; }",
|
|
@@ -4373,6 +5895,8 @@ var Page = class {
|
|
|
4373
5895
|
if (options.optional) return false;
|
|
4374
5896
|
throw e;
|
|
4375
5897
|
}
|
|
5898
|
+
const uncheckPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
5899
|
+
if (uncheckPos) this.setLastActionPosition(uncheckPos.center, uncheckPos.bbox);
|
|
4376
5900
|
const isRadio = await this.cdp.send(
|
|
4377
5901
|
"Runtime.callFunctionOn",
|
|
4378
5902
|
{
|
|
@@ -4428,6 +5952,8 @@ var Page = class {
|
|
|
4428
5952
|
throw new ElementNotFoundError(selector, hints);
|
|
4429
5953
|
}
|
|
4430
5954
|
const objectId = await this.resolveObjectId(element.nodeId);
|
|
5955
|
+
const submitPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
5956
|
+
if (submitPos) this.setLastActionPosition(submitPos.center, submitPos.bbox);
|
|
4431
5957
|
const isFormElement = await this.cdp.send(
|
|
4432
5958
|
"Runtime.callFunctionOn",
|
|
4433
5959
|
{
|
|
@@ -4524,6 +6050,8 @@ var Page = class {
|
|
|
4524
6050
|
const hints = await generateHints(this, selectorList, "focus");
|
|
4525
6051
|
throw new ElementNotFoundError(selector, hints);
|
|
4526
6052
|
}
|
|
6053
|
+
const focusPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
6054
|
+
if (focusPos) this.setLastActionPosition(focusPos.center, focusPos.bbox);
|
|
4527
6055
|
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
4528
6056
|
return true;
|
|
4529
6057
|
}
|
|
@@ -4559,6 +6087,14 @@ var Page = class {
|
|
|
4559
6087
|
const quad = quads[0];
|
|
4560
6088
|
x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
4561
6089
|
y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
6090
|
+
const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
|
6091
|
+
const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
|
|
6092
|
+
const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
|
6093
|
+
const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
|
|
6094
|
+
this.setLastActionPosition(
|
|
6095
|
+
{ x, y },
|
|
6096
|
+
{ x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
6097
|
+
);
|
|
4562
6098
|
} else {
|
|
4563
6099
|
throw new Error("No quads");
|
|
4564
6100
|
}
|
|
@@ -4570,6 +6106,10 @@ var Page = class {
|
|
|
4570
6106
|
}
|
|
4571
6107
|
x = box.content[0] + box.width / 2;
|
|
4572
6108
|
y = box.content[1] + box.height / 2;
|
|
6109
|
+
this.setLastActionPosition(
|
|
6110
|
+
{ x, y },
|
|
6111
|
+
{ x: box.content[0], y: box.content[1], width: box.width, height: box.height }
|
|
6112
|
+
);
|
|
4573
6113
|
}
|
|
4574
6114
|
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
4575
6115
|
type: "mouseMoved",
|
|
@@ -4595,6 +6135,8 @@ var Page = class {
|
|
|
4595
6135
|
if (options.optional) return false;
|
|
4596
6136
|
throw new ElementNotFoundError(selector);
|
|
4597
6137
|
}
|
|
6138
|
+
const scrollPos = await this.getElementPosition({ nodeId: element.nodeId });
|
|
6139
|
+
if (scrollPos) this.setLastActionPosition(scrollPos.center, scrollPos.bbox);
|
|
4598
6140
|
await this.scrollIntoView(element.nodeId);
|
|
4599
6141
|
return true;
|
|
4600
6142
|
}
|
|
@@ -5356,7 +6898,7 @@ var Page = class {
|
|
|
5356
6898
|
return {
|
|
5357
6899
|
role,
|
|
5358
6900
|
name,
|
|
5359
|
-
value: value !== void 0 ?
|
|
6901
|
+
value: value !== void 0 ? stringifyUnknown(value) : void 0,
|
|
5360
6902
|
ref,
|
|
5361
6903
|
children: children.length > 0 ? children : void 0,
|
|
5362
6904
|
disabled,
|
|
@@ -5418,7 +6960,7 @@ var Page = class {
|
|
|
5418
6960
|
selector,
|
|
5419
6961
|
disabled,
|
|
5420
6962
|
checked,
|
|
5421
|
-
value: value !== void 0 ?
|
|
6963
|
+
value: value !== void 0 ? stringifyUnknown(value) : void 0
|
|
5422
6964
|
});
|
|
5423
6965
|
}
|
|
5424
6966
|
}
|
|
@@ -5888,7 +7430,7 @@ var Page = class {
|
|
|
5888
7430
|
*/
|
|
5889
7431
|
formatConsoleArgs(args) {
|
|
5890
7432
|
return args.map((arg) => {
|
|
5891
|
-
if (arg.value !== void 0) return
|
|
7433
|
+
if (arg.value !== void 0) return stringifyUnknown(arg.value);
|
|
5892
7434
|
if (arg.description) return arg.description;
|
|
5893
7435
|
return "[object]";
|
|
5894
7436
|
}).join(" ");
|
|
@@ -6677,6 +8219,25 @@ var Browser = class _Browser {
|
|
|
6677
8219
|
this.cdp = cdp;
|
|
6678
8220
|
this.providerSession = providerSession;
|
|
6679
8221
|
}
|
|
8222
|
+
/**
|
|
8223
|
+
* Create a Browser from an existing CDPClient (used by daemon fast-path).
|
|
8224
|
+
* The caller is responsible for the CDP connection lifecycle.
|
|
8225
|
+
*/
|
|
8226
|
+
static fromCDP(cdp, sessionInfo) {
|
|
8227
|
+
const providerSession = {
|
|
8228
|
+
wsUrl: sessionInfo.wsUrl,
|
|
8229
|
+
sessionId: sessionInfo.sessionId,
|
|
8230
|
+
async close() {
|
|
8231
|
+
}
|
|
8232
|
+
};
|
|
8233
|
+
const provider = {
|
|
8234
|
+
name: sessionInfo.provider ?? "daemon",
|
|
8235
|
+
async createSession() {
|
|
8236
|
+
return providerSession;
|
|
8237
|
+
}
|
|
8238
|
+
};
|
|
8239
|
+
return new _Browser(cdp, provider, providerSession, { provider: "generic" });
|
|
8240
|
+
}
|
|
6680
8241
|
/**
|
|
6681
8242
|
* Connect to a browser instance
|
|
6682
8243
|
*/
|