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/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(src_exports, {
|
|
|
35
35
|
BatchExecutor: () => BatchExecutor,
|
|
36
36
|
Browser: () => Browser,
|
|
37
37
|
BrowserBaseProvider: () => BrowserBaseProvider,
|
|
38
|
+
BrowserEndpointResolutionError: () => BrowserEndpointResolutionError,
|
|
38
39
|
BrowserlessProvider: () => BrowserlessProvider,
|
|
39
40
|
CDPError: () => CDPError,
|
|
40
41
|
ElementNotFoundError: () => ElementNotFoundError,
|
|
@@ -46,12 +47,14 @@ __export(src_exports, {
|
|
|
46
47
|
Tracer: () => Tracer,
|
|
47
48
|
addBatchToPage: () => addBatchToPage,
|
|
48
49
|
bufferToBase64: () => bufferToBase64,
|
|
50
|
+
buildLocalBrowserScanTargets: () => buildLocalBrowserScanTargets,
|
|
49
51
|
calculateRMS: () => calculateRMS,
|
|
50
52
|
connect: () => connect,
|
|
51
53
|
createCDPClient: () => createCDPClient,
|
|
52
54
|
createProvider: () => createProvider,
|
|
53
55
|
devices: () => devices,
|
|
54
56
|
disableTracing: () => disableTracing,
|
|
57
|
+
discoverLocalBrowsers: () => discoverLocalBrowsers,
|
|
55
58
|
discoverTargets: () => discoverTargets,
|
|
56
59
|
enableTracing: () => enableTracing,
|
|
57
60
|
generateSilence: () => generateSilence,
|
|
@@ -61,8 +64,11 @@ __export(src_exports, {
|
|
|
61
64
|
getTracer: () => getTracer,
|
|
62
65
|
grantAudioPermissions: () => grantAudioPermissions,
|
|
63
66
|
isTranscriptionAvailable: () => isTranscriptionAvailable,
|
|
67
|
+
parseDevToolsActivePortFile: () => parseDevToolsActivePortFile,
|
|
64
68
|
parseWavHeader: () => parseWavHeader,
|
|
65
69
|
pcmToWav: () => pcmToWav,
|
|
70
|
+
resolveBrowserEndpoint: () => resolveBrowserEndpoint,
|
|
71
|
+
resolveChromeUserDataDirs: () => resolveChromeUserDataDirs,
|
|
66
72
|
transcribe: () => transcribe,
|
|
67
73
|
validateSteps: () => validateSteps,
|
|
68
74
|
waitForAnyElement: () => waitForAnyElement,
|
|
@@ -920,6 +926,624 @@ var CDPError = class extends Error {
|
|
|
920
926
|
}
|
|
921
927
|
};
|
|
922
928
|
|
|
929
|
+
// src/trace/views.ts
|
|
930
|
+
function takeRecent(events, limit = 5) {
|
|
931
|
+
return events.slice(-limit).map((event) => ({
|
|
932
|
+
ts: event.ts,
|
|
933
|
+
event: event.event,
|
|
934
|
+
summary: event.summary,
|
|
935
|
+
severity: event.severity,
|
|
936
|
+
url: event.url
|
|
937
|
+
}));
|
|
938
|
+
}
|
|
939
|
+
function buildTraceSummaries(events) {
|
|
940
|
+
return {
|
|
941
|
+
ws: summarizeWs(events),
|
|
942
|
+
voice: summarizeVoice(events),
|
|
943
|
+
console: summarizeConsole(events),
|
|
944
|
+
permissions: summarizePermissions(events),
|
|
945
|
+
media: summarizeMedia(events),
|
|
946
|
+
ui: summarizeUi(events),
|
|
947
|
+
session: summarizeSession(events)
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function summarizeWs(events) {
|
|
951
|
+
const relevant = events.filter(
|
|
952
|
+
(event) => event.channel === "ws" || event.event.startsWith("ws.")
|
|
953
|
+
);
|
|
954
|
+
const connections = /* @__PURE__ */ new Map();
|
|
955
|
+
for (const event of relevant) {
|
|
956
|
+
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
957
|
+
let connection = connections.get(id);
|
|
958
|
+
if (!connection) {
|
|
959
|
+
connection = { id, sent: 0, received: 0, lastMessages: [] };
|
|
960
|
+
connections.set(id, connection);
|
|
961
|
+
}
|
|
962
|
+
connection.url = event.url ?? connection.url;
|
|
963
|
+
if (event.event === "ws.connection.created") {
|
|
964
|
+
connection.createdAt = event.ts;
|
|
965
|
+
}
|
|
966
|
+
if (event.event === "ws.connection.closed") {
|
|
967
|
+
connection.closedAt = event.ts;
|
|
968
|
+
}
|
|
969
|
+
if (event.event === "ws.frame.sent") {
|
|
970
|
+
connection.sent += 1;
|
|
971
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
972
|
+
if (payload) connection.lastMessages.push(`sent: ${payload}`);
|
|
973
|
+
}
|
|
974
|
+
if (event.event === "ws.frame.received") {
|
|
975
|
+
connection.received += 1;
|
|
976
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
977
|
+
if (payload) connection.lastMessages.push(`recv: ${payload}`);
|
|
978
|
+
}
|
|
979
|
+
connection.lastMessages = connection.lastMessages.slice(-3);
|
|
980
|
+
}
|
|
981
|
+
const values = [...connections.values()];
|
|
982
|
+
const reconnects = values.reduce((count, connection) => {
|
|
983
|
+
return connection.closedAt && !connection.createdAt ? count + 1 : count;
|
|
984
|
+
}, 0);
|
|
985
|
+
return {
|
|
986
|
+
view: "ws",
|
|
987
|
+
totalEvents: relevant.length,
|
|
988
|
+
connections: values.map((connection) => ({
|
|
989
|
+
id: connection.id,
|
|
990
|
+
url: connection.url ?? null,
|
|
991
|
+
createdAt: connection.createdAt ?? null,
|
|
992
|
+
closedAt: connection.closedAt ?? null,
|
|
993
|
+
sent: connection.sent,
|
|
994
|
+
received: connection.received,
|
|
995
|
+
lastMessages: connection.lastMessages,
|
|
996
|
+
connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
|
|
997
|
+
})),
|
|
998
|
+
reconnects,
|
|
999
|
+
recent: takeRecent(relevant)
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function summarizeConsole(events) {
|
|
1003
|
+
const relevant = events.filter(
|
|
1004
|
+
(event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
1005
|
+
);
|
|
1006
|
+
return {
|
|
1007
|
+
view: "console",
|
|
1008
|
+
errors: relevant.filter(
|
|
1009
|
+
(event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
1010
|
+
).length,
|
|
1011
|
+
warnings: relevant.filter((event) => event.event === "console.warn").length,
|
|
1012
|
+
logs: relevant.filter((event) => event.event === "console.log").length,
|
|
1013
|
+
recent: takeRecent(relevant)
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function summarizePermissions(events) {
|
|
1017
|
+
const relevant = events.filter(
|
|
1018
|
+
(event) => event.channel === "permission" || event.event.startsWith("permission.")
|
|
1019
|
+
);
|
|
1020
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1021
|
+
for (const event of relevant) {
|
|
1022
|
+
const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
|
|
1023
|
+
const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
|
|
1024
|
+
if (name && state) {
|
|
1025
|
+
latest.set(name, state);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
view: "permissions",
|
|
1030
|
+
states: Object.fromEntries(latest),
|
|
1031
|
+
changes: relevant.filter((event) => event.event === "permission.changed").length,
|
|
1032
|
+
recent: takeRecent(relevant)
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function summarizeMedia(events) {
|
|
1036
|
+
const relevant = events.filter(
|
|
1037
|
+
(event) => event.channel === "media" || event.event.startsWith("media.")
|
|
1038
|
+
);
|
|
1039
|
+
const liveTracks = /* @__PURE__ */ new Map();
|
|
1040
|
+
for (const event of relevant) {
|
|
1041
|
+
const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
|
|
1042
|
+
const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
|
|
1043
|
+
const key = `${kind}:${label}`;
|
|
1044
|
+
if (event.event === "media.track.started") {
|
|
1045
|
+
liveTracks.set(key, kind);
|
|
1046
|
+
}
|
|
1047
|
+
if (event.event === "media.track.ended") {
|
|
1048
|
+
liveTracks.delete(key);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
view: "media",
|
|
1053
|
+
tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
|
|
1054
|
+
tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
|
|
1055
|
+
playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
|
|
1056
|
+
playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
|
|
1057
|
+
liveTracks: [...liveTracks.values()],
|
|
1058
|
+
recent: takeRecent(relevant)
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
function summarizeVoice(events) {
|
|
1062
|
+
const relevant = events.filter(
|
|
1063
|
+
(event) => event.channel === "voice" || event.event.startsWith("voice.")
|
|
1064
|
+
);
|
|
1065
|
+
return {
|
|
1066
|
+
view: "voice",
|
|
1067
|
+
ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
|
|
1068
|
+
notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
|
|
1069
|
+
captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
|
|
1070
|
+
captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
|
|
1071
|
+
detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
|
|
1072
|
+
recent: takeRecent(relevant)
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
function summarizeUi(events) {
|
|
1076
|
+
const relevant = events.filter(
|
|
1077
|
+
(event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
|
|
1078
|
+
);
|
|
1079
|
+
return {
|
|
1080
|
+
view: "ui",
|
|
1081
|
+
actions: relevant.filter((event) => event.channel === "action").length,
|
|
1082
|
+
domChanges: relevant.filter((event) => event.channel === "dom").length,
|
|
1083
|
+
recent: takeRecent(relevant)
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
function summarizeSession(events) {
|
|
1087
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
1088
|
+
const failedActions = events.filter((event) => event.event === "action.failed").length;
|
|
1089
|
+
for (const event of events) {
|
|
1090
|
+
byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
view: "session",
|
|
1094
|
+
totalEvents: events.length,
|
|
1095
|
+
byChannel: Object.fromEntries(byChannel),
|
|
1096
|
+
failedActions,
|
|
1097
|
+
recent: takeRecent(events)
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/recording/manifest.ts
|
|
1102
|
+
function isCanonicalRecordingManifest(value) {
|
|
1103
|
+
return Boolean(
|
|
1104
|
+
value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
function isLegacyRecordingManifest(value) {
|
|
1108
|
+
return Boolean(
|
|
1109
|
+
value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
function createRecordingManifest(input) {
|
|
1113
|
+
const actions = input.frames.map((frame) => {
|
|
1114
|
+
const actionId = frame.actionId ?? `action-${frame.seq}`;
|
|
1115
|
+
return {
|
|
1116
|
+
id: actionId,
|
|
1117
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1118
|
+
action: frame.action,
|
|
1119
|
+
selector: frame.selector,
|
|
1120
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1121
|
+
value: frame.value,
|
|
1122
|
+
url: frame.url,
|
|
1123
|
+
success: frame.success,
|
|
1124
|
+
durationMs: frame.durationMs,
|
|
1125
|
+
error: frame.error,
|
|
1126
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1127
|
+
pageUrl: frame.pageUrl,
|
|
1128
|
+
pageTitle: frame.pageTitle,
|
|
1129
|
+
coordinates: frame.coordinates,
|
|
1130
|
+
boundingBox: frame.boundingBox
|
|
1131
|
+
};
|
|
1132
|
+
});
|
|
1133
|
+
const screenshots = input.frames.map((frame) => ({
|
|
1134
|
+
id: `shot-${frame.seq}`,
|
|
1135
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1136
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1137
|
+
file: frame.screenshot,
|
|
1138
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1139
|
+
success: frame.success,
|
|
1140
|
+
pageUrl: frame.pageUrl,
|
|
1141
|
+
pageTitle: frame.pageTitle,
|
|
1142
|
+
coordinates: frame.coordinates,
|
|
1143
|
+
boundingBox: frame.boundingBox
|
|
1144
|
+
}));
|
|
1145
|
+
return {
|
|
1146
|
+
version: 2,
|
|
1147
|
+
recordedAt: input.recordedAt,
|
|
1148
|
+
session: {
|
|
1149
|
+
id: input.sessionId,
|
|
1150
|
+
startUrl: input.startUrl,
|
|
1151
|
+
endUrl: input.endUrl,
|
|
1152
|
+
targetId: input.targetId,
|
|
1153
|
+
profile: input.profile
|
|
1154
|
+
},
|
|
1155
|
+
recipe: {
|
|
1156
|
+
steps: input.steps
|
|
1157
|
+
},
|
|
1158
|
+
actions,
|
|
1159
|
+
screenshots,
|
|
1160
|
+
trace: {
|
|
1161
|
+
events: input.traceEvents,
|
|
1162
|
+
summaries: buildTraceSummaries(input.traceEvents)
|
|
1163
|
+
},
|
|
1164
|
+
assertions: input.assertions ?? [],
|
|
1165
|
+
notes: input.notes ?? [],
|
|
1166
|
+
artifacts: {
|
|
1167
|
+
recordingManifest: input.recordingManifest ?? "recording.json",
|
|
1168
|
+
screenshotDir: input.screenshotDir ?? "screenshots/"
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
function canonicalizeRecordingArtifact(value) {
|
|
1173
|
+
if (isCanonicalRecordingManifest(value)) {
|
|
1174
|
+
return value;
|
|
1175
|
+
}
|
|
1176
|
+
if (!isLegacyRecordingManifest(value)) {
|
|
1177
|
+
throw new Error("Unsupported recording artifact");
|
|
1178
|
+
}
|
|
1179
|
+
const traceEvents = buildTraceEventsFromLegacy(value);
|
|
1180
|
+
const steps = value.frames.map((frame) => frameToStep(frame));
|
|
1181
|
+
return createRecordingManifest({
|
|
1182
|
+
recordedAt: value.recordedAt,
|
|
1183
|
+
sessionId: value.sessionId,
|
|
1184
|
+
startUrl: value.startUrl,
|
|
1185
|
+
endUrl: value.endUrl,
|
|
1186
|
+
steps,
|
|
1187
|
+
frames: value.frames,
|
|
1188
|
+
traceEvents,
|
|
1189
|
+
notes: ["Converted from legacy recording manifest"]
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
function buildTraceEventsFromLegacy(value) {
|
|
1193
|
+
const events = [];
|
|
1194
|
+
for (const frame of value.frames) {
|
|
1195
|
+
events.push({
|
|
1196
|
+
traceId: frame.actionId ?? `legacy-${frame.seq}`,
|
|
1197
|
+
sessionId: value.sessionId,
|
|
1198
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1199
|
+
elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
|
|
1200
|
+
channel: "action",
|
|
1201
|
+
event: frame.success ? "action.succeeded" : "action.failed",
|
|
1202
|
+
severity: frame.success ? "info" : "error",
|
|
1203
|
+
summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
|
|
1204
|
+
data: {
|
|
1205
|
+
action: frame.action,
|
|
1206
|
+
selector: frame.selector,
|
|
1207
|
+
value: frame.value ?? null,
|
|
1208
|
+
pageUrl: frame.pageUrl ?? null,
|
|
1209
|
+
pageTitle: frame.pageTitle ?? null,
|
|
1210
|
+
screenshot: frame.screenshot
|
|
1211
|
+
},
|
|
1212
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1213
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1214
|
+
selector: frame.selector,
|
|
1215
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1216
|
+
url: frame.pageUrl ?? frame.url
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
return events;
|
|
1220
|
+
}
|
|
1221
|
+
function frameToStep(frame) {
|
|
1222
|
+
switch (frame.action) {
|
|
1223
|
+
case "fill":
|
|
1224
|
+
return { action: "fill", selector: frame.selector, value: frame.value };
|
|
1225
|
+
case "submit":
|
|
1226
|
+
return { action: "submit", selector: frame.selector };
|
|
1227
|
+
case "goto":
|
|
1228
|
+
return { action: "goto", url: frame.url ?? frame.pageUrl };
|
|
1229
|
+
case "press":
|
|
1230
|
+
return { action: "press", key: frame.value ?? "Enter" };
|
|
1231
|
+
default:
|
|
1232
|
+
return { action: "click", selector: frame.selector };
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/trace/model.ts
|
|
1237
|
+
function createTraceId(prefix = "evt") {
|
|
1238
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1239
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1240
|
+
}
|
|
1241
|
+
function normalizeTraceEvent(event) {
|
|
1242
|
+
return {
|
|
1243
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1244
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1245
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
1246
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
1247
|
+
data: event.data ?? {},
|
|
1248
|
+
...event
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function inferSeverity(eventName) {
|
|
1252
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1253
|
+
return "error";
|
|
1254
|
+
}
|
|
1255
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1256
|
+
return "warn";
|
|
1257
|
+
}
|
|
1258
|
+
return "info";
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// src/trace/script.ts
|
|
1262
|
+
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
1263
|
+
var TRACE_SCRIPT = `
|
|
1264
|
+
(() => {
|
|
1265
|
+
if (window.__bpTraceInstalled) return;
|
|
1266
|
+
window.__bpTraceInstalled = true;
|
|
1267
|
+
|
|
1268
|
+
const binding = globalThis.${TRACE_BINDING_NAME};
|
|
1269
|
+
if (typeof binding !== 'function') return;
|
|
1270
|
+
|
|
1271
|
+
const emit = (event, data = {}, severity = 'info', summary) => {
|
|
1272
|
+
try {
|
|
1273
|
+
globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
|
|
1274
|
+
const payload = {
|
|
1275
|
+
event,
|
|
1276
|
+
severity,
|
|
1277
|
+
summary: summary || event,
|
|
1278
|
+
ts: Date.now(),
|
|
1279
|
+
data,
|
|
1280
|
+
};
|
|
1281
|
+
globalThis.__bpTraceRecentEvents.push(payload);
|
|
1282
|
+
if (globalThis.__bpTraceRecentEvents.length > 200) {
|
|
1283
|
+
globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
|
|
1284
|
+
}
|
|
1285
|
+
binding(JSON.stringify(payload));
|
|
1286
|
+
} catch {}
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
const patchWebSocket = () => {
|
|
1290
|
+
const NativeWebSocket = window.WebSocket;
|
|
1291
|
+
if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
|
|
1292
|
+
window.__bpTraceWebSocketInstalled = true;
|
|
1293
|
+
|
|
1294
|
+
const nextId = () => Math.random().toString(36).slice(2, 10);
|
|
1295
|
+
|
|
1296
|
+
const patchInstance = (socket, urlValue) => {
|
|
1297
|
+
if (!socket || socket.__bpTracePatched) return socket;
|
|
1298
|
+
socket.__bpTracePatched = true;
|
|
1299
|
+
socket.__bpTraceId = socket.__bpTraceId || nextId();
|
|
1300
|
+
socket.__bpTraceUrl = String(urlValue || socket.url || '');
|
|
1301
|
+
globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
|
|
1302
|
+
globalThis.__bpTrackedWebSockets.add(socket);
|
|
1303
|
+
|
|
1304
|
+
emit(
|
|
1305
|
+
'ws.connection.created',
|
|
1306
|
+
{ connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
|
|
1307
|
+
'info',
|
|
1308
|
+
'WebSocket opened ' + socket.__bpTraceUrl
|
|
1309
|
+
);
|
|
1310
|
+
|
|
1311
|
+
const originalSend = socket.send;
|
|
1312
|
+
socket.send = function(data) {
|
|
1313
|
+
const payload =
|
|
1314
|
+
typeof data === 'string'
|
|
1315
|
+
? data
|
|
1316
|
+
: data && typeof data.toString === 'function'
|
|
1317
|
+
? data.toString()
|
|
1318
|
+
: '[binary]';
|
|
1319
|
+
emit(
|
|
1320
|
+
'ws.frame.sent',
|
|
1321
|
+
{
|
|
1322
|
+
connectionId: socket.__bpTraceId,
|
|
1323
|
+
url: socket.__bpTraceUrl,
|
|
1324
|
+
payload,
|
|
1325
|
+
length: payload.length,
|
|
1326
|
+
},
|
|
1327
|
+
'info',
|
|
1328
|
+
'WebSocket frame sent'
|
|
1329
|
+
);
|
|
1330
|
+
return originalSend.call(this, data);
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
socket.addEventListener('message', (event) => {
|
|
1334
|
+
if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const data = event && 'data' in event ? event.data : '';
|
|
1338
|
+
const payload =
|
|
1339
|
+
typeof data === 'string'
|
|
1340
|
+
? data
|
|
1341
|
+
: data && typeof data.toString === 'function'
|
|
1342
|
+
? data.toString()
|
|
1343
|
+
: '[binary]';
|
|
1344
|
+
emit(
|
|
1345
|
+
'ws.frame.received',
|
|
1346
|
+
{
|
|
1347
|
+
connectionId: socket.__bpTraceId,
|
|
1348
|
+
url: socket.__bpTraceUrl,
|
|
1349
|
+
payload,
|
|
1350
|
+
length: payload.length,
|
|
1351
|
+
},
|
|
1352
|
+
'info',
|
|
1353
|
+
'WebSocket frame received'
|
|
1354
|
+
);
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
socket.addEventListener('close', (event) => {
|
|
1358
|
+
if (socket.__bpTraceClosed) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
socket.__bpTraceClosed = true;
|
|
1362
|
+
try {
|
|
1363
|
+
globalThis.__bpTrackedWebSockets.delete(socket);
|
|
1364
|
+
} catch {}
|
|
1365
|
+
emit(
|
|
1366
|
+
'ws.connection.closed',
|
|
1367
|
+
{
|
|
1368
|
+
connectionId: socket.__bpTraceId,
|
|
1369
|
+
url: socket.__bpTraceUrl,
|
|
1370
|
+
code: event.code,
|
|
1371
|
+
reason: event.reason,
|
|
1372
|
+
},
|
|
1373
|
+
'warn',
|
|
1374
|
+
'WebSocket closed'
|
|
1375
|
+
);
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
return socket;
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
const TracedWebSocket = function(url, protocols) {
|
|
1382
|
+
return arguments.length > 1
|
|
1383
|
+
? patchInstance(new NativeWebSocket(url, protocols), url)
|
|
1384
|
+
: patchInstance(new NativeWebSocket(url), url);
|
|
1385
|
+
};
|
|
1386
|
+
TracedWebSocket.prototype = NativeWebSocket.prototype;
|
|
1387
|
+
Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
|
|
1388
|
+
window.WebSocket = TracedWebSocket;
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
window.addEventListener('error', (errorEvent) => {
|
|
1392
|
+
emit(
|
|
1393
|
+
'runtime.exception',
|
|
1394
|
+
{
|
|
1395
|
+
message: errorEvent.message,
|
|
1396
|
+
filename: errorEvent.filename,
|
|
1397
|
+
line: errorEvent.lineno,
|
|
1398
|
+
column: errorEvent.colno,
|
|
1399
|
+
},
|
|
1400
|
+
'error',
|
|
1401
|
+
errorEvent.message || 'Uncaught error'
|
|
1402
|
+
);
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
1406
|
+
const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
|
|
1407
|
+
emit('runtime.unhandledRejection', { reason }, 'error', reason);
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
const patchPermissions = async () => {
|
|
1411
|
+
if (!navigator.permissions || !navigator.permissions.query) return;
|
|
1412
|
+
|
|
1413
|
+
const names = ['geolocation', 'microphone', 'camera', 'notifications'];
|
|
1414
|
+
for (const name of names) {
|
|
1415
|
+
try {
|
|
1416
|
+
const status = await navigator.permissions.query({ name });
|
|
1417
|
+
emit(
|
|
1418
|
+
'permission.state',
|
|
1419
|
+
{ name, state: status.state },
|
|
1420
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1421
|
+
name + ': ' + status.state
|
|
1422
|
+
);
|
|
1423
|
+
status.addEventListener('change', () => {
|
|
1424
|
+
emit(
|
|
1425
|
+
'permission.changed',
|
|
1426
|
+
{ name, state: status.state },
|
|
1427
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1428
|
+
name + ': ' + status.state
|
|
1429
|
+
);
|
|
1430
|
+
});
|
|
1431
|
+
} catch {}
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
const patchMediaElement = (element) => {
|
|
1436
|
+
if (!element || element.__bpTracePatched) return;
|
|
1437
|
+
element.__bpTracePatched = true;
|
|
1438
|
+
|
|
1439
|
+
element.addEventListener('play', () => {
|
|
1440
|
+
emit(
|
|
1441
|
+
'media.playback.started',
|
|
1442
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1443
|
+
'info',
|
|
1444
|
+
'Media playback started'
|
|
1445
|
+
);
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
const onStop = () => {
|
|
1449
|
+
emit(
|
|
1450
|
+
'media.playback.stopped',
|
|
1451
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1452
|
+
'warn',
|
|
1453
|
+
'Media playback stopped'
|
|
1454
|
+
);
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
element.addEventListener('pause', onStop);
|
|
1458
|
+
element.addEventListener('ended', onStop);
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
const patchMediaElements = () => {
|
|
1462
|
+
document.querySelectorAll('audio,video').forEach(patchMediaElement);
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
patchMediaElements();
|
|
1466
|
+
patchWebSocket();
|
|
1467
|
+
|
|
1468
|
+
if (document.documentElement) {
|
|
1469
|
+
const observer = new MutationObserver(() => {
|
|
1470
|
+
patchMediaElements();
|
|
1471
|
+
});
|
|
1472
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
1476
|
+
const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
|
1477
|
+
navigator.mediaDevices.getUserMedia = async (...args) => {
|
|
1478
|
+
emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
|
|
1479
|
+
try {
|
|
1480
|
+
const stream = await original(...args);
|
|
1481
|
+
const tracks = stream.getTracks();
|
|
1482
|
+
|
|
1483
|
+
for (const track of tracks) {
|
|
1484
|
+
emit(
|
|
1485
|
+
'media.track.started',
|
|
1486
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1487
|
+
'info',
|
|
1488
|
+
track.kind + ' track started'
|
|
1489
|
+
);
|
|
1490
|
+
track.addEventListener('ended', () => {
|
|
1491
|
+
emit(
|
|
1492
|
+
'media.track.ended',
|
|
1493
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1494
|
+
'warn',
|
|
1495
|
+
track.kind + ' track ended'
|
|
1496
|
+
);
|
|
1497
|
+
emit(
|
|
1498
|
+
'voice.capture.stopped',
|
|
1499
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1500
|
+
'warn',
|
|
1501
|
+
'Voice capture stopped'
|
|
1502
|
+
);
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
emit(
|
|
1507
|
+
'voice.capture.detectedAudio',
|
|
1508
|
+
{ trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
|
|
1509
|
+
'info',
|
|
1510
|
+
'Voice capture detected audio'
|
|
1511
|
+
);
|
|
1512
|
+
|
|
1513
|
+
return stream;
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
emit(
|
|
1516
|
+
'voice.pipeline.notReady',
|
|
1517
|
+
{ message: String(error && error.message ? error.message : error) },
|
|
1518
|
+
'error',
|
|
1519
|
+
String(error && error.message ? error.message : error)
|
|
1520
|
+
);
|
|
1521
|
+
throw error;
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
document.addEventListener('visibilitychange', () => {
|
|
1527
|
+
emit(
|
|
1528
|
+
'dom.state.changed',
|
|
1529
|
+
{ visibilityState: document.visibilityState },
|
|
1530
|
+
document.visibilityState === 'hidden' ? 'warn' : 'info',
|
|
1531
|
+
'Visibility ' + document.visibilityState
|
|
1532
|
+
);
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
patchPermissions();
|
|
1536
|
+
emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
|
|
1537
|
+
})();
|
|
1538
|
+
`;
|
|
1539
|
+
|
|
1540
|
+
// src/trace/live.ts
|
|
1541
|
+
function globToRegex(pattern) {
|
|
1542
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1543
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1544
|
+
return new RegExp(`^${withWildcards}$`);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
923
1547
|
// src/actions/executor.ts
|
|
924
1548
|
var DEFAULT_TIMEOUT = 3e4;
|
|
925
1549
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -929,6 +1553,61 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
|
929
1553
|
"text",
|
|
930
1554
|
"screenshot"
|
|
931
1555
|
];
|
|
1556
|
+
function readString(value) {
|
|
1557
|
+
return typeof value === "string" ? value : void 0;
|
|
1558
|
+
}
|
|
1559
|
+
function readStringOr(value, fallback = "") {
|
|
1560
|
+
return readString(value) ?? fallback;
|
|
1561
|
+
}
|
|
1562
|
+
function formatConsoleArg(entry) {
|
|
1563
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
1564
|
+
}
|
|
1565
|
+
function loadExistingRecording(manifestPath) {
|
|
1566
|
+
try {
|
|
1567
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1568
|
+
if (raw.version === 1) {
|
|
1569
|
+
const legacy = raw;
|
|
1570
|
+
return {
|
|
1571
|
+
frames: Array.isArray(legacy.frames) ? legacy.frames : [],
|
|
1572
|
+
traceEvents: [],
|
|
1573
|
+
recordedAt: legacy.recordedAt,
|
|
1574
|
+
startUrl: legacy.startUrl
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
const artifact = canonicalizeRecordingArtifact(raw);
|
|
1578
|
+
const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
|
|
1579
|
+
const frames = artifact.actions.map((action, index) => {
|
|
1580
|
+
const screenshot = screenshotsByAction.get(action.id);
|
|
1581
|
+
return {
|
|
1582
|
+
seq: index + 1,
|
|
1583
|
+
timestamp: Date.parse(action.ts),
|
|
1584
|
+
action: action.action,
|
|
1585
|
+
selector: action.selector,
|
|
1586
|
+
selectorUsed: action.selectorUsed,
|
|
1587
|
+
value: action.value,
|
|
1588
|
+
url: action.url,
|
|
1589
|
+
coordinates: action.coordinates,
|
|
1590
|
+
boundingBox: action.boundingBox,
|
|
1591
|
+
success: action.success,
|
|
1592
|
+
durationMs: action.durationMs,
|
|
1593
|
+
error: action.error,
|
|
1594
|
+
screenshot: screenshot?.file ?? "",
|
|
1595
|
+
pageUrl: action.pageUrl,
|
|
1596
|
+
pageTitle: action.pageTitle,
|
|
1597
|
+
stepIndex: action.stepIndex,
|
|
1598
|
+
actionId: action.id
|
|
1599
|
+
};
|
|
1600
|
+
});
|
|
1601
|
+
return {
|
|
1602
|
+
frames,
|
|
1603
|
+
traceEvents: artifact.trace.events,
|
|
1604
|
+
recordedAt: artifact.recordedAt,
|
|
1605
|
+
startUrl: artifact.session.startUrl
|
|
1606
|
+
};
|
|
1607
|
+
} catch {
|
|
1608
|
+
return { frames: [], traceEvents: [] };
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
932
1611
|
function classifyFailure(error) {
|
|
933
1612
|
if (error instanceof ElementNotFoundError) {
|
|
934
1613
|
return { reason: "missing" };
|
|
@@ -1009,6 +1688,9 @@ var BatchExecutor = class {
|
|
|
1009
1688
|
const results = [];
|
|
1010
1689
|
const startTime = Date.now();
|
|
1011
1690
|
const recording = options.record ? this.createRecordingContext(options.record) : null;
|
|
1691
|
+
if (steps.some((step) => step.action === "waitForWsMessage")) {
|
|
1692
|
+
await this.ensureTraceHooks();
|
|
1693
|
+
}
|
|
1012
1694
|
const startUrl = recording ? await this.getPageUrlSafe() : "";
|
|
1013
1695
|
let stoppedAtIndex;
|
|
1014
1696
|
for (let i = 0; i < steps.length; i++) {
|
|
@@ -1018,6 +1700,26 @@ var BatchExecutor = class {
|
|
|
1018
1700
|
const retryDelay = step.retryDelay ?? 500;
|
|
1019
1701
|
let lastError;
|
|
1020
1702
|
let succeeded = false;
|
|
1703
|
+
if (recording) {
|
|
1704
|
+
recording.traceEvents.push(
|
|
1705
|
+
normalizeTraceEvent({
|
|
1706
|
+
traceId: createTraceId("action"),
|
|
1707
|
+
elapsedMs: Date.now() - startTime,
|
|
1708
|
+
channel: "action",
|
|
1709
|
+
event: "action.started",
|
|
1710
|
+
summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
|
|
1711
|
+
data: {
|
|
1712
|
+
action: step.action,
|
|
1713
|
+
selector: step.selector ?? null,
|
|
1714
|
+
url: step.url ?? null
|
|
1715
|
+
},
|
|
1716
|
+
actionId: `action-${i + 1}`,
|
|
1717
|
+
stepIndex: i,
|
|
1718
|
+
selector: step.selector,
|
|
1719
|
+
url: step.url
|
|
1720
|
+
})
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1021
1723
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1022
1724
|
if (attempt > 0) {
|
|
1023
1725
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
@@ -1041,6 +1743,28 @@ var BatchExecutor = class {
|
|
|
1041
1743
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1042
1744
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1043
1745
|
}
|
|
1746
|
+
if (recording) {
|
|
1747
|
+
recording.traceEvents.push(
|
|
1748
|
+
normalizeTraceEvent({
|
|
1749
|
+
traceId: createTraceId("action"),
|
|
1750
|
+
elapsedMs: Date.now() - startTime,
|
|
1751
|
+
channel: "action",
|
|
1752
|
+
event: "action.succeeded",
|
|
1753
|
+
summary: `${step.action} succeeded`,
|
|
1754
|
+
data: {
|
|
1755
|
+
action: step.action,
|
|
1756
|
+
selector: step.selector ?? null,
|
|
1757
|
+
selectorUsed: result.selectorUsed ?? null,
|
|
1758
|
+
durationMs: Date.now() - stepStart
|
|
1759
|
+
},
|
|
1760
|
+
actionId: `action-${i + 1}`,
|
|
1761
|
+
stepIndex: i,
|
|
1762
|
+
selector: step.selector,
|
|
1763
|
+
selectorUsed: result.selectorUsed,
|
|
1764
|
+
url: step.url
|
|
1765
|
+
})
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1044
1768
|
results.push(stepResult);
|
|
1045
1769
|
succeeded = true;
|
|
1046
1770
|
break;
|
|
@@ -1078,6 +1802,28 @@ var BatchExecutor = class {
|
|
|
1078
1802
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1079
1803
|
await this.captureRecordingFrame(step, failedResult, recording);
|
|
1080
1804
|
}
|
|
1805
|
+
if (recording) {
|
|
1806
|
+
recording.traceEvents.push(
|
|
1807
|
+
normalizeTraceEvent({
|
|
1808
|
+
traceId: createTraceId("action"),
|
|
1809
|
+
elapsedMs: Date.now() - startTime,
|
|
1810
|
+
channel: "action",
|
|
1811
|
+
event: "action.failed",
|
|
1812
|
+
severity: "error",
|
|
1813
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
1814
|
+
data: {
|
|
1815
|
+
action: step.action,
|
|
1816
|
+
selector: step.selector ?? null,
|
|
1817
|
+
error: errorMessage,
|
|
1818
|
+
reason
|
|
1819
|
+
},
|
|
1820
|
+
actionId: `action-${i + 1}`,
|
|
1821
|
+
stepIndex: i,
|
|
1822
|
+
selector: step.selector,
|
|
1823
|
+
url: step.url
|
|
1824
|
+
})
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1081
1827
|
results.push(failedResult);
|
|
1082
1828
|
if (onFail === "stop" && !step.optional) {
|
|
1083
1829
|
stoppedAtIndex = i;
|
|
@@ -1093,7 +1839,8 @@ var BatchExecutor = class {
|
|
|
1093
1839
|
recording,
|
|
1094
1840
|
startTime,
|
|
1095
1841
|
startUrl,
|
|
1096
|
-
allSuccess
|
|
1842
|
+
allSuccess,
|
|
1843
|
+
steps
|
|
1097
1844
|
);
|
|
1098
1845
|
}
|
|
1099
1846
|
return {
|
|
@@ -1108,20 +1855,14 @@ var BatchExecutor = class {
|
|
|
1108
1855
|
const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
|
|
1109
1856
|
const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
|
|
1110
1857
|
const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
|
|
1111
|
-
|
|
1112
|
-
try {
|
|
1113
|
-
const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1114
|
-
if (existing.frames && Array.isArray(existing.frames)) {
|
|
1115
|
-
existingFrames = existing.frames;
|
|
1116
|
-
}
|
|
1117
|
-
} catch {
|
|
1118
|
-
}
|
|
1858
|
+
const existing = loadExistingRecording(manifestPath);
|
|
1119
1859
|
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
1120
1860
|
return {
|
|
1121
1861
|
baseDir,
|
|
1122
1862
|
screenshotDir,
|
|
1123
1863
|
sessionId: record.sessionId ?? this.page.targetId,
|
|
1124
|
-
frames:
|
|
1864
|
+
frames: existing.frames,
|
|
1865
|
+
traceEvents: existing.traceEvents,
|
|
1125
1866
|
format: record.format ?? "webp",
|
|
1126
1867
|
quality: Math.max(0, Math.min(100, record.quality ?? 40)),
|
|
1127
1868
|
highlights: record.highlights !== false,
|
|
@@ -1177,6 +1918,7 @@ var BatchExecutor = class {
|
|
|
1177
1918
|
timestamp: ts,
|
|
1178
1919
|
action: stepResult.action,
|
|
1179
1920
|
selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
|
|
1921
|
+
selectorUsed: stepResult.selectorUsed,
|
|
1180
1922
|
value: redactValueForRecording(
|
|
1181
1923
|
typeof step.value === "string" ? step.value : void 0,
|
|
1182
1924
|
targetMetadata
|
|
@@ -1189,7 +1931,9 @@ var BatchExecutor = class {
|
|
|
1189
1931
|
error: stepResult.error,
|
|
1190
1932
|
screenshot: filename,
|
|
1191
1933
|
pageUrl,
|
|
1192
|
-
pageTitle
|
|
1934
|
+
pageTitle,
|
|
1935
|
+
stepIndex: stepResult.index,
|
|
1936
|
+
actionId: `action-${stepResult.index + 1}`
|
|
1193
1937
|
});
|
|
1194
1938
|
} catch {
|
|
1195
1939
|
} finally {
|
|
@@ -1201,45 +1945,31 @@ var BatchExecutor = class {
|
|
|
1201
1945
|
/**
|
|
1202
1946
|
* Write recording manifest to disk
|
|
1203
1947
|
*/
|
|
1204
|
-
async writeRecordingManifest(recording, startTime, startUrl, success) {
|
|
1948
|
+
async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
|
|
1205
1949
|
let endUrl = startUrl;
|
|
1206
|
-
let viewport = { width: 1280, height: 720 };
|
|
1207
1950
|
try {
|
|
1208
1951
|
endUrl = await this.page.url();
|
|
1209
1952
|
} catch {
|
|
1210
1953
|
}
|
|
1211
|
-
try {
|
|
1212
|
-
const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
|
|
1213
|
-
viewport = {
|
|
1214
|
-
width: metrics.cssVisualViewport.clientWidth,
|
|
1215
|
-
height: metrics.cssVisualViewport.clientHeight
|
|
1216
|
-
};
|
|
1217
|
-
} catch {
|
|
1218
|
-
}
|
|
1219
1954
|
const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
|
|
1220
1955
|
let recordedAt = new Date(startTime).toISOString();
|
|
1221
1956
|
let originalStartUrl = startUrl;
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
} catch {
|
|
1227
|
-
}
|
|
1228
|
-
const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
|
|
1229
|
-
const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
|
|
1230
|
-
const manifest = {
|
|
1231
|
-
version: 1,
|
|
1957
|
+
const existing = loadExistingRecording(manifestPath);
|
|
1958
|
+
if (existing.recordedAt) recordedAt = existing.recordedAt;
|
|
1959
|
+
if (existing.startUrl) originalStartUrl = existing.startUrl;
|
|
1960
|
+
const manifest = createRecordingManifest({
|
|
1232
1961
|
recordedAt,
|
|
1233
1962
|
sessionId: recording.sessionId,
|
|
1234
1963
|
startUrl: originalStartUrl,
|
|
1235
1964
|
endUrl,
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
success,
|
|
1241
|
-
|
|
1242
|
-
|
|
1965
|
+
targetId: this.page.targetId,
|
|
1966
|
+
steps,
|
|
1967
|
+
frames: recording.frames,
|
|
1968
|
+
traceEvents: recording.traceEvents,
|
|
1969
|
+
notes: success ? [] : ["Replay ended with at least one failed action."],
|
|
1970
|
+
recordingManifest: "recording.json",
|
|
1971
|
+
screenshotDir: "screenshots/"
|
|
1972
|
+
});
|
|
1243
1973
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1244
1974
|
return manifestPath;
|
|
1245
1975
|
}
|
|
@@ -1522,6 +2252,39 @@ var BatchExecutor = class {
|
|
|
1522
2252
|
}
|
|
1523
2253
|
return { selectorUsed: usedSelector, value: actual };
|
|
1524
2254
|
}
|
|
2255
|
+
case "waitForWsMessage": {
|
|
2256
|
+
if (typeof step.match !== "string") {
|
|
2257
|
+
throw new Error("waitForWsMessage requires match");
|
|
2258
|
+
}
|
|
2259
|
+
const message = await this.waitForWsMessage(step.match, step.where, timeout);
|
|
2260
|
+
return { value: message };
|
|
2261
|
+
}
|
|
2262
|
+
case "assertNoConsoleErrors": {
|
|
2263
|
+
await this.assertNoConsoleErrors(step.windowMs ?? timeout);
|
|
2264
|
+
return {};
|
|
2265
|
+
}
|
|
2266
|
+
case "assertTextChanged": {
|
|
2267
|
+
const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
|
|
2268
|
+
if (typeof step.to !== "string") {
|
|
2269
|
+
throw new Error("assertTextChanged requires to");
|
|
2270
|
+
}
|
|
2271
|
+
const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
|
|
2272
|
+
return { selectorUsed: selector, text };
|
|
2273
|
+
}
|
|
2274
|
+
case "assertPermission": {
|
|
2275
|
+
if (!step.name || !step.state) {
|
|
2276
|
+
throw new Error("assertPermission requires name and state");
|
|
2277
|
+
}
|
|
2278
|
+
const permission = await this.assertPermission(step.name, step.state);
|
|
2279
|
+
return { value: permission };
|
|
2280
|
+
}
|
|
2281
|
+
case "assertMediaTrackLive": {
|
|
2282
|
+
if (!step.kind) {
|
|
2283
|
+
throw new Error("assertMediaTrackLive requires kind");
|
|
2284
|
+
}
|
|
2285
|
+
const media = await this.assertMediaTrackLive(step.kind);
|
|
2286
|
+
return { value: media };
|
|
2287
|
+
}
|
|
1525
2288
|
default: {
|
|
1526
2289
|
const action = step.action;
|
|
1527
2290
|
const aliases = {
|
|
@@ -1575,7 +2338,7 @@ var BatchExecutor = class {
|
|
|
1575
2338
|
};
|
|
1576
2339
|
const suggestion = aliases[action.toLowerCase()];
|
|
1577
2340
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
1578
|
-
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";
|
|
2341
|
+
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";
|
|
1579
2342
|
throw new Error(`Unknown action "${action}".${hint}
|
|
1580
2343
|
|
|
1581
2344
|
Valid actions: ${valid}`);
|
|
@@ -1591,6 +2354,237 @@ Valid actions: ${valid}`);
|
|
|
1591
2354
|
if (matched) return matched;
|
|
1592
2355
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
1593
2356
|
}
|
|
2357
|
+
async ensureTraceHooks() {
|
|
2358
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2359
|
+
await this.page.cdpClient.send("Page.enable");
|
|
2360
|
+
await this.page.cdpClient.send("Network.enable");
|
|
2361
|
+
try {
|
|
2362
|
+
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
2363
|
+
} catch {
|
|
2364
|
+
}
|
|
2365
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2366
|
+
source: TRACE_SCRIPT
|
|
2367
|
+
});
|
|
2368
|
+
await this.page.cdpClient.send("Runtime.evaluate", {
|
|
2369
|
+
expression: TRACE_SCRIPT,
|
|
2370
|
+
awaitPromise: false
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
async waitForWsMessage(match, where, timeout) {
|
|
2374
|
+
await this.ensureTraceHooks();
|
|
2375
|
+
const regex = globToRegex(match);
|
|
2376
|
+
const wsUrls = /* @__PURE__ */ new Map();
|
|
2377
|
+
const recentMatch = await this.findRecentWsMessage(regex, where);
|
|
2378
|
+
if (recentMatch) {
|
|
2379
|
+
return recentMatch;
|
|
2380
|
+
}
|
|
2381
|
+
return new Promise((resolve, reject) => {
|
|
2382
|
+
const cleanup = () => {
|
|
2383
|
+
this.page.cdpClient.off("Network.webSocketCreated", onCreated);
|
|
2384
|
+
this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
|
|
2385
|
+
this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
|
|
2386
|
+
clearTimeout(timer);
|
|
2387
|
+
};
|
|
2388
|
+
const onCreated = (params) => {
|
|
2389
|
+
wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
|
|
2390
|
+
};
|
|
2391
|
+
const onFrame = (params) => {
|
|
2392
|
+
const requestId = readStringOr(params["requestId"]);
|
|
2393
|
+
const response = params["response"] ?? {};
|
|
2394
|
+
const payload = response.payloadData ?? "";
|
|
2395
|
+
const url = wsUrls.get(requestId) ?? "";
|
|
2396
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
cleanup();
|
|
2403
|
+
resolve({ requestId, url, payload });
|
|
2404
|
+
};
|
|
2405
|
+
const onBinding = (params) => {
|
|
2406
|
+
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
try {
|
|
2410
|
+
const parsed = JSON.parse(readStringOr(params["payload"]));
|
|
2411
|
+
if (parsed.event !== "ws.frame.received") {
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
const data = parsed.data ?? {};
|
|
2415
|
+
const payload = readStringOr(data["payload"]);
|
|
2416
|
+
const url = readStringOr(data["url"]);
|
|
2417
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
cleanup();
|
|
2424
|
+
resolve({
|
|
2425
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2426
|
+
url,
|
|
2427
|
+
payload
|
|
2428
|
+
});
|
|
2429
|
+
} catch {
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
const timer = setTimeout(() => {
|
|
2433
|
+
cleanup();
|
|
2434
|
+
reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
|
|
2435
|
+
}, timeout);
|
|
2436
|
+
this.page.cdpClient.on("Network.webSocketCreated", onCreated);
|
|
2437
|
+
this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
|
|
2438
|
+
this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
payloadMatchesWhere(payload, where) {
|
|
2442
|
+
try {
|
|
2443
|
+
const parsed = JSON.parse(payload);
|
|
2444
|
+
return Object.entries(where).every(([key, expected]) => {
|
|
2445
|
+
const actual = key.split(".").reduce((current, part) => {
|
|
2446
|
+
if (!current || typeof current !== "object") {
|
|
2447
|
+
return void 0;
|
|
2448
|
+
}
|
|
2449
|
+
return current[part];
|
|
2450
|
+
}, parsed);
|
|
2451
|
+
return actual === expected;
|
|
2452
|
+
});
|
|
2453
|
+
} catch {
|
|
2454
|
+
return false;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
async findRecentWsMessage(regex, where) {
|
|
2458
|
+
const recent = await this.page.evaluate(
|
|
2459
|
+
"(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
|
|
2460
|
+
);
|
|
2461
|
+
if (!Array.isArray(recent)) {
|
|
2462
|
+
return null;
|
|
2463
|
+
}
|
|
2464
|
+
for (let i = recent.length - 1; i >= 0; i--) {
|
|
2465
|
+
const entry = recent[i];
|
|
2466
|
+
if (!entry || typeof entry !== "object") {
|
|
2467
|
+
continue;
|
|
2468
|
+
}
|
|
2469
|
+
const record = entry;
|
|
2470
|
+
const event = readStringOr(record["event"]);
|
|
2471
|
+
if (event !== "ws.frame.received") {
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2474
|
+
const data = record["data"] ?? {};
|
|
2475
|
+
const payload = readStringOr(data["payload"]);
|
|
2476
|
+
const url = readStringOr(data["url"]);
|
|
2477
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
return {
|
|
2484
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2485
|
+
url,
|
|
2486
|
+
payload
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
return null;
|
|
2490
|
+
}
|
|
2491
|
+
async assertNoConsoleErrors(windowMs) {
|
|
2492
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2493
|
+
return new Promise((resolve, reject) => {
|
|
2494
|
+
const errors = [];
|
|
2495
|
+
const cleanup = () => {
|
|
2496
|
+
this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
|
|
2497
|
+
this.page.cdpClient.off("Runtime.exceptionThrown", onException);
|
|
2498
|
+
clearTimeout(timer);
|
|
2499
|
+
};
|
|
2500
|
+
const onConsole = (params) => {
|
|
2501
|
+
if (params["type"] !== "error") {
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2505
|
+
errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
|
|
2506
|
+
};
|
|
2507
|
+
const onException = (params) => {
|
|
2508
|
+
const details = params["exceptionDetails"] ?? {};
|
|
2509
|
+
errors.push(readString(details["text"]) ?? "Runtime exception");
|
|
2510
|
+
};
|
|
2511
|
+
const timer = setTimeout(() => {
|
|
2512
|
+
cleanup();
|
|
2513
|
+
if (errors.length > 0) {
|
|
2514
|
+
reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
resolve();
|
|
2518
|
+
}, windowMs);
|
|
2519
|
+
this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
|
|
2520
|
+
this.page.cdpClient.on("Runtime.exceptionThrown", onException);
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
async assertTextChanged(selector, from, to, timeout) {
|
|
2524
|
+
const initialText = from ?? await this.page.text(selector);
|
|
2525
|
+
const deadline = Date.now() + timeout;
|
|
2526
|
+
while (Date.now() < deadline) {
|
|
2527
|
+
const text = await this.page.text(selector);
|
|
2528
|
+
if (text !== initialText && text.includes(to)) {
|
|
2529
|
+
return text;
|
|
2530
|
+
}
|
|
2531
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2532
|
+
}
|
|
2533
|
+
throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
|
|
2534
|
+
}
|
|
2535
|
+
async assertPermission(name, state) {
|
|
2536
|
+
const result = await this.page.evaluate(
|
|
2537
|
+
`(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
|
|
2538
|
+
);
|
|
2539
|
+
if (!result || typeof result !== "object" || result.state !== state) {
|
|
2540
|
+
throw new Error(`Permission ${name} is not ${state}`);
|
|
2541
|
+
}
|
|
2542
|
+
return result;
|
|
2543
|
+
}
|
|
2544
|
+
async assertMediaTrackLive(kind) {
|
|
2545
|
+
const result = await this.page.evaluate(
|
|
2546
|
+
`(() => {
|
|
2547
|
+
const requestedKind = ${JSON.stringify(kind)};
|
|
2548
|
+
const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
|
|
2549
|
+
const tracks = [];
|
|
2550
|
+
if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
|
|
2551
|
+
tracks.push(...el.srcObject.getTracks());
|
|
2552
|
+
}
|
|
2553
|
+
return {
|
|
2554
|
+
tag: el.tagName.toLowerCase(),
|
|
2555
|
+
paused: !!el.paused,
|
|
2556
|
+
tracks: tracks.map((track) => ({
|
|
2557
|
+
kind: track.kind,
|
|
2558
|
+
readyState: track.readyState,
|
|
2559
|
+
enabled: track.enabled,
|
|
2560
|
+
label: track.label,
|
|
2561
|
+
})),
|
|
2562
|
+
};
|
|
2563
|
+
});
|
|
2564
|
+
|
|
2565
|
+
const globalTracks =
|
|
2566
|
+
window.__bpStream && typeof window.__bpStream.getTracks === 'function'
|
|
2567
|
+
? window.__bpStream.getTracks().map((track) => ({
|
|
2568
|
+
kind: track.kind,
|
|
2569
|
+
readyState: track.readyState,
|
|
2570
|
+
enabled: track.enabled,
|
|
2571
|
+
label: track.label,
|
|
2572
|
+
}))
|
|
2573
|
+
: [];
|
|
2574
|
+
|
|
2575
|
+
const liveTracks = mediaElements
|
|
2576
|
+
.flatMap((entry) => entry.tracks)
|
|
2577
|
+
.concat(globalTracks)
|
|
2578
|
+
.filter((track) => track.kind === requestedKind && track.readyState === 'live');
|
|
2579
|
+
|
|
2580
|
+
return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
|
|
2581
|
+
})()`
|
|
2582
|
+
);
|
|
2583
|
+
if (!result || typeof result !== "object" || !result.live) {
|
|
2584
|
+
throw new Error(`No live ${kind} media track detected`);
|
|
2585
|
+
}
|
|
2586
|
+
return result;
|
|
2587
|
+
}
|
|
1594
2588
|
};
|
|
1595
2589
|
function addBatchToPage(page) {
|
|
1596
2590
|
const executor = new BatchExecutor(page);
|
|
@@ -1721,7 +2715,7 @@ var ACTION_RULES = {
|
|
|
1721
2715
|
value: { type: "string|string[]" },
|
|
1722
2716
|
trigger: { type: "string|string[]" },
|
|
1723
2717
|
option: { type: "string|string[]" },
|
|
1724
|
-
match: { type: "string"
|
|
2718
|
+
match: { type: "string" }
|
|
1725
2719
|
}
|
|
1726
2720
|
},
|
|
1727
2721
|
check: {
|
|
@@ -1852,6 +2846,38 @@ var ACTION_RULES = {
|
|
|
1852
2846
|
expect: { type: "string" },
|
|
1853
2847
|
value: { type: "string" }
|
|
1854
2848
|
}
|
|
2849
|
+
},
|
|
2850
|
+
waitForWsMessage: {
|
|
2851
|
+
required: { match: { type: "string" } },
|
|
2852
|
+
optional: {
|
|
2853
|
+
where: { type: "object" }
|
|
2854
|
+
}
|
|
2855
|
+
},
|
|
2856
|
+
assertNoConsoleErrors: {
|
|
2857
|
+
required: {},
|
|
2858
|
+
optional: {
|
|
2859
|
+
windowMs: { type: "number" }
|
|
2860
|
+
}
|
|
2861
|
+
},
|
|
2862
|
+
assertTextChanged: {
|
|
2863
|
+
required: { to: { type: "string" } },
|
|
2864
|
+
optional: {
|
|
2865
|
+
selector: { type: "string|string[]" },
|
|
2866
|
+
from: { type: "string" }
|
|
2867
|
+
}
|
|
2868
|
+
},
|
|
2869
|
+
assertPermission: {
|
|
2870
|
+
required: {
|
|
2871
|
+
name: { type: "string" },
|
|
2872
|
+
state: { type: "string" }
|
|
2873
|
+
},
|
|
2874
|
+
optional: {}
|
|
2875
|
+
},
|
|
2876
|
+
assertMediaTrackLive: {
|
|
2877
|
+
required: {
|
|
2878
|
+
kind: { type: "string", enum: ["audio", "video"] }
|
|
2879
|
+
},
|
|
2880
|
+
optional: {}
|
|
1855
2881
|
}
|
|
1856
2882
|
};
|
|
1857
2883
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -1875,6 +2901,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1875
2901
|
"trigger",
|
|
1876
2902
|
"option",
|
|
1877
2903
|
"match",
|
|
2904
|
+
"where",
|
|
1878
2905
|
"x",
|
|
1879
2906
|
"y",
|
|
1880
2907
|
"direction",
|
|
@@ -1884,7 +2911,13 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1884
2911
|
"fullPage",
|
|
1885
2912
|
"expect",
|
|
1886
2913
|
"retry",
|
|
1887
|
-
"retryDelay"
|
|
2914
|
+
"retryDelay",
|
|
2915
|
+
"from",
|
|
2916
|
+
"to",
|
|
2917
|
+
"name",
|
|
2918
|
+
"state",
|
|
2919
|
+
"kind",
|
|
2920
|
+
"windowMs"
|
|
1888
2921
|
]);
|
|
1889
2922
|
function resolveAction(name) {
|
|
1890
2923
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -1957,6 +2990,11 @@ function checkFieldType(value, rule) {
|
|
|
1957
2990
|
return `expected boolean or "auto", got ${typeof value}`;
|
|
1958
2991
|
}
|
|
1959
2992
|
return null;
|
|
2993
|
+
case "object":
|
|
2994
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2995
|
+
return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
|
|
2996
|
+
}
|
|
2997
|
+
return null;
|
|
1960
2998
|
default: {
|
|
1961
2999
|
const _exhaustive = rule.type;
|
|
1962
3000
|
return `unknown type: ${_exhaustive}`;
|
|
@@ -2241,12 +3279,12 @@ function parseWavHeader(data) {
|
|
|
2241
3279
|
if (data.byteLength < 44) {
|
|
2242
3280
|
throw new Error("Invalid WAV: file too small");
|
|
2243
3281
|
}
|
|
2244
|
-
const riff =
|
|
2245
|
-
const wave =
|
|
3282
|
+
const riff = readString2(view, 0, 4);
|
|
3283
|
+
const wave = readString2(view, 8, 4);
|
|
2246
3284
|
if (riff !== "RIFF" || wave !== "WAVE") {
|
|
2247
3285
|
throw new Error("Invalid WAV: missing RIFF/WAVE header");
|
|
2248
3286
|
}
|
|
2249
|
-
const fmt =
|
|
3287
|
+
const fmt = readString2(view, 12, 4);
|
|
2250
3288
|
if (fmt !== "fmt ") {
|
|
2251
3289
|
throw new Error("Invalid WAV: missing fmt chunk");
|
|
2252
3290
|
}
|
|
@@ -2255,7 +3293,7 @@ function parseWavHeader(data) {
|
|
|
2255
3293
|
const bitsPerSample = view.getUint16(34, true);
|
|
2256
3294
|
let dataOffset = 36;
|
|
2257
3295
|
while (dataOffset < data.byteLength - 8) {
|
|
2258
|
-
const chunkId =
|
|
3296
|
+
const chunkId = readString2(view, dataOffset, 4);
|
|
2259
3297
|
const chunkSize = view.getUint32(dataOffset + 4, true);
|
|
2260
3298
|
if (chunkId === "data") {
|
|
2261
3299
|
return {
|
|
@@ -2286,7 +3324,7 @@ function writeString(view, offset, str) {
|
|
|
2286
3324
|
view.setUint8(offset + i, str.charCodeAt(i));
|
|
2287
3325
|
}
|
|
2288
3326
|
}
|
|
2289
|
-
function
|
|
3327
|
+
function readString2(view, offset, length) {
|
|
2290
3328
|
let str = "";
|
|
2291
3329
|
for (let i = 0; i < length; i++) {
|
|
2292
3330
|
str += String.fromCharCode(view.getUint8(offset + i));
|
|
@@ -2320,6 +3358,10 @@ async function grantAudioPermissions(cdp, origin) {
|
|
|
2320
3358
|
await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2321
3359
|
source: PERMISSIONS_OVERRIDE_SCRIPT
|
|
2322
3360
|
});
|
|
3361
|
+
await cdp.send("Runtime.evaluate", {
|
|
3362
|
+
expression: PERMISSIONS_OVERRIDE_SCRIPT,
|
|
3363
|
+
awaitPromise: false
|
|
3364
|
+
});
|
|
2323
3365
|
}
|
|
2324
3366
|
var PERMISSIONS_OVERRIDE_SCRIPT = `
|
|
2325
3367
|
(function() {
|
|
@@ -3756,7 +4798,8 @@ function buildCDPClient(transport, options = {}) {
|
|
|
3756
4798
|
pending.delete(response.id);
|
|
3757
4799
|
clearTimeout(request.timer);
|
|
3758
4800
|
if (response.error) {
|
|
3759
|
-
|
|
4801
|
+
const error = typeof response.error === "string" ? { code: -32e3, message: response.error } : response.error;
|
|
4802
|
+
request.reject(new CDPError(error));
|
|
3760
4803
|
} else {
|
|
3761
4804
|
request.resolve(response.result);
|
|
3762
4805
|
}
|
|
@@ -3872,6 +4915,9 @@ function buildCDPClient(transport, options = {}) {
|
|
|
3872
4915
|
get sessionId() {
|
|
3873
4916
|
return currentSessionId;
|
|
3874
4917
|
},
|
|
4918
|
+
setSessionId(sessionId) {
|
|
4919
|
+
currentSessionId = sessionId;
|
|
4920
|
+
},
|
|
3875
4921
|
get isConnected() {
|
|
3876
4922
|
return connected;
|
|
3877
4923
|
}
|
|
@@ -4067,6 +5113,330 @@ async function getBrowserWebSocketUrl(host = "localhost:9222") {
|
|
|
4067
5113
|
return info.webSocketDebuggerUrl;
|
|
4068
5114
|
}
|
|
4069
5115
|
|
|
5116
|
+
// src/providers/local-discovery.ts
|
|
5117
|
+
var CHANNEL_ORDER = ["stable", "beta", "dev", "canary"];
|
|
5118
|
+
var DEFAULT_PROBE_TIMEOUT_MS = 1e3;
|
|
5119
|
+
var DevToolsActivePortParseError = class extends Error {
|
|
5120
|
+
constructor(message, reason) {
|
|
5121
|
+
super(message);
|
|
5122
|
+
this.reason = reason;
|
|
5123
|
+
this.name = "DevToolsActivePortParseError";
|
|
5124
|
+
}
|
|
5125
|
+
};
|
|
5126
|
+
function getRuntimeEnv() {
|
|
5127
|
+
if (typeof process === "undefined") {
|
|
5128
|
+
return {};
|
|
5129
|
+
}
|
|
5130
|
+
return process.env;
|
|
5131
|
+
}
|
|
5132
|
+
function getRuntimePlatform() {
|
|
5133
|
+
if (typeof process === "undefined") {
|
|
5134
|
+
return void 0;
|
|
5135
|
+
}
|
|
5136
|
+
return process.platform;
|
|
5137
|
+
}
|
|
5138
|
+
function normalizePlatform(platform) {
|
|
5139
|
+
if (platform === "darwin" || platform === "linux" || platform === "win32") {
|
|
5140
|
+
return platform;
|
|
5141
|
+
}
|
|
5142
|
+
throw new Error(`Unsupported platform: ${platform ?? "unknown"}`);
|
|
5143
|
+
}
|
|
5144
|
+
function trimTrailingSeparator(path) {
|
|
5145
|
+
return path.replace(/[\\/]+$/, "");
|
|
5146
|
+
}
|
|
5147
|
+
function joinPath(platform, ...parts) {
|
|
5148
|
+
const separator = platform === "win32" ? "\\" : "/";
|
|
5149
|
+
const cleaned = parts.map((part, index) => {
|
|
5150
|
+
if (index === 0) return trimTrailingSeparator(part);
|
|
5151
|
+
return part.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
5152
|
+
}).filter((part) => part.length > 0);
|
|
5153
|
+
return cleaned.join(separator);
|
|
5154
|
+
}
|
|
5155
|
+
function resolveHomeDir(platform, env, explicitHomeDir) {
|
|
5156
|
+
if (explicitHomeDir) {
|
|
5157
|
+
return explicitHomeDir;
|
|
5158
|
+
}
|
|
5159
|
+
if (platform === "win32") {
|
|
5160
|
+
return env["USERPROFILE"] ?? env["HOME"] ?? "";
|
|
5161
|
+
}
|
|
5162
|
+
return env["HOME"] ?? env["USERPROFILE"] ?? "";
|
|
5163
|
+
}
|
|
5164
|
+
function toFileFailure(target, error) {
|
|
5165
|
+
const errno = error?.code;
|
|
5166
|
+
if (errno === "ENOENT") {
|
|
5167
|
+
return {
|
|
5168
|
+
...target,
|
|
5169
|
+
reason: "missing-file",
|
|
5170
|
+
message: `DevToolsActivePort not found at ${target.portFile}`
|
|
5171
|
+
};
|
|
5172
|
+
}
|
|
5173
|
+
return {
|
|
5174
|
+
...target,
|
|
5175
|
+
reason: "unreadable-file",
|
|
5176
|
+
message: error instanceof Error ? error.message : `Could not read DevToolsActivePort at ${target.portFile}`
|
|
5177
|
+
};
|
|
5178
|
+
}
|
|
5179
|
+
function toProbeFailure(target, wsUrl, error) {
|
|
5180
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5181
|
+
const lowerMessage = message.toLowerCase();
|
|
5182
|
+
let reason = "connection-error";
|
|
5183
|
+
if (lowerMessage.includes("refused") || lowerMessage.includes("econnrefused")) {
|
|
5184
|
+
reason = "connection-refused";
|
|
5185
|
+
} else if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
|
|
5186
|
+
reason = "connection-timeout";
|
|
5187
|
+
} else if (lowerMessage.includes("closed")) {
|
|
5188
|
+
reason = "unexpected-close";
|
|
5189
|
+
} else if (lowerMessage.includes("browser.getversion") || lowerMessage.includes("cdp") || lowerMessage.includes("protocol")) {
|
|
5190
|
+
reason = "cdp-error";
|
|
5191
|
+
}
|
|
5192
|
+
return {
|
|
5193
|
+
...target,
|
|
5194
|
+
wsUrl,
|
|
5195
|
+
reason,
|
|
5196
|
+
message
|
|
5197
|
+
};
|
|
5198
|
+
}
|
|
5199
|
+
async function readTextFile(path) {
|
|
5200
|
+
const fs2 = await import("fs/promises");
|
|
5201
|
+
return fs2.readFile(path, "utf-8");
|
|
5202
|
+
}
|
|
5203
|
+
async function probeBrowserWebSocket(wsUrl, timeoutMs) {
|
|
5204
|
+
let client;
|
|
5205
|
+
try {
|
|
5206
|
+
client = await createCDPClient(wsUrl, { timeout: timeoutMs });
|
|
5207
|
+
const version = await client.send("Browser.getVersion", void 0, null);
|
|
5208
|
+
return { browserVersion: version.product };
|
|
5209
|
+
} finally {
|
|
5210
|
+
await client?.close().catch(() => {
|
|
5211
|
+
});
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
var defaultDependencies = {
|
|
5215
|
+
readTextFile,
|
|
5216
|
+
probeBrowserWebSocket,
|
|
5217
|
+
getLegacyBrowserWebSocketUrl: getBrowserWebSocketUrl
|
|
5218
|
+
};
|
|
5219
|
+
function resolveChromeUserDataDirs(options = {}) {
|
|
5220
|
+
const env = options.env ?? getRuntimeEnv();
|
|
5221
|
+
const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
|
|
5222
|
+
const homeDir = resolveHomeDir(platform, env, options.homeDir);
|
|
5223
|
+
if (!homeDir) {
|
|
5224
|
+
throw new Error("Could not determine home directory for local Chrome discovery");
|
|
5225
|
+
}
|
|
5226
|
+
switch (platform) {
|
|
5227
|
+
case "darwin": {
|
|
5228
|
+
const base = joinPath(platform, homeDir, "Library", "Application Support", "Google");
|
|
5229
|
+
return {
|
|
5230
|
+
stable: joinPath(platform, base, "Chrome"),
|
|
5231
|
+
beta: joinPath(platform, base, "Chrome Beta"),
|
|
5232
|
+
dev: joinPath(platform, base, "Chrome Dev"),
|
|
5233
|
+
canary: joinPath(platform, base, "Chrome Canary")
|
|
5234
|
+
};
|
|
5235
|
+
}
|
|
5236
|
+
case "linux": {
|
|
5237
|
+
const configHome = env["CHROME_CONFIG_HOME"] ?? env["XDG_CONFIG_HOME"] ?? joinPath(platform, homeDir, ".config");
|
|
5238
|
+
return {
|
|
5239
|
+
stable: joinPath(platform, configHome, "google-chrome"),
|
|
5240
|
+
beta: joinPath(platform, configHome, "google-chrome-beta"),
|
|
5241
|
+
dev: joinPath(platform, configHome, "google-chrome-dev"),
|
|
5242
|
+
canary: joinPath(platform, configHome, "google-chrome-canary")
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5245
|
+
case "win32": {
|
|
5246
|
+
const localAppData = env["LOCALAPPDATA"] ?? joinPath(platform, homeDir, "AppData", "Local");
|
|
5247
|
+
const base = joinPath(platform, localAppData, "Google");
|
|
5248
|
+
return {
|
|
5249
|
+
stable: joinPath(platform, base, "Chrome", "User Data"),
|
|
5250
|
+
beta: joinPath(platform, base, "Chrome Beta", "User Data"),
|
|
5251
|
+
dev: joinPath(platform, base, "Chrome Dev", "User Data"),
|
|
5252
|
+
canary: joinPath(platform, base, "Chrome SxS", "User Data")
|
|
5253
|
+
};
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
throw new Error(`Unsupported platform for local Chrome discovery: ${platform}`);
|
|
5257
|
+
}
|
|
5258
|
+
function buildLocalBrowserScanTargets(options = {}) {
|
|
5259
|
+
const env = options.env ?? getRuntimeEnv();
|
|
5260
|
+
const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
|
|
5261
|
+
if (options.userDataDir) {
|
|
5262
|
+
return [
|
|
5263
|
+
{
|
|
5264
|
+
channel: options.channel ?? "custom",
|
|
5265
|
+
userDataDir: options.userDataDir,
|
|
5266
|
+
portFile: joinPath(platform, options.userDataDir, "DevToolsActivePort")
|
|
5267
|
+
}
|
|
5268
|
+
];
|
|
5269
|
+
}
|
|
5270
|
+
const dirs = resolveChromeUserDataDirs({
|
|
5271
|
+
platform,
|
|
5272
|
+
env,
|
|
5273
|
+
homeDir: options.homeDir
|
|
5274
|
+
});
|
|
5275
|
+
const channels = options.channel ? [options.channel] : CHANNEL_ORDER;
|
|
5276
|
+
return channels.map((channel) => ({
|
|
5277
|
+
channel,
|
|
5278
|
+
userDataDir: dirs[channel],
|
|
5279
|
+
portFile: joinPath(platform, dirs[channel], "DevToolsActivePort")
|
|
5280
|
+
}));
|
|
5281
|
+
}
|
|
5282
|
+
function parseDevToolsActivePortFile(content) {
|
|
5283
|
+
const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
5284
|
+
if (lines.length !== 2) {
|
|
5285
|
+
throw new DevToolsActivePortParseError(
|
|
5286
|
+
`Expected exactly 2 non-empty lines in DevToolsActivePort, got ${lines.length}`,
|
|
5287
|
+
"malformed-file"
|
|
5288
|
+
);
|
|
5289
|
+
}
|
|
5290
|
+
const portText = lines[0];
|
|
5291
|
+
const browserPath = lines[1];
|
|
5292
|
+
const port = Number.parseInt(portText, 10);
|
|
5293
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5294
|
+
throw new DevToolsActivePortParseError(
|
|
5295
|
+
`Invalid DevToolsActivePort port: ${portText}`,
|
|
5296
|
+
"invalid-port"
|
|
5297
|
+
);
|
|
5298
|
+
}
|
|
5299
|
+
if (!browserPath.startsWith("/devtools/browser/") || browserPath.includes("..") || /[?#\s\\]/u.test(browserPath)) {
|
|
5300
|
+
throw new DevToolsActivePortParseError(
|
|
5301
|
+
`Invalid DevToolsActivePort browser path: ${browserPath}`,
|
|
5302
|
+
"invalid-path"
|
|
5303
|
+
);
|
|
5304
|
+
}
|
|
5305
|
+
return {
|
|
5306
|
+
port,
|
|
5307
|
+
browserPath,
|
|
5308
|
+
wsUrl: `ws://127.0.0.1:${port}${browserPath}`
|
|
5309
|
+
};
|
|
5310
|
+
}
|
|
5311
|
+
async function inspectScanTarget(target, options, deps) {
|
|
5312
|
+
let content;
|
|
5313
|
+
try {
|
|
5314
|
+
content = await deps.readTextFile(target.portFile);
|
|
5315
|
+
} catch (error) {
|
|
5316
|
+
return { kind: "failure", failure: toFileFailure(target, error) };
|
|
5317
|
+
}
|
|
5318
|
+
let parsed;
|
|
5319
|
+
try {
|
|
5320
|
+
parsed = parseDevToolsActivePortFile(content);
|
|
5321
|
+
} catch (error) {
|
|
5322
|
+
if (error instanceof DevToolsActivePortParseError) {
|
|
5323
|
+
return {
|
|
5324
|
+
kind: "failure",
|
|
5325
|
+
failure: {
|
|
5326
|
+
...target,
|
|
5327
|
+
reason: error.reason,
|
|
5328
|
+
message: error.message
|
|
5329
|
+
}
|
|
5330
|
+
};
|
|
5331
|
+
}
|
|
5332
|
+
throw error;
|
|
5333
|
+
}
|
|
5334
|
+
try {
|
|
5335
|
+
const probe = await deps.probeBrowserWebSocket(
|
|
5336
|
+
parsed.wsUrl,
|
|
5337
|
+
options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
|
|
5338
|
+
);
|
|
5339
|
+
return {
|
|
5340
|
+
kind: "candidate",
|
|
5341
|
+
candidate: {
|
|
5342
|
+
...target,
|
|
5343
|
+
port: parsed.port,
|
|
5344
|
+
browserPath: parsed.browserPath,
|
|
5345
|
+
wsUrl: parsed.wsUrl,
|
|
5346
|
+
browserVersion: probe.browserVersion
|
|
5347
|
+
}
|
|
5348
|
+
};
|
|
5349
|
+
} catch (error) {
|
|
5350
|
+
return {
|
|
5351
|
+
kind: "failure",
|
|
5352
|
+
failure: toProbeFailure(target, parsed.wsUrl, error)
|
|
5353
|
+
};
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
async function discoverLocalBrowsers(options = {}, deps = defaultDependencies) {
|
|
5357
|
+
const scanTargets = buildLocalBrowserScanTargets(options);
|
|
5358
|
+
const outcomes = await Promise.all(
|
|
5359
|
+
scanTargets.map((target) => inspectScanTarget(target, options, deps))
|
|
5360
|
+
);
|
|
5361
|
+
const candidates = [];
|
|
5362
|
+
const failures = [];
|
|
5363
|
+
for (const outcome of outcomes) {
|
|
5364
|
+
if (outcome.kind === "candidate") {
|
|
5365
|
+
candidates.push(outcome.candidate);
|
|
5366
|
+
} else {
|
|
5367
|
+
failures.push(outcome.failure);
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
return { candidates, failures };
|
|
5371
|
+
}
|
|
5372
|
+
var BrowserEndpointResolutionError = class extends Error {
|
|
5373
|
+
constructor(code, message, details = {}) {
|
|
5374
|
+
super(message);
|
|
5375
|
+
this.code = code;
|
|
5376
|
+
this.details = details;
|
|
5377
|
+
}
|
|
5378
|
+
name = "BrowserEndpointResolutionError";
|
|
5379
|
+
};
|
|
5380
|
+
async function resolveBrowserEndpoint(options = {}, deps = defaultDependencies) {
|
|
5381
|
+
if (options.explicitWsUrl) {
|
|
5382
|
+
return {
|
|
5383
|
+
wsUrl: options.explicitWsUrl,
|
|
5384
|
+
source: "explicit-ws"
|
|
5385
|
+
};
|
|
5386
|
+
}
|
|
5387
|
+
let localDiscovery;
|
|
5388
|
+
if (options.allowLocalDiscovery ?? true) {
|
|
5389
|
+
localDiscovery = await discoverLocalBrowsers(options, deps);
|
|
5390
|
+
if (localDiscovery.candidates.length === 1) {
|
|
5391
|
+
const candidate = localDiscovery.candidates[0];
|
|
5392
|
+
return {
|
|
5393
|
+
wsUrl: candidate.wsUrl,
|
|
5394
|
+
source: "devtools-active-port",
|
|
5395
|
+
channel: candidate.channel,
|
|
5396
|
+
userDataDir: candidate.userDataDir
|
|
5397
|
+
};
|
|
5398
|
+
}
|
|
5399
|
+
if (localDiscovery.candidates.length > 1) {
|
|
5400
|
+
throw new BrowserEndpointResolutionError(
|
|
5401
|
+
"multiple-local-browsers",
|
|
5402
|
+
"Multiple local Chrome profiles are available for auto-discovery",
|
|
5403
|
+
{
|
|
5404
|
+
candidates: localDiscovery.candidates,
|
|
5405
|
+
failures: localDiscovery.failures
|
|
5406
|
+
}
|
|
5407
|
+
);
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
if (options.allowLegacyHostFallback ?? true) {
|
|
5411
|
+
const legacyHost = options.legacyHost ?? "localhost:9222";
|
|
5412
|
+
try {
|
|
5413
|
+
return {
|
|
5414
|
+
wsUrl: await deps.getLegacyBrowserWebSocketUrl(legacyHost),
|
|
5415
|
+
source: "json-version"
|
|
5416
|
+
};
|
|
5417
|
+
} catch (error) {
|
|
5418
|
+
throw new BrowserEndpointResolutionError(
|
|
5419
|
+
"browser-not-found",
|
|
5420
|
+
"Could not resolve a browser endpoint",
|
|
5421
|
+
{
|
|
5422
|
+
candidates: localDiscovery?.candidates,
|
|
5423
|
+
failures: localDiscovery?.failures,
|
|
5424
|
+
legacyError: error instanceof Error ? error : new Error(String(error)),
|
|
5425
|
+
legacyHost
|
|
5426
|
+
}
|
|
5427
|
+
);
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5430
|
+
throw new BrowserEndpointResolutionError(
|
|
5431
|
+
"browser-not-found",
|
|
5432
|
+
"Could not resolve a browser endpoint",
|
|
5433
|
+
{
|
|
5434
|
+
candidates: localDiscovery?.candidates,
|
|
5435
|
+
failures: localDiscovery?.failures
|
|
5436
|
+
}
|
|
5437
|
+
);
|
|
5438
|
+
}
|
|
5439
|
+
|
|
4070
5440
|
// src/providers/index.ts
|
|
4071
5441
|
function createProvider(options) {
|
|
4072
5442
|
switch (options.provider) {
|
|
@@ -8161,13 +9531,26 @@ var Browser = class _Browser {
|
|
|
8161
9531
|
* Connect to a browser instance
|
|
8162
9532
|
*/
|
|
8163
9533
|
static async connect(options) {
|
|
8164
|
-
|
|
8165
|
-
|
|
9534
|
+
let connectOptions = options;
|
|
9535
|
+
if (options.provider === "generic" && !options.wsUrl) {
|
|
9536
|
+
const endpoint = await resolveBrowserEndpoint({
|
|
9537
|
+
channel: options.channel,
|
|
9538
|
+
userDataDir: options.userDataDir,
|
|
9539
|
+
allowLocalDiscovery: true,
|
|
9540
|
+
allowLegacyHostFallback: true
|
|
9541
|
+
});
|
|
9542
|
+
connectOptions = {
|
|
9543
|
+
...options,
|
|
9544
|
+
wsUrl: endpoint.wsUrl
|
|
9545
|
+
};
|
|
9546
|
+
}
|
|
9547
|
+
const provider = createProvider(connectOptions);
|
|
9548
|
+
const session = await provider.createSession(connectOptions.session);
|
|
8166
9549
|
const cdp = await createCDPClient(session.wsUrl, {
|
|
8167
|
-
debug:
|
|
8168
|
-
timeout:
|
|
9550
|
+
debug: connectOptions.debug,
|
|
9551
|
+
timeout: connectOptions.timeout
|
|
8169
9552
|
});
|
|
8170
|
-
return new _Browser(cdp, provider, session,
|
|
9553
|
+
return new _Browser(cdp, provider, session, connectOptions);
|
|
8171
9554
|
}
|
|
8172
9555
|
/**
|
|
8173
9556
|
* Get or create a page by name.
|
|
@@ -8573,6 +9956,7 @@ function disableTracing() {
|
|
|
8573
9956
|
BatchExecutor,
|
|
8574
9957
|
Browser,
|
|
8575
9958
|
BrowserBaseProvider,
|
|
9959
|
+
BrowserEndpointResolutionError,
|
|
8576
9960
|
BrowserlessProvider,
|
|
8577
9961
|
CDPError,
|
|
8578
9962
|
ElementNotFoundError,
|
|
@@ -8584,12 +9968,14 @@ function disableTracing() {
|
|
|
8584
9968
|
Tracer,
|
|
8585
9969
|
addBatchToPage,
|
|
8586
9970
|
bufferToBase64,
|
|
9971
|
+
buildLocalBrowserScanTargets,
|
|
8587
9972
|
calculateRMS,
|
|
8588
9973
|
connect,
|
|
8589
9974
|
createCDPClient,
|
|
8590
9975
|
createProvider,
|
|
8591
9976
|
devices,
|
|
8592
9977
|
disableTracing,
|
|
9978
|
+
discoverLocalBrowsers,
|
|
8593
9979
|
discoverTargets,
|
|
8594
9980
|
enableTracing,
|
|
8595
9981
|
generateSilence,
|
|
@@ -8599,8 +9985,11 @@ function disableTracing() {
|
|
|
8599
9985
|
getTracer,
|
|
8600
9986
|
grantAudioPermissions,
|
|
8601
9987
|
isTranscriptionAvailable,
|
|
9988
|
+
parseDevToolsActivePortFile,
|
|
8602
9989
|
parseWavHeader,
|
|
8603
9990
|
pcmToWav,
|
|
9991
|
+
resolveBrowserEndpoint,
|
|
9992
|
+
resolveChromeUserDataDirs,
|
|
8604
9993
|
transcribe,
|
|
8605
9994
|
validateSteps,
|
|
8606
9995
|
waitForAnyElement,
|