browser-pilot 0.0.14 → 0.0.16
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 +89 -667
- package/dist/actions.cjs +1073 -41
- package/dist/actions.d.cts +11 -3
- package/dist/actions.d.ts +11 -3
- package/dist/actions.mjs +1 -1
- package/dist/browser-ZCR6AA4D.mjs +11 -0
- package/dist/browser.cjs +1431 -62
- package/dist/browser.d.cts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.mjs +4 -4
- package/dist/cdp.cjs +5 -1
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +1 -1
- package/dist/{chunk-7NDR6V7S.mjs → chunk-6GBYX7C2.mjs} +1405 -528
- package/dist/{chunk-KIFB526Y.mjs → chunk-BVZALQT4.mjs} +5 -1
- package/dist/chunk-DTVRFXKI.mjs +35 -0
- package/dist/chunk-EZNZ72VA.mjs +563 -0
- package/dist/{chunk-SPSZZH22.mjs → chunk-LCNFBXB5.mjs} +9 -33
- package/dist/{chunk-IN5HPAPB.mjs → chunk-NNEHWWHL.mjs} +28 -10
- package/dist/chunk-TJ5B56NV.mjs +804 -0
- package/dist/{chunk-XMJABKCF.mjs → chunk-V3VLBQAM.mjs} +1073 -41
- package/dist/cli.mjs +2799 -1176
- package/dist/{client-Ck2nQksT.d.cts → client-B5QBRgIy.d.cts} +2 -0
- package/dist/{client-Ck2nQksT.d.ts → client-B5QBRgIy.d.ts} +2 -0
- package/dist/{client-3AFV2IAF.mjs → client-JWWZWO6L.mjs} +4 -2
- package/dist/index.cjs +1441 -52
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.mjs +19 -7
- package/dist/page-IUUTJ3SW.mjs +7 -0
- package/dist/providers.cjs +637 -2
- package/dist/providers.d.cts +2 -2
- package/dist/providers.d.ts +2 -2
- package/dist/providers.mjs +17 -3
- package/dist/{types-CjT0vClo.d.ts → types-BflRmiDz.d.cts} +17 -3
- package/dist/{types-BSoh5v1Y.d.cts → types-BzM-IfsL.d.ts} +17 -3
- package/dist/types-DeVSWhXj.d.cts +142 -0
- package/dist/types-DeVSWhXj.d.ts +142 -0
- package/package.json +1 -1
- package/dist/browser-LZTEHUDI.mjs +0 -9
- package/dist/chunk-BRAFQUMG.mjs +0 -229
- package/dist/types--wXNHUwt.d.cts +0 -56
- package/dist/types--wXNHUwt.d.ts +0 -56
package/dist/browser.cjs
CHANGED
|
@@ -214,7 +214,8 @@ function buildCDPClient(transport, options = {}) {
|
|
|
214
214
|
pending.delete(response.id);
|
|
215
215
|
clearTimeout(request.timer);
|
|
216
216
|
if (response.error) {
|
|
217
|
-
|
|
217
|
+
const error = typeof response.error === "string" ? { code: -32e3, message: response.error } : response.error;
|
|
218
|
+
request.reject(new CDPError(error));
|
|
218
219
|
} else {
|
|
219
220
|
request.resolve(response.result);
|
|
220
221
|
}
|
|
@@ -330,6 +331,9 @@ function buildCDPClient(transport, options = {}) {
|
|
|
330
331
|
get sessionId() {
|
|
331
332
|
return currentSessionId;
|
|
332
333
|
},
|
|
334
|
+
setSessionId(sessionId) {
|
|
335
|
+
currentSessionId = sessionId;
|
|
336
|
+
},
|
|
333
337
|
get isConnected() {
|
|
334
338
|
return connected;
|
|
335
339
|
}
|
|
@@ -468,6 +472,34 @@ var BrowserlessProvider = class {
|
|
|
468
472
|
};
|
|
469
473
|
|
|
470
474
|
// src/providers/generic.ts
|
|
475
|
+
function sleep(ms) {
|
|
476
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
477
|
+
}
|
|
478
|
+
async function fetchDevToolsJson(host, path, errorPrefix, options = {}) {
|
|
479
|
+
const protocol = host.includes("://") ? "" : "http://";
|
|
480
|
+
const attempts = options.attempts ?? 1;
|
|
481
|
+
let delayMs = options.initialDelayMs ?? 50;
|
|
482
|
+
const maxDelayMs = options.maxDelayMs ?? 250;
|
|
483
|
+
let lastError;
|
|
484
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
485
|
+
try {
|
|
486
|
+
const response = await fetch(`${protocol}${host}${path}`);
|
|
487
|
+
if (response.ok) {
|
|
488
|
+
return await response.json();
|
|
489
|
+
}
|
|
490
|
+
lastError = new Error(`${errorPrefix}: ${response.status}`);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
lastError = new Error(
|
|
493
|
+
`${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (attempt < attempts) {
|
|
497
|
+
await sleep(delayMs);
|
|
498
|
+
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
throw lastError ?? new Error(errorPrefix);
|
|
502
|
+
}
|
|
471
503
|
var GenericProvider = class {
|
|
472
504
|
name = "generic";
|
|
473
505
|
wsUrl;
|
|
@@ -485,6 +517,338 @@ var GenericProvider = class {
|
|
|
485
517
|
};
|
|
486
518
|
}
|
|
487
519
|
};
|
|
520
|
+
async function getBrowserWebSocketUrl(host = "localhost:9222") {
|
|
521
|
+
const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
|
|
522
|
+
attempts: 10,
|
|
523
|
+
initialDelayMs: 50,
|
|
524
|
+
maxDelayMs: 250
|
|
525
|
+
});
|
|
526
|
+
return info.webSocketDebuggerUrl;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/providers/local-discovery.ts
|
|
530
|
+
var CHANNEL_ORDER = ["stable", "beta", "dev", "canary"];
|
|
531
|
+
var DEFAULT_PROBE_TIMEOUT_MS = 1e3;
|
|
532
|
+
var DevToolsActivePortParseError = class extends Error {
|
|
533
|
+
constructor(message, reason) {
|
|
534
|
+
super(message);
|
|
535
|
+
this.reason = reason;
|
|
536
|
+
this.name = "DevToolsActivePortParseError";
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
function getRuntimeEnv() {
|
|
540
|
+
if (typeof process === "undefined") {
|
|
541
|
+
return {};
|
|
542
|
+
}
|
|
543
|
+
return process.env;
|
|
544
|
+
}
|
|
545
|
+
function getRuntimePlatform() {
|
|
546
|
+
if (typeof process === "undefined") {
|
|
547
|
+
return void 0;
|
|
548
|
+
}
|
|
549
|
+
return process.platform;
|
|
550
|
+
}
|
|
551
|
+
function normalizePlatform(platform) {
|
|
552
|
+
if (platform === "darwin" || platform === "linux" || platform === "win32") {
|
|
553
|
+
return platform;
|
|
554
|
+
}
|
|
555
|
+
throw new Error(`Unsupported platform: ${platform ?? "unknown"}`);
|
|
556
|
+
}
|
|
557
|
+
function trimTrailingSeparator(path) {
|
|
558
|
+
return path.replace(/[\\/]+$/, "");
|
|
559
|
+
}
|
|
560
|
+
function joinPath(platform, ...parts) {
|
|
561
|
+
const separator = platform === "win32" ? "\\" : "/";
|
|
562
|
+
const cleaned = parts.map((part, index) => {
|
|
563
|
+
if (index === 0) return trimTrailingSeparator(part);
|
|
564
|
+
return part.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
565
|
+
}).filter((part) => part.length > 0);
|
|
566
|
+
return cleaned.join(separator);
|
|
567
|
+
}
|
|
568
|
+
function resolveHomeDir(platform, env, explicitHomeDir) {
|
|
569
|
+
if (explicitHomeDir) {
|
|
570
|
+
return explicitHomeDir;
|
|
571
|
+
}
|
|
572
|
+
if (platform === "win32") {
|
|
573
|
+
return env["USERPROFILE"] ?? env["HOME"] ?? "";
|
|
574
|
+
}
|
|
575
|
+
return env["HOME"] ?? env["USERPROFILE"] ?? "";
|
|
576
|
+
}
|
|
577
|
+
function toFileFailure(target, error) {
|
|
578
|
+
const errno = error?.code;
|
|
579
|
+
if (errno === "ENOENT") {
|
|
580
|
+
return {
|
|
581
|
+
...target,
|
|
582
|
+
reason: "missing-file",
|
|
583
|
+
message: `DevToolsActivePort not found at ${target.portFile}`
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
...target,
|
|
588
|
+
reason: "unreadable-file",
|
|
589
|
+
message: error instanceof Error ? error.message : `Could not read DevToolsActivePort at ${target.portFile}`
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function toProbeFailure(target, wsUrl, error) {
|
|
593
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
594
|
+
const lowerMessage = message.toLowerCase();
|
|
595
|
+
let reason = "connection-error";
|
|
596
|
+
if (lowerMessage.includes("refused") || lowerMessage.includes("econnrefused")) {
|
|
597
|
+
reason = "connection-refused";
|
|
598
|
+
} else if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
|
|
599
|
+
reason = "connection-timeout";
|
|
600
|
+
} else if (lowerMessage.includes("closed")) {
|
|
601
|
+
reason = "unexpected-close";
|
|
602
|
+
} else if (lowerMessage.includes("browser.getversion") || lowerMessage.includes("cdp") || lowerMessage.includes("protocol")) {
|
|
603
|
+
reason = "cdp-error";
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
...target,
|
|
607
|
+
wsUrl,
|
|
608
|
+
reason,
|
|
609
|
+
message
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
async function readTextFile(path) {
|
|
613
|
+
const fs2 = await import("fs/promises");
|
|
614
|
+
return fs2.readFile(path, "utf-8");
|
|
615
|
+
}
|
|
616
|
+
async function probeBrowserWebSocket(wsUrl, timeoutMs) {
|
|
617
|
+
let client;
|
|
618
|
+
try {
|
|
619
|
+
client = await createCDPClient(wsUrl, { timeout: timeoutMs });
|
|
620
|
+
const version = await client.send("Browser.getVersion", void 0, null);
|
|
621
|
+
return { browserVersion: version.product };
|
|
622
|
+
} finally {
|
|
623
|
+
await client?.close().catch(() => {
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
var defaultDependencies = {
|
|
628
|
+
readTextFile,
|
|
629
|
+
probeBrowserWebSocket,
|
|
630
|
+
getLegacyBrowserWebSocketUrl: getBrowserWebSocketUrl
|
|
631
|
+
};
|
|
632
|
+
function resolveChromeUserDataDirs(options = {}) {
|
|
633
|
+
const env = options.env ?? getRuntimeEnv();
|
|
634
|
+
const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
|
|
635
|
+
const homeDir = resolveHomeDir(platform, env, options.homeDir);
|
|
636
|
+
if (!homeDir) {
|
|
637
|
+
throw new Error("Could not determine home directory for local Chrome discovery");
|
|
638
|
+
}
|
|
639
|
+
switch (platform) {
|
|
640
|
+
case "darwin": {
|
|
641
|
+
const base = joinPath(platform, homeDir, "Library", "Application Support", "Google");
|
|
642
|
+
return {
|
|
643
|
+
stable: joinPath(platform, base, "Chrome"),
|
|
644
|
+
beta: joinPath(platform, base, "Chrome Beta"),
|
|
645
|
+
dev: joinPath(platform, base, "Chrome Dev"),
|
|
646
|
+
canary: joinPath(platform, base, "Chrome Canary")
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
case "linux": {
|
|
650
|
+
const configHome = env["CHROME_CONFIG_HOME"] ?? env["XDG_CONFIG_HOME"] ?? joinPath(platform, homeDir, ".config");
|
|
651
|
+
return {
|
|
652
|
+
stable: joinPath(platform, configHome, "google-chrome"),
|
|
653
|
+
beta: joinPath(platform, configHome, "google-chrome-beta"),
|
|
654
|
+
dev: joinPath(platform, configHome, "google-chrome-dev"),
|
|
655
|
+
canary: joinPath(platform, configHome, "google-chrome-canary")
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
case "win32": {
|
|
659
|
+
const localAppData = env["LOCALAPPDATA"] ?? joinPath(platform, homeDir, "AppData", "Local");
|
|
660
|
+
const base = joinPath(platform, localAppData, "Google");
|
|
661
|
+
return {
|
|
662
|
+
stable: joinPath(platform, base, "Chrome", "User Data"),
|
|
663
|
+
beta: joinPath(platform, base, "Chrome Beta", "User Data"),
|
|
664
|
+
dev: joinPath(platform, base, "Chrome Dev", "User Data"),
|
|
665
|
+
canary: joinPath(platform, base, "Chrome SxS", "User Data")
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
throw new Error(`Unsupported platform for local Chrome discovery: ${platform}`);
|
|
670
|
+
}
|
|
671
|
+
function buildLocalBrowserScanTargets(options = {}) {
|
|
672
|
+
const env = options.env ?? getRuntimeEnv();
|
|
673
|
+
const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
|
|
674
|
+
if (options.userDataDir) {
|
|
675
|
+
return [
|
|
676
|
+
{
|
|
677
|
+
channel: options.channel ?? "custom",
|
|
678
|
+
userDataDir: options.userDataDir,
|
|
679
|
+
portFile: joinPath(platform, options.userDataDir, "DevToolsActivePort")
|
|
680
|
+
}
|
|
681
|
+
];
|
|
682
|
+
}
|
|
683
|
+
const dirs = resolveChromeUserDataDirs({
|
|
684
|
+
platform,
|
|
685
|
+
env,
|
|
686
|
+
homeDir: options.homeDir
|
|
687
|
+
});
|
|
688
|
+
const channels = options.channel ? [options.channel] : CHANNEL_ORDER;
|
|
689
|
+
return channels.map((channel) => ({
|
|
690
|
+
channel,
|
|
691
|
+
userDataDir: dirs[channel],
|
|
692
|
+
portFile: joinPath(platform, dirs[channel], "DevToolsActivePort")
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
function parseDevToolsActivePortFile(content) {
|
|
696
|
+
const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
697
|
+
if (lines.length !== 2) {
|
|
698
|
+
throw new DevToolsActivePortParseError(
|
|
699
|
+
`Expected exactly 2 non-empty lines in DevToolsActivePort, got ${lines.length}`,
|
|
700
|
+
"malformed-file"
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
const portText = lines[0];
|
|
704
|
+
const browserPath = lines[1];
|
|
705
|
+
const port = Number.parseInt(portText, 10);
|
|
706
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
707
|
+
throw new DevToolsActivePortParseError(
|
|
708
|
+
`Invalid DevToolsActivePort port: ${portText}`,
|
|
709
|
+
"invalid-port"
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
if (!browserPath.startsWith("/devtools/browser/") || browserPath.includes("..") || /[?#\s\\]/u.test(browserPath)) {
|
|
713
|
+
throw new DevToolsActivePortParseError(
|
|
714
|
+
`Invalid DevToolsActivePort browser path: ${browserPath}`,
|
|
715
|
+
"invalid-path"
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
port,
|
|
720
|
+
browserPath,
|
|
721
|
+
wsUrl: `ws://127.0.0.1:${port}${browserPath}`
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
async function inspectScanTarget(target, options, deps) {
|
|
725
|
+
let content;
|
|
726
|
+
try {
|
|
727
|
+
content = await deps.readTextFile(target.portFile);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
return { kind: "failure", failure: toFileFailure(target, error) };
|
|
730
|
+
}
|
|
731
|
+
let parsed;
|
|
732
|
+
try {
|
|
733
|
+
parsed = parseDevToolsActivePortFile(content);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (error instanceof DevToolsActivePortParseError) {
|
|
736
|
+
return {
|
|
737
|
+
kind: "failure",
|
|
738
|
+
failure: {
|
|
739
|
+
...target,
|
|
740
|
+
reason: error.reason,
|
|
741
|
+
message: error.message
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
const probe = await deps.probeBrowserWebSocket(
|
|
749
|
+
parsed.wsUrl,
|
|
750
|
+
options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
|
|
751
|
+
);
|
|
752
|
+
return {
|
|
753
|
+
kind: "candidate",
|
|
754
|
+
candidate: {
|
|
755
|
+
...target,
|
|
756
|
+
port: parsed.port,
|
|
757
|
+
browserPath: parsed.browserPath,
|
|
758
|
+
wsUrl: parsed.wsUrl,
|
|
759
|
+
browserVersion: probe.browserVersion
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
} catch (error) {
|
|
763
|
+
return {
|
|
764
|
+
kind: "failure",
|
|
765
|
+
failure: toProbeFailure(target, parsed.wsUrl, error)
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async function discoverLocalBrowsers(options = {}, deps = defaultDependencies) {
|
|
770
|
+
const scanTargets = buildLocalBrowserScanTargets(options);
|
|
771
|
+
const outcomes = await Promise.all(
|
|
772
|
+
scanTargets.map((target) => inspectScanTarget(target, options, deps))
|
|
773
|
+
);
|
|
774
|
+
const candidates = [];
|
|
775
|
+
const failures = [];
|
|
776
|
+
for (const outcome of outcomes) {
|
|
777
|
+
if (outcome.kind === "candidate") {
|
|
778
|
+
candidates.push(outcome.candidate);
|
|
779
|
+
} else {
|
|
780
|
+
failures.push(outcome.failure);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return { candidates, failures };
|
|
784
|
+
}
|
|
785
|
+
var BrowserEndpointResolutionError = class extends Error {
|
|
786
|
+
constructor(code, message, details = {}) {
|
|
787
|
+
super(message);
|
|
788
|
+
this.code = code;
|
|
789
|
+
this.details = details;
|
|
790
|
+
}
|
|
791
|
+
name = "BrowserEndpointResolutionError";
|
|
792
|
+
};
|
|
793
|
+
async function resolveBrowserEndpoint(options = {}, deps = defaultDependencies) {
|
|
794
|
+
if (options.explicitWsUrl) {
|
|
795
|
+
return {
|
|
796
|
+
wsUrl: options.explicitWsUrl,
|
|
797
|
+
source: "explicit-ws"
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
let localDiscovery;
|
|
801
|
+
if (options.allowLocalDiscovery ?? true) {
|
|
802
|
+
localDiscovery = await discoverLocalBrowsers(options, deps);
|
|
803
|
+
if (localDiscovery.candidates.length === 1) {
|
|
804
|
+
const candidate = localDiscovery.candidates[0];
|
|
805
|
+
return {
|
|
806
|
+
wsUrl: candidate.wsUrl,
|
|
807
|
+
source: "devtools-active-port",
|
|
808
|
+
channel: candidate.channel,
|
|
809
|
+
userDataDir: candidate.userDataDir
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
if (localDiscovery.candidates.length > 1) {
|
|
813
|
+
throw new BrowserEndpointResolutionError(
|
|
814
|
+
"multiple-local-browsers",
|
|
815
|
+
"Multiple local Chrome profiles are available for auto-discovery",
|
|
816
|
+
{
|
|
817
|
+
candidates: localDiscovery.candidates,
|
|
818
|
+
failures: localDiscovery.failures
|
|
819
|
+
}
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (options.allowLegacyHostFallback ?? true) {
|
|
824
|
+
const legacyHost = options.legacyHost ?? "localhost:9222";
|
|
825
|
+
try {
|
|
826
|
+
return {
|
|
827
|
+
wsUrl: await deps.getLegacyBrowserWebSocketUrl(legacyHost),
|
|
828
|
+
source: "json-version"
|
|
829
|
+
};
|
|
830
|
+
} catch (error) {
|
|
831
|
+
throw new BrowserEndpointResolutionError(
|
|
832
|
+
"browser-not-found",
|
|
833
|
+
"Could not resolve a browser endpoint",
|
|
834
|
+
{
|
|
835
|
+
candidates: localDiscovery?.candidates,
|
|
836
|
+
failures: localDiscovery?.failures,
|
|
837
|
+
legacyError: error instanceof Error ? error : new Error(String(error)),
|
|
838
|
+
legacyHost
|
|
839
|
+
}
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
throw new BrowserEndpointResolutionError(
|
|
844
|
+
"browser-not-found",
|
|
845
|
+
"Could not resolve a browser endpoint",
|
|
846
|
+
{
|
|
847
|
+
candidates: localDiscovery?.candidates,
|
|
848
|
+
failures: localDiscovery?.failures
|
|
849
|
+
}
|
|
850
|
+
);
|
|
851
|
+
}
|
|
488
852
|
|
|
489
853
|
// src/providers/index.ts
|
|
490
854
|
function createProvider(options) {
|
|
@@ -990,7 +1354,7 @@ var CHECK_EDITABLE = `function() {
|
|
|
990
1354
|
|
|
991
1355
|
return { actionable: true };
|
|
992
1356
|
}`;
|
|
993
|
-
function
|
|
1357
|
+
function sleep2(ms) {
|
|
994
1358
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
995
1359
|
}
|
|
996
1360
|
var BACKOFF = [0, 20, 100, 100];
|
|
@@ -1074,7 +1438,7 @@ async function ensureActionable(cdp, objectId, checks, options) {
|
|
|
1074
1438
|
);
|
|
1075
1439
|
}
|
|
1076
1440
|
const delay = attempt < BACKOFF.length ? BACKOFF[attempt] ?? 0 : 500;
|
|
1077
|
-
if (delay > 0) await
|
|
1441
|
+
if (delay > 0) await sleep2(delay);
|
|
1078
1442
|
attempt++;
|
|
1079
1443
|
}
|
|
1080
1444
|
}
|
|
@@ -1355,6 +1719,624 @@ var NavigationError = class extends Error {
|
|
|
1355
1719
|
}
|
|
1356
1720
|
};
|
|
1357
1721
|
|
|
1722
|
+
// src/trace/views.ts
|
|
1723
|
+
function takeRecent(events, limit = 5) {
|
|
1724
|
+
return events.slice(-limit).map((event) => ({
|
|
1725
|
+
ts: event.ts,
|
|
1726
|
+
event: event.event,
|
|
1727
|
+
summary: event.summary,
|
|
1728
|
+
severity: event.severity,
|
|
1729
|
+
url: event.url
|
|
1730
|
+
}));
|
|
1731
|
+
}
|
|
1732
|
+
function buildTraceSummaries(events) {
|
|
1733
|
+
return {
|
|
1734
|
+
ws: summarizeWs(events),
|
|
1735
|
+
voice: summarizeVoice(events),
|
|
1736
|
+
console: summarizeConsole(events),
|
|
1737
|
+
permissions: summarizePermissions(events),
|
|
1738
|
+
media: summarizeMedia(events),
|
|
1739
|
+
ui: summarizeUi(events),
|
|
1740
|
+
session: summarizeSession(events)
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
function summarizeWs(events) {
|
|
1744
|
+
const relevant = events.filter(
|
|
1745
|
+
(event) => event.channel === "ws" || event.event.startsWith("ws.")
|
|
1746
|
+
);
|
|
1747
|
+
const connections = /* @__PURE__ */ new Map();
|
|
1748
|
+
for (const event of relevant) {
|
|
1749
|
+
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
1750
|
+
let connection = connections.get(id);
|
|
1751
|
+
if (!connection) {
|
|
1752
|
+
connection = { id, sent: 0, received: 0, lastMessages: [] };
|
|
1753
|
+
connections.set(id, connection);
|
|
1754
|
+
}
|
|
1755
|
+
connection.url = event.url ?? connection.url;
|
|
1756
|
+
if (event.event === "ws.connection.created") {
|
|
1757
|
+
connection.createdAt = event.ts;
|
|
1758
|
+
}
|
|
1759
|
+
if (event.event === "ws.connection.closed") {
|
|
1760
|
+
connection.closedAt = event.ts;
|
|
1761
|
+
}
|
|
1762
|
+
if (event.event === "ws.frame.sent") {
|
|
1763
|
+
connection.sent += 1;
|
|
1764
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
1765
|
+
if (payload) connection.lastMessages.push(`sent: ${payload}`);
|
|
1766
|
+
}
|
|
1767
|
+
if (event.event === "ws.frame.received") {
|
|
1768
|
+
connection.received += 1;
|
|
1769
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
1770
|
+
if (payload) connection.lastMessages.push(`recv: ${payload}`);
|
|
1771
|
+
}
|
|
1772
|
+
connection.lastMessages = connection.lastMessages.slice(-3);
|
|
1773
|
+
}
|
|
1774
|
+
const values = [...connections.values()];
|
|
1775
|
+
const reconnects = values.reduce((count, connection) => {
|
|
1776
|
+
return connection.closedAt && !connection.createdAt ? count + 1 : count;
|
|
1777
|
+
}, 0);
|
|
1778
|
+
return {
|
|
1779
|
+
view: "ws",
|
|
1780
|
+
totalEvents: relevant.length,
|
|
1781
|
+
connections: values.map((connection) => ({
|
|
1782
|
+
id: connection.id,
|
|
1783
|
+
url: connection.url ?? null,
|
|
1784
|
+
createdAt: connection.createdAt ?? null,
|
|
1785
|
+
closedAt: connection.closedAt ?? null,
|
|
1786
|
+
sent: connection.sent,
|
|
1787
|
+
received: connection.received,
|
|
1788
|
+
lastMessages: connection.lastMessages,
|
|
1789
|
+
connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
|
|
1790
|
+
})),
|
|
1791
|
+
reconnects,
|
|
1792
|
+
recent: takeRecent(relevant)
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
function summarizeConsole(events) {
|
|
1796
|
+
const relevant = events.filter(
|
|
1797
|
+
(event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
1798
|
+
);
|
|
1799
|
+
return {
|
|
1800
|
+
view: "console",
|
|
1801
|
+
errors: relevant.filter(
|
|
1802
|
+
(event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
1803
|
+
).length,
|
|
1804
|
+
warnings: relevant.filter((event) => event.event === "console.warn").length,
|
|
1805
|
+
logs: relevant.filter((event) => event.event === "console.log").length,
|
|
1806
|
+
recent: takeRecent(relevant)
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
function summarizePermissions(events) {
|
|
1810
|
+
const relevant = events.filter(
|
|
1811
|
+
(event) => event.channel === "permission" || event.event.startsWith("permission.")
|
|
1812
|
+
);
|
|
1813
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1814
|
+
for (const event of relevant) {
|
|
1815
|
+
const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
|
|
1816
|
+
const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
|
|
1817
|
+
if (name && state) {
|
|
1818
|
+
latest.set(name, state);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return {
|
|
1822
|
+
view: "permissions",
|
|
1823
|
+
states: Object.fromEntries(latest),
|
|
1824
|
+
changes: relevant.filter((event) => event.event === "permission.changed").length,
|
|
1825
|
+
recent: takeRecent(relevant)
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function summarizeMedia(events) {
|
|
1829
|
+
const relevant = events.filter(
|
|
1830
|
+
(event) => event.channel === "media" || event.event.startsWith("media.")
|
|
1831
|
+
);
|
|
1832
|
+
const liveTracks = /* @__PURE__ */ new Map();
|
|
1833
|
+
for (const event of relevant) {
|
|
1834
|
+
const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
|
|
1835
|
+
const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
|
|
1836
|
+
const key = `${kind}:${label}`;
|
|
1837
|
+
if (event.event === "media.track.started") {
|
|
1838
|
+
liveTracks.set(key, kind);
|
|
1839
|
+
}
|
|
1840
|
+
if (event.event === "media.track.ended") {
|
|
1841
|
+
liveTracks.delete(key);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return {
|
|
1845
|
+
view: "media",
|
|
1846
|
+
tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
|
|
1847
|
+
tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
|
|
1848
|
+
playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
|
|
1849
|
+
playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
|
|
1850
|
+
liveTracks: [...liveTracks.values()],
|
|
1851
|
+
recent: takeRecent(relevant)
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
function summarizeVoice(events) {
|
|
1855
|
+
const relevant = events.filter(
|
|
1856
|
+
(event) => event.channel === "voice" || event.event.startsWith("voice.")
|
|
1857
|
+
);
|
|
1858
|
+
return {
|
|
1859
|
+
view: "voice",
|
|
1860
|
+
ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
|
|
1861
|
+
notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
|
|
1862
|
+
captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
|
|
1863
|
+
captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
|
|
1864
|
+
detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
|
|
1865
|
+
recent: takeRecent(relevant)
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
function summarizeUi(events) {
|
|
1869
|
+
const relevant = events.filter(
|
|
1870
|
+
(event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
|
|
1871
|
+
);
|
|
1872
|
+
return {
|
|
1873
|
+
view: "ui",
|
|
1874
|
+
actions: relevant.filter((event) => event.channel === "action").length,
|
|
1875
|
+
domChanges: relevant.filter((event) => event.channel === "dom").length,
|
|
1876
|
+
recent: takeRecent(relevant)
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function summarizeSession(events) {
|
|
1880
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
1881
|
+
const failedActions = events.filter((event) => event.event === "action.failed").length;
|
|
1882
|
+
for (const event of events) {
|
|
1883
|
+
byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
|
|
1884
|
+
}
|
|
1885
|
+
return {
|
|
1886
|
+
view: "session",
|
|
1887
|
+
totalEvents: events.length,
|
|
1888
|
+
byChannel: Object.fromEntries(byChannel),
|
|
1889
|
+
failedActions,
|
|
1890
|
+
recent: takeRecent(events)
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/recording/manifest.ts
|
|
1895
|
+
function isCanonicalRecordingManifest(value) {
|
|
1896
|
+
return Boolean(
|
|
1897
|
+
value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
function isLegacyRecordingManifest(value) {
|
|
1901
|
+
return Boolean(
|
|
1902
|
+
value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
function createRecordingManifest(input) {
|
|
1906
|
+
const actions = input.frames.map((frame) => {
|
|
1907
|
+
const actionId = frame.actionId ?? `action-${frame.seq}`;
|
|
1908
|
+
return {
|
|
1909
|
+
id: actionId,
|
|
1910
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1911
|
+
action: frame.action,
|
|
1912
|
+
selector: frame.selector,
|
|
1913
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1914
|
+
value: frame.value,
|
|
1915
|
+
url: frame.url,
|
|
1916
|
+
success: frame.success,
|
|
1917
|
+
durationMs: frame.durationMs,
|
|
1918
|
+
error: frame.error,
|
|
1919
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1920
|
+
pageUrl: frame.pageUrl,
|
|
1921
|
+
pageTitle: frame.pageTitle,
|
|
1922
|
+
coordinates: frame.coordinates,
|
|
1923
|
+
boundingBox: frame.boundingBox
|
|
1924
|
+
};
|
|
1925
|
+
});
|
|
1926
|
+
const screenshots = input.frames.map((frame) => ({
|
|
1927
|
+
id: `shot-${frame.seq}`,
|
|
1928
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1929
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1930
|
+
file: frame.screenshot,
|
|
1931
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1932
|
+
success: frame.success,
|
|
1933
|
+
pageUrl: frame.pageUrl,
|
|
1934
|
+
pageTitle: frame.pageTitle,
|
|
1935
|
+
coordinates: frame.coordinates,
|
|
1936
|
+
boundingBox: frame.boundingBox
|
|
1937
|
+
}));
|
|
1938
|
+
return {
|
|
1939
|
+
version: 2,
|
|
1940
|
+
recordedAt: input.recordedAt,
|
|
1941
|
+
session: {
|
|
1942
|
+
id: input.sessionId,
|
|
1943
|
+
startUrl: input.startUrl,
|
|
1944
|
+
endUrl: input.endUrl,
|
|
1945
|
+
targetId: input.targetId,
|
|
1946
|
+
profile: input.profile
|
|
1947
|
+
},
|
|
1948
|
+
recipe: {
|
|
1949
|
+
steps: input.steps
|
|
1950
|
+
},
|
|
1951
|
+
actions,
|
|
1952
|
+
screenshots,
|
|
1953
|
+
trace: {
|
|
1954
|
+
events: input.traceEvents,
|
|
1955
|
+
summaries: buildTraceSummaries(input.traceEvents)
|
|
1956
|
+
},
|
|
1957
|
+
assertions: input.assertions ?? [],
|
|
1958
|
+
notes: input.notes ?? [],
|
|
1959
|
+
artifacts: {
|
|
1960
|
+
recordingManifest: input.recordingManifest ?? "recording.json",
|
|
1961
|
+
screenshotDir: input.screenshotDir ?? "screenshots/"
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function canonicalizeRecordingArtifact(value) {
|
|
1966
|
+
if (isCanonicalRecordingManifest(value)) {
|
|
1967
|
+
return value;
|
|
1968
|
+
}
|
|
1969
|
+
if (!isLegacyRecordingManifest(value)) {
|
|
1970
|
+
throw new Error("Unsupported recording artifact");
|
|
1971
|
+
}
|
|
1972
|
+
const traceEvents = buildTraceEventsFromLegacy(value);
|
|
1973
|
+
const steps = value.frames.map((frame) => frameToStep(frame));
|
|
1974
|
+
return createRecordingManifest({
|
|
1975
|
+
recordedAt: value.recordedAt,
|
|
1976
|
+
sessionId: value.sessionId,
|
|
1977
|
+
startUrl: value.startUrl,
|
|
1978
|
+
endUrl: value.endUrl,
|
|
1979
|
+
steps,
|
|
1980
|
+
frames: value.frames,
|
|
1981
|
+
traceEvents,
|
|
1982
|
+
notes: ["Converted from legacy recording manifest"]
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
function buildTraceEventsFromLegacy(value) {
|
|
1986
|
+
const events = [];
|
|
1987
|
+
for (const frame of value.frames) {
|
|
1988
|
+
events.push({
|
|
1989
|
+
traceId: frame.actionId ?? `legacy-${frame.seq}`,
|
|
1990
|
+
sessionId: value.sessionId,
|
|
1991
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1992
|
+
elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
|
|
1993
|
+
channel: "action",
|
|
1994
|
+
event: frame.success ? "action.succeeded" : "action.failed",
|
|
1995
|
+
severity: frame.success ? "info" : "error",
|
|
1996
|
+
summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
|
|
1997
|
+
data: {
|
|
1998
|
+
action: frame.action,
|
|
1999
|
+
selector: frame.selector,
|
|
2000
|
+
value: frame.value ?? null,
|
|
2001
|
+
pageUrl: frame.pageUrl ?? null,
|
|
2002
|
+
pageTitle: frame.pageTitle ?? null,
|
|
2003
|
+
screenshot: frame.screenshot
|
|
2004
|
+
},
|
|
2005
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
2006
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
2007
|
+
selector: frame.selector,
|
|
2008
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
2009
|
+
url: frame.pageUrl ?? frame.url
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
return events;
|
|
2013
|
+
}
|
|
2014
|
+
function frameToStep(frame) {
|
|
2015
|
+
switch (frame.action) {
|
|
2016
|
+
case "fill":
|
|
2017
|
+
return { action: "fill", selector: frame.selector, value: frame.value };
|
|
2018
|
+
case "submit":
|
|
2019
|
+
return { action: "submit", selector: frame.selector };
|
|
2020
|
+
case "goto":
|
|
2021
|
+
return { action: "goto", url: frame.url ?? frame.pageUrl };
|
|
2022
|
+
case "press":
|
|
2023
|
+
return { action: "press", key: frame.value ?? "Enter" };
|
|
2024
|
+
default:
|
|
2025
|
+
return { action: "click", selector: frame.selector };
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// src/trace/model.ts
|
|
2030
|
+
function createTraceId(prefix = "evt") {
|
|
2031
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
2032
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
2033
|
+
}
|
|
2034
|
+
function normalizeTraceEvent(event) {
|
|
2035
|
+
return {
|
|
2036
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
2037
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2038
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
2039
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
2040
|
+
data: event.data ?? {},
|
|
2041
|
+
...event
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
function inferSeverity(eventName) {
|
|
2045
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
2046
|
+
return "error";
|
|
2047
|
+
}
|
|
2048
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
2049
|
+
return "warn";
|
|
2050
|
+
}
|
|
2051
|
+
return "info";
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/trace/script.ts
|
|
2055
|
+
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
2056
|
+
var TRACE_SCRIPT = `
|
|
2057
|
+
(() => {
|
|
2058
|
+
if (window.__bpTraceInstalled) return;
|
|
2059
|
+
window.__bpTraceInstalled = true;
|
|
2060
|
+
|
|
2061
|
+
const binding = globalThis.${TRACE_BINDING_NAME};
|
|
2062
|
+
if (typeof binding !== 'function') return;
|
|
2063
|
+
|
|
2064
|
+
const emit = (event, data = {}, severity = 'info', summary) => {
|
|
2065
|
+
try {
|
|
2066
|
+
globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
|
|
2067
|
+
const payload = {
|
|
2068
|
+
event,
|
|
2069
|
+
severity,
|
|
2070
|
+
summary: summary || event,
|
|
2071
|
+
ts: Date.now(),
|
|
2072
|
+
data,
|
|
2073
|
+
};
|
|
2074
|
+
globalThis.__bpTraceRecentEvents.push(payload);
|
|
2075
|
+
if (globalThis.__bpTraceRecentEvents.length > 200) {
|
|
2076
|
+
globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
|
|
2077
|
+
}
|
|
2078
|
+
binding(JSON.stringify(payload));
|
|
2079
|
+
} catch {}
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
const patchWebSocket = () => {
|
|
2083
|
+
const NativeWebSocket = window.WebSocket;
|
|
2084
|
+
if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
|
|
2085
|
+
window.__bpTraceWebSocketInstalled = true;
|
|
2086
|
+
|
|
2087
|
+
const nextId = () => Math.random().toString(36).slice(2, 10);
|
|
2088
|
+
|
|
2089
|
+
const patchInstance = (socket, urlValue) => {
|
|
2090
|
+
if (!socket || socket.__bpTracePatched) return socket;
|
|
2091
|
+
socket.__bpTracePatched = true;
|
|
2092
|
+
socket.__bpTraceId = socket.__bpTraceId || nextId();
|
|
2093
|
+
socket.__bpTraceUrl = String(urlValue || socket.url || '');
|
|
2094
|
+
globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
|
|
2095
|
+
globalThis.__bpTrackedWebSockets.add(socket);
|
|
2096
|
+
|
|
2097
|
+
emit(
|
|
2098
|
+
'ws.connection.created',
|
|
2099
|
+
{ connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
|
|
2100
|
+
'info',
|
|
2101
|
+
'WebSocket opened ' + socket.__bpTraceUrl
|
|
2102
|
+
);
|
|
2103
|
+
|
|
2104
|
+
const originalSend = socket.send;
|
|
2105
|
+
socket.send = function(data) {
|
|
2106
|
+
const payload =
|
|
2107
|
+
typeof data === 'string'
|
|
2108
|
+
? data
|
|
2109
|
+
: data && typeof data.toString === 'function'
|
|
2110
|
+
? data.toString()
|
|
2111
|
+
: '[binary]';
|
|
2112
|
+
emit(
|
|
2113
|
+
'ws.frame.sent',
|
|
2114
|
+
{
|
|
2115
|
+
connectionId: socket.__bpTraceId,
|
|
2116
|
+
url: socket.__bpTraceUrl,
|
|
2117
|
+
payload,
|
|
2118
|
+
length: payload.length,
|
|
2119
|
+
},
|
|
2120
|
+
'info',
|
|
2121
|
+
'WebSocket frame sent'
|
|
2122
|
+
);
|
|
2123
|
+
return originalSend.call(this, data);
|
|
2124
|
+
};
|
|
2125
|
+
|
|
2126
|
+
socket.addEventListener('message', (event) => {
|
|
2127
|
+
if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
const data = event && 'data' in event ? event.data : '';
|
|
2131
|
+
const payload =
|
|
2132
|
+
typeof data === 'string'
|
|
2133
|
+
? data
|
|
2134
|
+
: data && typeof data.toString === 'function'
|
|
2135
|
+
? data.toString()
|
|
2136
|
+
: '[binary]';
|
|
2137
|
+
emit(
|
|
2138
|
+
'ws.frame.received',
|
|
2139
|
+
{
|
|
2140
|
+
connectionId: socket.__bpTraceId,
|
|
2141
|
+
url: socket.__bpTraceUrl,
|
|
2142
|
+
payload,
|
|
2143
|
+
length: payload.length,
|
|
2144
|
+
},
|
|
2145
|
+
'info',
|
|
2146
|
+
'WebSocket frame received'
|
|
2147
|
+
);
|
|
2148
|
+
});
|
|
2149
|
+
|
|
2150
|
+
socket.addEventListener('close', (event) => {
|
|
2151
|
+
if (socket.__bpTraceClosed) {
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
socket.__bpTraceClosed = true;
|
|
2155
|
+
try {
|
|
2156
|
+
globalThis.__bpTrackedWebSockets.delete(socket);
|
|
2157
|
+
} catch {}
|
|
2158
|
+
emit(
|
|
2159
|
+
'ws.connection.closed',
|
|
2160
|
+
{
|
|
2161
|
+
connectionId: socket.__bpTraceId,
|
|
2162
|
+
url: socket.__bpTraceUrl,
|
|
2163
|
+
code: event.code,
|
|
2164
|
+
reason: event.reason,
|
|
2165
|
+
},
|
|
2166
|
+
'warn',
|
|
2167
|
+
'WebSocket closed'
|
|
2168
|
+
);
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
return socket;
|
|
2172
|
+
};
|
|
2173
|
+
|
|
2174
|
+
const TracedWebSocket = function(url, protocols) {
|
|
2175
|
+
return arguments.length > 1
|
|
2176
|
+
? patchInstance(new NativeWebSocket(url, protocols), url)
|
|
2177
|
+
: patchInstance(new NativeWebSocket(url), url);
|
|
2178
|
+
};
|
|
2179
|
+
TracedWebSocket.prototype = NativeWebSocket.prototype;
|
|
2180
|
+
Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
|
|
2181
|
+
window.WebSocket = TracedWebSocket;
|
|
2182
|
+
};
|
|
2183
|
+
|
|
2184
|
+
window.addEventListener('error', (errorEvent) => {
|
|
2185
|
+
emit(
|
|
2186
|
+
'runtime.exception',
|
|
2187
|
+
{
|
|
2188
|
+
message: errorEvent.message,
|
|
2189
|
+
filename: errorEvent.filename,
|
|
2190
|
+
line: errorEvent.lineno,
|
|
2191
|
+
column: errorEvent.colno,
|
|
2192
|
+
},
|
|
2193
|
+
'error',
|
|
2194
|
+
errorEvent.message || 'Uncaught error'
|
|
2195
|
+
);
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
2199
|
+
const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
|
|
2200
|
+
emit('runtime.unhandledRejection', { reason }, 'error', reason);
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
const patchPermissions = async () => {
|
|
2204
|
+
if (!navigator.permissions || !navigator.permissions.query) return;
|
|
2205
|
+
|
|
2206
|
+
const names = ['geolocation', 'microphone', 'camera', 'notifications'];
|
|
2207
|
+
for (const name of names) {
|
|
2208
|
+
try {
|
|
2209
|
+
const status = await navigator.permissions.query({ name });
|
|
2210
|
+
emit(
|
|
2211
|
+
'permission.state',
|
|
2212
|
+
{ name, state: status.state },
|
|
2213
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
2214
|
+
name + ': ' + status.state
|
|
2215
|
+
);
|
|
2216
|
+
status.addEventListener('change', () => {
|
|
2217
|
+
emit(
|
|
2218
|
+
'permission.changed',
|
|
2219
|
+
{ name, state: status.state },
|
|
2220
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
2221
|
+
name + ': ' + status.state
|
|
2222
|
+
);
|
|
2223
|
+
});
|
|
2224
|
+
} catch {}
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
const patchMediaElement = (element) => {
|
|
2229
|
+
if (!element || element.__bpTracePatched) return;
|
|
2230
|
+
element.__bpTracePatched = true;
|
|
2231
|
+
|
|
2232
|
+
element.addEventListener('play', () => {
|
|
2233
|
+
emit(
|
|
2234
|
+
'media.playback.started',
|
|
2235
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
2236
|
+
'info',
|
|
2237
|
+
'Media playback started'
|
|
2238
|
+
);
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
const onStop = () => {
|
|
2242
|
+
emit(
|
|
2243
|
+
'media.playback.stopped',
|
|
2244
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
2245
|
+
'warn',
|
|
2246
|
+
'Media playback stopped'
|
|
2247
|
+
);
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
element.addEventListener('pause', onStop);
|
|
2251
|
+
element.addEventListener('ended', onStop);
|
|
2252
|
+
};
|
|
2253
|
+
|
|
2254
|
+
const patchMediaElements = () => {
|
|
2255
|
+
document.querySelectorAll('audio,video').forEach(patchMediaElement);
|
|
2256
|
+
};
|
|
2257
|
+
|
|
2258
|
+
patchMediaElements();
|
|
2259
|
+
patchWebSocket();
|
|
2260
|
+
|
|
2261
|
+
if (document.documentElement) {
|
|
2262
|
+
const observer = new MutationObserver(() => {
|
|
2263
|
+
patchMediaElements();
|
|
2264
|
+
});
|
|
2265
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
2269
|
+
const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
|
2270
|
+
navigator.mediaDevices.getUserMedia = async (...args) => {
|
|
2271
|
+
emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
|
|
2272
|
+
try {
|
|
2273
|
+
const stream = await original(...args);
|
|
2274
|
+
const tracks = stream.getTracks();
|
|
2275
|
+
|
|
2276
|
+
for (const track of tracks) {
|
|
2277
|
+
emit(
|
|
2278
|
+
'media.track.started',
|
|
2279
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
2280
|
+
'info',
|
|
2281
|
+
track.kind + ' track started'
|
|
2282
|
+
);
|
|
2283
|
+
track.addEventListener('ended', () => {
|
|
2284
|
+
emit(
|
|
2285
|
+
'media.track.ended',
|
|
2286
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
2287
|
+
'warn',
|
|
2288
|
+
track.kind + ' track ended'
|
|
2289
|
+
);
|
|
2290
|
+
emit(
|
|
2291
|
+
'voice.capture.stopped',
|
|
2292
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
2293
|
+
'warn',
|
|
2294
|
+
'Voice capture stopped'
|
|
2295
|
+
);
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
emit(
|
|
2300
|
+
'voice.capture.detectedAudio',
|
|
2301
|
+
{ trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
|
|
2302
|
+
'info',
|
|
2303
|
+
'Voice capture detected audio'
|
|
2304
|
+
);
|
|
2305
|
+
|
|
2306
|
+
return stream;
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
emit(
|
|
2309
|
+
'voice.pipeline.notReady',
|
|
2310
|
+
{ message: String(error && error.message ? error.message : error) },
|
|
2311
|
+
'error',
|
|
2312
|
+
String(error && error.message ? error.message : error)
|
|
2313
|
+
);
|
|
2314
|
+
throw error;
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
document.addEventListener('visibilitychange', () => {
|
|
2320
|
+
emit(
|
|
2321
|
+
'dom.state.changed',
|
|
2322
|
+
{ visibilityState: document.visibilityState },
|
|
2323
|
+
document.visibilityState === 'hidden' ? 'warn' : 'info',
|
|
2324
|
+
'Visibility ' + document.visibilityState
|
|
2325
|
+
);
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
patchPermissions();
|
|
2329
|
+
emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
|
|
2330
|
+
})();
|
|
2331
|
+
`;
|
|
2332
|
+
|
|
2333
|
+
// src/trace/live.ts
|
|
2334
|
+
function globToRegex(pattern) {
|
|
2335
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2336
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
2337
|
+
return new RegExp(`^${withWildcards}$`);
|
|
2338
|
+
}
|
|
2339
|
+
|
|
1358
2340
|
// src/actions/executor.ts
|
|
1359
2341
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1360
2342
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -1364,6 +2346,61 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
|
1364
2346
|
"text",
|
|
1365
2347
|
"screenshot"
|
|
1366
2348
|
];
|
|
2349
|
+
function readString(value) {
|
|
2350
|
+
return typeof value === "string" ? value : void 0;
|
|
2351
|
+
}
|
|
2352
|
+
function readStringOr(value, fallback = "") {
|
|
2353
|
+
return readString(value) ?? fallback;
|
|
2354
|
+
}
|
|
2355
|
+
function formatConsoleArg(entry) {
|
|
2356
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
2357
|
+
}
|
|
2358
|
+
function loadExistingRecording(manifestPath) {
|
|
2359
|
+
try {
|
|
2360
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
2361
|
+
if (raw.version === 1) {
|
|
2362
|
+
const legacy = raw;
|
|
2363
|
+
return {
|
|
2364
|
+
frames: Array.isArray(legacy.frames) ? legacy.frames : [],
|
|
2365
|
+
traceEvents: [],
|
|
2366
|
+
recordedAt: legacy.recordedAt,
|
|
2367
|
+
startUrl: legacy.startUrl
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
const artifact = canonicalizeRecordingArtifact(raw);
|
|
2371
|
+
const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
|
|
2372
|
+
const frames = artifact.actions.map((action, index) => {
|
|
2373
|
+
const screenshot = screenshotsByAction.get(action.id);
|
|
2374
|
+
return {
|
|
2375
|
+
seq: index + 1,
|
|
2376
|
+
timestamp: Date.parse(action.ts),
|
|
2377
|
+
action: action.action,
|
|
2378
|
+
selector: action.selector,
|
|
2379
|
+
selectorUsed: action.selectorUsed,
|
|
2380
|
+
value: action.value,
|
|
2381
|
+
url: action.url,
|
|
2382
|
+
coordinates: action.coordinates,
|
|
2383
|
+
boundingBox: action.boundingBox,
|
|
2384
|
+
success: action.success,
|
|
2385
|
+
durationMs: action.durationMs,
|
|
2386
|
+
error: action.error,
|
|
2387
|
+
screenshot: screenshot?.file ?? "",
|
|
2388
|
+
pageUrl: action.pageUrl,
|
|
2389
|
+
pageTitle: action.pageTitle,
|
|
2390
|
+
stepIndex: action.stepIndex,
|
|
2391
|
+
actionId: action.id
|
|
2392
|
+
};
|
|
2393
|
+
});
|
|
2394
|
+
return {
|
|
2395
|
+
frames,
|
|
2396
|
+
traceEvents: artifact.trace.events,
|
|
2397
|
+
recordedAt: artifact.recordedAt,
|
|
2398
|
+
startUrl: artifact.session.startUrl
|
|
2399
|
+
};
|
|
2400
|
+
} catch {
|
|
2401
|
+
return { frames: [], traceEvents: [] };
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
1367
2404
|
function classifyFailure(error) {
|
|
1368
2405
|
if (error instanceof ElementNotFoundError) {
|
|
1369
2406
|
return { reason: "missing" };
|
|
@@ -1444,6 +2481,9 @@ var BatchExecutor = class {
|
|
|
1444
2481
|
const results = [];
|
|
1445
2482
|
const startTime = Date.now();
|
|
1446
2483
|
const recording = options.record ? this.createRecordingContext(options.record) : null;
|
|
2484
|
+
if (steps.some((step) => step.action === "waitForWsMessage")) {
|
|
2485
|
+
await this.ensureTraceHooks();
|
|
2486
|
+
}
|
|
1447
2487
|
const startUrl = recording ? await this.getPageUrlSafe() : "";
|
|
1448
2488
|
let stoppedAtIndex;
|
|
1449
2489
|
for (let i = 0; i < steps.length; i++) {
|
|
@@ -1453,6 +2493,26 @@ var BatchExecutor = class {
|
|
|
1453
2493
|
const retryDelay = step.retryDelay ?? 500;
|
|
1454
2494
|
let lastError;
|
|
1455
2495
|
let succeeded = false;
|
|
2496
|
+
if (recording) {
|
|
2497
|
+
recording.traceEvents.push(
|
|
2498
|
+
normalizeTraceEvent({
|
|
2499
|
+
traceId: createTraceId("action"),
|
|
2500
|
+
elapsedMs: Date.now() - startTime,
|
|
2501
|
+
channel: "action",
|
|
2502
|
+
event: "action.started",
|
|
2503
|
+
summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
|
|
2504
|
+
data: {
|
|
2505
|
+
action: step.action,
|
|
2506
|
+
selector: step.selector ?? null,
|
|
2507
|
+
url: step.url ?? null
|
|
2508
|
+
},
|
|
2509
|
+
actionId: `action-${i + 1}`,
|
|
2510
|
+
stepIndex: i,
|
|
2511
|
+
selector: step.selector,
|
|
2512
|
+
url: step.url
|
|
2513
|
+
})
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
1456
2516
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1457
2517
|
if (attempt > 0) {
|
|
1458
2518
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
@@ -1476,6 +2536,28 @@ var BatchExecutor = class {
|
|
|
1476
2536
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1477
2537
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1478
2538
|
}
|
|
2539
|
+
if (recording) {
|
|
2540
|
+
recording.traceEvents.push(
|
|
2541
|
+
normalizeTraceEvent({
|
|
2542
|
+
traceId: createTraceId("action"),
|
|
2543
|
+
elapsedMs: Date.now() - startTime,
|
|
2544
|
+
channel: "action",
|
|
2545
|
+
event: "action.succeeded",
|
|
2546
|
+
summary: `${step.action} succeeded`,
|
|
2547
|
+
data: {
|
|
2548
|
+
action: step.action,
|
|
2549
|
+
selector: step.selector ?? null,
|
|
2550
|
+
selectorUsed: result.selectorUsed ?? null,
|
|
2551
|
+
durationMs: Date.now() - stepStart
|
|
2552
|
+
},
|
|
2553
|
+
actionId: `action-${i + 1}`,
|
|
2554
|
+
stepIndex: i,
|
|
2555
|
+
selector: step.selector,
|
|
2556
|
+
selectorUsed: result.selectorUsed,
|
|
2557
|
+
url: step.url
|
|
2558
|
+
})
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
1479
2561
|
results.push(stepResult);
|
|
1480
2562
|
succeeded = true;
|
|
1481
2563
|
break;
|
|
@@ -1513,6 +2595,28 @@ var BatchExecutor = class {
|
|
|
1513
2595
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1514
2596
|
await this.captureRecordingFrame(step, failedResult, recording);
|
|
1515
2597
|
}
|
|
2598
|
+
if (recording) {
|
|
2599
|
+
recording.traceEvents.push(
|
|
2600
|
+
normalizeTraceEvent({
|
|
2601
|
+
traceId: createTraceId("action"),
|
|
2602
|
+
elapsedMs: Date.now() - startTime,
|
|
2603
|
+
channel: "action",
|
|
2604
|
+
event: "action.failed",
|
|
2605
|
+
severity: "error",
|
|
2606
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
2607
|
+
data: {
|
|
2608
|
+
action: step.action,
|
|
2609
|
+
selector: step.selector ?? null,
|
|
2610
|
+
error: errorMessage,
|
|
2611
|
+
reason
|
|
2612
|
+
},
|
|
2613
|
+
actionId: `action-${i + 1}`,
|
|
2614
|
+
stepIndex: i,
|
|
2615
|
+
selector: step.selector,
|
|
2616
|
+
url: step.url
|
|
2617
|
+
})
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
1516
2620
|
results.push(failedResult);
|
|
1517
2621
|
if (onFail === "stop" && !step.optional) {
|
|
1518
2622
|
stoppedAtIndex = i;
|
|
@@ -1528,7 +2632,8 @@ var BatchExecutor = class {
|
|
|
1528
2632
|
recording,
|
|
1529
2633
|
startTime,
|
|
1530
2634
|
startUrl,
|
|
1531
|
-
allSuccess
|
|
2635
|
+
allSuccess,
|
|
2636
|
+
steps
|
|
1532
2637
|
);
|
|
1533
2638
|
}
|
|
1534
2639
|
return {
|
|
@@ -1543,20 +2648,14 @@ var BatchExecutor = class {
|
|
|
1543
2648
|
const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
|
|
1544
2649
|
const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
|
|
1545
2650
|
const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
|
|
1546
|
-
|
|
1547
|
-
try {
|
|
1548
|
-
const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1549
|
-
if (existing.frames && Array.isArray(existing.frames)) {
|
|
1550
|
-
existingFrames = existing.frames;
|
|
1551
|
-
}
|
|
1552
|
-
} catch {
|
|
1553
|
-
}
|
|
2651
|
+
const existing = loadExistingRecording(manifestPath);
|
|
1554
2652
|
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
1555
2653
|
return {
|
|
1556
2654
|
baseDir,
|
|
1557
2655
|
screenshotDir,
|
|
1558
2656
|
sessionId: record.sessionId ?? this.page.targetId,
|
|
1559
|
-
frames:
|
|
2657
|
+
frames: existing.frames,
|
|
2658
|
+
traceEvents: existing.traceEvents,
|
|
1560
2659
|
format: record.format ?? "webp",
|
|
1561
2660
|
quality: Math.max(0, Math.min(100, record.quality ?? 40)),
|
|
1562
2661
|
highlights: record.highlights !== false,
|
|
@@ -1612,6 +2711,7 @@ var BatchExecutor = class {
|
|
|
1612
2711
|
timestamp: ts,
|
|
1613
2712
|
action: stepResult.action,
|
|
1614
2713
|
selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
|
|
2714
|
+
selectorUsed: stepResult.selectorUsed,
|
|
1615
2715
|
value: redactValueForRecording(
|
|
1616
2716
|
typeof step.value === "string" ? step.value : void 0,
|
|
1617
2717
|
targetMetadata
|
|
@@ -1624,7 +2724,9 @@ var BatchExecutor = class {
|
|
|
1624
2724
|
error: stepResult.error,
|
|
1625
2725
|
screenshot: filename,
|
|
1626
2726
|
pageUrl,
|
|
1627
|
-
pageTitle
|
|
2727
|
+
pageTitle,
|
|
2728
|
+
stepIndex: stepResult.index,
|
|
2729
|
+
actionId: `action-${stepResult.index + 1}`
|
|
1628
2730
|
});
|
|
1629
2731
|
} catch {
|
|
1630
2732
|
} finally {
|
|
@@ -1636,45 +2738,31 @@ var BatchExecutor = class {
|
|
|
1636
2738
|
/**
|
|
1637
2739
|
* Write recording manifest to disk
|
|
1638
2740
|
*/
|
|
1639
|
-
async writeRecordingManifest(recording, startTime, startUrl, success) {
|
|
2741
|
+
async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
|
|
1640
2742
|
let endUrl = startUrl;
|
|
1641
|
-
let viewport = { width: 1280, height: 720 };
|
|
1642
2743
|
try {
|
|
1643
2744
|
endUrl = await this.page.url();
|
|
1644
2745
|
} catch {
|
|
1645
2746
|
}
|
|
1646
|
-
try {
|
|
1647
|
-
const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
|
|
1648
|
-
viewport = {
|
|
1649
|
-
width: metrics.cssVisualViewport.clientWidth,
|
|
1650
|
-
height: metrics.cssVisualViewport.clientHeight
|
|
1651
|
-
};
|
|
1652
|
-
} catch {
|
|
1653
|
-
}
|
|
1654
2747
|
const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
|
|
1655
2748
|
let recordedAt = new Date(startTime).toISOString();
|
|
1656
2749
|
let originalStartUrl = startUrl;
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
} catch {
|
|
1662
|
-
}
|
|
1663
|
-
const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
|
|
1664
|
-
const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
|
|
1665
|
-
const manifest = {
|
|
1666
|
-
version: 1,
|
|
2750
|
+
const existing = loadExistingRecording(manifestPath);
|
|
2751
|
+
if (existing.recordedAt) recordedAt = existing.recordedAt;
|
|
2752
|
+
if (existing.startUrl) originalStartUrl = existing.startUrl;
|
|
2753
|
+
const manifest = createRecordingManifest({
|
|
1667
2754
|
recordedAt,
|
|
1668
2755
|
sessionId: recording.sessionId,
|
|
1669
2756
|
startUrl: originalStartUrl,
|
|
1670
2757
|
endUrl,
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
success,
|
|
1676
|
-
|
|
1677
|
-
|
|
2758
|
+
targetId: this.page.targetId,
|
|
2759
|
+
steps,
|
|
2760
|
+
frames: recording.frames,
|
|
2761
|
+
traceEvents: recording.traceEvents,
|
|
2762
|
+
notes: success ? [] : ["Replay ended with at least one failed action."],
|
|
2763
|
+
recordingManifest: "recording.json",
|
|
2764
|
+
screenshotDir: "screenshots/"
|
|
2765
|
+
});
|
|
1678
2766
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1679
2767
|
return manifestPath;
|
|
1680
2768
|
}
|
|
@@ -1957,6 +3045,39 @@ var BatchExecutor = class {
|
|
|
1957
3045
|
}
|
|
1958
3046
|
return { selectorUsed: usedSelector, value: actual };
|
|
1959
3047
|
}
|
|
3048
|
+
case "waitForWsMessage": {
|
|
3049
|
+
if (typeof step.match !== "string") {
|
|
3050
|
+
throw new Error("waitForWsMessage requires match");
|
|
3051
|
+
}
|
|
3052
|
+
const message = await this.waitForWsMessage(step.match, step.where, timeout);
|
|
3053
|
+
return { value: message };
|
|
3054
|
+
}
|
|
3055
|
+
case "assertNoConsoleErrors": {
|
|
3056
|
+
await this.assertNoConsoleErrors(step.windowMs ?? timeout);
|
|
3057
|
+
return {};
|
|
3058
|
+
}
|
|
3059
|
+
case "assertTextChanged": {
|
|
3060
|
+
const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
|
|
3061
|
+
if (typeof step.to !== "string") {
|
|
3062
|
+
throw new Error("assertTextChanged requires to");
|
|
3063
|
+
}
|
|
3064
|
+
const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
|
|
3065
|
+
return { selectorUsed: selector, text };
|
|
3066
|
+
}
|
|
3067
|
+
case "assertPermission": {
|
|
3068
|
+
if (!step.name || !step.state) {
|
|
3069
|
+
throw new Error("assertPermission requires name and state");
|
|
3070
|
+
}
|
|
3071
|
+
const permission = await this.assertPermission(step.name, step.state);
|
|
3072
|
+
return { value: permission };
|
|
3073
|
+
}
|
|
3074
|
+
case "assertMediaTrackLive": {
|
|
3075
|
+
if (!step.kind) {
|
|
3076
|
+
throw new Error("assertMediaTrackLive requires kind");
|
|
3077
|
+
}
|
|
3078
|
+
const media = await this.assertMediaTrackLive(step.kind);
|
|
3079
|
+
return { value: media };
|
|
3080
|
+
}
|
|
1960
3081
|
default: {
|
|
1961
3082
|
const action = step.action;
|
|
1962
3083
|
const aliases = {
|
|
@@ -2010,7 +3131,7 @@ var BatchExecutor = class {
|
|
|
2010
3131
|
};
|
|
2011
3132
|
const suggestion = aliases[action.toLowerCase()];
|
|
2012
3133
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
2013
|
-
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";
|
|
3134
|
+
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";
|
|
2014
3135
|
throw new Error(`Unknown action "${action}".${hint}
|
|
2015
3136
|
|
|
2016
3137
|
Valid actions: ${valid}`);
|
|
@@ -2026,6 +3147,237 @@ Valid actions: ${valid}`);
|
|
|
2026
3147
|
if (matched) return matched;
|
|
2027
3148
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
2028
3149
|
}
|
|
3150
|
+
async ensureTraceHooks() {
|
|
3151
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
3152
|
+
await this.page.cdpClient.send("Page.enable");
|
|
3153
|
+
await this.page.cdpClient.send("Network.enable");
|
|
3154
|
+
try {
|
|
3155
|
+
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
3156
|
+
} catch {
|
|
3157
|
+
}
|
|
3158
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
3159
|
+
source: TRACE_SCRIPT
|
|
3160
|
+
});
|
|
3161
|
+
await this.page.cdpClient.send("Runtime.evaluate", {
|
|
3162
|
+
expression: TRACE_SCRIPT,
|
|
3163
|
+
awaitPromise: false
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
async waitForWsMessage(match, where, timeout) {
|
|
3167
|
+
await this.ensureTraceHooks();
|
|
3168
|
+
const regex = globToRegex(match);
|
|
3169
|
+
const wsUrls = /* @__PURE__ */ new Map();
|
|
3170
|
+
const recentMatch = await this.findRecentWsMessage(regex, where);
|
|
3171
|
+
if (recentMatch) {
|
|
3172
|
+
return recentMatch;
|
|
3173
|
+
}
|
|
3174
|
+
return new Promise((resolve, reject) => {
|
|
3175
|
+
const cleanup = () => {
|
|
3176
|
+
this.page.cdpClient.off("Network.webSocketCreated", onCreated);
|
|
3177
|
+
this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
|
|
3178
|
+
this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
|
|
3179
|
+
clearTimeout(timer);
|
|
3180
|
+
};
|
|
3181
|
+
const onCreated = (params) => {
|
|
3182
|
+
wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
|
|
3183
|
+
};
|
|
3184
|
+
const onFrame = (params) => {
|
|
3185
|
+
const requestId = readStringOr(params["requestId"]);
|
|
3186
|
+
const response = params["response"] ?? {};
|
|
3187
|
+
const payload = response.payloadData ?? "";
|
|
3188
|
+
const url = wsUrls.get(requestId) ?? "";
|
|
3189
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
cleanup();
|
|
3196
|
+
resolve({ requestId, url, payload });
|
|
3197
|
+
};
|
|
3198
|
+
const onBinding = (params) => {
|
|
3199
|
+
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
try {
|
|
3203
|
+
const parsed = JSON.parse(readStringOr(params["payload"]));
|
|
3204
|
+
if (parsed.event !== "ws.frame.received") {
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
const data = parsed.data ?? {};
|
|
3208
|
+
const payload = readStringOr(data["payload"]);
|
|
3209
|
+
const url = readStringOr(data["url"]);
|
|
3210
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
cleanup();
|
|
3217
|
+
resolve({
|
|
3218
|
+
requestId: readStringOr(data["connectionId"]),
|
|
3219
|
+
url,
|
|
3220
|
+
payload
|
|
3221
|
+
});
|
|
3222
|
+
} catch {
|
|
3223
|
+
}
|
|
3224
|
+
};
|
|
3225
|
+
const timer = setTimeout(() => {
|
|
3226
|
+
cleanup();
|
|
3227
|
+
reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
|
|
3228
|
+
}, timeout);
|
|
3229
|
+
this.page.cdpClient.on("Network.webSocketCreated", onCreated);
|
|
3230
|
+
this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
|
|
3231
|
+
this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
payloadMatchesWhere(payload, where) {
|
|
3235
|
+
try {
|
|
3236
|
+
const parsed = JSON.parse(payload);
|
|
3237
|
+
return Object.entries(where).every(([key, expected]) => {
|
|
3238
|
+
const actual = key.split(".").reduce((current, part) => {
|
|
3239
|
+
if (!current || typeof current !== "object") {
|
|
3240
|
+
return void 0;
|
|
3241
|
+
}
|
|
3242
|
+
return current[part];
|
|
3243
|
+
}, parsed);
|
|
3244
|
+
return actual === expected;
|
|
3245
|
+
});
|
|
3246
|
+
} catch {
|
|
3247
|
+
return false;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
async findRecentWsMessage(regex, where) {
|
|
3251
|
+
const recent = await this.page.evaluate(
|
|
3252
|
+
"(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
|
|
3253
|
+
);
|
|
3254
|
+
if (!Array.isArray(recent)) {
|
|
3255
|
+
return null;
|
|
3256
|
+
}
|
|
3257
|
+
for (let i = recent.length - 1; i >= 0; i--) {
|
|
3258
|
+
const entry = recent[i];
|
|
3259
|
+
if (!entry || typeof entry !== "object") {
|
|
3260
|
+
continue;
|
|
3261
|
+
}
|
|
3262
|
+
const record = entry;
|
|
3263
|
+
const event = readStringOr(record["event"]);
|
|
3264
|
+
if (event !== "ws.frame.received") {
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
const data = record["data"] ?? {};
|
|
3268
|
+
const payload = readStringOr(data["payload"]);
|
|
3269
|
+
const url = readStringOr(data["url"]);
|
|
3270
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
3274
|
+
continue;
|
|
3275
|
+
}
|
|
3276
|
+
return {
|
|
3277
|
+
requestId: readStringOr(data["connectionId"]),
|
|
3278
|
+
url,
|
|
3279
|
+
payload
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
return null;
|
|
3283
|
+
}
|
|
3284
|
+
async assertNoConsoleErrors(windowMs) {
|
|
3285
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
3286
|
+
return new Promise((resolve, reject) => {
|
|
3287
|
+
const errors = [];
|
|
3288
|
+
const cleanup = () => {
|
|
3289
|
+
this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
|
|
3290
|
+
this.page.cdpClient.off("Runtime.exceptionThrown", onException);
|
|
3291
|
+
clearTimeout(timer);
|
|
3292
|
+
};
|
|
3293
|
+
const onConsole = (params) => {
|
|
3294
|
+
if (params["type"] !== "error") {
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
3298
|
+
errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
|
|
3299
|
+
};
|
|
3300
|
+
const onException = (params) => {
|
|
3301
|
+
const details = params["exceptionDetails"] ?? {};
|
|
3302
|
+
errors.push(readString(details["text"]) ?? "Runtime exception");
|
|
3303
|
+
};
|
|
3304
|
+
const timer = setTimeout(() => {
|
|
3305
|
+
cleanup();
|
|
3306
|
+
if (errors.length > 0) {
|
|
3307
|
+
reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
resolve();
|
|
3311
|
+
}, windowMs);
|
|
3312
|
+
this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
|
|
3313
|
+
this.page.cdpClient.on("Runtime.exceptionThrown", onException);
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
async assertTextChanged(selector, from, to, timeout) {
|
|
3317
|
+
const initialText = from ?? await this.page.text(selector);
|
|
3318
|
+
const deadline = Date.now() + timeout;
|
|
3319
|
+
while (Date.now() < deadline) {
|
|
3320
|
+
const text = await this.page.text(selector);
|
|
3321
|
+
if (text !== initialText && text.includes(to)) {
|
|
3322
|
+
return text;
|
|
3323
|
+
}
|
|
3324
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
3325
|
+
}
|
|
3326
|
+
throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
|
|
3327
|
+
}
|
|
3328
|
+
async assertPermission(name, state) {
|
|
3329
|
+
const result = await this.page.evaluate(
|
|
3330
|
+
`(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
|
|
3331
|
+
);
|
|
3332
|
+
if (!result || typeof result !== "object" || result.state !== state) {
|
|
3333
|
+
throw new Error(`Permission ${name} is not ${state}`);
|
|
3334
|
+
}
|
|
3335
|
+
return result;
|
|
3336
|
+
}
|
|
3337
|
+
async assertMediaTrackLive(kind) {
|
|
3338
|
+
const result = await this.page.evaluate(
|
|
3339
|
+
`(() => {
|
|
3340
|
+
const requestedKind = ${JSON.stringify(kind)};
|
|
3341
|
+
const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
|
|
3342
|
+
const tracks = [];
|
|
3343
|
+
if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
|
|
3344
|
+
tracks.push(...el.srcObject.getTracks());
|
|
3345
|
+
}
|
|
3346
|
+
return {
|
|
3347
|
+
tag: el.tagName.toLowerCase(),
|
|
3348
|
+
paused: !!el.paused,
|
|
3349
|
+
tracks: tracks.map((track) => ({
|
|
3350
|
+
kind: track.kind,
|
|
3351
|
+
readyState: track.readyState,
|
|
3352
|
+
enabled: track.enabled,
|
|
3353
|
+
label: track.label,
|
|
3354
|
+
})),
|
|
3355
|
+
};
|
|
3356
|
+
});
|
|
3357
|
+
|
|
3358
|
+
const globalTracks =
|
|
3359
|
+
window.__bpStream && typeof window.__bpStream.getTracks === 'function'
|
|
3360
|
+
? window.__bpStream.getTracks().map((track) => ({
|
|
3361
|
+
kind: track.kind,
|
|
3362
|
+
readyState: track.readyState,
|
|
3363
|
+
enabled: track.enabled,
|
|
3364
|
+
label: track.label,
|
|
3365
|
+
}))
|
|
3366
|
+
: [];
|
|
3367
|
+
|
|
3368
|
+
const liveTracks = mediaElements
|
|
3369
|
+
.flatMap((entry) => entry.tracks)
|
|
3370
|
+
.concat(globalTracks)
|
|
3371
|
+
.filter((track) => track.kind === requestedKind && track.readyState === 'live');
|
|
3372
|
+
|
|
3373
|
+
return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
|
|
3374
|
+
})()`
|
|
3375
|
+
);
|
|
3376
|
+
if (!result || typeof result !== "object" || !result.live) {
|
|
3377
|
+
throw new Error(`No live ${kind} media track detected`);
|
|
3378
|
+
}
|
|
3379
|
+
return result;
|
|
3380
|
+
}
|
|
2029
3381
|
};
|
|
2030
3382
|
|
|
2031
3383
|
// src/audio/encoding.ts
|
|
@@ -2063,6 +3415,10 @@ async function grantAudioPermissions(cdp, origin) {
|
|
|
2063
3415
|
await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2064
3416
|
source: PERMISSIONS_OVERRIDE_SCRIPT
|
|
2065
3417
|
});
|
|
3418
|
+
await cdp.send("Runtime.evaluate", {
|
|
3419
|
+
expression: PERMISSIONS_OVERRIDE_SCRIPT,
|
|
3420
|
+
awaitPromise: false
|
|
3421
|
+
});
|
|
2066
3422
|
}
|
|
2067
3423
|
var PERMISSIONS_OVERRIDE_SCRIPT = `
|
|
2068
3424
|
(function() {
|
|
@@ -3021,7 +4377,7 @@ var AudioOutput = class {
|
|
|
3021
4377
|
awaitPromise: false
|
|
3022
4378
|
});
|
|
3023
4379
|
this.capturing = false;
|
|
3024
|
-
await
|
|
4380
|
+
await sleep3(250);
|
|
3025
4381
|
return this.mergeChunks();
|
|
3026
4382
|
}
|
|
3027
4383
|
/**
|
|
@@ -3237,7 +4593,7 @@ function emptyCaptureResult() {
|
|
|
3237
4593
|
chunkCount: 0
|
|
3238
4594
|
};
|
|
3239
4595
|
}
|
|
3240
|
-
function
|
|
4596
|
+
function sleep3(ms) {
|
|
3241
4597
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3242
4598
|
}
|
|
3243
4599
|
|
|
@@ -3778,7 +5134,7 @@ async function isElementAttached(cdp, selector, contextId) {
|
|
|
3778
5134
|
const result = await cdp.send("Runtime.evaluate", params);
|
|
3779
5135
|
return result.result.value === true;
|
|
3780
5136
|
}
|
|
3781
|
-
function
|
|
5137
|
+
function sleep4(ms) {
|
|
3782
5138
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3783
5139
|
}
|
|
3784
5140
|
async function isPageStatic(cdp, windowMs = 200, contextId) {
|
|
@@ -3846,7 +5202,7 @@ async function waitForAnyElement(cdp, selectors, options = {}) {
|
|
|
3846
5202
|
}
|
|
3847
5203
|
}
|
|
3848
5204
|
while (Date.now() < deadline) {
|
|
3849
|
-
await
|
|
5205
|
+
await sleep4(pollInterval);
|
|
3850
5206
|
for (const selector of selectors) {
|
|
3851
5207
|
if (await checkSelector(selector)) {
|
|
3852
5208
|
return { success: true, selector, waitedMs: Date.now() - startTime };
|
|
@@ -3907,7 +5263,7 @@ async function waitForNavigation(cdp, options = {}) {
|
|
|
3907
5263
|
cleanup.push(() => cdp.off("Page.lifecycleEvent", onLifecycle));
|
|
3908
5264
|
const pollUrl = async () => {
|
|
3909
5265
|
while (!resolved && Date.now() < startTime + timeout) {
|
|
3910
|
-
await
|
|
5266
|
+
await sleep4(100);
|
|
3911
5267
|
if (resolved) return;
|
|
3912
5268
|
try {
|
|
3913
5269
|
const currentUrl = await getCurrentUrl(cdp);
|
|
@@ -4512,7 +5868,7 @@ var Page = class {
|
|
|
4512
5868
|
} catch (e) {
|
|
4513
5869
|
if (options.optional) return false;
|
|
4514
5870
|
if (e instanceof ActionabilityError && e.failureType === "hitTarget" && attempt < HIT_TARGET_RETRIES - 1) {
|
|
4515
|
-
await
|
|
5871
|
+
await sleep5(HIT_TARGET_DELAY);
|
|
4516
5872
|
await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId: element.nodeId });
|
|
4517
5873
|
continue;
|
|
4518
5874
|
}
|
|
@@ -4687,7 +6043,7 @@ var Page = class {
|
|
|
4687
6043
|
await this.cdp.send("Input.insertText", { text: char });
|
|
4688
6044
|
}
|
|
4689
6045
|
if (delay > 0) {
|
|
4690
|
-
await
|
|
6046
|
+
await sleep5(delay);
|
|
4691
6047
|
}
|
|
4692
6048
|
}
|
|
4693
6049
|
if (options.blur) {
|
|
@@ -4783,7 +6139,7 @@ var Page = class {
|
|
|
4783
6139
|
state: "visible",
|
|
4784
6140
|
timeout: 500,
|
|
4785
6141
|
contextId: this.currentFrameContextId ?? void 0
|
|
4786
|
-
}).catch(() =>
|
|
6142
|
+
}).catch(() => sleep5(100));
|
|
4787
6143
|
const optionHandle = await this.evaluateInFrame(
|
|
4788
6144
|
`(() => {
|
|
4789
6145
|
const selectors = ${JSON.stringify(optionSelectors)};
|
|
@@ -5000,7 +6356,7 @@ var Page = class {
|
|
|
5000
6356
|
() => "navigation"
|
|
5001
6357
|
),
|
|
5002
6358
|
this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
|
|
5003
|
-
|
|
6359
|
+
sleep5(1500).then(() => "timeout")
|
|
5004
6360
|
]);
|
|
5005
6361
|
}
|
|
5006
6362
|
return true;
|
|
@@ -5020,7 +6376,7 @@ var Page = class {
|
|
|
5020
6376
|
(success) => success ? "nav" : null
|
|
5021
6377
|
),
|
|
5022
6378
|
this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
|
|
5023
|
-
|
|
6379
|
+
sleep5(1500).then(() => "timeout")
|
|
5024
6380
|
]);
|
|
5025
6381
|
if (navigationDetected === "nav") {
|
|
5026
6382
|
return true;
|
|
@@ -5034,7 +6390,7 @@ var Page = class {
|
|
|
5034
6390
|
if (shouldWait === true) {
|
|
5035
6391
|
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
5036
6392
|
} else if (shouldWait === "auto") {
|
|
5037
|
-
await
|
|
6393
|
+
await sleep5(100);
|
|
5038
6394
|
}
|
|
5039
6395
|
}
|
|
5040
6396
|
return true;
|
|
@@ -6431,7 +7787,7 @@ var Page = class {
|
|
|
6431
7787
|
try {
|
|
6432
7788
|
await Promise.race([
|
|
6433
7789
|
this.dialogHandler(dialog),
|
|
6434
|
-
|
|
7790
|
+
sleep5(DIALOG_TIMEOUT).then(() => {
|
|
6435
7791
|
console.warn("[browser-pilot] Dialog handler timed out after 5s, auto-dismissing");
|
|
6436
7792
|
return dialog.dismiss();
|
|
6437
7793
|
})
|
|
@@ -6556,7 +7912,7 @@ var Page = class {
|
|
|
6556
7912
|
if (attempt < retries) {
|
|
6557
7913
|
this.rootNodeId = null;
|
|
6558
7914
|
this.currentFrameContextId = null;
|
|
6559
|
-
await
|
|
7915
|
+
await sleep5(delay);
|
|
6560
7916
|
continue;
|
|
6561
7917
|
}
|
|
6562
7918
|
}
|
|
@@ -7140,7 +8496,7 @@ var Page = class {
|
|
|
7140
8496
|
const start = Date.now();
|
|
7141
8497
|
await this.audioOutput.start();
|
|
7142
8498
|
if (options.preDelay && options.preDelay > 0) {
|
|
7143
|
-
await
|
|
8499
|
+
await sleep5(options.preDelay);
|
|
7144
8500
|
}
|
|
7145
8501
|
const inputDone = this.audioInput.play(options.input, {
|
|
7146
8502
|
waitForEnd: !!options.sendSelector
|
|
@@ -7208,7 +8564,7 @@ var Page = class {
|
|
|
7208
8564
|
});
|
|
7209
8565
|
}
|
|
7210
8566
|
};
|
|
7211
|
-
function
|
|
8567
|
+
function sleep5(ms) {
|
|
7212
8568
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7213
8569
|
}
|
|
7214
8570
|
|
|
@@ -7261,13 +8617,26 @@ var Browser = class _Browser {
|
|
|
7261
8617
|
* Connect to a browser instance
|
|
7262
8618
|
*/
|
|
7263
8619
|
static async connect(options) {
|
|
7264
|
-
|
|
7265
|
-
|
|
8620
|
+
let connectOptions = options;
|
|
8621
|
+
if (options.provider === "generic" && !options.wsUrl) {
|
|
8622
|
+
const endpoint = await resolveBrowserEndpoint({
|
|
8623
|
+
channel: options.channel,
|
|
8624
|
+
userDataDir: options.userDataDir,
|
|
8625
|
+
allowLocalDiscovery: true,
|
|
8626
|
+
allowLegacyHostFallback: true
|
|
8627
|
+
});
|
|
8628
|
+
connectOptions = {
|
|
8629
|
+
...options,
|
|
8630
|
+
wsUrl: endpoint.wsUrl
|
|
8631
|
+
};
|
|
8632
|
+
}
|
|
8633
|
+
const provider = createProvider(connectOptions);
|
|
8634
|
+
const session = await provider.createSession(connectOptions.session);
|
|
7266
8635
|
const cdp = await createCDPClient(session.wsUrl, {
|
|
7267
|
-
debug:
|
|
7268
|
-
timeout:
|
|
8636
|
+
debug: connectOptions.debug,
|
|
8637
|
+
timeout: connectOptions.timeout
|
|
7269
8638
|
});
|
|
7270
|
-
return new _Browser(cdp, provider, session,
|
|
8639
|
+
return new _Browser(cdp, provider, session, connectOptions);
|
|
7271
8640
|
}
|
|
7272
8641
|
/**
|
|
7273
8642
|
* Get or create a page by name.
|