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
|
@@ -1,226 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CDPError,
|
|
3
|
-
createCDPClient,
|
|
4
3
|
stringifyUnknown
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
|
|
7
|
-
// src/providers/browserbase.ts
|
|
8
|
-
var BrowserBaseProvider = class {
|
|
9
|
-
name = "browserbase";
|
|
10
|
-
apiKey;
|
|
11
|
-
projectId;
|
|
12
|
-
baseUrl;
|
|
13
|
-
constructor(options) {
|
|
14
|
-
this.apiKey = options.apiKey;
|
|
15
|
-
this.projectId = options.projectId;
|
|
16
|
-
this.baseUrl = options.baseUrl ?? "https://api.browserbase.com";
|
|
17
|
-
}
|
|
18
|
-
async createSession(options = {}) {
|
|
19
|
-
const response = await fetch(`${this.baseUrl}/v1/sessions`, {
|
|
20
|
-
method: "POST",
|
|
21
|
-
headers: {
|
|
22
|
-
"X-BB-API-Key": this.apiKey,
|
|
23
|
-
"Content-Type": "application/json"
|
|
24
|
-
},
|
|
25
|
-
body: JSON.stringify({
|
|
26
|
-
projectId: this.projectId,
|
|
27
|
-
browserSettings: {
|
|
28
|
-
viewport: options.width && options.height ? {
|
|
29
|
-
width: options.width,
|
|
30
|
-
height: options.height
|
|
31
|
-
} : void 0
|
|
32
|
-
},
|
|
33
|
-
...options
|
|
34
|
-
})
|
|
35
|
-
});
|
|
36
|
-
if (!response.ok) {
|
|
37
|
-
const text = await response.text();
|
|
38
|
-
throw new Error(`BrowserBase createSession failed: ${response.status} ${text}`);
|
|
39
|
-
}
|
|
40
|
-
const session = await response.json();
|
|
41
|
-
const connectResponse = await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
|
|
42
|
-
headers: {
|
|
43
|
-
"X-BB-API-Key": this.apiKey
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
if (!connectResponse.ok) {
|
|
47
|
-
throw new Error(`BrowserBase getSession failed: ${connectResponse.status}`);
|
|
48
|
-
}
|
|
49
|
-
const sessionDetails = await connectResponse.json();
|
|
50
|
-
if (!sessionDetails.connectUrl) {
|
|
51
|
-
throw new Error("BrowserBase session does not have a connectUrl");
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
wsUrl: sessionDetails.connectUrl,
|
|
55
|
-
sessionId: session.id,
|
|
56
|
-
metadata: {
|
|
57
|
-
debugUrl: sessionDetails.debugUrl,
|
|
58
|
-
projectId: this.projectId,
|
|
59
|
-
status: sessionDetails.status
|
|
60
|
-
},
|
|
61
|
-
close: async () => {
|
|
62
|
-
await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
|
|
63
|
-
method: "DELETE",
|
|
64
|
-
headers: {
|
|
65
|
-
"X-BB-API-Key": this.apiKey
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
async resumeSession(sessionId) {
|
|
72
|
-
const response = await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
|
|
73
|
-
headers: {
|
|
74
|
-
"X-BB-API-Key": this.apiKey
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
if (!response.ok) {
|
|
78
|
-
throw new Error(`BrowserBase resumeSession failed: ${response.status}`);
|
|
79
|
-
}
|
|
80
|
-
const session = await response.json();
|
|
81
|
-
if (!session.connectUrl) {
|
|
82
|
-
throw new Error("BrowserBase session does not have a connectUrl (may be closed)");
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
wsUrl: session.connectUrl,
|
|
86
|
-
sessionId: session.id,
|
|
87
|
-
metadata: {
|
|
88
|
-
debugUrl: session.debugUrl,
|
|
89
|
-
projectId: this.projectId,
|
|
90
|
-
status: session.status
|
|
91
|
-
},
|
|
92
|
-
close: async () => {
|
|
93
|
-
await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
|
|
94
|
-
method: "DELETE",
|
|
95
|
-
headers: {
|
|
96
|
-
"X-BB-API-Key": this.apiKey
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// src/providers/browserless.ts
|
|
105
|
-
var BrowserlessProvider = class {
|
|
106
|
-
name = "browserless";
|
|
107
|
-
token;
|
|
108
|
-
baseUrl;
|
|
109
|
-
constructor(options) {
|
|
110
|
-
this.token = options.token;
|
|
111
|
-
this.baseUrl = options.baseUrl ?? "wss://chrome.browserless.io";
|
|
112
|
-
}
|
|
113
|
-
async createSession(options = {}) {
|
|
114
|
-
const params = new URLSearchParams({
|
|
115
|
-
token: this.token
|
|
116
|
-
});
|
|
117
|
-
if (options.width && options.height) {
|
|
118
|
-
params.set("--window-size", `${options.width},${options.height}`);
|
|
119
|
-
}
|
|
120
|
-
if (options.proxy?.server) {
|
|
121
|
-
params.set("--proxy-server", options.proxy.server);
|
|
122
|
-
}
|
|
123
|
-
const wsUrl = `${this.baseUrl}?${params.toString()}`;
|
|
124
|
-
return {
|
|
125
|
-
wsUrl,
|
|
126
|
-
metadata: {
|
|
127
|
-
provider: "browserless"
|
|
128
|
-
},
|
|
129
|
-
close: async () => {
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
// Browserless doesn't support session resumption in the same way
|
|
134
|
-
// Each connection is a fresh browser instance
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// src/providers/generic.ts
|
|
138
|
-
function sleep(ms) {
|
|
139
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
140
|
-
}
|
|
141
|
-
async function fetchDevToolsJson(host, path, errorPrefix, options = {}) {
|
|
142
|
-
const protocol = host.includes("://") ? "" : "http://";
|
|
143
|
-
const attempts = options.attempts ?? 1;
|
|
144
|
-
let delayMs = options.initialDelayMs ?? 50;
|
|
145
|
-
const maxDelayMs = options.maxDelayMs ?? 250;
|
|
146
|
-
let lastError;
|
|
147
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
148
|
-
try {
|
|
149
|
-
const response = await fetch(`${protocol}${host}${path}`);
|
|
150
|
-
if (response.ok) {
|
|
151
|
-
return await response.json();
|
|
152
|
-
}
|
|
153
|
-
lastError = new Error(`${errorPrefix}: ${response.status}`);
|
|
154
|
-
} catch (error) {
|
|
155
|
-
lastError = new Error(
|
|
156
|
-
`${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
if (attempt < attempts) {
|
|
160
|
-
await sleep(delayMs);
|
|
161
|
-
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
throw lastError ?? new Error(errorPrefix);
|
|
165
|
-
}
|
|
166
|
-
var GenericProvider = class {
|
|
167
|
-
name = "generic";
|
|
168
|
-
wsUrl;
|
|
169
|
-
constructor(options) {
|
|
170
|
-
this.wsUrl = options.wsUrl;
|
|
171
|
-
}
|
|
172
|
-
async createSession(_options = {}) {
|
|
173
|
-
return {
|
|
174
|
-
wsUrl: this.wsUrl,
|
|
175
|
-
metadata: {
|
|
176
|
-
provider: "generic"
|
|
177
|
-
},
|
|
178
|
-
close: async () => {
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
async function getBrowserWebSocketUrl(host = "localhost:9222") {
|
|
184
|
-
const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
|
|
185
|
-
attempts: 10,
|
|
186
|
-
initialDelayMs: 50,
|
|
187
|
-
maxDelayMs: 250
|
|
188
|
-
});
|
|
189
|
-
return info.webSocketDebuggerUrl;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// src/providers/index.ts
|
|
193
|
-
function createProvider(options) {
|
|
194
|
-
switch (options.provider) {
|
|
195
|
-
case "browserbase":
|
|
196
|
-
if (!options.apiKey) {
|
|
197
|
-
throw new Error("BrowserBase provider requires apiKey");
|
|
198
|
-
}
|
|
199
|
-
if (!options.projectId) {
|
|
200
|
-
throw new Error("BrowserBase provider requires projectId");
|
|
201
|
-
}
|
|
202
|
-
return new BrowserBaseProvider({
|
|
203
|
-
apiKey: options.apiKey,
|
|
204
|
-
projectId: options.projectId
|
|
205
|
-
});
|
|
206
|
-
case "browserless":
|
|
207
|
-
if (!options.apiKey) {
|
|
208
|
-
throw new Error("Browserless provider requires apiKey (token)");
|
|
209
|
-
}
|
|
210
|
-
return new BrowserlessProvider({
|
|
211
|
-
token: options.apiKey
|
|
212
|
-
});
|
|
213
|
-
case "generic":
|
|
214
|
-
if (!options.wsUrl) {
|
|
215
|
-
throw new Error("Generic provider requires wsUrl");
|
|
216
|
-
}
|
|
217
|
-
return new GenericProvider({
|
|
218
|
-
wsUrl: options.wsUrl
|
|
219
|
-
});
|
|
220
|
-
default:
|
|
221
|
-
throw new Error(`Unknown provider: ${options.provider}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
4
|
+
} from "./chunk-DTVRFXKI.mjs";
|
|
224
5
|
|
|
225
6
|
// src/actions/executor.ts
|
|
226
7
|
import * as fs from "fs";
|
|
@@ -693,7 +474,7 @@ var CHECK_EDITABLE = `function() {
|
|
|
693
474
|
|
|
694
475
|
return { actionable: true };
|
|
695
476
|
}`;
|
|
696
|
-
function
|
|
477
|
+
function sleep(ms) {
|
|
697
478
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
698
479
|
}
|
|
699
480
|
var BACKOFF = [0, 20, 100, 100];
|
|
@@ -777,7 +558,7 @@ async function ensureActionable(cdp, objectId, checks, options) {
|
|
|
777
558
|
);
|
|
778
559
|
}
|
|
779
560
|
const delay = attempt < BACKOFF.length ? BACKOFF[attempt] ?? 0 : 500;
|
|
780
|
-
if (delay > 0) await
|
|
561
|
+
if (delay > 0) await sleep(delay);
|
|
781
562
|
attempt++;
|
|
782
563
|
}
|
|
783
564
|
}
|
|
@@ -1056,17 +837,978 @@ var NavigationError = class extends Error {
|
|
|
1056
837
|
super(message);
|
|
1057
838
|
this.name = "NavigationError";
|
|
1058
839
|
}
|
|
1059
|
-
};
|
|
1060
|
-
|
|
1061
|
-
// src/
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// src/trace/views.ts
|
|
843
|
+
function takeRecent(events, limit = 5) {
|
|
844
|
+
return events.slice(-limit).map((event) => ({
|
|
845
|
+
ts: event.ts,
|
|
846
|
+
event: event.event,
|
|
847
|
+
summary: event.summary,
|
|
848
|
+
severity: event.severity,
|
|
849
|
+
url: event.url
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
function buildTraceSummary(events, view) {
|
|
853
|
+
switch (view) {
|
|
854
|
+
case "ws":
|
|
855
|
+
return summarizeWs(events);
|
|
856
|
+
case "voice":
|
|
857
|
+
return summarizeVoice(events);
|
|
858
|
+
case "console":
|
|
859
|
+
return summarizeConsole(events);
|
|
860
|
+
case "permissions":
|
|
861
|
+
return summarizePermissions(events);
|
|
862
|
+
case "media":
|
|
863
|
+
return summarizeMedia(events);
|
|
864
|
+
case "ui":
|
|
865
|
+
return summarizeUi(events);
|
|
866
|
+
case "session":
|
|
867
|
+
return summarizeSession(events);
|
|
868
|
+
}
|
|
869
|
+
throw new Error(`Unsupported trace view: ${view}`);
|
|
870
|
+
}
|
|
871
|
+
function buildTraceSummaries(events) {
|
|
872
|
+
return {
|
|
873
|
+
ws: summarizeWs(events),
|
|
874
|
+
voice: summarizeVoice(events),
|
|
875
|
+
console: summarizeConsole(events),
|
|
876
|
+
permissions: summarizePermissions(events),
|
|
877
|
+
media: summarizeMedia(events),
|
|
878
|
+
ui: summarizeUi(events),
|
|
879
|
+
session: summarizeSession(events)
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function summarizeWs(events) {
|
|
883
|
+
const relevant = events.filter(
|
|
884
|
+
(event) => event.channel === "ws" || event.event.startsWith("ws.")
|
|
885
|
+
);
|
|
886
|
+
const connections = /* @__PURE__ */ new Map();
|
|
887
|
+
for (const event of relevant) {
|
|
888
|
+
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
889
|
+
let connection = connections.get(id);
|
|
890
|
+
if (!connection) {
|
|
891
|
+
connection = { id, sent: 0, received: 0, lastMessages: [] };
|
|
892
|
+
connections.set(id, connection);
|
|
893
|
+
}
|
|
894
|
+
connection.url = event.url ?? connection.url;
|
|
895
|
+
if (event.event === "ws.connection.created") {
|
|
896
|
+
connection.createdAt = event.ts;
|
|
897
|
+
}
|
|
898
|
+
if (event.event === "ws.connection.closed") {
|
|
899
|
+
connection.closedAt = event.ts;
|
|
900
|
+
}
|
|
901
|
+
if (event.event === "ws.frame.sent") {
|
|
902
|
+
connection.sent += 1;
|
|
903
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
904
|
+
if (payload) connection.lastMessages.push(`sent: ${payload}`);
|
|
905
|
+
}
|
|
906
|
+
if (event.event === "ws.frame.received") {
|
|
907
|
+
connection.received += 1;
|
|
908
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
909
|
+
if (payload) connection.lastMessages.push(`recv: ${payload}`);
|
|
910
|
+
}
|
|
911
|
+
connection.lastMessages = connection.lastMessages.slice(-3);
|
|
912
|
+
}
|
|
913
|
+
const values = [...connections.values()];
|
|
914
|
+
const reconnects = values.reduce((count, connection) => {
|
|
915
|
+
return connection.closedAt && !connection.createdAt ? count + 1 : count;
|
|
916
|
+
}, 0);
|
|
917
|
+
return {
|
|
918
|
+
view: "ws",
|
|
919
|
+
totalEvents: relevant.length,
|
|
920
|
+
connections: values.map((connection) => ({
|
|
921
|
+
id: connection.id,
|
|
922
|
+
url: connection.url ?? null,
|
|
923
|
+
createdAt: connection.createdAt ?? null,
|
|
924
|
+
closedAt: connection.closedAt ?? null,
|
|
925
|
+
sent: connection.sent,
|
|
926
|
+
received: connection.received,
|
|
927
|
+
lastMessages: connection.lastMessages,
|
|
928
|
+
connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
|
|
929
|
+
})),
|
|
930
|
+
reconnects,
|
|
931
|
+
recent: takeRecent(relevant)
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function summarizeConsole(events) {
|
|
935
|
+
const relevant = events.filter(
|
|
936
|
+
(event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
937
|
+
);
|
|
938
|
+
return {
|
|
939
|
+
view: "console",
|
|
940
|
+
errors: relevant.filter(
|
|
941
|
+
(event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
942
|
+
).length,
|
|
943
|
+
warnings: relevant.filter((event) => event.event === "console.warn").length,
|
|
944
|
+
logs: relevant.filter((event) => event.event === "console.log").length,
|
|
945
|
+
recent: takeRecent(relevant)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function summarizePermissions(events) {
|
|
949
|
+
const relevant = events.filter(
|
|
950
|
+
(event) => event.channel === "permission" || event.event.startsWith("permission.")
|
|
951
|
+
);
|
|
952
|
+
const latest = /* @__PURE__ */ new Map();
|
|
953
|
+
for (const event of relevant) {
|
|
954
|
+
const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
|
|
955
|
+
const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
|
|
956
|
+
if (name && state) {
|
|
957
|
+
latest.set(name, state);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
view: "permissions",
|
|
962
|
+
states: Object.fromEntries(latest),
|
|
963
|
+
changes: relevant.filter((event) => event.event === "permission.changed").length,
|
|
964
|
+
recent: takeRecent(relevant)
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function summarizeMedia(events) {
|
|
968
|
+
const relevant = events.filter(
|
|
969
|
+
(event) => event.channel === "media" || event.event.startsWith("media.")
|
|
970
|
+
);
|
|
971
|
+
const liveTracks = /* @__PURE__ */ new Map();
|
|
972
|
+
for (const event of relevant) {
|
|
973
|
+
const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
|
|
974
|
+
const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
|
|
975
|
+
const key = `${kind}:${label}`;
|
|
976
|
+
if (event.event === "media.track.started") {
|
|
977
|
+
liveTracks.set(key, kind);
|
|
978
|
+
}
|
|
979
|
+
if (event.event === "media.track.ended") {
|
|
980
|
+
liveTracks.delete(key);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
view: "media",
|
|
985
|
+
tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
|
|
986
|
+
tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
|
|
987
|
+
playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
|
|
988
|
+
playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
|
|
989
|
+
liveTracks: [...liveTracks.values()],
|
|
990
|
+
recent: takeRecent(relevant)
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function summarizeVoice(events) {
|
|
994
|
+
const relevant = events.filter(
|
|
995
|
+
(event) => event.channel === "voice" || event.event.startsWith("voice.")
|
|
996
|
+
);
|
|
997
|
+
return {
|
|
998
|
+
view: "voice",
|
|
999
|
+
ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
|
|
1000
|
+
notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
|
|
1001
|
+
captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
|
|
1002
|
+
captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
|
|
1003
|
+
detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
|
|
1004
|
+
recent: takeRecent(relevant)
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function summarizeUi(events) {
|
|
1008
|
+
const relevant = events.filter(
|
|
1009
|
+
(event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
|
|
1010
|
+
);
|
|
1011
|
+
return {
|
|
1012
|
+
view: "ui",
|
|
1013
|
+
actions: relevant.filter((event) => event.channel === "action").length,
|
|
1014
|
+
domChanges: relevant.filter((event) => event.channel === "dom").length,
|
|
1015
|
+
recent: takeRecent(relevant)
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function summarizeSession(events) {
|
|
1019
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
1020
|
+
const failedActions = events.filter((event) => event.event === "action.failed").length;
|
|
1021
|
+
for (const event of events) {
|
|
1022
|
+
byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
view: "session",
|
|
1026
|
+
totalEvents: events.length,
|
|
1027
|
+
byChannel: Object.fromEntries(byChannel),
|
|
1028
|
+
failedActions,
|
|
1029
|
+
recent: takeRecent(events)
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/recording/manifest.ts
|
|
1034
|
+
function isCanonicalRecordingManifest(value) {
|
|
1035
|
+
return Boolean(
|
|
1036
|
+
value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
function isLegacyRecordingManifest(value) {
|
|
1040
|
+
return Boolean(
|
|
1041
|
+
value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
function createRecordingManifest(input) {
|
|
1045
|
+
const actions = input.frames.map((frame) => {
|
|
1046
|
+
const actionId = frame.actionId ?? `action-${frame.seq}`;
|
|
1047
|
+
return {
|
|
1048
|
+
id: actionId,
|
|
1049
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1050
|
+
action: frame.action,
|
|
1051
|
+
selector: frame.selector,
|
|
1052
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1053
|
+
value: frame.value,
|
|
1054
|
+
url: frame.url,
|
|
1055
|
+
success: frame.success,
|
|
1056
|
+
durationMs: frame.durationMs,
|
|
1057
|
+
error: frame.error,
|
|
1058
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1059
|
+
pageUrl: frame.pageUrl,
|
|
1060
|
+
pageTitle: frame.pageTitle,
|
|
1061
|
+
coordinates: frame.coordinates,
|
|
1062
|
+
boundingBox: frame.boundingBox
|
|
1063
|
+
};
|
|
1064
|
+
});
|
|
1065
|
+
const screenshots = input.frames.map((frame) => ({
|
|
1066
|
+
id: `shot-${frame.seq}`,
|
|
1067
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1068
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1069
|
+
file: frame.screenshot,
|
|
1070
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1071
|
+
success: frame.success,
|
|
1072
|
+
pageUrl: frame.pageUrl,
|
|
1073
|
+
pageTitle: frame.pageTitle,
|
|
1074
|
+
coordinates: frame.coordinates,
|
|
1075
|
+
boundingBox: frame.boundingBox
|
|
1076
|
+
}));
|
|
1077
|
+
return {
|
|
1078
|
+
version: 2,
|
|
1079
|
+
recordedAt: input.recordedAt,
|
|
1080
|
+
session: {
|
|
1081
|
+
id: input.sessionId,
|
|
1082
|
+
startUrl: input.startUrl,
|
|
1083
|
+
endUrl: input.endUrl,
|
|
1084
|
+
targetId: input.targetId,
|
|
1085
|
+
profile: input.profile
|
|
1086
|
+
},
|
|
1087
|
+
recipe: {
|
|
1088
|
+
steps: input.steps
|
|
1089
|
+
},
|
|
1090
|
+
actions,
|
|
1091
|
+
screenshots,
|
|
1092
|
+
trace: {
|
|
1093
|
+
events: input.traceEvents,
|
|
1094
|
+
summaries: buildTraceSummaries(input.traceEvents)
|
|
1095
|
+
},
|
|
1096
|
+
assertions: input.assertions ?? [],
|
|
1097
|
+
notes: input.notes ?? [],
|
|
1098
|
+
artifacts: {
|
|
1099
|
+
recordingManifest: input.recordingManifest ?? "recording.json",
|
|
1100
|
+
screenshotDir: input.screenshotDir ?? "screenshots/"
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
function canonicalizeRecordingArtifact(value) {
|
|
1105
|
+
if (isCanonicalRecordingManifest(value)) {
|
|
1106
|
+
return value;
|
|
1107
|
+
}
|
|
1108
|
+
if (!isLegacyRecordingManifest(value)) {
|
|
1109
|
+
throw new Error("Unsupported recording artifact");
|
|
1110
|
+
}
|
|
1111
|
+
const traceEvents = buildTraceEventsFromLegacy(value);
|
|
1112
|
+
const steps = value.frames.map((frame) => frameToStep(frame));
|
|
1113
|
+
return createRecordingManifest({
|
|
1114
|
+
recordedAt: value.recordedAt,
|
|
1115
|
+
sessionId: value.sessionId,
|
|
1116
|
+
startUrl: value.startUrl,
|
|
1117
|
+
endUrl: value.endUrl,
|
|
1118
|
+
steps,
|
|
1119
|
+
frames: value.frames,
|
|
1120
|
+
traceEvents,
|
|
1121
|
+
notes: ["Converted from legacy recording manifest"]
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
function buildTraceEventsFromLegacy(value) {
|
|
1125
|
+
const events = [];
|
|
1126
|
+
for (const frame of value.frames) {
|
|
1127
|
+
events.push({
|
|
1128
|
+
traceId: frame.actionId ?? `legacy-${frame.seq}`,
|
|
1129
|
+
sessionId: value.sessionId,
|
|
1130
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1131
|
+
elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
|
|
1132
|
+
channel: "action",
|
|
1133
|
+
event: frame.success ? "action.succeeded" : "action.failed",
|
|
1134
|
+
severity: frame.success ? "info" : "error",
|
|
1135
|
+
summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
|
|
1136
|
+
data: {
|
|
1137
|
+
action: frame.action,
|
|
1138
|
+
selector: frame.selector,
|
|
1139
|
+
value: frame.value ?? null,
|
|
1140
|
+
pageUrl: frame.pageUrl ?? null,
|
|
1141
|
+
pageTitle: frame.pageTitle ?? null,
|
|
1142
|
+
screenshot: frame.screenshot
|
|
1143
|
+
},
|
|
1144
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1145
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1146
|
+
selector: frame.selector,
|
|
1147
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1148
|
+
url: frame.pageUrl ?? frame.url
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
return events;
|
|
1152
|
+
}
|
|
1153
|
+
function frameToStep(frame) {
|
|
1154
|
+
switch (frame.action) {
|
|
1155
|
+
case "fill":
|
|
1156
|
+
return { action: "fill", selector: frame.selector, value: frame.value };
|
|
1157
|
+
case "submit":
|
|
1158
|
+
return { action: "submit", selector: frame.selector };
|
|
1159
|
+
case "goto":
|
|
1160
|
+
return { action: "goto", url: frame.url ?? frame.pageUrl };
|
|
1161
|
+
case "press":
|
|
1162
|
+
return { action: "press", key: frame.value ?? "Enter" };
|
|
1163
|
+
default:
|
|
1164
|
+
return { action: "click", selector: frame.selector };
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/trace/model.ts
|
|
1169
|
+
function createTraceId(prefix = "evt") {
|
|
1170
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1171
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1172
|
+
}
|
|
1173
|
+
function normalizeTraceEvent(event) {
|
|
1174
|
+
return {
|
|
1175
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1176
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1177
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
1178
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
1179
|
+
data: event.data ?? {},
|
|
1180
|
+
...event
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function inferSeverity(eventName) {
|
|
1184
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1185
|
+
return "error";
|
|
1186
|
+
}
|
|
1187
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1188
|
+
return "warn";
|
|
1189
|
+
}
|
|
1190
|
+
return "info";
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/trace/script.ts
|
|
1194
|
+
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
1195
|
+
var TRACE_SCRIPT = `
|
|
1196
|
+
(() => {
|
|
1197
|
+
if (window.__bpTraceInstalled) return;
|
|
1198
|
+
window.__bpTraceInstalled = true;
|
|
1199
|
+
|
|
1200
|
+
const binding = globalThis.${TRACE_BINDING_NAME};
|
|
1201
|
+
if (typeof binding !== 'function') return;
|
|
1202
|
+
|
|
1203
|
+
const emit = (event, data = {}, severity = 'info', summary) => {
|
|
1204
|
+
try {
|
|
1205
|
+
globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
|
|
1206
|
+
const payload = {
|
|
1207
|
+
event,
|
|
1208
|
+
severity,
|
|
1209
|
+
summary: summary || event,
|
|
1210
|
+
ts: Date.now(),
|
|
1211
|
+
data,
|
|
1212
|
+
};
|
|
1213
|
+
globalThis.__bpTraceRecentEvents.push(payload);
|
|
1214
|
+
if (globalThis.__bpTraceRecentEvents.length > 200) {
|
|
1215
|
+
globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
|
|
1216
|
+
}
|
|
1217
|
+
binding(JSON.stringify(payload));
|
|
1218
|
+
} catch {}
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const patchWebSocket = () => {
|
|
1222
|
+
const NativeWebSocket = window.WebSocket;
|
|
1223
|
+
if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
|
|
1224
|
+
window.__bpTraceWebSocketInstalled = true;
|
|
1225
|
+
|
|
1226
|
+
const nextId = () => Math.random().toString(36).slice(2, 10);
|
|
1227
|
+
|
|
1228
|
+
const patchInstance = (socket, urlValue) => {
|
|
1229
|
+
if (!socket || socket.__bpTracePatched) return socket;
|
|
1230
|
+
socket.__bpTracePatched = true;
|
|
1231
|
+
socket.__bpTraceId = socket.__bpTraceId || nextId();
|
|
1232
|
+
socket.__bpTraceUrl = String(urlValue || socket.url || '');
|
|
1233
|
+
globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
|
|
1234
|
+
globalThis.__bpTrackedWebSockets.add(socket);
|
|
1235
|
+
|
|
1236
|
+
emit(
|
|
1237
|
+
'ws.connection.created',
|
|
1238
|
+
{ connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
|
|
1239
|
+
'info',
|
|
1240
|
+
'WebSocket opened ' + socket.__bpTraceUrl
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
const originalSend = socket.send;
|
|
1244
|
+
socket.send = function(data) {
|
|
1245
|
+
const payload =
|
|
1246
|
+
typeof data === 'string'
|
|
1247
|
+
? data
|
|
1248
|
+
: data && typeof data.toString === 'function'
|
|
1249
|
+
? data.toString()
|
|
1250
|
+
: '[binary]';
|
|
1251
|
+
emit(
|
|
1252
|
+
'ws.frame.sent',
|
|
1253
|
+
{
|
|
1254
|
+
connectionId: socket.__bpTraceId,
|
|
1255
|
+
url: socket.__bpTraceUrl,
|
|
1256
|
+
payload,
|
|
1257
|
+
length: payload.length,
|
|
1258
|
+
},
|
|
1259
|
+
'info',
|
|
1260
|
+
'WebSocket frame sent'
|
|
1261
|
+
);
|
|
1262
|
+
return originalSend.call(this, data);
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
socket.addEventListener('message', (event) => {
|
|
1266
|
+
if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const data = event && 'data' in event ? event.data : '';
|
|
1270
|
+
const payload =
|
|
1271
|
+
typeof data === 'string'
|
|
1272
|
+
? data
|
|
1273
|
+
: data && typeof data.toString === 'function'
|
|
1274
|
+
? data.toString()
|
|
1275
|
+
: '[binary]';
|
|
1276
|
+
emit(
|
|
1277
|
+
'ws.frame.received',
|
|
1278
|
+
{
|
|
1279
|
+
connectionId: socket.__bpTraceId,
|
|
1280
|
+
url: socket.__bpTraceUrl,
|
|
1281
|
+
payload,
|
|
1282
|
+
length: payload.length,
|
|
1283
|
+
},
|
|
1284
|
+
'info',
|
|
1285
|
+
'WebSocket frame received'
|
|
1286
|
+
);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
socket.addEventListener('close', (event) => {
|
|
1290
|
+
if (socket.__bpTraceClosed) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
socket.__bpTraceClosed = true;
|
|
1294
|
+
try {
|
|
1295
|
+
globalThis.__bpTrackedWebSockets.delete(socket);
|
|
1296
|
+
} catch {}
|
|
1297
|
+
emit(
|
|
1298
|
+
'ws.connection.closed',
|
|
1299
|
+
{
|
|
1300
|
+
connectionId: socket.__bpTraceId,
|
|
1301
|
+
url: socket.__bpTraceUrl,
|
|
1302
|
+
code: event.code,
|
|
1303
|
+
reason: event.reason,
|
|
1304
|
+
},
|
|
1305
|
+
'warn',
|
|
1306
|
+
'WebSocket closed'
|
|
1307
|
+
);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
return socket;
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
const TracedWebSocket = function(url, protocols) {
|
|
1314
|
+
return arguments.length > 1
|
|
1315
|
+
? patchInstance(new NativeWebSocket(url, protocols), url)
|
|
1316
|
+
: patchInstance(new NativeWebSocket(url), url);
|
|
1317
|
+
};
|
|
1318
|
+
TracedWebSocket.prototype = NativeWebSocket.prototype;
|
|
1319
|
+
Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
|
|
1320
|
+
window.WebSocket = TracedWebSocket;
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
window.addEventListener('error', (errorEvent) => {
|
|
1324
|
+
emit(
|
|
1325
|
+
'runtime.exception',
|
|
1326
|
+
{
|
|
1327
|
+
message: errorEvent.message,
|
|
1328
|
+
filename: errorEvent.filename,
|
|
1329
|
+
line: errorEvent.lineno,
|
|
1330
|
+
column: errorEvent.colno,
|
|
1331
|
+
},
|
|
1332
|
+
'error',
|
|
1333
|
+
errorEvent.message || 'Uncaught error'
|
|
1334
|
+
);
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
1338
|
+
const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
|
|
1339
|
+
emit('runtime.unhandledRejection', { reason }, 'error', reason);
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
const patchPermissions = async () => {
|
|
1343
|
+
if (!navigator.permissions || !navigator.permissions.query) return;
|
|
1344
|
+
|
|
1345
|
+
const names = ['geolocation', 'microphone', 'camera', 'notifications'];
|
|
1346
|
+
for (const name of names) {
|
|
1347
|
+
try {
|
|
1348
|
+
const status = await navigator.permissions.query({ name });
|
|
1349
|
+
emit(
|
|
1350
|
+
'permission.state',
|
|
1351
|
+
{ name, state: status.state },
|
|
1352
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1353
|
+
name + ': ' + status.state
|
|
1354
|
+
);
|
|
1355
|
+
status.addEventListener('change', () => {
|
|
1356
|
+
emit(
|
|
1357
|
+
'permission.changed',
|
|
1358
|
+
{ name, state: status.state },
|
|
1359
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1360
|
+
name + ': ' + status.state
|
|
1361
|
+
);
|
|
1362
|
+
});
|
|
1363
|
+
} catch {}
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
const patchMediaElement = (element) => {
|
|
1368
|
+
if (!element || element.__bpTracePatched) return;
|
|
1369
|
+
element.__bpTracePatched = true;
|
|
1370
|
+
|
|
1371
|
+
element.addEventListener('play', () => {
|
|
1372
|
+
emit(
|
|
1373
|
+
'media.playback.started',
|
|
1374
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1375
|
+
'info',
|
|
1376
|
+
'Media playback started'
|
|
1377
|
+
);
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
const onStop = () => {
|
|
1381
|
+
emit(
|
|
1382
|
+
'media.playback.stopped',
|
|
1383
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1384
|
+
'warn',
|
|
1385
|
+
'Media playback stopped'
|
|
1386
|
+
);
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
element.addEventListener('pause', onStop);
|
|
1390
|
+
element.addEventListener('ended', onStop);
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
const patchMediaElements = () => {
|
|
1394
|
+
document.querySelectorAll('audio,video').forEach(patchMediaElement);
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
patchMediaElements();
|
|
1398
|
+
patchWebSocket();
|
|
1399
|
+
|
|
1400
|
+
if (document.documentElement) {
|
|
1401
|
+
const observer = new MutationObserver(() => {
|
|
1402
|
+
patchMediaElements();
|
|
1403
|
+
});
|
|
1404
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
1408
|
+
const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
|
1409
|
+
navigator.mediaDevices.getUserMedia = async (...args) => {
|
|
1410
|
+
emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
|
|
1411
|
+
try {
|
|
1412
|
+
const stream = await original(...args);
|
|
1413
|
+
const tracks = stream.getTracks();
|
|
1414
|
+
|
|
1415
|
+
for (const track of tracks) {
|
|
1416
|
+
emit(
|
|
1417
|
+
'media.track.started',
|
|
1418
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1419
|
+
'info',
|
|
1420
|
+
track.kind + ' track started'
|
|
1421
|
+
);
|
|
1422
|
+
track.addEventListener('ended', () => {
|
|
1423
|
+
emit(
|
|
1424
|
+
'media.track.ended',
|
|
1425
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1426
|
+
'warn',
|
|
1427
|
+
track.kind + ' track ended'
|
|
1428
|
+
);
|
|
1429
|
+
emit(
|
|
1430
|
+
'voice.capture.stopped',
|
|
1431
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1432
|
+
'warn',
|
|
1433
|
+
'Voice capture stopped'
|
|
1434
|
+
);
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
emit(
|
|
1439
|
+
'voice.capture.detectedAudio',
|
|
1440
|
+
{ trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
|
|
1441
|
+
'info',
|
|
1442
|
+
'Voice capture detected audio'
|
|
1443
|
+
);
|
|
1444
|
+
|
|
1445
|
+
return stream;
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
emit(
|
|
1448
|
+
'voice.pipeline.notReady',
|
|
1449
|
+
{ message: String(error && error.message ? error.message : error) },
|
|
1450
|
+
'error',
|
|
1451
|
+
String(error && error.message ? error.message : error)
|
|
1452
|
+
);
|
|
1453
|
+
throw error;
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
document.addEventListener('visibilitychange', () => {
|
|
1459
|
+
emit(
|
|
1460
|
+
'dom.state.changed',
|
|
1461
|
+
{ visibilityState: document.visibilityState },
|
|
1462
|
+
document.visibilityState === 'hidden' ? 'warn' : 'info',
|
|
1463
|
+
'Visibility ' + document.visibilityState
|
|
1464
|
+
);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
patchPermissions();
|
|
1468
|
+
emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
|
|
1469
|
+
})();
|
|
1470
|
+
`;
|
|
1471
|
+
|
|
1472
|
+
// src/trace/live.ts
|
|
1473
|
+
function globToRegex(pattern) {
|
|
1474
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1475
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1476
|
+
return new RegExp(`^${withWildcards}$`);
|
|
1477
|
+
}
|
|
1478
|
+
function readString(value) {
|
|
1479
|
+
return typeof value === "string" ? value : void 0;
|
|
1480
|
+
}
|
|
1481
|
+
function readStringOr(value, fallback = "") {
|
|
1482
|
+
return readString(value) ?? fallback;
|
|
1483
|
+
}
|
|
1484
|
+
function formatConsoleArg(entry) {
|
|
1485
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
1486
|
+
}
|
|
1487
|
+
var LiveTraceCollector = class {
|
|
1488
|
+
cdp;
|
|
1489
|
+
options;
|
|
1490
|
+
handlers = [];
|
|
1491
|
+
wsUrls = /* @__PURE__ */ new Map();
|
|
1492
|
+
httpUrls = /* @__PURE__ */ new Map();
|
|
1493
|
+
events = [];
|
|
1494
|
+
startTime = Date.now();
|
|
1495
|
+
matchRegex;
|
|
1496
|
+
constructor(cdp, options = {}) {
|
|
1497
|
+
this.cdp = cdp;
|
|
1498
|
+
this.options = options;
|
|
1499
|
+
this.matchRegex = options.match ? globToRegex(options.match) : null;
|
|
1500
|
+
}
|
|
1501
|
+
async start() {
|
|
1502
|
+
await this.cdp.send("Runtime.enable");
|
|
1503
|
+
await this.cdp.send("Page.enable");
|
|
1504
|
+
await this.cdp.send("Network.enable");
|
|
1505
|
+
await this.cdp.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
1506
|
+
await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
|
|
1507
|
+
await this.cdp.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
|
|
1508
|
+
if ((this.options.mode ?? "all") !== "http") {
|
|
1509
|
+
this.subscribe("Network.webSocketCreated", (params) => {
|
|
1510
|
+
const requestId = readStringOr(params["requestId"]);
|
|
1511
|
+
const url = readStringOr(params["url"]);
|
|
1512
|
+
if (!this.matchesUrl(url)) {
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
this.wsUrls.set(requestId, url);
|
|
1516
|
+
void this.emit({
|
|
1517
|
+
channel: "ws",
|
|
1518
|
+
event: "ws.connection.created",
|
|
1519
|
+
summary: `WebSocket opened ${url}`,
|
|
1520
|
+
connectionId: requestId,
|
|
1521
|
+
requestId,
|
|
1522
|
+
url,
|
|
1523
|
+
data: { url }
|
|
1524
|
+
});
|
|
1525
|
+
});
|
|
1526
|
+
this.subscribe("Network.webSocketFrameSent", (params) => {
|
|
1527
|
+
const requestId = readStringOr(params["requestId"]);
|
|
1528
|
+
const response = params["response"];
|
|
1529
|
+
const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
|
|
1530
|
+
const url = this.wsUrls.get(requestId);
|
|
1531
|
+
if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
void this.emit({
|
|
1535
|
+
channel: "ws",
|
|
1536
|
+
event: "ws.frame.sent",
|
|
1537
|
+
summary: `WebSocket frame sent ${requestId}`,
|
|
1538
|
+
connectionId: requestId,
|
|
1539
|
+
requestId,
|
|
1540
|
+
url,
|
|
1541
|
+
data: {
|
|
1542
|
+
opcode: response?.opcode ?? 1,
|
|
1543
|
+
payload,
|
|
1544
|
+
length: response?.payloadData?.length ?? 0
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
});
|
|
1548
|
+
this.subscribe("Network.webSocketFrameReceived", (params) => {
|
|
1549
|
+
const requestId = readStringOr(params["requestId"]);
|
|
1550
|
+
const response = params["response"];
|
|
1551
|
+
const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
|
|
1552
|
+
const url = this.wsUrls.get(requestId);
|
|
1553
|
+
if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
void this.emit({
|
|
1557
|
+
channel: "ws",
|
|
1558
|
+
event: "ws.frame.received",
|
|
1559
|
+
summary: `WebSocket frame received ${requestId}`,
|
|
1560
|
+
connectionId: requestId,
|
|
1561
|
+
requestId,
|
|
1562
|
+
url,
|
|
1563
|
+
data: {
|
|
1564
|
+
opcode: response?.opcode ?? 1,
|
|
1565
|
+
payload,
|
|
1566
|
+
length: response?.payloadData?.length ?? 0
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
});
|
|
1570
|
+
this.subscribe("Network.webSocketClosed", (params) => {
|
|
1571
|
+
const requestId = readStringOr(params["requestId"]);
|
|
1572
|
+
const url = this.wsUrls.get(requestId);
|
|
1573
|
+
this.wsUrls.delete(requestId);
|
|
1574
|
+
void this.emit({
|
|
1575
|
+
channel: "ws",
|
|
1576
|
+
event: "ws.connection.closed",
|
|
1577
|
+
summary: `WebSocket closed ${requestId}`,
|
|
1578
|
+
severity: "warn",
|
|
1579
|
+
connectionId: requestId,
|
|
1580
|
+
requestId,
|
|
1581
|
+
url,
|
|
1582
|
+
data: { url }
|
|
1583
|
+
});
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
if ((this.options.mode ?? "all") !== "ws") {
|
|
1587
|
+
this.subscribe("Network.requestWillBeSent", (params) => {
|
|
1588
|
+
const request = params["request"];
|
|
1589
|
+
const requestId = readStringOr(params["requestId"]);
|
|
1590
|
+
const url = request?.url ?? "";
|
|
1591
|
+
if (!this.matchesUrl(url)) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
this.httpUrls.set(requestId, url);
|
|
1595
|
+
void this.emit({
|
|
1596
|
+
channel: "http",
|
|
1597
|
+
event: "http.request.sent",
|
|
1598
|
+
summary: `${request?.method ?? "GET"} ${url}`,
|
|
1599
|
+
requestId,
|
|
1600
|
+
url,
|
|
1601
|
+
data: {
|
|
1602
|
+
method: request?.method ?? "GET",
|
|
1603
|
+
headers: request?.headers ?? {},
|
|
1604
|
+
body: request?.postData ?? null
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
});
|
|
1608
|
+
this.subscribe("Network.responseReceived", (params) => {
|
|
1609
|
+
const requestId = readStringOr(params["requestId"]);
|
|
1610
|
+
if (!this.httpUrls.has(requestId)) {
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const response = params["response"];
|
|
1614
|
+
void this.emit({
|
|
1615
|
+
channel: "http",
|
|
1616
|
+
event: "http.response.received",
|
|
1617
|
+
summary: `${response?.status ?? 0} ${response?.url ?? this.httpUrls.get(requestId) ?? ""}`,
|
|
1618
|
+
requestId,
|
|
1619
|
+
url: response?.url ?? this.httpUrls.get(requestId),
|
|
1620
|
+
data: {
|
|
1621
|
+
status: response?.status ?? 0,
|
|
1622
|
+
headers: response?.headers ?? {},
|
|
1623
|
+
mimeType: response?.mimeType ?? null
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
});
|
|
1627
|
+
this.subscribe("Network.loadingFailed", (params) => {
|
|
1628
|
+
const requestId = readStringOr(params["requestId"]);
|
|
1629
|
+
const url = readString(params["blockedReason"]) ?? this.httpUrls.get(requestId) ?? "";
|
|
1630
|
+
void this.emit({
|
|
1631
|
+
channel: "http",
|
|
1632
|
+
event: "http.response.failed",
|
|
1633
|
+
summary: `HTTP request failed ${requestId}`,
|
|
1634
|
+
severity: "error",
|
|
1635
|
+
requestId,
|
|
1636
|
+
url,
|
|
1637
|
+
data: {
|
|
1638
|
+
errorText: params["errorText"] ?? null,
|
|
1639
|
+
blockedReason: params["blockedReason"] ?? null,
|
|
1640
|
+
canceled: params["canceled"] ?? false
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
this.subscribe("Runtime.consoleAPICalled", (params) => {
|
|
1646
|
+
const type = readStringOr(params["type"], "log");
|
|
1647
|
+
if (type !== "log" && type !== "warn" && type !== "error") {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
1651
|
+
const text = args.map(formatConsoleArg).filter(Boolean).join(" ");
|
|
1652
|
+
void this.emit({
|
|
1653
|
+
channel: "console",
|
|
1654
|
+
event: `console.${type}`,
|
|
1655
|
+
severity: type === "error" ? "error" : type === "warn" ? "warn" : "info",
|
|
1656
|
+
summary: text || `console.${type}`,
|
|
1657
|
+
data: { args }
|
|
1658
|
+
});
|
|
1659
|
+
});
|
|
1660
|
+
this.subscribe("Runtime.exceptionThrown", (params) => {
|
|
1661
|
+
const details = params["exceptionDetails"] ?? {};
|
|
1662
|
+
const text = readString(details["text"]) ?? "Runtime exception";
|
|
1663
|
+
void this.emit({
|
|
1664
|
+
channel: "runtime",
|
|
1665
|
+
event: "runtime.exception",
|
|
1666
|
+
severity: "error",
|
|
1667
|
+
summary: text,
|
|
1668
|
+
data: details
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
this.subscribe("Runtime.bindingCalled", (params) => {
|
|
1672
|
+
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
const raw = readStringOr(params["payload"]);
|
|
1676
|
+
try {
|
|
1677
|
+
const payload = JSON.parse(raw);
|
|
1678
|
+
const channel = this.channelForTraceEvent(payload.event);
|
|
1679
|
+
void this.emit({
|
|
1680
|
+
channel,
|
|
1681
|
+
event: payload.event,
|
|
1682
|
+
severity: payload.severity,
|
|
1683
|
+
summary: payload.summary ?? payload.event,
|
|
1684
|
+
ts: payload.ts ? new Date(payload.ts).toISOString() : void 0,
|
|
1685
|
+
data: payload.data ?? {},
|
|
1686
|
+
url: readString(payload.data?.["url"])
|
|
1687
|
+
});
|
|
1688
|
+
} catch {
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
async stop() {
|
|
1693
|
+
for (const { event, handler } of this.handlers) {
|
|
1694
|
+
this.cdp.off(event, handler);
|
|
1695
|
+
}
|
|
1696
|
+
this.handlers.length = 0;
|
|
1697
|
+
return [...this.events];
|
|
1698
|
+
}
|
|
1699
|
+
getEvents() {
|
|
1700
|
+
return [...this.events];
|
|
1701
|
+
}
|
|
1702
|
+
subscribe(event, handler) {
|
|
1703
|
+
this.cdp.on(event, handler);
|
|
1704
|
+
this.handlers.push({ event, handler });
|
|
1705
|
+
}
|
|
1706
|
+
matchesUrl(url) {
|
|
1707
|
+
if (!this.matchRegex) {
|
|
1708
|
+
return true;
|
|
1709
|
+
}
|
|
1710
|
+
return this.matchRegex.test(url);
|
|
1711
|
+
}
|
|
1712
|
+
formatPayload(payloadData, opcode) {
|
|
1713
|
+
const data = payloadData ?? "";
|
|
1714
|
+
const maxPayload = this.options.maxPayload ?? 256;
|
|
1715
|
+
if (opcode === 2) {
|
|
1716
|
+
const byteLength = Math.floor(data.length * 3 / 4);
|
|
1717
|
+
return `[binary: ${byteLength} bytes]`;
|
|
1718
|
+
}
|
|
1719
|
+
if (data.length > maxPayload) {
|
|
1720
|
+
return `${data.slice(0, maxPayload)}... [truncated, ${data.length} total]`;
|
|
1721
|
+
}
|
|
1722
|
+
return data;
|
|
1723
|
+
}
|
|
1724
|
+
channelForTraceEvent(eventName) {
|
|
1725
|
+
if (eventName.startsWith("ws.")) return "ws";
|
|
1726
|
+
if (eventName.startsWith("http.")) return "http";
|
|
1727
|
+
if (eventName.startsWith("console.")) return "console";
|
|
1728
|
+
if (eventName.startsWith("permission.")) return "permission";
|
|
1729
|
+
if (eventName.startsWith("media.")) return "media";
|
|
1730
|
+
if (eventName.startsWith("voice.")) return "voice";
|
|
1731
|
+
if (eventName.startsWith("dom.")) return "dom";
|
|
1732
|
+
if (eventName.startsWith("runtime.")) return "runtime";
|
|
1733
|
+
return "session";
|
|
1734
|
+
}
|
|
1735
|
+
async emit(event) {
|
|
1736
|
+
const normalized = normalizeTraceEvent({
|
|
1737
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1738
|
+
sessionId: this.options.sessionId,
|
|
1739
|
+
targetId: this.options.targetId,
|
|
1740
|
+
elapsedMs: event.elapsedMs ?? Date.now() - this.startTime,
|
|
1741
|
+
...event
|
|
1742
|
+
});
|
|
1743
|
+
this.events.push(normalized);
|
|
1744
|
+
await this.options.onEvent?.(normalized);
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1748
|
+
// src/actions/executor.ts
|
|
1749
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
1750
|
+
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
1751
|
+
"wait",
|
|
1752
|
+
"snapshot",
|
|
1753
|
+
"forms",
|
|
1754
|
+
"text",
|
|
1755
|
+
"screenshot"
|
|
1756
|
+
];
|
|
1757
|
+
function readString2(value) {
|
|
1758
|
+
return typeof value === "string" ? value : void 0;
|
|
1759
|
+
}
|
|
1760
|
+
function readStringOr2(value, fallback = "") {
|
|
1761
|
+
return readString2(value) ?? fallback;
|
|
1762
|
+
}
|
|
1763
|
+
function formatConsoleArg2(entry) {
|
|
1764
|
+
return readString2(entry["value"]) ?? readString2(entry["description"]) ?? "";
|
|
1765
|
+
}
|
|
1766
|
+
function loadExistingRecording(manifestPath) {
|
|
1767
|
+
try {
|
|
1768
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1769
|
+
if (raw.version === 1) {
|
|
1770
|
+
const legacy = raw;
|
|
1771
|
+
return {
|
|
1772
|
+
frames: Array.isArray(legacy.frames) ? legacy.frames : [],
|
|
1773
|
+
traceEvents: [],
|
|
1774
|
+
recordedAt: legacy.recordedAt,
|
|
1775
|
+
startUrl: legacy.startUrl
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
const artifact = canonicalizeRecordingArtifact(raw);
|
|
1779
|
+
const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
|
|
1780
|
+
const frames = artifact.actions.map((action, index) => {
|
|
1781
|
+
const screenshot = screenshotsByAction.get(action.id);
|
|
1782
|
+
return {
|
|
1783
|
+
seq: index + 1,
|
|
1784
|
+
timestamp: Date.parse(action.ts),
|
|
1785
|
+
action: action.action,
|
|
1786
|
+
selector: action.selector,
|
|
1787
|
+
selectorUsed: action.selectorUsed,
|
|
1788
|
+
value: action.value,
|
|
1789
|
+
url: action.url,
|
|
1790
|
+
coordinates: action.coordinates,
|
|
1791
|
+
boundingBox: action.boundingBox,
|
|
1792
|
+
success: action.success,
|
|
1793
|
+
durationMs: action.durationMs,
|
|
1794
|
+
error: action.error,
|
|
1795
|
+
screenshot: screenshot?.file ?? "",
|
|
1796
|
+
pageUrl: action.pageUrl,
|
|
1797
|
+
pageTitle: action.pageTitle,
|
|
1798
|
+
stepIndex: action.stepIndex,
|
|
1799
|
+
actionId: action.id
|
|
1800
|
+
};
|
|
1801
|
+
});
|
|
1802
|
+
return {
|
|
1803
|
+
frames,
|
|
1804
|
+
traceEvents: artifact.trace.events,
|
|
1805
|
+
recordedAt: artifact.recordedAt,
|
|
1806
|
+
startUrl: artifact.session.startUrl
|
|
1807
|
+
};
|
|
1808
|
+
} catch {
|
|
1809
|
+
return { frames: [], traceEvents: [] };
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1070
1812
|
function classifyFailure(error) {
|
|
1071
1813
|
if (error instanceof ElementNotFoundError) {
|
|
1072
1814
|
return { reason: "missing" };
|
|
@@ -1147,6 +1889,9 @@ var BatchExecutor = class {
|
|
|
1147
1889
|
const results = [];
|
|
1148
1890
|
const startTime = Date.now();
|
|
1149
1891
|
const recording = options.record ? this.createRecordingContext(options.record) : null;
|
|
1892
|
+
if (steps.some((step) => step.action === "waitForWsMessage")) {
|
|
1893
|
+
await this.ensureTraceHooks();
|
|
1894
|
+
}
|
|
1150
1895
|
const startUrl = recording ? await this.getPageUrlSafe() : "";
|
|
1151
1896
|
let stoppedAtIndex;
|
|
1152
1897
|
for (let i = 0; i < steps.length; i++) {
|
|
@@ -1156,6 +1901,26 @@ var BatchExecutor = class {
|
|
|
1156
1901
|
const retryDelay = step.retryDelay ?? 500;
|
|
1157
1902
|
let lastError;
|
|
1158
1903
|
let succeeded = false;
|
|
1904
|
+
if (recording) {
|
|
1905
|
+
recording.traceEvents.push(
|
|
1906
|
+
normalizeTraceEvent({
|
|
1907
|
+
traceId: createTraceId("action"),
|
|
1908
|
+
elapsedMs: Date.now() - startTime,
|
|
1909
|
+
channel: "action",
|
|
1910
|
+
event: "action.started",
|
|
1911
|
+
summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
|
|
1912
|
+
data: {
|
|
1913
|
+
action: step.action,
|
|
1914
|
+
selector: step.selector ?? null,
|
|
1915
|
+
url: step.url ?? null
|
|
1916
|
+
},
|
|
1917
|
+
actionId: `action-${i + 1}`,
|
|
1918
|
+
stepIndex: i,
|
|
1919
|
+
selector: step.selector,
|
|
1920
|
+
url: step.url
|
|
1921
|
+
})
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1159
1924
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1160
1925
|
if (attempt > 0) {
|
|
1161
1926
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
@@ -1179,6 +1944,28 @@ var BatchExecutor = class {
|
|
|
1179
1944
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1180
1945
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1181
1946
|
}
|
|
1947
|
+
if (recording) {
|
|
1948
|
+
recording.traceEvents.push(
|
|
1949
|
+
normalizeTraceEvent({
|
|
1950
|
+
traceId: createTraceId("action"),
|
|
1951
|
+
elapsedMs: Date.now() - startTime,
|
|
1952
|
+
channel: "action",
|
|
1953
|
+
event: "action.succeeded",
|
|
1954
|
+
summary: `${step.action} succeeded`,
|
|
1955
|
+
data: {
|
|
1956
|
+
action: step.action,
|
|
1957
|
+
selector: step.selector ?? null,
|
|
1958
|
+
selectorUsed: result.selectorUsed ?? null,
|
|
1959
|
+
durationMs: Date.now() - stepStart
|
|
1960
|
+
},
|
|
1961
|
+
actionId: `action-${i + 1}`,
|
|
1962
|
+
stepIndex: i,
|
|
1963
|
+
selector: step.selector,
|
|
1964
|
+
selectorUsed: result.selectorUsed,
|
|
1965
|
+
url: step.url
|
|
1966
|
+
})
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1182
1969
|
results.push(stepResult);
|
|
1183
1970
|
succeeded = true;
|
|
1184
1971
|
break;
|
|
@@ -1216,6 +2003,28 @@ var BatchExecutor = class {
|
|
|
1216
2003
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1217
2004
|
await this.captureRecordingFrame(step, failedResult, recording);
|
|
1218
2005
|
}
|
|
2006
|
+
if (recording) {
|
|
2007
|
+
recording.traceEvents.push(
|
|
2008
|
+
normalizeTraceEvent({
|
|
2009
|
+
traceId: createTraceId("action"),
|
|
2010
|
+
elapsedMs: Date.now() - startTime,
|
|
2011
|
+
channel: "action",
|
|
2012
|
+
event: "action.failed",
|
|
2013
|
+
severity: "error",
|
|
2014
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
2015
|
+
data: {
|
|
2016
|
+
action: step.action,
|
|
2017
|
+
selector: step.selector ?? null,
|
|
2018
|
+
error: errorMessage,
|
|
2019
|
+
reason
|
|
2020
|
+
},
|
|
2021
|
+
actionId: `action-${i + 1}`,
|
|
2022
|
+
stepIndex: i,
|
|
2023
|
+
selector: step.selector,
|
|
2024
|
+
url: step.url
|
|
2025
|
+
})
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
1219
2028
|
results.push(failedResult);
|
|
1220
2029
|
if (onFail === "stop" && !step.optional) {
|
|
1221
2030
|
stoppedAtIndex = i;
|
|
@@ -1231,7 +2040,8 @@ var BatchExecutor = class {
|
|
|
1231
2040
|
recording,
|
|
1232
2041
|
startTime,
|
|
1233
2042
|
startUrl,
|
|
1234
|
-
allSuccess
|
|
2043
|
+
allSuccess,
|
|
2044
|
+
steps
|
|
1235
2045
|
);
|
|
1236
2046
|
}
|
|
1237
2047
|
return {
|
|
@@ -1246,20 +2056,14 @@ var BatchExecutor = class {
|
|
|
1246
2056
|
const baseDir = record.outputDir ?? join(process.cwd(), ".browser-pilot");
|
|
1247
2057
|
const screenshotDir = join(baseDir, "screenshots");
|
|
1248
2058
|
const manifestPath = join(baseDir, "recording.json");
|
|
1249
|
-
|
|
1250
|
-
try {
|
|
1251
|
-
const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1252
|
-
if (existing.frames && Array.isArray(existing.frames)) {
|
|
1253
|
-
existingFrames = existing.frames;
|
|
1254
|
-
}
|
|
1255
|
-
} catch {
|
|
1256
|
-
}
|
|
2059
|
+
const existing = loadExistingRecording(manifestPath);
|
|
1257
2060
|
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
1258
2061
|
return {
|
|
1259
2062
|
baseDir,
|
|
1260
2063
|
screenshotDir,
|
|
1261
2064
|
sessionId: record.sessionId ?? this.page.targetId,
|
|
1262
|
-
frames:
|
|
2065
|
+
frames: existing.frames,
|
|
2066
|
+
traceEvents: existing.traceEvents,
|
|
1263
2067
|
format: record.format ?? "webp",
|
|
1264
2068
|
quality: Math.max(0, Math.min(100, record.quality ?? 40)),
|
|
1265
2069
|
highlights: record.highlights !== false,
|
|
@@ -1315,6 +2119,7 @@ var BatchExecutor = class {
|
|
|
1315
2119
|
timestamp: ts,
|
|
1316
2120
|
action: stepResult.action,
|
|
1317
2121
|
selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
|
|
2122
|
+
selectorUsed: stepResult.selectorUsed,
|
|
1318
2123
|
value: redactValueForRecording(
|
|
1319
2124
|
typeof step.value === "string" ? step.value : void 0,
|
|
1320
2125
|
targetMetadata
|
|
@@ -1327,7 +2132,9 @@ var BatchExecutor = class {
|
|
|
1327
2132
|
error: stepResult.error,
|
|
1328
2133
|
screenshot: filename,
|
|
1329
2134
|
pageUrl,
|
|
1330
|
-
pageTitle
|
|
2135
|
+
pageTitle,
|
|
2136
|
+
stepIndex: stepResult.index,
|
|
2137
|
+
actionId: `action-${stepResult.index + 1}`
|
|
1331
2138
|
});
|
|
1332
2139
|
} catch {
|
|
1333
2140
|
} finally {
|
|
@@ -1339,45 +2146,31 @@ var BatchExecutor = class {
|
|
|
1339
2146
|
/**
|
|
1340
2147
|
* Write recording manifest to disk
|
|
1341
2148
|
*/
|
|
1342
|
-
async writeRecordingManifest(recording, startTime, startUrl, success) {
|
|
2149
|
+
async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
|
|
1343
2150
|
let endUrl = startUrl;
|
|
1344
|
-
let viewport = { width: 1280, height: 720 };
|
|
1345
2151
|
try {
|
|
1346
2152
|
endUrl = await this.page.url();
|
|
1347
2153
|
} catch {
|
|
1348
2154
|
}
|
|
1349
|
-
try {
|
|
1350
|
-
const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
|
|
1351
|
-
viewport = {
|
|
1352
|
-
width: metrics.cssVisualViewport.clientWidth,
|
|
1353
|
-
height: metrics.cssVisualViewport.clientHeight
|
|
1354
|
-
};
|
|
1355
|
-
} catch {
|
|
1356
|
-
}
|
|
1357
2155
|
const manifestPath = join(recording.baseDir, "recording.json");
|
|
1358
2156
|
let recordedAt = new Date(startTime).toISOString();
|
|
1359
2157
|
let originalStartUrl = startUrl;
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
} catch {
|
|
1365
|
-
}
|
|
1366
|
-
const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
|
|
1367
|
-
const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
|
|
1368
|
-
const manifest = {
|
|
1369
|
-
version: 1,
|
|
2158
|
+
const existing = loadExistingRecording(manifestPath);
|
|
2159
|
+
if (existing.recordedAt) recordedAt = existing.recordedAt;
|
|
2160
|
+
if (existing.startUrl) originalStartUrl = existing.startUrl;
|
|
2161
|
+
const manifest = createRecordingManifest({
|
|
1370
2162
|
recordedAt,
|
|
1371
2163
|
sessionId: recording.sessionId,
|
|
1372
2164
|
startUrl: originalStartUrl,
|
|
1373
2165
|
endUrl,
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
success,
|
|
1379
|
-
|
|
1380
|
-
|
|
2166
|
+
targetId: this.page.targetId,
|
|
2167
|
+
steps,
|
|
2168
|
+
frames: recording.frames,
|
|
2169
|
+
traceEvents: recording.traceEvents,
|
|
2170
|
+
notes: success ? [] : ["Replay ended with at least one failed action."],
|
|
2171
|
+
recordingManifest: "recording.json",
|
|
2172
|
+
screenshotDir: "screenshots/"
|
|
2173
|
+
});
|
|
1381
2174
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1382
2175
|
return manifestPath;
|
|
1383
2176
|
}
|
|
@@ -1660,6 +2453,39 @@ var BatchExecutor = class {
|
|
|
1660
2453
|
}
|
|
1661
2454
|
return { selectorUsed: usedSelector, value: actual };
|
|
1662
2455
|
}
|
|
2456
|
+
case "waitForWsMessage": {
|
|
2457
|
+
if (typeof step.match !== "string") {
|
|
2458
|
+
throw new Error("waitForWsMessage requires match");
|
|
2459
|
+
}
|
|
2460
|
+
const message = await this.waitForWsMessage(step.match, step.where, timeout);
|
|
2461
|
+
return { value: message };
|
|
2462
|
+
}
|
|
2463
|
+
case "assertNoConsoleErrors": {
|
|
2464
|
+
await this.assertNoConsoleErrors(step.windowMs ?? timeout);
|
|
2465
|
+
return {};
|
|
2466
|
+
}
|
|
2467
|
+
case "assertTextChanged": {
|
|
2468
|
+
const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
|
|
2469
|
+
if (typeof step.to !== "string") {
|
|
2470
|
+
throw new Error("assertTextChanged requires to");
|
|
2471
|
+
}
|
|
2472
|
+
const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
|
|
2473
|
+
return { selectorUsed: selector, text };
|
|
2474
|
+
}
|
|
2475
|
+
case "assertPermission": {
|
|
2476
|
+
if (!step.name || !step.state) {
|
|
2477
|
+
throw new Error("assertPermission requires name and state");
|
|
2478
|
+
}
|
|
2479
|
+
const permission = await this.assertPermission(step.name, step.state);
|
|
2480
|
+
return { value: permission };
|
|
2481
|
+
}
|
|
2482
|
+
case "assertMediaTrackLive": {
|
|
2483
|
+
if (!step.kind) {
|
|
2484
|
+
throw new Error("assertMediaTrackLive requires kind");
|
|
2485
|
+
}
|
|
2486
|
+
const media = await this.assertMediaTrackLive(step.kind);
|
|
2487
|
+
return { value: media };
|
|
2488
|
+
}
|
|
1663
2489
|
default: {
|
|
1664
2490
|
const action = step.action;
|
|
1665
2491
|
const aliases = {
|
|
@@ -1713,7 +2539,7 @@ var BatchExecutor = class {
|
|
|
1713
2539
|
};
|
|
1714
2540
|
const suggestion = aliases[action.toLowerCase()];
|
|
1715
2541
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
1716
|
-
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";
|
|
2542
|
+
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";
|
|
1717
2543
|
throw new Error(`Unknown action "${action}".${hint}
|
|
1718
2544
|
|
|
1719
2545
|
Valid actions: ${valid}`);
|
|
@@ -1729,6 +2555,237 @@ Valid actions: ${valid}`);
|
|
|
1729
2555
|
if (matched) return matched;
|
|
1730
2556
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
1731
2557
|
}
|
|
2558
|
+
async ensureTraceHooks() {
|
|
2559
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2560
|
+
await this.page.cdpClient.send("Page.enable");
|
|
2561
|
+
await this.page.cdpClient.send("Network.enable");
|
|
2562
|
+
try {
|
|
2563
|
+
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
2564
|
+
} catch {
|
|
2565
|
+
}
|
|
2566
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2567
|
+
source: TRACE_SCRIPT
|
|
2568
|
+
});
|
|
2569
|
+
await this.page.cdpClient.send("Runtime.evaluate", {
|
|
2570
|
+
expression: TRACE_SCRIPT,
|
|
2571
|
+
awaitPromise: false
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
async waitForWsMessage(match, where, timeout) {
|
|
2575
|
+
await this.ensureTraceHooks();
|
|
2576
|
+
const regex = globToRegex(match);
|
|
2577
|
+
const wsUrls = /* @__PURE__ */ new Map();
|
|
2578
|
+
const recentMatch = await this.findRecentWsMessage(regex, where);
|
|
2579
|
+
if (recentMatch) {
|
|
2580
|
+
return recentMatch;
|
|
2581
|
+
}
|
|
2582
|
+
return new Promise((resolve, reject) => {
|
|
2583
|
+
const cleanup = () => {
|
|
2584
|
+
this.page.cdpClient.off("Network.webSocketCreated", onCreated);
|
|
2585
|
+
this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
|
|
2586
|
+
this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
|
|
2587
|
+
clearTimeout(timer);
|
|
2588
|
+
};
|
|
2589
|
+
const onCreated = (params) => {
|
|
2590
|
+
wsUrls.set(readStringOr2(params["requestId"]), readStringOr2(params["url"]));
|
|
2591
|
+
};
|
|
2592
|
+
const onFrame = (params) => {
|
|
2593
|
+
const requestId = readStringOr2(params["requestId"]);
|
|
2594
|
+
const response = params["response"] ?? {};
|
|
2595
|
+
const payload = response.payloadData ?? "";
|
|
2596
|
+
const url = wsUrls.get(requestId) ?? "";
|
|
2597
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
cleanup();
|
|
2604
|
+
resolve({ requestId, url, payload });
|
|
2605
|
+
};
|
|
2606
|
+
const onBinding = (params) => {
|
|
2607
|
+
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
try {
|
|
2611
|
+
const parsed = JSON.parse(readStringOr2(params["payload"]));
|
|
2612
|
+
if (parsed.event !== "ws.frame.received") {
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
const data = parsed.data ?? {};
|
|
2616
|
+
const payload = readStringOr2(data["payload"]);
|
|
2617
|
+
const url = readStringOr2(data["url"]);
|
|
2618
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
cleanup();
|
|
2625
|
+
resolve({
|
|
2626
|
+
requestId: readStringOr2(data["connectionId"]),
|
|
2627
|
+
url,
|
|
2628
|
+
payload
|
|
2629
|
+
});
|
|
2630
|
+
} catch {
|
|
2631
|
+
}
|
|
2632
|
+
};
|
|
2633
|
+
const timer = setTimeout(() => {
|
|
2634
|
+
cleanup();
|
|
2635
|
+
reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
|
|
2636
|
+
}, timeout);
|
|
2637
|
+
this.page.cdpClient.on("Network.webSocketCreated", onCreated);
|
|
2638
|
+
this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
|
|
2639
|
+
this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
payloadMatchesWhere(payload, where) {
|
|
2643
|
+
try {
|
|
2644
|
+
const parsed = JSON.parse(payload);
|
|
2645
|
+
return Object.entries(where).every(([key, expected]) => {
|
|
2646
|
+
const actual = key.split(".").reduce((current, part) => {
|
|
2647
|
+
if (!current || typeof current !== "object") {
|
|
2648
|
+
return void 0;
|
|
2649
|
+
}
|
|
2650
|
+
return current[part];
|
|
2651
|
+
}, parsed);
|
|
2652
|
+
return actual === expected;
|
|
2653
|
+
});
|
|
2654
|
+
} catch {
|
|
2655
|
+
return false;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
async findRecentWsMessage(regex, where) {
|
|
2659
|
+
const recent = await this.page.evaluate(
|
|
2660
|
+
"(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
|
|
2661
|
+
);
|
|
2662
|
+
if (!Array.isArray(recent)) {
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
for (let i = recent.length - 1; i >= 0; i--) {
|
|
2666
|
+
const entry = recent[i];
|
|
2667
|
+
if (!entry || typeof entry !== "object") {
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
const record = entry;
|
|
2671
|
+
const event = readStringOr2(record["event"]);
|
|
2672
|
+
if (event !== "ws.frame.received") {
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
const data = record["data"] ?? {};
|
|
2676
|
+
const payload = readStringOr2(data["payload"]);
|
|
2677
|
+
const url = readStringOr2(data["url"]);
|
|
2678
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2682
|
+
continue;
|
|
2683
|
+
}
|
|
2684
|
+
return {
|
|
2685
|
+
requestId: readStringOr2(data["connectionId"]),
|
|
2686
|
+
url,
|
|
2687
|
+
payload
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
return null;
|
|
2691
|
+
}
|
|
2692
|
+
async assertNoConsoleErrors(windowMs) {
|
|
2693
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2694
|
+
return new Promise((resolve, reject) => {
|
|
2695
|
+
const errors = [];
|
|
2696
|
+
const cleanup = () => {
|
|
2697
|
+
this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
|
|
2698
|
+
this.page.cdpClient.off("Runtime.exceptionThrown", onException);
|
|
2699
|
+
clearTimeout(timer);
|
|
2700
|
+
};
|
|
2701
|
+
const onConsole = (params) => {
|
|
2702
|
+
if (params["type"] !== "error") {
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2706
|
+
errors.push(args.map(formatConsoleArg2).filter(Boolean).join(" "));
|
|
2707
|
+
};
|
|
2708
|
+
const onException = (params) => {
|
|
2709
|
+
const details = params["exceptionDetails"] ?? {};
|
|
2710
|
+
errors.push(readString2(details["text"]) ?? "Runtime exception");
|
|
2711
|
+
};
|
|
2712
|
+
const timer = setTimeout(() => {
|
|
2713
|
+
cleanup();
|
|
2714
|
+
if (errors.length > 0) {
|
|
2715
|
+
reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
resolve();
|
|
2719
|
+
}, windowMs);
|
|
2720
|
+
this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
|
|
2721
|
+
this.page.cdpClient.on("Runtime.exceptionThrown", onException);
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
async assertTextChanged(selector, from, to, timeout) {
|
|
2725
|
+
const initialText = from ?? await this.page.text(selector);
|
|
2726
|
+
const deadline = Date.now() + timeout;
|
|
2727
|
+
while (Date.now() < deadline) {
|
|
2728
|
+
const text = await this.page.text(selector);
|
|
2729
|
+
if (text !== initialText && text.includes(to)) {
|
|
2730
|
+
return text;
|
|
2731
|
+
}
|
|
2732
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2733
|
+
}
|
|
2734
|
+
throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
|
|
2735
|
+
}
|
|
2736
|
+
async assertPermission(name, state) {
|
|
2737
|
+
const result = await this.page.evaluate(
|
|
2738
|
+
`(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
|
|
2739
|
+
);
|
|
2740
|
+
if (!result || typeof result !== "object" || result.state !== state) {
|
|
2741
|
+
throw new Error(`Permission ${name} is not ${state}`);
|
|
2742
|
+
}
|
|
2743
|
+
return result;
|
|
2744
|
+
}
|
|
2745
|
+
async assertMediaTrackLive(kind) {
|
|
2746
|
+
const result = await this.page.evaluate(
|
|
2747
|
+
`(() => {
|
|
2748
|
+
const requestedKind = ${JSON.stringify(kind)};
|
|
2749
|
+
const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
|
|
2750
|
+
const tracks = [];
|
|
2751
|
+
if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
|
|
2752
|
+
tracks.push(...el.srcObject.getTracks());
|
|
2753
|
+
}
|
|
2754
|
+
return {
|
|
2755
|
+
tag: el.tagName.toLowerCase(),
|
|
2756
|
+
paused: !!el.paused,
|
|
2757
|
+
tracks: tracks.map((track) => ({
|
|
2758
|
+
kind: track.kind,
|
|
2759
|
+
readyState: track.readyState,
|
|
2760
|
+
enabled: track.enabled,
|
|
2761
|
+
label: track.label,
|
|
2762
|
+
})),
|
|
2763
|
+
};
|
|
2764
|
+
});
|
|
2765
|
+
|
|
2766
|
+
const globalTracks =
|
|
2767
|
+
window.__bpStream && typeof window.__bpStream.getTracks === 'function'
|
|
2768
|
+
? window.__bpStream.getTracks().map((track) => ({
|
|
2769
|
+
kind: track.kind,
|
|
2770
|
+
readyState: track.readyState,
|
|
2771
|
+
enabled: track.enabled,
|
|
2772
|
+
label: track.label,
|
|
2773
|
+
}))
|
|
2774
|
+
: [];
|
|
2775
|
+
|
|
2776
|
+
const liveTracks = mediaElements
|
|
2777
|
+
.flatMap((entry) => entry.tracks)
|
|
2778
|
+
.concat(globalTracks)
|
|
2779
|
+
.filter((track) => track.kind === requestedKind && track.readyState === 'live');
|
|
2780
|
+
|
|
2781
|
+
return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
|
|
2782
|
+
})()`
|
|
2783
|
+
);
|
|
2784
|
+
if (!result || typeof result !== "object" || !result.live) {
|
|
2785
|
+
throw new Error(`No live ${kind} media track detected`);
|
|
2786
|
+
}
|
|
2787
|
+
return result;
|
|
2788
|
+
}
|
|
1732
2789
|
};
|
|
1733
2790
|
function addBatchToPage(page) {
|
|
1734
2791
|
const executor = new BatchExecutor(page);
|
|
@@ -1859,7 +2916,7 @@ var ACTION_RULES = {
|
|
|
1859
2916
|
value: { type: "string|string[]" },
|
|
1860
2917
|
trigger: { type: "string|string[]" },
|
|
1861
2918
|
option: { type: "string|string[]" },
|
|
1862
|
-
match: { type: "string"
|
|
2919
|
+
match: { type: "string" }
|
|
1863
2920
|
}
|
|
1864
2921
|
},
|
|
1865
2922
|
check: {
|
|
@@ -1990,6 +3047,38 @@ var ACTION_RULES = {
|
|
|
1990
3047
|
expect: { type: "string" },
|
|
1991
3048
|
value: { type: "string" }
|
|
1992
3049
|
}
|
|
3050
|
+
},
|
|
3051
|
+
waitForWsMessage: {
|
|
3052
|
+
required: { match: { type: "string" } },
|
|
3053
|
+
optional: {
|
|
3054
|
+
where: { type: "object" }
|
|
3055
|
+
}
|
|
3056
|
+
},
|
|
3057
|
+
assertNoConsoleErrors: {
|
|
3058
|
+
required: {},
|
|
3059
|
+
optional: {
|
|
3060
|
+
windowMs: { type: "number" }
|
|
3061
|
+
}
|
|
3062
|
+
},
|
|
3063
|
+
assertTextChanged: {
|
|
3064
|
+
required: { to: { type: "string" } },
|
|
3065
|
+
optional: {
|
|
3066
|
+
selector: { type: "string|string[]" },
|
|
3067
|
+
from: { type: "string" }
|
|
3068
|
+
}
|
|
3069
|
+
},
|
|
3070
|
+
assertPermission: {
|
|
3071
|
+
required: {
|
|
3072
|
+
name: { type: "string" },
|
|
3073
|
+
state: { type: "string" }
|
|
3074
|
+
},
|
|
3075
|
+
optional: {}
|
|
3076
|
+
},
|
|
3077
|
+
assertMediaTrackLive: {
|
|
3078
|
+
required: {
|
|
3079
|
+
kind: { type: "string", enum: ["audio", "video"] }
|
|
3080
|
+
},
|
|
3081
|
+
optional: {}
|
|
1993
3082
|
}
|
|
1994
3083
|
};
|
|
1995
3084
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -2013,6 +3102,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2013
3102
|
"trigger",
|
|
2014
3103
|
"option",
|
|
2015
3104
|
"match",
|
|
3105
|
+
"where",
|
|
2016
3106
|
"x",
|
|
2017
3107
|
"y",
|
|
2018
3108
|
"direction",
|
|
@@ -2022,7 +3112,13 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2022
3112
|
"fullPage",
|
|
2023
3113
|
"expect",
|
|
2024
3114
|
"retry",
|
|
2025
|
-
"retryDelay"
|
|
3115
|
+
"retryDelay",
|
|
3116
|
+
"from",
|
|
3117
|
+
"to",
|
|
3118
|
+
"name",
|
|
3119
|
+
"state",
|
|
3120
|
+
"kind",
|
|
3121
|
+
"windowMs"
|
|
2026
3122
|
]);
|
|
2027
3123
|
function resolveAction(name) {
|
|
2028
3124
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -2095,6 +3191,11 @@ function checkFieldType(value, rule) {
|
|
|
2095
3191
|
return `expected boolean or "auto", got ${typeof value}`;
|
|
2096
3192
|
}
|
|
2097
3193
|
return null;
|
|
3194
|
+
case "object":
|
|
3195
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3196
|
+
return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
|
|
3197
|
+
}
|
|
3198
|
+
return null;
|
|
2098
3199
|
default: {
|
|
2099
3200
|
const _exhaustive = rule.type;
|
|
2100
3201
|
return `unknown type: ${_exhaustive}`;
|
|
@@ -2389,6 +3490,10 @@ async function grantAudioPermissions(cdp, origin) {
|
|
|
2389
3490
|
await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2390
3491
|
source: PERMISSIONS_OVERRIDE_SCRIPT
|
|
2391
3492
|
});
|
|
3493
|
+
await cdp.send("Runtime.evaluate", {
|
|
3494
|
+
expression: PERMISSIONS_OVERRIDE_SCRIPT,
|
|
3495
|
+
awaitPromise: false
|
|
3496
|
+
});
|
|
2392
3497
|
}
|
|
2393
3498
|
var PERMISSIONS_OVERRIDE_SCRIPT = `
|
|
2394
3499
|
(function() {
|
|
@@ -3347,7 +4452,7 @@ var AudioOutput = class {
|
|
|
3347
4452
|
awaitPromise: false
|
|
3348
4453
|
});
|
|
3349
4454
|
this.capturing = false;
|
|
3350
|
-
await
|
|
4455
|
+
await sleep2(250);
|
|
3351
4456
|
return this.mergeChunks();
|
|
3352
4457
|
}
|
|
3353
4458
|
/**
|
|
@@ -3563,7 +4668,7 @@ function emptyCaptureResult() {
|
|
|
3563
4668
|
chunkCount: 0
|
|
3564
4669
|
};
|
|
3565
4670
|
}
|
|
3566
|
-
function
|
|
4671
|
+
function sleep2(ms) {
|
|
3567
4672
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3568
4673
|
}
|
|
3569
4674
|
|
|
@@ -4104,7 +5209,7 @@ async function isElementAttached(cdp, selector, contextId) {
|
|
|
4104
5209
|
const result = await cdp.send("Runtime.evaluate", params);
|
|
4105
5210
|
return result.result.value === true;
|
|
4106
5211
|
}
|
|
4107
|
-
function
|
|
5212
|
+
function sleep3(ms) {
|
|
4108
5213
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4109
5214
|
}
|
|
4110
5215
|
async function isPageStatic(cdp, windowMs = 200, contextId) {
|
|
@@ -4172,7 +5277,7 @@ async function waitForAnyElement(cdp, selectors, options = {}) {
|
|
|
4172
5277
|
}
|
|
4173
5278
|
}
|
|
4174
5279
|
while (Date.now() < deadline) {
|
|
4175
|
-
await
|
|
5280
|
+
await sleep3(pollInterval);
|
|
4176
5281
|
for (const selector of selectors) {
|
|
4177
5282
|
if (await checkSelector(selector)) {
|
|
4178
5283
|
return { success: true, selector, waitedMs: Date.now() - startTime };
|
|
@@ -4233,7 +5338,7 @@ async function waitForNavigation(cdp, options = {}) {
|
|
|
4233
5338
|
cleanup.push(() => cdp.off("Page.lifecycleEvent", onLifecycle));
|
|
4234
5339
|
const pollUrl = async () => {
|
|
4235
5340
|
while (!resolved && Date.now() < startTime + timeout) {
|
|
4236
|
-
await
|
|
5341
|
+
await sleep3(100);
|
|
4237
5342
|
if (resolved) return;
|
|
4238
5343
|
try {
|
|
4239
5344
|
const currentUrl = await getCurrentUrl(cdp);
|
|
@@ -4838,7 +5943,7 @@ var Page = class {
|
|
|
4838
5943
|
} catch (e) {
|
|
4839
5944
|
if (options.optional) return false;
|
|
4840
5945
|
if (e instanceof ActionabilityError && e.failureType === "hitTarget" && attempt < HIT_TARGET_RETRIES - 1) {
|
|
4841
|
-
await
|
|
5946
|
+
await sleep4(HIT_TARGET_DELAY);
|
|
4842
5947
|
await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId: element.nodeId });
|
|
4843
5948
|
continue;
|
|
4844
5949
|
}
|
|
@@ -5013,7 +6118,7 @@ var Page = class {
|
|
|
5013
6118
|
await this.cdp.send("Input.insertText", { text: char });
|
|
5014
6119
|
}
|
|
5015
6120
|
if (delay > 0) {
|
|
5016
|
-
await
|
|
6121
|
+
await sleep4(delay);
|
|
5017
6122
|
}
|
|
5018
6123
|
}
|
|
5019
6124
|
if (options.blur) {
|
|
@@ -5109,7 +6214,7 @@ var Page = class {
|
|
|
5109
6214
|
state: "visible",
|
|
5110
6215
|
timeout: 500,
|
|
5111
6216
|
contextId: this.currentFrameContextId ?? void 0
|
|
5112
|
-
}).catch(() =>
|
|
6217
|
+
}).catch(() => sleep4(100));
|
|
5113
6218
|
const optionHandle = await this.evaluateInFrame(
|
|
5114
6219
|
`(() => {
|
|
5115
6220
|
const selectors = ${JSON.stringify(optionSelectors)};
|
|
@@ -5326,7 +6431,7 @@ var Page = class {
|
|
|
5326
6431
|
() => "navigation"
|
|
5327
6432
|
),
|
|
5328
6433
|
this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
|
|
5329
|
-
|
|
6434
|
+
sleep4(1500).then(() => "timeout")
|
|
5330
6435
|
]);
|
|
5331
6436
|
}
|
|
5332
6437
|
return true;
|
|
@@ -5346,7 +6451,7 @@ var Page = class {
|
|
|
5346
6451
|
(success) => success ? "nav" : null
|
|
5347
6452
|
),
|
|
5348
6453
|
this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
|
|
5349
|
-
|
|
6454
|
+
sleep4(1500).then(() => "timeout")
|
|
5350
6455
|
]);
|
|
5351
6456
|
if (navigationDetected === "nav") {
|
|
5352
6457
|
return true;
|
|
@@ -5360,7 +6465,7 @@ var Page = class {
|
|
|
5360
6465
|
if (shouldWait === true) {
|
|
5361
6466
|
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
5362
6467
|
} else if (shouldWait === "auto") {
|
|
5363
|
-
await
|
|
6468
|
+
await sleep4(100);
|
|
5364
6469
|
}
|
|
5365
6470
|
}
|
|
5366
6471
|
return true;
|
|
@@ -6757,7 +7862,7 @@ var Page = class {
|
|
|
6757
7862
|
try {
|
|
6758
7863
|
await Promise.race([
|
|
6759
7864
|
this.dialogHandler(dialog),
|
|
6760
|
-
|
|
7865
|
+
sleep4(DIALOG_TIMEOUT).then(() => {
|
|
6761
7866
|
console.warn("[browser-pilot] Dialog handler timed out after 5s, auto-dismissing");
|
|
6762
7867
|
return dialog.dismiss();
|
|
6763
7868
|
})
|
|
@@ -6882,7 +7987,7 @@ var Page = class {
|
|
|
6882
7987
|
if (attempt < retries) {
|
|
6883
7988
|
this.rootNodeId = null;
|
|
6884
7989
|
this.currentFrameContextId = null;
|
|
6885
|
-
await
|
|
7990
|
+
await sleep4(delay);
|
|
6886
7991
|
continue;
|
|
6887
7992
|
}
|
|
6888
7993
|
}
|
|
@@ -7466,7 +8571,7 @@ var Page = class {
|
|
|
7466
8571
|
const start = Date.now();
|
|
7467
8572
|
await this.audioOutput.start();
|
|
7468
8573
|
if (options.preDelay && options.preDelay > 0) {
|
|
7469
|
-
await
|
|
8574
|
+
await sleep4(options.preDelay);
|
|
7470
8575
|
}
|
|
7471
8576
|
const inputDone = this.audioInput.play(options.input, {
|
|
7472
8577
|
waitForEnd: !!options.sendSelector
|
|
@@ -7534,255 +8639,27 @@ var Page = class {
|
|
|
7534
8639
|
});
|
|
7535
8640
|
}
|
|
7536
8641
|
};
|
|
7537
|
-
function
|
|
8642
|
+
function sleep4(ms) {
|
|
7538
8643
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7539
8644
|
}
|
|
7540
8645
|
|
|
7541
|
-
// src/browser/browser.ts
|
|
7542
|
-
function scoreTarget(t) {
|
|
7543
|
-
let score = 0;
|
|
7544
|
-
if (t.url.startsWith("http://") || t.url.startsWith("https://")) score += 10;
|
|
7545
|
-
if (t.url.startsWith("chrome://")) score -= 20;
|
|
7546
|
-
if (t.url.startsWith("chrome-extension://")) score -= 15;
|
|
7547
|
-
if (t.url.startsWith("devtools://")) score -= 25;
|
|
7548
|
-
if (t.url === "about:blank") score -= 5;
|
|
7549
|
-
if (!t.attached) score += 3;
|
|
7550
|
-
if (t.title && t.title.length > 0) score += 2;
|
|
7551
|
-
return score;
|
|
7552
|
-
}
|
|
7553
|
-
function pickBestTarget(targets) {
|
|
7554
|
-
if (targets.length === 0) return void 0;
|
|
7555
|
-
const sorted = [...targets].sort((a, b) => scoreTarget(b) - scoreTarget(a));
|
|
7556
|
-
return sorted[0].targetId;
|
|
7557
|
-
}
|
|
7558
|
-
var Browser = class _Browser {
|
|
7559
|
-
cdp;
|
|
7560
|
-
providerSession;
|
|
7561
|
-
pages = /* @__PURE__ */ new Map();
|
|
7562
|
-
pageCounter = 0;
|
|
7563
|
-
constructor(cdp, _provider, providerSession, _options) {
|
|
7564
|
-
this.cdp = cdp;
|
|
7565
|
-
this.providerSession = providerSession;
|
|
7566
|
-
}
|
|
7567
|
-
/**
|
|
7568
|
-
* Create a Browser from an existing CDPClient (used by daemon fast-path).
|
|
7569
|
-
* The caller is responsible for the CDP connection lifecycle.
|
|
7570
|
-
*/
|
|
7571
|
-
static fromCDP(cdp, sessionInfo) {
|
|
7572
|
-
const providerSession = {
|
|
7573
|
-
wsUrl: sessionInfo.wsUrl,
|
|
7574
|
-
sessionId: sessionInfo.sessionId,
|
|
7575
|
-
async close() {
|
|
7576
|
-
}
|
|
7577
|
-
};
|
|
7578
|
-
const provider = {
|
|
7579
|
-
name: sessionInfo.provider ?? "daemon",
|
|
7580
|
-
async createSession() {
|
|
7581
|
-
return providerSession;
|
|
7582
|
-
}
|
|
7583
|
-
};
|
|
7584
|
-
return new _Browser(cdp, provider, providerSession, { provider: "generic" });
|
|
7585
|
-
}
|
|
7586
|
-
/**
|
|
7587
|
-
* Connect to a browser instance
|
|
7588
|
-
*/
|
|
7589
|
-
static async connect(options) {
|
|
7590
|
-
const provider = createProvider(options);
|
|
7591
|
-
const session = await provider.createSession(options.session);
|
|
7592
|
-
const cdp = await createCDPClient(session.wsUrl, {
|
|
7593
|
-
debug: options.debug,
|
|
7594
|
-
timeout: options.timeout
|
|
7595
|
-
});
|
|
7596
|
-
return new _Browser(cdp, provider, session, options);
|
|
7597
|
-
}
|
|
7598
|
-
/**
|
|
7599
|
-
* Get or create a page by name.
|
|
7600
|
-
* If no name is provided, returns the first available page or creates a new one.
|
|
7601
|
-
*
|
|
7602
|
-
* Target selection heuristics (when no targetId is specified):
|
|
7603
|
-
* - Prefer http/https URLs over chrome://, devtools://, about:blank
|
|
7604
|
-
* - Prefer unattached targets (not already controlled by another client)
|
|
7605
|
-
* - Filter by targetUrl if provided
|
|
7606
|
-
*/
|
|
7607
|
-
async page(name, options) {
|
|
7608
|
-
const pageName = name ?? "default";
|
|
7609
|
-
const cached = this.pages.get(pageName);
|
|
7610
|
-
if (cached) return cached;
|
|
7611
|
-
const targets = await this.cdp.send(
|
|
7612
|
-
"Target.getTargets",
|
|
7613
|
-
void 0,
|
|
7614
|
-
null
|
|
7615
|
-
);
|
|
7616
|
-
let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
7617
|
-
if (options?.targetUrl) {
|
|
7618
|
-
const urlFilter = options.targetUrl;
|
|
7619
|
-
const filtered = pageTargets.filter((t) => t.url.includes(urlFilter));
|
|
7620
|
-
if (filtered.length > 0) {
|
|
7621
|
-
pageTargets = filtered;
|
|
7622
|
-
} else {
|
|
7623
|
-
console.warn(
|
|
7624
|
-
`[browser-pilot] No targets match URL filter "${urlFilter}", falling back to all page targets`
|
|
7625
|
-
);
|
|
7626
|
-
}
|
|
7627
|
-
}
|
|
7628
|
-
let targetId;
|
|
7629
|
-
if (options?.targetId) {
|
|
7630
|
-
const targetExists = targets.targetInfos.some(
|
|
7631
|
-
(t) => t.type === "page" && t.targetId === options.targetId
|
|
7632
|
-
);
|
|
7633
|
-
if (targetExists) {
|
|
7634
|
-
targetId = options.targetId;
|
|
7635
|
-
} else {
|
|
7636
|
-
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
7637
|
-
targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
|
|
7638
|
-
"Target.createTarget",
|
|
7639
|
-
{
|
|
7640
|
-
url: "about:blank"
|
|
7641
|
-
},
|
|
7642
|
-
null
|
|
7643
|
-
)).targetId;
|
|
7644
|
-
}
|
|
7645
|
-
} else if (pageTargets.length > 0) {
|
|
7646
|
-
targetId = pickBestTarget(pageTargets);
|
|
7647
|
-
} else {
|
|
7648
|
-
const result = await this.cdp.send(
|
|
7649
|
-
"Target.createTarget",
|
|
7650
|
-
{
|
|
7651
|
-
url: "about:blank"
|
|
7652
|
-
},
|
|
7653
|
-
null
|
|
7654
|
-
);
|
|
7655
|
-
targetId = result.targetId;
|
|
7656
|
-
}
|
|
7657
|
-
await this.cdp.attachToTarget(targetId);
|
|
7658
|
-
const page = new Page(this.cdp, targetId);
|
|
7659
|
-
await page.init();
|
|
7660
|
-
const minViewport = options?.minViewport !== void 0 ? options.minViewport : { width: 200, height: 200 };
|
|
7661
|
-
if (minViewport !== false) {
|
|
7662
|
-
try {
|
|
7663
|
-
const viewport = await page.evaluate(
|
|
7664
|
-
"({ w: window.innerWidth, h: window.innerHeight })"
|
|
7665
|
-
);
|
|
7666
|
-
if (viewport.w < minViewport.width || viewport.h < minViewport.height) {
|
|
7667
|
-
console.warn(
|
|
7668
|
-
`[browser-pilot] Attached target has small viewport (${viewport.w}x${viewport.h}). Applying default viewport override (1280x720). Use { minViewport: false } to disable this check.`
|
|
7669
|
-
);
|
|
7670
|
-
await page.setViewport({ width: 1280, height: 720 });
|
|
7671
|
-
}
|
|
7672
|
-
} catch {
|
|
7673
|
-
}
|
|
7674
|
-
}
|
|
7675
|
-
this.pages.set(pageName, page);
|
|
7676
|
-
return page;
|
|
7677
|
-
}
|
|
7678
|
-
/**
|
|
7679
|
-
* Create a new page (tab)
|
|
7680
|
-
*/
|
|
7681
|
-
async newPage(url = "about:blank") {
|
|
7682
|
-
const result = await this.cdp.send(
|
|
7683
|
-
"Target.createTarget",
|
|
7684
|
-
{
|
|
7685
|
-
url
|
|
7686
|
-
},
|
|
7687
|
-
null
|
|
7688
|
-
);
|
|
7689
|
-
await this.cdp.attachToTarget(result.targetId);
|
|
7690
|
-
const page = new Page(this.cdp, result.targetId);
|
|
7691
|
-
await page.init();
|
|
7692
|
-
const name = `page-${++this.pageCounter}`;
|
|
7693
|
-
this.pages.set(name, page);
|
|
7694
|
-
return page;
|
|
7695
|
-
}
|
|
7696
|
-
/**
|
|
7697
|
-
* Close a page by name
|
|
7698
|
-
*/
|
|
7699
|
-
async closePage(name) {
|
|
7700
|
-
const page = this.pages.get(name);
|
|
7701
|
-
if (!page) return;
|
|
7702
|
-
const targetId = page.targetId;
|
|
7703
|
-
await this.cdp.send("Target.closeTarget", { targetId }, null);
|
|
7704
|
-
this.pages.delete(name);
|
|
7705
|
-
const deadline = Date.now() + 5e3;
|
|
7706
|
-
while (Date.now() < deadline) {
|
|
7707
|
-
const { targetInfos } = await this.cdp.send(
|
|
7708
|
-
"Target.getTargets",
|
|
7709
|
-
void 0,
|
|
7710
|
-
null
|
|
7711
|
-
);
|
|
7712
|
-
if (!targetInfos.some((t) => t.targetId === targetId)) return;
|
|
7713
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
7714
|
-
}
|
|
7715
|
-
}
|
|
7716
|
-
/**
|
|
7717
|
-
* List all page targets in the connected browser.
|
|
7718
|
-
*/
|
|
7719
|
-
async listTargets() {
|
|
7720
|
-
const { targetInfos } = await this.cdp.send(
|
|
7721
|
-
"Target.getTargets",
|
|
7722
|
-
void 0,
|
|
7723
|
-
null
|
|
7724
|
-
);
|
|
7725
|
-
return targetInfos.filter((target) => target.type === "page");
|
|
7726
|
-
}
|
|
7727
|
-
/**
|
|
7728
|
-
* Get the WebSocket URL for this browser connection
|
|
7729
|
-
*/
|
|
7730
|
-
get wsUrl() {
|
|
7731
|
-
return this.providerSession.wsUrl;
|
|
7732
|
-
}
|
|
7733
|
-
/**
|
|
7734
|
-
* Get the provider session ID (for resumption)
|
|
7735
|
-
*/
|
|
7736
|
-
get sessionId() {
|
|
7737
|
-
return this.providerSession.sessionId;
|
|
7738
|
-
}
|
|
7739
|
-
/**
|
|
7740
|
-
* Get provider metadata
|
|
7741
|
-
*/
|
|
7742
|
-
get metadata() {
|
|
7743
|
-
return this.providerSession.metadata;
|
|
7744
|
-
}
|
|
7745
|
-
/**
|
|
7746
|
-
* Check if connected
|
|
7747
|
-
*/
|
|
7748
|
-
get isConnected() {
|
|
7749
|
-
return this.cdp.isConnected;
|
|
7750
|
-
}
|
|
7751
|
-
/**
|
|
7752
|
-
* Disconnect from the browser (keeps provider session alive for reconnection)
|
|
7753
|
-
*/
|
|
7754
|
-
async disconnect() {
|
|
7755
|
-
this.pages.clear();
|
|
7756
|
-
await this.cdp.close();
|
|
7757
|
-
}
|
|
7758
|
-
/**
|
|
7759
|
-
* Close the browser session completely
|
|
7760
|
-
*/
|
|
7761
|
-
async close() {
|
|
7762
|
-
this.pages.clear();
|
|
7763
|
-
await this.cdp.close();
|
|
7764
|
-
await this.providerSession.close();
|
|
7765
|
-
}
|
|
7766
|
-
/**
|
|
7767
|
-
* Get the underlying CDP client (for advanced usage)
|
|
7768
|
-
*/
|
|
7769
|
-
get cdpClient() {
|
|
7770
|
-
return this.cdp;
|
|
7771
|
-
}
|
|
7772
|
-
};
|
|
7773
|
-
function connect(options) {
|
|
7774
|
-
return Browser.connect(options);
|
|
7775
|
-
}
|
|
7776
|
-
|
|
7777
8646
|
export {
|
|
7778
8647
|
pcmToWav,
|
|
7779
8648
|
SENSITIVE_AUTOCOMPLETE_TOKENS,
|
|
7780
8649
|
redactValueForRecording,
|
|
7781
8650
|
fuzzyMatchElements,
|
|
8651
|
+
buildTraceSummary,
|
|
8652
|
+
buildTraceSummaries,
|
|
8653
|
+
createRecordingManifest,
|
|
8654
|
+
canonicalizeRecordingArtifact,
|
|
8655
|
+
createTraceId,
|
|
8656
|
+
normalizeTraceEvent,
|
|
8657
|
+
TRACE_BINDING_NAME,
|
|
8658
|
+
TRACE_SCRIPT,
|
|
8659
|
+
LiveTraceCollector,
|
|
7782
8660
|
addBatchToPage,
|
|
7783
8661
|
validateSteps,
|
|
7784
|
-
|
|
8662
|
+
grantAudioPermissions,
|
|
7785
8663
|
DEEP_QUERY_SCRIPT,
|
|
7786
|
-
|
|
7787
|
-
connect
|
|
8664
|
+
Page
|
|
7788
8665
|
};
|