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
|
@@ -838,6 +838,624 @@ var NavigationError = class extends Error {
|
|
|
838
838
|
}
|
|
839
839
|
};
|
|
840
840
|
|
|
841
|
+
// src/trace/views.ts
|
|
842
|
+
function takeRecent(events, limit = 5) {
|
|
843
|
+
return events.slice(-limit).map((event) => ({
|
|
844
|
+
ts: event.ts,
|
|
845
|
+
event: event.event,
|
|
846
|
+
summary: event.summary,
|
|
847
|
+
severity: event.severity,
|
|
848
|
+
url: event.url
|
|
849
|
+
}));
|
|
850
|
+
}
|
|
851
|
+
function buildTraceSummaries(events) {
|
|
852
|
+
return {
|
|
853
|
+
ws: summarizeWs(events),
|
|
854
|
+
voice: summarizeVoice(events),
|
|
855
|
+
console: summarizeConsole(events),
|
|
856
|
+
permissions: summarizePermissions(events),
|
|
857
|
+
media: summarizeMedia(events),
|
|
858
|
+
ui: summarizeUi(events),
|
|
859
|
+
session: summarizeSession(events)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function summarizeWs(events) {
|
|
863
|
+
const relevant = events.filter(
|
|
864
|
+
(event) => event.channel === "ws" || event.event.startsWith("ws.")
|
|
865
|
+
);
|
|
866
|
+
const connections = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const event of relevant) {
|
|
868
|
+
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
869
|
+
let connection = connections.get(id);
|
|
870
|
+
if (!connection) {
|
|
871
|
+
connection = { id, sent: 0, received: 0, lastMessages: [] };
|
|
872
|
+
connections.set(id, connection);
|
|
873
|
+
}
|
|
874
|
+
connection.url = event.url ?? connection.url;
|
|
875
|
+
if (event.event === "ws.connection.created") {
|
|
876
|
+
connection.createdAt = event.ts;
|
|
877
|
+
}
|
|
878
|
+
if (event.event === "ws.connection.closed") {
|
|
879
|
+
connection.closedAt = event.ts;
|
|
880
|
+
}
|
|
881
|
+
if (event.event === "ws.frame.sent") {
|
|
882
|
+
connection.sent += 1;
|
|
883
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
884
|
+
if (payload) connection.lastMessages.push(`sent: ${payload}`);
|
|
885
|
+
}
|
|
886
|
+
if (event.event === "ws.frame.received") {
|
|
887
|
+
connection.received += 1;
|
|
888
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
889
|
+
if (payload) connection.lastMessages.push(`recv: ${payload}`);
|
|
890
|
+
}
|
|
891
|
+
connection.lastMessages = connection.lastMessages.slice(-3);
|
|
892
|
+
}
|
|
893
|
+
const values = [...connections.values()];
|
|
894
|
+
const reconnects = values.reduce((count, connection) => {
|
|
895
|
+
return connection.closedAt && !connection.createdAt ? count + 1 : count;
|
|
896
|
+
}, 0);
|
|
897
|
+
return {
|
|
898
|
+
view: "ws",
|
|
899
|
+
totalEvents: relevant.length,
|
|
900
|
+
connections: values.map((connection) => ({
|
|
901
|
+
id: connection.id,
|
|
902
|
+
url: connection.url ?? null,
|
|
903
|
+
createdAt: connection.createdAt ?? null,
|
|
904
|
+
closedAt: connection.closedAt ?? null,
|
|
905
|
+
sent: connection.sent,
|
|
906
|
+
received: connection.received,
|
|
907
|
+
lastMessages: connection.lastMessages,
|
|
908
|
+
connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
|
|
909
|
+
})),
|
|
910
|
+
reconnects,
|
|
911
|
+
recent: takeRecent(relevant)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function summarizeConsole(events) {
|
|
915
|
+
const relevant = events.filter(
|
|
916
|
+
(event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
917
|
+
);
|
|
918
|
+
return {
|
|
919
|
+
view: "console",
|
|
920
|
+
errors: relevant.filter(
|
|
921
|
+
(event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
922
|
+
).length,
|
|
923
|
+
warnings: relevant.filter((event) => event.event === "console.warn").length,
|
|
924
|
+
logs: relevant.filter((event) => event.event === "console.log").length,
|
|
925
|
+
recent: takeRecent(relevant)
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function summarizePermissions(events) {
|
|
929
|
+
const relevant = events.filter(
|
|
930
|
+
(event) => event.channel === "permission" || event.event.startsWith("permission.")
|
|
931
|
+
);
|
|
932
|
+
const latest = /* @__PURE__ */ new Map();
|
|
933
|
+
for (const event of relevant) {
|
|
934
|
+
const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
|
|
935
|
+
const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
|
|
936
|
+
if (name && state) {
|
|
937
|
+
latest.set(name, state);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
view: "permissions",
|
|
942
|
+
states: Object.fromEntries(latest),
|
|
943
|
+
changes: relevant.filter((event) => event.event === "permission.changed").length,
|
|
944
|
+
recent: takeRecent(relevant)
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function summarizeMedia(events) {
|
|
948
|
+
const relevant = events.filter(
|
|
949
|
+
(event) => event.channel === "media" || event.event.startsWith("media.")
|
|
950
|
+
);
|
|
951
|
+
const liveTracks = /* @__PURE__ */ new Map();
|
|
952
|
+
for (const event of relevant) {
|
|
953
|
+
const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
|
|
954
|
+
const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
|
|
955
|
+
const key = `${kind}:${label}`;
|
|
956
|
+
if (event.event === "media.track.started") {
|
|
957
|
+
liveTracks.set(key, kind);
|
|
958
|
+
}
|
|
959
|
+
if (event.event === "media.track.ended") {
|
|
960
|
+
liveTracks.delete(key);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
view: "media",
|
|
965
|
+
tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
|
|
966
|
+
tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
|
|
967
|
+
playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
|
|
968
|
+
playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
|
|
969
|
+
liveTracks: [...liveTracks.values()],
|
|
970
|
+
recent: takeRecent(relevant)
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function summarizeVoice(events) {
|
|
974
|
+
const relevant = events.filter(
|
|
975
|
+
(event) => event.channel === "voice" || event.event.startsWith("voice.")
|
|
976
|
+
);
|
|
977
|
+
return {
|
|
978
|
+
view: "voice",
|
|
979
|
+
ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
|
|
980
|
+
notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
|
|
981
|
+
captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
|
|
982
|
+
captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
|
|
983
|
+
detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
|
|
984
|
+
recent: takeRecent(relevant)
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function summarizeUi(events) {
|
|
988
|
+
const relevant = events.filter(
|
|
989
|
+
(event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
|
|
990
|
+
);
|
|
991
|
+
return {
|
|
992
|
+
view: "ui",
|
|
993
|
+
actions: relevant.filter((event) => event.channel === "action").length,
|
|
994
|
+
domChanges: relevant.filter((event) => event.channel === "dom").length,
|
|
995
|
+
recent: takeRecent(relevant)
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function summarizeSession(events) {
|
|
999
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
1000
|
+
const failedActions = events.filter((event) => event.event === "action.failed").length;
|
|
1001
|
+
for (const event of events) {
|
|
1002
|
+
byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
view: "session",
|
|
1006
|
+
totalEvents: events.length,
|
|
1007
|
+
byChannel: Object.fromEntries(byChannel),
|
|
1008
|
+
failedActions,
|
|
1009
|
+
recent: takeRecent(events)
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/recording/manifest.ts
|
|
1014
|
+
function isCanonicalRecordingManifest(value) {
|
|
1015
|
+
return Boolean(
|
|
1016
|
+
value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
function isLegacyRecordingManifest(value) {
|
|
1020
|
+
return Boolean(
|
|
1021
|
+
value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
function createRecordingManifest(input) {
|
|
1025
|
+
const actions = input.frames.map((frame) => {
|
|
1026
|
+
const actionId = frame.actionId ?? `action-${frame.seq}`;
|
|
1027
|
+
return {
|
|
1028
|
+
id: actionId,
|
|
1029
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1030
|
+
action: frame.action,
|
|
1031
|
+
selector: frame.selector,
|
|
1032
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1033
|
+
value: frame.value,
|
|
1034
|
+
url: frame.url,
|
|
1035
|
+
success: frame.success,
|
|
1036
|
+
durationMs: frame.durationMs,
|
|
1037
|
+
error: frame.error,
|
|
1038
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1039
|
+
pageUrl: frame.pageUrl,
|
|
1040
|
+
pageTitle: frame.pageTitle,
|
|
1041
|
+
coordinates: frame.coordinates,
|
|
1042
|
+
boundingBox: frame.boundingBox
|
|
1043
|
+
};
|
|
1044
|
+
});
|
|
1045
|
+
const screenshots = input.frames.map((frame) => ({
|
|
1046
|
+
id: `shot-${frame.seq}`,
|
|
1047
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1048
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1049
|
+
file: frame.screenshot,
|
|
1050
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1051
|
+
success: frame.success,
|
|
1052
|
+
pageUrl: frame.pageUrl,
|
|
1053
|
+
pageTitle: frame.pageTitle,
|
|
1054
|
+
coordinates: frame.coordinates,
|
|
1055
|
+
boundingBox: frame.boundingBox
|
|
1056
|
+
}));
|
|
1057
|
+
return {
|
|
1058
|
+
version: 2,
|
|
1059
|
+
recordedAt: input.recordedAt,
|
|
1060
|
+
session: {
|
|
1061
|
+
id: input.sessionId,
|
|
1062
|
+
startUrl: input.startUrl,
|
|
1063
|
+
endUrl: input.endUrl,
|
|
1064
|
+
targetId: input.targetId,
|
|
1065
|
+
profile: input.profile
|
|
1066
|
+
},
|
|
1067
|
+
recipe: {
|
|
1068
|
+
steps: input.steps
|
|
1069
|
+
},
|
|
1070
|
+
actions,
|
|
1071
|
+
screenshots,
|
|
1072
|
+
trace: {
|
|
1073
|
+
events: input.traceEvents,
|
|
1074
|
+
summaries: buildTraceSummaries(input.traceEvents)
|
|
1075
|
+
},
|
|
1076
|
+
assertions: input.assertions ?? [],
|
|
1077
|
+
notes: input.notes ?? [],
|
|
1078
|
+
artifacts: {
|
|
1079
|
+
recordingManifest: input.recordingManifest ?? "recording.json",
|
|
1080
|
+
screenshotDir: input.screenshotDir ?? "screenshots/"
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function canonicalizeRecordingArtifact(value) {
|
|
1085
|
+
if (isCanonicalRecordingManifest(value)) {
|
|
1086
|
+
return value;
|
|
1087
|
+
}
|
|
1088
|
+
if (!isLegacyRecordingManifest(value)) {
|
|
1089
|
+
throw new Error("Unsupported recording artifact");
|
|
1090
|
+
}
|
|
1091
|
+
const traceEvents = buildTraceEventsFromLegacy(value);
|
|
1092
|
+
const steps = value.frames.map((frame) => frameToStep(frame));
|
|
1093
|
+
return createRecordingManifest({
|
|
1094
|
+
recordedAt: value.recordedAt,
|
|
1095
|
+
sessionId: value.sessionId,
|
|
1096
|
+
startUrl: value.startUrl,
|
|
1097
|
+
endUrl: value.endUrl,
|
|
1098
|
+
steps,
|
|
1099
|
+
frames: value.frames,
|
|
1100
|
+
traceEvents,
|
|
1101
|
+
notes: ["Converted from legacy recording manifest"]
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
function buildTraceEventsFromLegacy(value) {
|
|
1105
|
+
const events = [];
|
|
1106
|
+
for (const frame of value.frames) {
|
|
1107
|
+
events.push({
|
|
1108
|
+
traceId: frame.actionId ?? `legacy-${frame.seq}`,
|
|
1109
|
+
sessionId: value.sessionId,
|
|
1110
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
1111
|
+
elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
|
|
1112
|
+
channel: "action",
|
|
1113
|
+
event: frame.success ? "action.succeeded" : "action.failed",
|
|
1114
|
+
severity: frame.success ? "info" : "error",
|
|
1115
|
+
summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
|
|
1116
|
+
data: {
|
|
1117
|
+
action: frame.action,
|
|
1118
|
+
selector: frame.selector,
|
|
1119
|
+
value: frame.value ?? null,
|
|
1120
|
+
pageUrl: frame.pageUrl ?? null,
|
|
1121
|
+
pageTitle: frame.pageTitle ?? null,
|
|
1122
|
+
screenshot: frame.screenshot
|
|
1123
|
+
},
|
|
1124
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
1125
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
1126
|
+
selector: frame.selector,
|
|
1127
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
1128
|
+
url: frame.pageUrl ?? frame.url
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
return events;
|
|
1132
|
+
}
|
|
1133
|
+
function frameToStep(frame) {
|
|
1134
|
+
switch (frame.action) {
|
|
1135
|
+
case "fill":
|
|
1136
|
+
return { action: "fill", selector: frame.selector, value: frame.value };
|
|
1137
|
+
case "submit":
|
|
1138
|
+
return { action: "submit", selector: frame.selector };
|
|
1139
|
+
case "goto":
|
|
1140
|
+
return { action: "goto", url: frame.url ?? frame.pageUrl };
|
|
1141
|
+
case "press":
|
|
1142
|
+
return { action: "press", key: frame.value ?? "Enter" };
|
|
1143
|
+
default:
|
|
1144
|
+
return { action: "click", selector: frame.selector };
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// src/trace/model.ts
|
|
1149
|
+
function createTraceId(prefix = "evt") {
|
|
1150
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1151
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1152
|
+
}
|
|
1153
|
+
function normalizeTraceEvent(event) {
|
|
1154
|
+
return {
|
|
1155
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1156
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1157
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
1158
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
1159
|
+
data: event.data ?? {},
|
|
1160
|
+
...event
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function inferSeverity(eventName) {
|
|
1164
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1165
|
+
return "error";
|
|
1166
|
+
}
|
|
1167
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1168
|
+
return "warn";
|
|
1169
|
+
}
|
|
1170
|
+
return "info";
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/trace/script.ts
|
|
1174
|
+
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
1175
|
+
var TRACE_SCRIPT = `
|
|
1176
|
+
(() => {
|
|
1177
|
+
if (window.__bpTraceInstalled) return;
|
|
1178
|
+
window.__bpTraceInstalled = true;
|
|
1179
|
+
|
|
1180
|
+
const binding = globalThis.${TRACE_BINDING_NAME};
|
|
1181
|
+
if (typeof binding !== 'function') return;
|
|
1182
|
+
|
|
1183
|
+
const emit = (event, data = {}, severity = 'info', summary) => {
|
|
1184
|
+
try {
|
|
1185
|
+
globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
|
|
1186
|
+
const payload = {
|
|
1187
|
+
event,
|
|
1188
|
+
severity,
|
|
1189
|
+
summary: summary || event,
|
|
1190
|
+
ts: Date.now(),
|
|
1191
|
+
data,
|
|
1192
|
+
};
|
|
1193
|
+
globalThis.__bpTraceRecentEvents.push(payload);
|
|
1194
|
+
if (globalThis.__bpTraceRecentEvents.length > 200) {
|
|
1195
|
+
globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
|
|
1196
|
+
}
|
|
1197
|
+
binding(JSON.stringify(payload));
|
|
1198
|
+
} catch {}
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const patchWebSocket = () => {
|
|
1202
|
+
const NativeWebSocket = window.WebSocket;
|
|
1203
|
+
if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
|
|
1204
|
+
window.__bpTraceWebSocketInstalled = true;
|
|
1205
|
+
|
|
1206
|
+
const nextId = () => Math.random().toString(36).slice(2, 10);
|
|
1207
|
+
|
|
1208
|
+
const patchInstance = (socket, urlValue) => {
|
|
1209
|
+
if (!socket || socket.__bpTracePatched) return socket;
|
|
1210
|
+
socket.__bpTracePatched = true;
|
|
1211
|
+
socket.__bpTraceId = socket.__bpTraceId || nextId();
|
|
1212
|
+
socket.__bpTraceUrl = String(urlValue || socket.url || '');
|
|
1213
|
+
globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
|
|
1214
|
+
globalThis.__bpTrackedWebSockets.add(socket);
|
|
1215
|
+
|
|
1216
|
+
emit(
|
|
1217
|
+
'ws.connection.created',
|
|
1218
|
+
{ connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
|
|
1219
|
+
'info',
|
|
1220
|
+
'WebSocket opened ' + socket.__bpTraceUrl
|
|
1221
|
+
);
|
|
1222
|
+
|
|
1223
|
+
const originalSend = socket.send;
|
|
1224
|
+
socket.send = function(data) {
|
|
1225
|
+
const payload =
|
|
1226
|
+
typeof data === 'string'
|
|
1227
|
+
? data
|
|
1228
|
+
: data && typeof data.toString === 'function'
|
|
1229
|
+
? data.toString()
|
|
1230
|
+
: '[binary]';
|
|
1231
|
+
emit(
|
|
1232
|
+
'ws.frame.sent',
|
|
1233
|
+
{
|
|
1234
|
+
connectionId: socket.__bpTraceId,
|
|
1235
|
+
url: socket.__bpTraceUrl,
|
|
1236
|
+
payload,
|
|
1237
|
+
length: payload.length,
|
|
1238
|
+
},
|
|
1239
|
+
'info',
|
|
1240
|
+
'WebSocket frame sent'
|
|
1241
|
+
);
|
|
1242
|
+
return originalSend.call(this, data);
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
socket.addEventListener('message', (event) => {
|
|
1246
|
+
if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const data = event && 'data' in event ? event.data : '';
|
|
1250
|
+
const payload =
|
|
1251
|
+
typeof data === 'string'
|
|
1252
|
+
? data
|
|
1253
|
+
: data && typeof data.toString === 'function'
|
|
1254
|
+
? data.toString()
|
|
1255
|
+
: '[binary]';
|
|
1256
|
+
emit(
|
|
1257
|
+
'ws.frame.received',
|
|
1258
|
+
{
|
|
1259
|
+
connectionId: socket.__bpTraceId,
|
|
1260
|
+
url: socket.__bpTraceUrl,
|
|
1261
|
+
payload,
|
|
1262
|
+
length: payload.length,
|
|
1263
|
+
},
|
|
1264
|
+
'info',
|
|
1265
|
+
'WebSocket frame received'
|
|
1266
|
+
);
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
socket.addEventListener('close', (event) => {
|
|
1270
|
+
if (socket.__bpTraceClosed) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
socket.__bpTraceClosed = true;
|
|
1274
|
+
try {
|
|
1275
|
+
globalThis.__bpTrackedWebSockets.delete(socket);
|
|
1276
|
+
} catch {}
|
|
1277
|
+
emit(
|
|
1278
|
+
'ws.connection.closed',
|
|
1279
|
+
{
|
|
1280
|
+
connectionId: socket.__bpTraceId,
|
|
1281
|
+
url: socket.__bpTraceUrl,
|
|
1282
|
+
code: event.code,
|
|
1283
|
+
reason: event.reason,
|
|
1284
|
+
},
|
|
1285
|
+
'warn',
|
|
1286
|
+
'WebSocket closed'
|
|
1287
|
+
);
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
return socket;
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
const TracedWebSocket = function(url, protocols) {
|
|
1294
|
+
return arguments.length > 1
|
|
1295
|
+
? patchInstance(new NativeWebSocket(url, protocols), url)
|
|
1296
|
+
: patchInstance(new NativeWebSocket(url), url);
|
|
1297
|
+
};
|
|
1298
|
+
TracedWebSocket.prototype = NativeWebSocket.prototype;
|
|
1299
|
+
Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
|
|
1300
|
+
window.WebSocket = TracedWebSocket;
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
window.addEventListener('error', (errorEvent) => {
|
|
1304
|
+
emit(
|
|
1305
|
+
'runtime.exception',
|
|
1306
|
+
{
|
|
1307
|
+
message: errorEvent.message,
|
|
1308
|
+
filename: errorEvent.filename,
|
|
1309
|
+
line: errorEvent.lineno,
|
|
1310
|
+
column: errorEvent.colno,
|
|
1311
|
+
},
|
|
1312
|
+
'error',
|
|
1313
|
+
errorEvent.message || 'Uncaught error'
|
|
1314
|
+
);
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
1318
|
+
const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
|
|
1319
|
+
emit('runtime.unhandledRejection', { reason }, 'error', reason);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
const patchPermissions = async () => {
|
|
1323
|
+
if (!navigator.permissions || !navigator.permissions.query) return;
|
|
1324
|
+
|
|
1325
|
+
const names = ['geolocation', 'microphone', 'camera', 'notifications'];
|
|
1326
|
+
for (const name of names) {
|
|
1327
|
+
try {
|
|
1328
|
+
const status = await navigator.permissions.query({ name });
|
|
1329
|
+
emit(
|
|
1330
|
+
'permission.state',
|
|
1331
|
+
{ name, state: status.state },
|
|
1332
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1333
|
+
name + ': ' + status.state
|
|
1334
|
+
);
|
|
1335
|
+
status.addEventListener('change', () => {
|
|
1336
|
+
emit(
|
|
1337
|
+
'permission.changed',
|
|
1338
|
+
{ name, state: status.state },
|
|
1339
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1340
|
+
name + ': ' + status.state
|
|
1341
|
+
);
|
|
1342
|
+
});
|
|
1343
|
+
} catch {}
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
const patchMediaElement = (element) => {
|
|
1348
|
+
if (!element || element.__bpTracePatched) return;
|
|
1349
|
+
element.__bpTracePatched = true;
|
|
1350
|
+
|
|
1351
|
+
element.addEventListener('play', () => {
|
|
1352
|
+
emit(
|
|
1353
|
+
'media.playback.started',
|
|
1354
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1355
|
+
'info',
|
|
1356
|
+
'Media playback started'
|
|
1357
|
+
);
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
const onStop = () => {
|
|
1361
|
+
emit(
|
|
1362
|
+
'media.playback.stopped',
|
|
1363
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1364
|
+
'warn',
|
|
1365
|
+
'Media playback stopped'
|
|
1366
|
+
);
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
element.addEventListener('pause', onStop);
|
|
1370
|
+
element.addEventListener('ended', onStop);
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
const patchMediaElements = () => {
|
|
1374
|
+
document.querySelectorAll('audio,video').forEach(patchMediaElement);
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
patchMediaElements();
|
|
1378
|
+
patchWebSocket();
|
|
1379
|
+
|
|
1380
|
+
if (document.documentElement) {
|
|
1381
|
+
const observer = new MutationObserver(() => {
|
|
1382
|
+
patchMediaElements();
|
|
1383
|
+
});
|
|
1384
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
1388
|
+
const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
|
1389
|
+
navigator.mediaDevices.getUserMedia = async (...args) => {
|
|
1390
|
+
emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
|
|
1391
|
+
try {
|
|
1392
|
+
const stream = await original(...args);
|
|
1393
|
+
const tracks = stream.getTracks();
|
|
1394
|
+
|
|
1395
|
+
for (const track of tracks) {
|
|
1396
|
+
emit(
|
|
1397
|
+
'media.track.started',
|
|
1398
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1399
|
+
'info',
|
|
1400
|
+
track.kind + ' track started'
|
|
1401
|
+
);
|
|
1402
|
+
track.addEventListener('ended', () => {
|
|
1403
|
+
emit(
|
|
1404
|
+
'media.track.ended',
|
|
1405
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1406
|
+
'warn',
|
|
1407
|
+
track.kind + ' track ended'
|
|
1408
|
+
);
|
|
1409
|
+
emit(
|
|
1410
|
+
'voice.capture.stopped',
|
|
1411
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1412
|
+
'warn',
|
|
1413
|
+
'Voice capture stopped'
|
|
1414
|
+
);
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
emit(
|
|
1419
|
+
'voice.capture.detectedAudio',
|
|
1420
|
+
{ trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
|
|
1421
|
+
'info',
|
|
1422
|
+
'Voice capture detected audio'
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1425
|
+
return stream;
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
emit(
|
|
1428
|
+
'voice.pipeline.notReady',
|
|
1429
|
+
{ message: String(error && error.message ? error.message : error) },
|
|
1430
|
+
'error',
|
|
1431
|
+
String(error && error.message ? error.message : error)
|
|
1432
|
+
);
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
document.addEventListener('visibilitychange', () => {
|
|
1439
|
+
emit(
|
|
1440
|
+
'dom.state.changed',
|
|
1441
|
+
{ visibilityState: document.visibilityState },
|
|
1442
|
+
document.visibilityState === 'hidden' ? 'warn' : 'info',
|
|
1443
|
+
'Visibility ' + document.visibilityState
|
|
1444
|
+
);
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
patchPermissions();
|
|
1448
|
+
emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
|
|
1449
|
+
})();
|
|
1450
|
+
`;
|
|
1451
|
+
|
|
1452
|
+
// src/trace/live.ts
|
|
1453
|
+
function globToRegex(pattern) {
|
|
1454
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1455
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1456
|
+
return new RegExp(`^${withWildcards}$`);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
841
1459
|
// src/actions/executor.ts
|
|
842
1460
|
var DEFAULT_TIMEOUT = 3e4;
|
|
843
1461
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -847,6 +1465,61 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
|
847
1465
|
"text",
|
|
848
1466
|
"screenshot"
|
|
849
1467
|
];
|
|
1468
|
+
function readString(value) {
|
|
1469
|
+
return typeof value === "string" ? value : void 0;
|
|
1470
|
+
}
|
|
1471
|
+
function readStringOr(value, fallback = "") {
|
|
1472
|
+
return readString(value) ?? fallback;
|
|
1473
|
+
}
|
|
1474
|
+
function formatConsoleArg(entry) {
|
|
1475
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
1476
|
+
}
|
|
1477
|
+
function loadExistingRecording(manifestPath) {
|
|
1478
|
+
try {
|
|
1479
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1480
|
+
if (raw.version === 1) {
|
|
1481
|
+
const legacy = raw;
|
|
1482
|
+
return {
|
|
1483
|
+
frames: Array.isArray(legacy.frames) ? legacy.frames : [],
|
|
1484
|
+
traceEvents: [],
|
|
1485
|
+
recordedAt: legacy.recordedAt,
|
|
1486
|
+
startUrl: legacy.startUrl
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
const artifact = canonicalizeRecordingArtifact(raw);
|
|
1490
|
+
const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
|
|
1491
|
+
const frames = artifact.actions.map((action, index) => {
|
|
1492
|
+
const screenshot = screenshotsByAction.get(action.id);
|
|
1493
|
+
return {
|
|
1494
|
+
seq: index + 1,
|
|
1495
|
+
timestamp: Date.parse(action.ts),
|
|
1496
|
+
action: action.action,
|
|
1497
|
+
selector: action.selector,
|
|
1498
|
+
selectorUsed: action.selectorUsed,
|
|
1499
|
+
value: action.value,
|
|
1500
|
+
url: action.url,
|
|
1501
|
+
coordinates: action.coordinates,
|
|
1502
|
+
boundingBox: action.boundingBox,
|
|
1503
|
+
success: action.success,
|
|
1504
|
+
durationMs: action.durationMs,
|
|
1505
|
+
error: action.error,
|
|
1506
|
+
screenshot: screenshot?.file ?? "",
|
|
1507
|
+
pageUrl: action.pageUrl,
|
|
1508
|
+
pageTitle: action.pageTitle,
|
|
1509
|
+
stepIndex: action.stepIndex,
|
|
1510
|
+
actionId: action.id
|
|
1511
|
+
};
|
|
1512
|
+
});
|
|
1513
|
+
return {
|
|
1514
|
+
frames,
|
|
1515
|
+
traceEvents: artifact.trace.events,
|
|
1516
|
+
recordedAt: artifact.recordedAt,
|
|
1517
|
+
startUrl: artifact.session.startUrl
|
|
1518
|
+
};
|
|
1519
|
+
} catch {
|
|
1520
|
+
return { frames: [], traceEvents: [] };
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
850
1523
|
function classifyFailure(error) {
|
|
851
1524
|
if (error instanceof ElementNotFoundError) {
|
|
852
1525
|
return { reason: "missing" };
|
|
@@ -927,6 +1600,9 @@ var BatchExecutor = class {
|
|
|
927
1600
|
const results = [];
|
|
928
1601
|
const startTime = Date.now();
|
|
929
1602
|
const recording = options.record ? this.createRecordingContext(options.record) : null;
|
|
1603
|
+
if (steps.some((step) => step.action === "waitForWsMessage")) {
|
|
1604
|
+
await this.ensureTraceHooks();
|
|
1605
|
+
}
|
|
930
1606
|
const startUrl = recording ? await this.getPageUrlSafe() : "";
|
|
931
1607
|
let stoppedAtIndex;
|
|
932
1608
|
for (let i = 0; i < steps.length; i++) {
|
|
@@ -936,6 +1612,26 @@ var BatchExecutor = class {
|
|
|
936
1612
|
const retryDelay = step.retryDelay ?? 500;
|
|
937
1613
|
let lastError;
|
|
938
1614
|
let succeeded = false;
|
|
1615
|
+
if (recording) {
|
|
1616
|
+
recording.traceEvents.push(
|
|
1617
|
+
normalizeTraceEvent({
|
|
1618
|
+
traceId: createTraceId("action"),
|
|
1619
|
+
elapsedMs: Date.now() - startTime,
|
|
1620
|
+
channel: "action",
|
|
1621
|
+
event: "action.started",
|
|
1622
|
+
summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
|
|
1623
|
+
data: {
|
|
1624
|
+
action: step.action,
|
|
1625
|
+
selector: step.selector ?? null,
|
|
1626
|
+
url: step.url ?? null
|
|
1627
|
+
},
|
|
1628
|
+
actionId: `action-${i + 1}`,
|
|
1629
|
+
stepIndex: i,
|
|
1630
|
+
selector: step.selector,
|
|
1631
|
+
url: step.url
|
|
1632
|
+
})
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
939
1635
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
940
1636
|
if (attempt > 0) {
|
|
941
1637
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
@@ -959,6 +1655,28 @@ var BatchExecutor = class {
|
|
|
959
1655
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
960
1656
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
961
1657
|
}
|
|
1658
|
+
if (recording) {
|
|
1659
|
+
recording.traceEvents.push(
|
|
1660
|
+
normalizeTraceEvent({
|
|
1661
|
+
traceId: createTraceId("action"),
|
|
1662
|
+
elapsedMs: Date.now() - startTime,
|
|
1663
|
+
channel: "action",
|
|
1664
|
+
event: "action.succeeded",
|
|
1665
|
+
summary: `${step.action} succeeded`,
|
|
1666
|
+
data: {
|
|
1667
|
+
action: step.action,
|
|
1668
|
+
selector: step.selector ?? null,
|
|
1669
|
+
selectorUsed: result.selectorUsed ?? null,
|
|
1670
|
+
durationMs: Date.now() - stepStart
|
|
1671
|
+
},
|
|
1672
|
+
actionId: `action-${i + 1}`,
|
|
1673
|
+
stepIndex: i,
|
|
1674
|
+
selector: step.selector,
|
|
1675
|
+
selectorUsed: result.selectorUsed,
|
|
1676
|
+
url: step.url
|
|
1677
|
+
})
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
962
1680
|
results.push(stepResult);
|
|
963
1681
|
succeeded = true;
|
|
964
1682
|
break;
|
|
@@ -996,6 +1714,28 @@ var BatchExecutor = class {
|
|
|
996
1714
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
997
1715
|
await this.captureRecordingFrame(step, failedResult, recording);
|
|
998
1716
|
}
|
|
1717
|
+
if (recording) {
|
|
1718
|
+
recording.traceEvents.push(
|
|
1719
|
+
normalizeTraceEvent({
|
|
1720
|
+
traceId: createTraceId("action"),
|
|
1721
|
+
elapsedMs: Date.now() - startTime,
|
|
1722
|
+
channel: "action",
|
|
1723
|
+
event: "action.failed",
|
|
1724
|
+
severity: "error",
|
|
1725
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
1726
|
+
data: {
|
|
1727
|
+
action: step.action,
|
|
1728
|
+
selector: step.selector ?? null,
|
|
1729
|
+
error: errorMessage,
|
|
1730
|
+
reason
|
|
1731
|
+
},
|
|
1732
|
+
actionId: `action-${i + 1}`,
|
|
1733
|
+
stepIndex: i,
|
|
1734
|
+
selector: step.selector,
|
|
1735
|
+
url: step.url
|
|
1736
|
+
})
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
999
1739
|
results.push(failedResult);
|
|
1000
1740
|
if (onFail === "stop" && !step.optional) {
|
|
1001
1741
|
stoppedAtIndex = i;
|
|
@@ -1011,7 +1751,8 @@ var BatchExecutor = class {
|
|
|
1011
1751
|
recording,
|
|
1012
1752
|
startTime,
|
|
1013
1753
|
startUrl,
|
|
1014
|
-
allSuccess
|
|
1754
|
+
allSuccess,
|
|
1755
|
+
steps
|
|
1015
1756
|
);
|
|
1016
1757
|
}
|
|
1017
1758
|
return {
|
|
@@ -1026,20 +1767,14 @@ var BatchExecutor = class {
|
|
|
1026
1767
|
const baseDir = record.outputDir ?? join(process.cwd(), ".browser-pilot");
|
|
1027
1768
|
const screenshotDir = join(baseDir, "screenshots");
|
|
1028
1769
|
const manifestPath = join(baseDir, "recording.json");
|
|
1029
|
-
|
|
1030
|
-
try {
|
|
1031
|
-
const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1032
|
-
if (existing.frames && Array.isArray(existing.frames)) {
|
|
1033
|
-
existingFrames = existing.frames;
|
|
1034
|
-
}
|
|
1035
|
-
} catch {
|
|
1036
|
-
}
|
|
1770
|
+
const existing = loadExistingRecording(manifestPath);
|
|
1037
1771
|
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
1038
1772
|
return {
|
|
1039
1773
|
baseDir,
|
|
1040
1774
|
screenshotDir,
|
|
1041
1775
|
sessionId: record.sessionId ?? this.page.targetId,
|
|
1042
|
-
frames:
|
|
1776
|
+
frames: existing.frames,
|
|
1777
|
+
traceEvents: existing.traceEvents,
|
|
1043
1778
|
format: record.format ?? "webp",
|
|
1044
1779
|
quality: Math.max(0, Math.min(100, record.quality ?? 40)),
|
|
1045
1780
|
highlights: record.highlights !== false,
|
|
@@ -1095,6 +1830,7 @@ var BatchExecutor = class {
|
|
|
1095
1830
|
timestamp: ts,
|
|
1096
1831
|
action: stepResult.action,
|
|
1097
1832
|
selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
|
|
1833
|
+
selectorUsed: stepResult.selectorUsed,
|
|
1098
1834
|
value: redactValueForRecording(
|
|
1099
1835
|
typeof step.value === "string" ? step.value : void 0,
|
|
1100
1836
|
targetMetadata
|
|
@@ -1107,7 +1843,9 @@ var BatchExecutor = class {
|
|
|
1107
1843
|
error: stepResult.error,
|
|
1108
1844
|
screenshot: filename,
|
|
1109
1845
|
pageUrl,
|
|
1110
|
-
pageTitle
|
|
1846
|
+
pageTitle,
|
|
1847
|
+
stepIndex: stepResult.index,
|
|
1848
|
+
actionId: `action-${stepResult.index + 1}`
|
|
1111
1849
|
});
|
|
1112
1850
|
} catch {
|
|
1113
1851
|
} finally {
|
|
@@ -1119,45 +1857,31 @@ var BatchExecutor = class {
|
|
|
1119
1857
|
/**
|
|
1120
1858
|
* Write recording manifest to disk
|
|
1121
1859
|
*/
|
|
1122
|
-
async writeRecordingManifest(recording, startTime, startUrl, success) {
|
|
1860
|
+
async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
|
|
1123
1861
|
let endUrl = startUrl;
|
|
1124
|
-
let viewport = { width: 1280, height: 720 };
|
|
1125
1862
|
try {
|
|
1126
1863
|
endUrl = await this.page.url();
|
|
1127
1864
|
} catch {
|
|
1128
1865
|
}
|
|
1129
|
-
try {
|
|
1130
|
-
const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
|
|
1131
|
-
viewport = {
|
|
1132
|
-
width: metrics.cssVisualViewport.clientWidth,
|
|
1133
|
-
height: metrics.cssVisualViewport.clientHeight
|
|
1134
|
-
};
|
|
1135
|
-
} catch {
|
|
1136
|
-
}
|
|
1137
1866
|
const manifestPath = join(recording.baseDir, "recording.json");
|
|
1138
1867
|
let recordedAt = new Date(startTime).toISOString();
|
|
1139
1868
|
let originalStartUrl = startUrl;
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
} catch {
|
|
1145
|
-
}
|
|
1146
|
-
const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
|
|
1147
|
-
const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
|
|
1148
|
-
const manifest = {
|
|
1149
|
-
version: 1,
|
|
1869
|
+
const existing = loadExistingRecording(manifestPath);
|
|
1870
|
+
if (existing.recordedAt) recordedAt = existing.recordedAt;
|
|
1871
|
+
if (existing.startUrl) originalStartUrl = existing.startUrl;
|
|
1872
|
+
const manifest = createRecordingManifest({
|
|
1150
1873
|
recordedAt,
|
|
1151
1874
|
sessionId: recording.sessionId,
|
|
1152
1875
|
startUrl: originalStartUrl,
|
|
1153
1876
|
endUrl,
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
success,
|
|
1159
|
-
|
|
1160
|
-
|
|
1877
|
+
targetId: this.page.targetId,
|
|
1878
|
+
steps,
|
|
1879
|
+
frames: recording.frames,
|
|
1880
|
+
traceEvents: recording.traceEvents,
|
|
1881
|
+
notes: success ? [] : ["Replay ended with at least one failed action."],
|
|
1882
|
+
recordingManifest: "recording.json",
|
|
1883
|
+
screenshotDir: "screenshots/"
|
|
1884
|
+
});
|
|
1161
1885
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1162
1886
|
return manifestPath;
|
|
1163
1887
|
}
|
|
@@ -1440,6 +2164,39 @@ var BatchExecutor = class {
|
|
|
1440
2164
|
}
|
|
1441
2165
|
return { selectorUsed: usedSelector, value: actual };
|
|
1442
2166
|
}
|
|
2167
|
+
case "waitForWsMessage": {
|
|
2168
|
+
if (typeof step.match !== "string") {
|
|
2169
|
+
throw new Error("waitForWsMessage requires match");
|
|
2170
|
+
}
|
|
2171
|
+
const message = await this.waitForWsMessage(step.match, step.where, timeout);
|
|
2172
|
+
return { value: message };
|
|
2173
|
+
}
|
|
2174
|
+
case "assertNoConsoleErrors": {
|
|
2175
|
+
await this.assertNoConsoleErrors(step.windowMs ?? timeout);
|
|
2176
|
+
return {};
|
|
2177
|
+
}
|
|
2178
|
+
case "assertTextChanged": {
|
|
2179
|
+
const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
|
|
2180
|
+
if (typeof step.to !== "string") {
|
|
2181
|
+
throw new Error("assertTextChanged requires to");
|
|
2182
|
+
}
|
|
2183
|
+
const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
|
|
2184
|
+
return { selectorUsed: selector, text };
|
|
2185
|
+
}
|
|
2186
|
+
case "assertPermission": {
|
|
2187
|
+
if (!step.name || !step.state) {
|
|
2188
|
+
throw new Error("assertPermission requires name and state");
|
|
2189
|
+
}
|
|
2190
|
+
const permission = await this.assertPermission(step.name, step.state);
|
|
2191
|
+
return { value: permission };
|
|
2192
|
+
}
|
|
2193
|
+
case "assertMediaTrackLive": {
|
|
2194
|
+
if (!step.kind) {
|
|
2195
|
+
throw new Error("assertMediaTrackLive requires kind");
|
|
2196
|
+
}
|
|
2197
|
+
const media = await this.assertMediaTrackLive(step.kind);
|
|
2198
|
+
return { value: media };
|
|
2199
|
+
}
|
|
1443
2200
|
default: {
|
|
1444
2201
|
const action = step.action;
|
|
1445
2202
|
const aliases = {
|
|
@@ -1493,7 +2250,7 @@ var BatchExecutor = class {
|
|
|
1493
2250
|
};
|
|
1494
2251
|
const suggestion = aliases[action.toLowerCase()];
|
|
1495
2252
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
1496
|
-
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";
|
|
2253
|
+
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";
|
|
1497
2254
|
throw new Error(`Unknown action "${action}".${hint}
|
|
1498
2255
|
|
|
1499
2256
|
Valid actions: ${valid}`);
|
|
@@ -1509,6 +2266,237 @@ Valid actions: ${valid}`);
|
|
|
1509
2266
|
if (matched) return matched;
|
|
1510
2267
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
1511
2268
|
}
|
|
2269
|
+
async ensureTraceHooks() {
|
|
2270
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2271
|
+
await this.page.cdpClient.send("Page.enable");
|
|
2272
|
+
await this.page.cdpClient.send("Network.enable");
|
|
2273
|
+
try {
|
|
2274
|
+
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
2275
|
+
} catch {
|
|
2276
|
+
}
|
|
2277
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2278
|
+
source: TRACE_SCRIPT
|
|
2279
|
+
});
|
|
2280
|
+
await this.page.cdpClient.send("Runtime.evaluate", {
|
|
2281
|
+
expression: TRACE_SCRIPT,
|
|
2282
|
+
awaitPromise: false
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
async waitForWsMessage(match, where, timeout) {
|
|
2286
|
+
await this.ensureTraceHooks();
|
|
2287
|
+
const regex = globToRegex(match);
|
|
2288
|
+
const wsUrls = /* @__PURE__ */ new Map();
|
|
2289
|
+
const recentMatch = await this.findRecentWsMessage(regex, where);
|
|
2290
|
+
if (recentMatch) {
|
|
2291
|
+
return recentMatch;
|
|
2292
|
+
}
|
|
2293
|
+
return new Promise((resolve, reject) => {
|
|
2294
|
+
const cleanup = () => {
|
|
2295
|
+
this.page.cdpClient.off("Network.webSocketCreated", onCreated);
|
|
2296
|
+
this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
|
|
2297
|
+
this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
|
|
2298
|
+
clearTimeout(timer);
|
|
2299
|
+
};
|
|
2300
|
+
const onCreated = (params) => {
|
|
2301
|
+
wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
|
|
2302
|
+
};
|
|
2303
|
+
const onFrame = (params) => {
|
|
2304
|
+
const requestId = readStringOr(params["requestId"]);
|
|
2305
|
+
const response = params["response"] ?? {};
|
|
2306
|
+
const payload = response.payloadData ?? "";
|
|
2307
|
+
const url = wsUrls.get(requestId) ?? "";
|
|
2308
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
cleanup();
|
|
2315
|
+
resolve({ requestId, url, payload });
|
|
2316
|
+
};
|
|
2317
|
+
const onBinding = (params) => {
|
|
2318
|
+
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
try {
|
|
2322
|
+
const parsed = JSON.parse(readStringOr(params["payload"]));
|
|
2323
|
+
if (parsed.event !== "ws.frame.received") {
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
const data = parsed.data ?? {};
|
|
2327
|
+
const payload = readStringOr(data["payload"]);
|
|
2328
|
+
const url = readStringOr(data["url"]);
|
|
2329
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
cleanup();
|
|
2336
|
+
resolve({
|
|
2337
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2338
|
+
url,
|
|
2339
|
+
payload
|
|
2340
|
+
});
|
|
2341
|
+
} catch {
|
|
2342
|
+
}
|
|
2343
|
+
};
|
|
2344
|
+
const timer = setTimeout(() => {
|
|
2345
|
+
cleanup();
|
|
2346
|
+
reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
|
|
2347
|
+
}, timeout);
|
|
2348
|
+
this.page.cdpClient.on("Network.webSocketCreated", onCreated);
|
|
2349
|
+
this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
|
|
2350
|
+
this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
payloadMatchesWhere(payload, where) {
|
|
2354
|
+
try {
|
|
2355
|
+
const parsed = JSON.parse(payload);
|
|
2356
|
+
return Object.entries(where).every(([key, expected]) => {
|
|
2357
|
+
const actual = key.split(".").reduce((current, part) => {
|
|
2358
|
+
if (!current || typeof current !== "object") {
|
|
2359
|
+
return void 0;
|
|
2360
|
+
}
|
|
2361
|
+
return current[part];
|
|
2362
|
+
}, parsed);
|
|
2363
|
+
return actual === expected;
|
|
2364
|
+
});
|
|
2365
|
+
} catch {
|
|
2366
|
+
return false;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
async findRecentWsMessage(regex, where) {
|
|
2370
|
+
const recent = await this.page.evaluate(
|
|
2371
|
+
"(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
|
|
2372
|
+
);
|
|
2373
|
+
if (!Array.isArray(recent)) {
|
|
2374
|
+
return null;
|
|
2375
|
+
}
|
|
2376
|
+
for (let i = recent.length - 1; i >= 0; i--) {
|
|
2377
|
+
const entry = recent[i];
|
|
2378
|
+
if (!entry || typeof entry !== "object") {
|
|
2379
|
+
continue;
|
|
2380
|
+
}
|
|
2381
|
+
const record = entry;
|
|
2382
|
+
const event = readStringOr(record["event"]);
|
|
2383
|
+
if (event !== "ws.frame.received") {
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
const data = record["data"] ?? {};
|
|
2387
|
+
const payload = readStringOr(data["payload"]);
|
|
2388
|
+
const url = readStringOr(data["url"]);
|
|
2389
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
return {
|
|
2396
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2397
|
+
url,
|
|
2398
|
+
payload
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
async assertNoConsoleErrors(windowMs) {
|
|
2404
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2405
|
+
return new Promise((resolve, reject) => {
|
|
2406
|
+
const errors = [];
|
|
2407
|
+
const cleanup = () => {
|
|
2408
|
+
this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
|
|
2409
|
+
this.page.cdpClient.off("Runtime.exceptionThrown", onException);
|
|
2410
|
+
clearTimeout(timer);
|
|
2411
|
+
};
|
|
2412
|
+
const onConsole = (params) => {
|
|
2413
|
+
if (params["type"] !== "error") {
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2417
|
+
errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
|
|
2418
|
+
};
|
|
2419
|
+
const onException = (params) => {
|
|
2420
|
+
const details = params["exceptionDetails"] ?? {};
|
|
2421
|
+
errors.push(readString(details["text"]) ?? "Runtime exception");
|
|
2422
|
+
};
|
|
2423
|
+
const timer = setTimeout(() => {
|
|
2424
|
+
cleanup();
|
|
2425
|
+
if (errors.length > 0) {
|
|
2426
|
+
reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
resolve();
|
|
2430
|
+
}, windowMs);
|
|
2431
|
+
this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
|
|
2432
|
+
this.page.cdpClient.on("Runtime.exceptionThrown", onException);
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
async assertTextChanged(selector, from, to, timeout) {
|
|
2436
|
+
const initialText = from ?? await this.page.text(selector);
|
|
2437
|
+
const deadline = Date.now() + timeout;
|
|
2438
|
+
while (Date.now() < deadline) {
|
|
2439
|
+
const text = await this.page.text(selector);
|
|
2440
|
+
if (text !== initialText && text.includes(to)) {
|
|
2441
|
+
return text;
|
|
2442
|
+
}
|
|
2443
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2444
|
+
}
|
|
2445
|
+
throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
|
|
2446
|
+
}
|
|
2447
|
+
async assertPermission(name, state) {
|
|
2448
|
+
const result = await this.page.evaluate(
|
|
2449
|
+
`(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
|
|
2450
|
+
);
|
|
2451
|
+
if (!result || typeof result !== "object" || result.state !== state) {
|
|
2452
|
+
throw new Error(`Permission ${name} is not ${state}`);
|
|
2453
|
+
}
|
|
2454
|
+
return result;
|
|
2455
|
+
}
|
|
2456
|
+
async assertMediaTrackLive(kind) {
|
|
2457
|
+
const result = await this.page.evaluate(
|
|
2458
|
+
`(() => {
|
|
2459
|
+
const requestedKind = ${JSON.stringify(kind)};
|
|
2460
|
+
const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
|
|
2461
|
+
const tracks = [];
|
|
2462
|
+
if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
|
|
2463
|
+
tracks.push(...el.srcObject.getTracks());
|
|
2464
|
+
}
|
|
2465
|
+
return {
|
|
2466
|
+
tag: el.tagName.toLowerCase(),
|
|
2467
|
+
paused: !!el.paused,
|
|
2468
|
+
tracks: tracks.map((track) => ({
|
|
2469
|
+
kind: track.kind,
|
|
2470
|
+
readyState: track.readyState,
|
|
2471
|
+
enabled: track.enabled,
|
|
2472
|
+
label: track.label,
|
|
2473
|
+
})),
|
|
2474
|
+
};
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
const globalTracks =
|
|
2478
|
+
window.__bpStream && typeof window.__bpStream.getTracks === 'function'
|
|
2479
|
+
? window.__bpStream.getTracks().map((track) => ({
|
|
2480
|
+
kind: track.kind,
|
|
2481
|
+
readyState: track.readyState,
|
|
2482
|
+
enabled: track.enabled,
|
|
2483
|
+
label: track.label,
|
|
2484
|
+
}))
|
|
2485
|
+
: [];
|
|
2486
|
+
|
|
2487
|
+
const liveTracks = mediaElements
|
|
2488
|
+
.flatMap((entry) => entry.tracks)
|
|
2489
|
+
.concat(globalTracks)
|
|
2490
|
+
.filter((track) => track.kind === requestedKind && track.readyState === 'live');
|
|
2491
|
+
|
|
2492
|
+
return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
|
|
2493
|
+
})()`
|
|
2494
|
+
);
|
|
2495
|
+
if (!result || typeof result !== "object" || !result.live) {
|
|
2496
|
+
throw new Error(`No live ${kind} media track detected`);
|
|
2497
|
+
}
|
|
2498
|
+
return result;
|
|
2499
|
+
}
|
|
1512
2500
|
};
|
|
1513
2501
|
function addBatchToPage(page) {
|
|
1514
2502
|
const executor = new BatchExecutor(page);
|
|
@@ -1639,7 +2627,7 @@ var ACTION_RULES = {
|
|
|
1639
2627
|
value: { type: "string|string[]" },
|
|
1640
2628
|
trigger: { type: "string|string[]" },
|
|
1641
2629
|
option: { type: "string|string[]" },
|
|
1642
|
-
match: { type: "string"
|
|
2630
|
+
match: { type: "string" }
|
|
1643
2631
|
}
|
|
1644
2632
|
},
|
|
1645
2633
|
check: {
|
|
@@ -1770,6 +2758,38 @@ var ACTION_RULES = {
|
|
|
1770
2758
|
expect: { type: "string" },
|
|
1771
2759
|
value: { type: "string" }
|
|
1772
2760
|
}
|
|
2761
|
+
},
|
|
2762
|
+
waitForWsMessage: {
|
|
2763
|
+
required: { match: { type: "string" } },
|
|
2764
|
+
optional: {
|
|
2765
|
+
where: { type: "object" }
|
|
2766
|
+
}
|
|
2767
|
+
},
|
|
2768
|
+
assertNoConsoleErrors: {
|
|
2769
|
+
required: {},
|
|
2770
|
+
optional: {
|
|
2771
|
+
windowMs: { type: "number" }
|
|
2772
|
+
}
|
|
2773
|
+
},
|
|
2774
|
+
assertTextChanged: {
|
|
2775
|
+
required: { to: { type: "string" } },
|
|
2776
|
+
optional: {
|
|
2777
|
+
selector: { type: "string|string[]" },
|
|
2778
|
+
from: { type: "string" }
|
|
2779
|
+
}
|
|
2780
|
+
},
|
|
2781
|
+
assertPermission: {
|
|
2782
|
+
required: {
|
|
2783
|
+
name: { type: "string" },
|
|
2784
|
+
state: { type: "string" }
|
|
2785
|
+
},
|
|
2786
|
+
optional: {}
|
|
2787
|
+
},
|
|
2788
|
+
assertMediaTrackLive: {
|
|
2789
|
+
required: {
|
|
2790
|
+
kind: { type: "string", enum: ["audio", "video"] }
|
|
2791
|
+
},
|
|
2792
|
+
optional: {}
|
|
1773
2793
|
}
|
|
1774
2794
|
};
|
|
1775
2795
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -1793,6 +2813,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1793
2813
|
"trigger",
|
|
1794
2814
|
"option",
|
|
1795
2815
|
"match",
|
|
2816
|
+
"where",
|
|
1796
2817
|
"x",
|
|
1797
2818
|
"y",
|
|
1798
2819
|
"direction",
|
|
@@ -1802,7 +2823,13 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1802
2823
|
"fullPage",
|
|
1803
2824
|
"expect",
|
|
1804
2825
|
"retry",
|
|
1805
|
-
"retryDelay"
|
|
2826
|
+
"retryDelay",
|
|
2827
|
+
"from",
|
|
2828
|
+
"to",
|
|
2829
|
+
"name",
|
|
2830
|
+
"state",
|
|
2831
|
+
"kind",
|
|
2832
|
+
"windowMs"
|
|
1806
2833
|
]);
|
|
1807
2834
|
function resolveAction(name) {
|
|
1808
2835
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -1875,6 +2902,11 @@ function checkFieldType(value, rule) {
|
|
|
1875
2902
|
return `expected boolean or "auto", got ${typeof value}`;
|
|
1876
2903
|
}
|
|
1877
2904
|
return null;
|
|
2905
|
+
case "object":
|
|
2906
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2907
|
+
return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
|
|
2908
|
+
}
|
|
2909
|
+
return null;
|
|
1878
2910
|
default: {
|
|
1879
2911
|
const _exhaustive = rule.type;
|
|
1880
2912
|
return `unknown type: ${_exhaustive}`;
|