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/actions.cjs
CHANGED
|
@@ -560,6 +560,624 @@ var CDPError = class extends Error {
|
|
|
560
560
|
}
|
|
561
561
|
};
|
|
562
562
|
|
|
563
|
+
// src/trace/views.ts
|
|
564
|
+
function takeRecent(events, limit = 5) {
|
|
565
|
+
return events.slice(-limit).map((event) => ({
|
|
566
|
+
ts: event.ts,
|
|
567
|
+
event: event.event,
|
|
568
|
+
summary: event.summary,
|
|
569
|
+
severity: event.severity,
|
|
570
|
+
url: event.url
|
|
571
|
+
}));
|
|
572
|
+
}
|
|
573
|
+
function buildTraceSummaries(events) {
|
|
574
|
+
return {
|
|
575
|
+
ws: summarizeWs(events),
|
|
576
|
+
voice: summarizeVoice(events),
|
|
577
|
+
console: summarizeConsole(events),
|
|
578
|
+
permissions: summarizePermissions(events),
|
|
579
|
+
media: summarizeMedia(events),
|
|
580
|
+
ui: summarizeUi(events),
|
|
581
|
+
session: summarizeSession(events)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function summarizeWs(events) {
|
|
585
|
+
const relevant = events.filter(
|
|
586
|
+
(event) => event.channel === "ws" || event.event.startsWith("ws.")
|
|
587
|
+
);
|
|
588
|
+
const connections = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const event of relevant) {
|
|
590
|
+
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
591
|
+
let connection = connections.get(id);
|
|
592
|
+
if (!connection) {
|
|
593
|
+
connection = { id, sent: 0, received: 0, lastMessages: [] };
|
|
594
|
+
connections.set(id, connection);
|
|
595
|
+
}
|
|
596
|
+
connection.url = event.url ?? connection.url;
|
|
597
|
+
if (event.event === "ws.connection.created") {
|
|
598
|
+
connection.createdAt = event.ts;
|
|
599
|
+
}
|
|
600
|
+
if (event.event === "ws.connection.closed") {
|
|
601
|
+
connection.closedAt = event.ts;
|
|
602
|
+
}
|
|
603
|
+
if (event.event === "ws.frame.sent") {
|
|
604
|
+
connection.sent += 1;
|
|
605
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
606
|
+
if (payload) connection.lastMessages.push(`sent: ${payload}`);
|
|
607
|
+
}
|
|
608
|
+
if (event.event === "ws.frame.received") {
|
|
609
|
+
connection.received += 1;
|
|
610
|
+
const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
|
|
611
|
+
if (payload) connection.lastMessages.push(`recv: ${payload}`);
|
|
612
|
+
}
|
|
613
|
+
connection.lastMessages = connection.lastMessages.slice(-3);
|
|
614
|
+
}
|
|
615
|
+
const values = [...connections.values()];
|
|
616
|
+
const reconnects = values.reduce((count, connection) => {
|
|
617
|
+
return connection.closedAt && !connection.createdAt ? count + 1 : count;
|
|
618
|
+
}, 0);
|
|
619
|
+
return {
|
|
620
|
+
view: "ws",
|
|
621
|
+
totalEvents: relevant.length,
|
|
622
|
+
connections: values.map((connection) => ({
|
|
623
|
+
id: connection.id,
|
|
624
|
+
url: connection.url ?? null,
|
|
625
|
+
createdAt: connection.createdAt ?? null,
|
|
626
|
+
closedAt: connection.closedAt ?? null,
|
|
627
|
+
sent: connection.sent,
|
|
628
|
+
received: connection.received,
|
|
629
|
+
lastMessages: connection.lastMessages,
|
|
630
|
+
connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
|
|
631
|
+
})),
|
|
632
|
+
reconnects,
|
|
633
|
+
recent: takeRecent(relevant)
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function summarizeConsole(events) {
|
|
637
|
+
const relevant = events.filter(
|
|
638
|
+
(event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
639
|
+
);
|
|
640
|
+
return {
|
|
641
|
+
view: "console",
|
|
642
|
+
errors: relevant.filter(
|
|
643
|
+
(event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
|
|
644
|
+
).length,
|
|
645
|
+
warnings: relevant.filter((event) => event.event === "console.warn").length,
|
|
646
|
+
logs: relevant.filter((event) => event.event === "console.log").length,
|
|
647
|
+
recent: takeRecent(relevant)
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function summarizePermissions(events) {
|
|
651
|
+
const relevant = events.filter(
|
|
652
|
+
(event) => event.channel === "permission" || event.event.startsWith("permission.")
|
|
653
|
+
);
|
|
654
|
+
const latest = /* @__PURE__ */ new Map();
|
|
655
|
+
for (const event of relevant) {
|
|
656
|
+
const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
|
|
657
|
+
const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
|
|
658
|
+
if (name && state) {
|
|
659
|
+
latest.set(name, state);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
view: "permissions",
|
|
664
|
+
states: Object.fromEntries(latest),
|
|
665
|
+
changes: relevant.filter((event) => event.event === "permission.changed").length,
|
|
666
|
+
recent: takeRecent(relevant)
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function summarizeMedia(events) {
|
|
670
|
+
const relevant = events.filter(
|
|
671
|
+
(event) => event.channel === "media" || event.event.startsWith("media.")
|
|
672
|
+
);
|
|
673
|
+
const liveTracks = /* @__PURE__ */ new Map();
|
|
674
|
+
for (const event of relevant) {
|
|
675
|
+
const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
|
|
676
|
+
const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
|
|
677
|
+
const key = `${kind}:${label}`;
|
|
678
|
+
if (event.event === "media.track.started") {
|
|
679
|
+
liveTracks.set(key, kind);
|
|
680
|
+
}
|
|
681
|
+
if (event.event === "media.track.ended") {
|
|
682
|
+
liveTracks.delete(key);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
view: "media",
|
|
687
|
+
tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
|
|
688
|
+
tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
|
|
689
|
+
playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
|
|
690
|
+
playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
|
|
691
|
+
liveTracks: [...liveTracks.values()],
|
|
692
|
+
recent: takeRecent(relevant)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function summarizeVoice(events) {
|
|
696
|
+
const relevant = events.filter(
|
|
697
|
+
(event) => event.channel === "voice" || event.event.startsWith("voice.")
|
|
698
|
+
);
|
|
699
|
+
return {
|
|
700
|
+
view: "voice",
|
|
701
|
+
ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
|
|
702
|
+
notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
|
|
703
|
+
captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
|
|
704
|
+
captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
|
|
705
|
+
detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
|
|
706
|
+
recent: takeRecent(relevant)
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function summarizeUi(events) {
|
|
710
|
+
const relevant = events.filter(
|
|
711
|
+
(event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
|
|
712
|
+
);
|
|
713
|
+
return {
|
|
714
|
+
view: "ui",
|
|
715
|
+
actions: relevant.filter((event) => event.channel === "action").length,
|
|
716
|
+
domChanges: relevant.filter((event) => event.channel === "dom").length,
|
|
717
|
+
recent: takeRecent(relevant)
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
function summarizeSession(events) {
|
|
721
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
722
|
+
const failedActions = events.filter((event) => event.event === "action.failed").length;
|
|
723
|
+
for (const event of events) {
|
|
724
|
+
byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
view: "session",
|
|
728
|
+
totalEvents: events.length,
|
|
729
|
+
byChannel: Object.fromEntries(byChannel),
|
|
730
|
+
failedActions,
|
|
731
|
+
recent: takeRecent(events)
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/recording/manifest.ts
|
|
736
|
+
function isCanonicalRecordingManifest(value) {
|
|
737
|
+
return Boolean(
|
|
738
|
+
value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
function isLegacyRecordingManifest(value) {
|
|
742
|
+
return Boolean(
|
|
743
|
+
value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
function createRecordingManifest(input) {
|
|
747
|
+
const actions = input.frames.map((frame) => {
|
|
748
|
+
const actionId = frame.actionId ?? `action-${frame.seq}`;
|
|
749
|
+
return {
|
|
750
|
+
id: actionId,
|
|
751
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
752
|
+
action: frame.action,
|
|
753
|
+
selector: frame.selector,
|
|
754
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
755
|
+
value: frame.value,
|
|
756
|
+
url: frame.url,
|
|
757
|
+
success: frame.success,
|
|
758
|
+
durationMs: frame.durationMs,
|
|
759
|
+
error: frame.error,
|
|
760
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
761
|
+
pageUrl: frame.pageUrl,
|
|
762
|
+
pageTitle: frame.pageTitle,
|
|
763
|
+
coordinates: frame.coordinates,
|
|
764
|
+
boundingBox: frame.boundingBox
|
|
765
|
+
};
|
|
766
|
+
});
|
|
767
|
+
const screenshots = input.frames.map((frame) => ({
|
|
768
|
+
id: `shot-${frame.seq}`,
|
|
769
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
770
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
771
|
+
file: frame.screenshot,
|
|
772
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
773
|
+
success: frame.success,
|
|
774
|
+
pageUrl: frame.pageUrl,
|
|
775
|
+
pageTitle: frame.pageTitle,
|
|
776
|
+
coordinates: frame.coordinates,
|
|
777
|
+
boundingBox: frame.boundingBox
|
|
778
|
+
}));
|
|
779
|
+
return {
|
|
780
|
+
version: 2,
|
|
781
|
+
recordedAt: input.recordedAt,
|
|
782
|
+
session: {
|
|
783
|
+
id: input.sessionId,
|
|
784
|
+
startUrl: input.startUrl,
|
|
785
|
+
endUrl: input.endUrl,
|
|
786
|
+
targetId: input.targetId,
|
|
787
|
+
profile: input.profile
|
|
788
|
+
},
|
|
789
|
+
recipe: {
|
|
790
|
+
steps: input.steps
|
|
791
|
+
},
|
|
792
|
+
actions,
|
|
793
|
+
screenshots,
|
|
794
|
+
trace: {
|
|
795
|
+
events: input.traceEvents,
|
|
796
|
+
summaries: buildTraceSummaries(input.traceEvents)
|
|
797
|
+
},
|
|
798
|
+
assertions: input.assertions ?? [],
|
|
799
|
+
notes: input.notes ?? [],
|
|
800
|
+
artifacts: {
|
|
801
|
+
recordingManifest: input.recordingManifest ?? "recording.json",
|
|
802
|
+
screenshotDir: input.screenshotDir ?? "screenshots/"
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function canonicalizeRecordingArtifact(value) {
|
|
807
|
+
if (isCanonicalRecordingManifest(value)) {
|
|
808
|
+
return value;
|
|
809
|
+
}
|
|
810
|
+
if (!isLegacyRecordingManifest(value)) {
|
|
811
|
+
throw new Error("Unsupported recording artifact");
|
|
812
|
+
}
|
|
813
|
+
const traceEvents = buildTraceEventsFromLegacy(value);
|
|
814
|
+
const steps = value.frames.map((frame) => frameToStep(frame));
|
|
815
|
+
return createRecordingManifest({
|
|
816
|
+
recordedAt: value.recordedAt,
|
|
817
|
+
sessionId: value.sessionId,
|
|
818
|
+
startUrl: value.startUrl,
|
|
819
|
+
endUrl: value.endUrl,
|
|
820
|
+
steps,
|
|
821
|
+
frames: value.frames,
|
|
822
|
+
traceEvents,
|
|
823
|
+
notes: ["Converted from legacy recording manifest"]
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
function buildTraceEventsFromLegacy(value) {
|
|
827
|
+
const events = [];
|
|
828
|
+
for (const frame of value.frames) {
|
|
829
|
+
events.push({
|
|
830
|
+
traceId: frame.actionId ?? `legacy-${frame.seq}`,
|
|
831
|
+
sessionId: value.sessionId,
|
|
832
|
+
ts: new Date(frame.timestamp).toISOString(),
|
|
833
|
+
elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
|
|
834
|
+
channel: "action",
|
|
835
|
+
event: frame.success ? "action.succeeded" : "action.failed",
|
|
836
|
+
severity: frame.success ? "info" : "error",
|
|
837
|
+
summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
|
|
838
|
+
data: {
|
|
839
|
+
action: frame.action,
|
|
840
|
+
selector: frame.selector,
|
|
841
|
+
value: frame.value ?? null,
|
|
842
|
+
pageUrl: frame.pageUrl ?? null,
|
|
843
|
+
pageTitle: frame.pageTitle ?? null,
|
|
844
|
+
screenshot: frame.screenshot
|
|
845
|
+
},
|
|
846
|
+
actionId: frame.actionId ?? `action-${frame.seq}`,
|
|
847
|
+
stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
|
|
848
|
+
selector: frame.selector,
|
|
849
|
+
selectorUsed: frame.selectorUsed ?? frame.selector,
|
|
850
|
+
url: frame.pageUrl ?? frame.url
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return events;
|
|
854
|
+
}
|
|
855
|
+
function frameToStep(frame) {
|
|
856
|
+
switch (frame.action) {
|
|
857
|
+
case "fill":
|
|
858
|
+
return { action: "fill", selector: frame.selector, value: frame.value };
|
|
859
|
+
case "submit":
|
|
860
|
+
return { action: "submit", selector: frame.selector };
|
|
861
|
+
case "goto":
|
|
862
|
+
return { action: "goto", url: frame.url ?? frame.pageUrl };
|
|
863
|
+
case "press":
|
|
864
|
+
return { action: "press", key: frame.value ?? "Enter" };
|
|
865
|
+
default:
|
|
866
|
+
return { action: "click", selector: frame.selector };
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/trace/model.ts
|
|
871
|
+
function createTraceId(prefix = "evt") {
|
|
872
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
873
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
874
|
+
}
|
|
875
|
+
function normalizeTraceEvent(event) {
|
|
876
|
+
return {
|
|
877
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
878
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
879
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
880
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
881
|
+
data: event.data ?? {},
|
|
882
|
+
...event
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function inferSeverity(eventName) {
|
|
886
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
887
|
+
return "error";
|
|
888
|
+
}
|
|
889
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
890
|
+
return "warn";
|
|
891
|
+
}
|
|
892
|
+
return "info";
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/trace/script.ts
|
|
896
|
+
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
897
|
+
var TRACE_SCRIPT = `
|
|
898
|
+
(() => {
|
|
899
|
+
if (window.__bpTraceInstalled) return;
|
|
900
|
+
window.__bpTraceInstalled = true;
|
|
901
|
+
|
|
902
|
+
const binding = globalThis.${TRACE_BINDING_NAME};
|
|
903
|
+
if (typeof binding !== 'function') return;
|
|
904
|
+
|
|
905
|
+
const emit = (event, data = {}, severity = 'info', summary) => {
|
|
906
|
+
try {
|
|
907
|
+
globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
|
|
908
|
+
const payload = {
|
|
909
|
+
event,
|
|
910
|
+
severity,
|
|
911
|
+
summary: summary || event,
|
|
912
|
+
ts: Date.now(),
|
|
913
|
+
data,
|
|
914
|
+
};
|
|
915
|
+
globalThis.__bpTraceRecentEvents.push(payload);
|
|
916
|
+
if (globalThis.__bpTraceRecentEvents.length > 200) {
|
|
917
|
+
globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
|
|
918
|
+
}
|
|
919
|
+
binding(JSON.stringify(payload));
|
|
920
|
+
} catch {}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const patchWebSocket = () => {
|
|
924
|
+
const NativeWebSocket = window.WebSocket;
|
|
925
|
+
if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
|
|
926
|
+
window.__bpTraceWebSocketInstalled = true;
|
|
927
|
+
|
|
928
|
+
const nextId = () => Math.random().toString(36).slice(2, 10);
|
|
929
|
+
|
|
930
|
+
const patchInstance = (socket, urlValue) => {
|
|
931
|
+
if (!socket || socket.__bpTracePatched) return socket;
|
|
932
|
+
socket.__bpTracePatched = true;
|
|
933
|
+
socket.__bpTraceId = socket.__bpTraceId || nextId();
|
|
934
|
+
socket.__bpTraceUrl = String(urlValue || socket.url || '');
|
|
935
|
+
globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
|
|
936
|
+
globalThis.__bpTrackedWebSockets.add(socket);
|
|
937
|
+
|
|
938
|
+
emit(
|
|
939
|
+
'ws.connection.created',
|
|
940
|
+
{ connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
|
|
941
|
+
'info',
|
|
942
|
+
'WebSocket opened ' + socket.__bpTraceUrl
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
const originalSend = socket.send;
|
|
946
|
+
socket.send = function(data) {
|
|
947
|
+
const payload =
|
|
948
|
+
typeof data === 'string'
|
|
949
|
+
? data
|
|
950
|
+
: data && typeof data.toString === 'function'
|
|
951
|
+
? data.toString()
|
|
952
|
+
: '[binary]';
|
|
953
|
+
emit(
|
|
954
|
+
'ws.frame.sent',
|
|
955
|
+
{
|
|
956
|
+
connectionId: socket.__bpTraceId,
|
|
957
|
+
url: socket.__bpTraceUrl,
|
|
958
|
+
payload,
|
|
959
|
+
length: payload.length,
|
|
960
|
+
},
|
|
961
|
+
'info',
|
|
962
|
+
'WebSocket frame sent'
|
|
963
|
+
);
|
|
964
|
+
return originalSend.call(this, data);
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
socket.addEventListener('message', (event) => {
|
|
968
|
+
if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const data = event && 'data' in event ? event.data : '';
|
|
972
|
+
const payload =
|
|
973
|
+
typeof data === 'string'
|
|
974
|
+
? data
|
|
975
|
+
: data && typeof data.toString === 'function'
|
|
976
|
+
? data.toString()
|
|
977
|
+
: '[binary]';
|
|
978
|
+
emit(
|
|
979
|
+
'ws.frame.received',
|
|
980
|
+
{
|
|
981
|
+
connectionId: socket.__bpTraceId,
|
|
982
|
+
url: socket.__bpTraceUrl,
|
|
983
|
+
payload,
|
|
984
|
+
length: payload.length,
|
|
985
|
+
},
|
|
986
|
+
'info',
|
|
987
|
+
'WebSocket frame received'
|
|
988
|
+
);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
socket.addEventListener('close', (event) => {
|
|
992
|
+
if (socket.__bpTraceClosed) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
socket.__bpTraceClosed = true;
|
|
996
|
+
try {
|
|
997
|
+
globalThis.__bpTrackedWebSockets.delete(socket);
|
|
998
|
+
} catch {}
|
|
999
|
+
emit(
|
|
1000
|
+
'ws.connection.closed',
|
|
1001
|
+
{
|
|
1002
|
+
connectionId: socket.__bpTraceId,
|
|
1003
|
+
url: socket.__bpTraceUrl,
|
|
1004
|
+
code: event.code,
|
|
1005
|
+
reason: event.reason,
|
|
1006
|
+
},
|
|
1007
|
+
'warn',
|
|
1008
|
+
'WebSocket closed'
|
|
1009
|
+
);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
return socket;
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
const TracedWebSocket = function(url, protocols) {
|
|
1016
|
+
return arguments.length > 1
|
|
1017
|
+
? patchInstance(new NativeWebSocket(url, protocols), url)
|
|
1018
|
+
: patchInstance(new NativeWebSocket(url), url);
|
|
1019
|
+
};
|
|
1020
|
+
TracedWebSocket.prototype = NativeWebSocket.prototype;
|
|
1021
|
+
Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
|
|
1022
|
+
window.WebSocket = TracedWebSocket;
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
window.addEventListener('error', (errorEvent) => {
|
|
1026
|
+
emit(
|
|
1027
|
+
'runtime.exception',
|
|
1028
|
+
{
|
|
1029
|
+
message: errorEvent.message,
|
|
1030
|
+
filename: errorEvent.filename,
|
|
1031
|
+
line: errorEvent.lineno,
|
|
1032
|
+
column: errorEvent.colno,
|
|
1033
|
+
},
|
|
1034
|
+
'error',
|
|
1035
|
+
errorEvent.message || 'Uncaught error'
|
|
1036
|
+
);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
1040
|
+
const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
|
|
1041
|
+
emit('runtime.unhandledRejection', { reason }, 'error', reason);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const patchPermissions = async () => {
|
|
1045
|
+
if (!navigator.permissions || !navigator.permissions.query) return;
|
|
1046
|
+
|
|
1047
|
+
const names = ['geolocation', 'microphone', 'camera', 'notifications'];
|
|
1048
|
+
for (const name of names) {
|
|
1049
|
+
try {
|
|
1050
|
+
const status = await navigator.permissions.query({ name });
|
|
1051
|
+
emit(
|
|
1052
|
+
'permission.state',
|
|
1053
|
+
{ name, state: status.state },
|
|
1054
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1055
|
+
name + ': ' + status.state
|
|
1056
|
+
);
|
|
1057
|
+
status.addEventListener('change', () => {
|
|
1058
|
+
emit(
|
|
1059
|
+
'permission.changed',
|
|
1060
|
+
{ name, state: status.state },
|
|
1061
|
+
status.state === 'denied' ? 'warn' : 'info',
|
|
1062
|
+
name + ': ' + status.state
|
|
1063
|
+
);
|
|
1064
|
+
});
|
|
1065
|
+
} catch {}
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
const patchMediaElement = (element) => {
|
|
1070
|
+
if (!element || element.__bpTracePatched) return;
|
|
1071
|
+
element.__bpTracePatched = true;
|
|
1072
|
+
|
|
1073
|
+
element.addEventListener('play', () => {
|
|
1074
|
+
emit(
|
|
1075
|
+
'media.playback.started',
|
|
1076
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1077
|
+
'info',
|
|
1078
|
+
'Media playback started'
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
const onStop = () => {
|
|
1083
|
+
emit(
|
|
1084
|
+
'media.playback.stopped',
|
|
1085
|
+
{ tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
|
|
1086
|
+
'warn',
|
|
1087
|
+
'Media playback stopped'
|
|
1088
|
+
);
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
element.addEventListener('pause', onStop);
|
|
1092
|
+
element.addEventListener('ended', onStop);
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const patchMediaElements = () => {
|
|
1096
|
+
document.querySelectorAll('audio,video').forEach(patchMediaElement);
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
patchMediaElements();
|
|
1100
|
+
patchWebSocket();
|
|
1101
|
+
|
|
1102
|
+
if (document.documentElement) {
|
|
1103
|
+
const observer = new MutationObserver(() => {
|
|
1104
|
+
patchMediaElements();
|
|
1105
|
+
});
|
|
1106
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
1110
|
+
const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
|
1111
|
+
navigator.mediaDevices.getUserMedia = async (...args) => {
|
|
1112
|
+
emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
|
|
1113
|
+
try {
|
|
1114
|
+
const stream = await original(...args);
|
|
1115
|
+
const tracks = stream.getTracks();
|
|
1116
|
+
|
|
1117
|
+
for (const track of tracks) {
|
|
1118
|
+
emit(
|
|
1119
|
+
'media.track.started',
|
|
1120
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1121
|
+
'info',
|
|
1122
|
+
track.kind + ' track started'
|
|
1123
|
+
);
|
|
1124
|
+
track.addEventListener('ended', () => {
|
|
1125
|
+
emit(
|
|
1126
|
+
'media.track.ended',
|
|
1127
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1128
|
+
'warn',
|
|
1129
|
+
track.kind + ' track ended'
|
|
1130
|
+
);
|
|
1131
|
+
emit(
|
|
1132
|
+
'voice.capture.stopped',
|
|
1133
|
+
{ kind: track.kind, label: track.label, readyState: track.readyState },
|
|
1134
|
+
'warn',
|
|
1135
|
+
'Voice capture stopped'
|
|
1136
|
+
);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
emit(
|
|
1141
|
+
'voice.capture.detectedAudio',
|
|
1142
|
+
{ trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
|
|
1143
|
+
'info',
|
|
1144
|
+
'Voice capture detected audio'
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
return stream;
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
emit(
|
|
1150
|
+
'voice.pipeline.notReady',
|
|
1151
|
+
{ message: String(error && error.message ? error.message : error) },
|
|
1152
|
+
'error',
|
|
1153
|
+
String(error && error.message ? error.message : error)
|
|
1154
|
+
);
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
document.addEventListener('visibilitychange', () => {
|
|
1161
|
+
emit(
|
|
1162
|
+
'dom.state.changed',
|
|
1163
|
+
{ visibilityState: document.visibilityState },
|
|
1164
|
+
document.visibilityState === 'hidden' ? 'warn' : 'info',
|
|
1165
|
+
'Visibility ' + document.visibilityState
|
|
1166
|
+
);
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
patchPermissions();
|
|
1170
|
+
emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
|
|
1171
|
+
})();
|
|
1172
|
+
`;
|
|
1173
|
+
|
|
1174
|
+
// src/trace/live.ts
|
|
1175
|
+
function globToRegex(pattern) {
|
|
1176
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1177
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1178
|
+
return new RegExp(`^${withWildcards}$`);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
563
1181
|
// src/actions/executor.ts
|
|
564
1182
|
var DEFAULT_TIMEOUT = 3e4;
|
|
565
1183
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -569,6 +1187,61 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
|
569
1187
|
"text",
|
|
570
1188
|
"screenshot"
|
|
571
1189
|
];
|
|
1190
|
+
function readString(value) {
|
|
1191
|
+
return typeof value === "string" ? value : void 0;
|
|
1192
|
+
}
|
|
1193
|
+
function readStringOr(value, fallback = "") {
|
|
1194
|
+
return readString(value) ?? fallback;
|
|
1195
|
+
}
|
|
1196
|
+
function formatConsoleArg(entry) {
|
|
1197
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
1198
|
+
}
|
|
1199
|
+
function loadExistingRecording(manifestPath) {
|
|
1200
|
+
try {
|
|
1201
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1202
|
+
if (raw.version === 1) {
|
|
1203
|
+
const legacy = raw;
|
|
1204
|
+
return {
|
|
1205
|
+
frames: Array.isArray(legacy.frames) ? legacy.frames : [],
|
|
1206
|
+
traceEvents: [],
|
|
1207
|
+
recordedAt: legacy.recordedAt,
|
|
1208
|
+
startUrl: legacy.startUrl
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
const artifact = canonicalizeRecordingArtifact(raw);
|
|
1212
|
+
const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
|
|
1213
|
+
const frames = artifact.actions.map((action, index) => {
|
|
1214
|
+
const screenshot = screenshotsByAction.get(action.id);
|
|
1215
|
+
return {
|
|
1216
|
+
seq: index + 1,
|
|
1217
|
+
timestamp: Date.parse(action.ts),
|
|
1218
|
+
action: action.action,
|
|
1219
|
+
selector: action.selector,
|
|
1220
|
+
selectorUsed: action.selectorUsed,
|
|
1221
|
+
value: action.value,
|
|
1222
|
+
url: action.url,
|
|
1223
|
+
coordinates: action.coordinates,
|
|
1224
|
+
boundingBox: action.boundingBox,
|
|
1225
|
+
success: action.success,
|
|
1226
|
+
durationMs: action.durationMs,
|
|
1227
|
+
error: action.error,
|
|
1228
|
+
screenshot: screenshot?.file ?? "",
|
|
1229
|
+
pageUrl: action.pageUrl,
|
|
1230
|
+
pageTitle: action.pageTitle,
|
|
1231
|
+
stepIndex: action.stepIndex,
|
|
1232
|
+
actionId: action.id
|
|
1233
|
+
};
|
|
1234
|
+
});
|
|
1235
|
+
return {
|
|
1236
|
+
frames,
|
|
1237
|
+
traceEvents: artifact.trace.events,
|
|
1238
|
+
recordedAt: artifact.recordedAt,
|
|
1239
|
+
startUrl: artifact.session.startUrl
|
|
1240
|
+
};
|
|
1241
|
+
} catch {
|
|
1242
|
+
return { frames: [], traceEvents: [] };
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
572
1245
|
function classifyFailure(error) {
|
|
573
1246
|
if (error instanceof ElementNotFoundError) {
|
|
574
1247
|
return { reason: "missing" };
|
|
@@ -649,6 +1322,9 @@ var BatchExecutor = class {
|
|
|
649
1322
|
const results = [];
|
|
650
1323
|
const startTime = Date.now();
|
|
651
1324
|
const recording = options.record ? this.createRecordingContext(options.record) : null;
|
|
1325
|
+
if (steps.some((step) => step.action === "waitForWsMessage")) {
|
|
1326
|
+
await this.ensureTraceHooks();
|
|
1327
|
+
}
|
|
652
1328
|
const startUrl = recording ? await this.getPageUrlSafe() : "";
|
|
653
1329
|
let stoppedAtIndex;
|
|
654
1330
|
for (let i = 0; i < steps.length; i++) {
|
|
@@ -658,6 +1334,26 @@ var BatchExecutor = class {
|
|
|
658
1334
|
const retryDelay = step.retryDelay ?? 500;
|
|
659
1335
|
let lastError;
|
|
660
1336
|
let succeeded = false;
|
|
1337
|
+
if (recording) {
|
|
1338
|
+
recording.traceEvents.push(
|
|
1339
|
+
normalizeTraceEvent({
|
|
1340
|
+
traceId: createTraceId("action"),
|
|
1341
|
+
elapsedMs: Date.now() - startTime,
|
|
1342
|
+
channel: "action",
|
|
1343
|
+
event: "action.started",
|
|
1344
|
+
summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
|
|
1345
|
+
data: {
|
|
1346
|
+
action: step.action,
|
|
1347
|
+
selector: step.selector ?? null,
|
|
1348
|
+
url: step.url ?? null
|
|
1349
|
+
},
|
|
1350
|
+
actionId: `action-${i + 1}`,
|
|
1351
|
+
stepIndex: i,
|
|
1352
|
+
selector: step.selector,
|
|
1353
|
+
url: step.url
|
|
1354
|
+
})
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
661
1357
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
662
1358
|
if (attempt > 0) {
|
|
663
1359
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
@@ -681,6 +1377,28 @@ var BatchExecutor = class {
|
|
|
681
1377
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
682
1378
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
683
1379
|
}
|
|
1380
|
+
if (recording) {
|
|
1381
|
+
recording.traceEvents.push(
|
|
1382
|
+
normalizeTraceEvent({
|
|
1383
|
+
traceId: createTraceId("action"),
|
|
1384
|
+
elapsedMs: Date.now() - startTime,
|
|
1385
|
+
channel: "action",
|
|
1386
|
+
event: "action.succeeded",
|
|
1387
|
+
summary: `${step.action} succeeded`,
|
|
1388
|
+
data: {
|
|
1389
|
+
action: step.action,
|
|
1390
|
+
selector: step.selector ?? null,
|
|
1391
|
+
selectorUsed: result.selectorUsed ?? null,
|
|
1392
|
+
durationMs: Date.now() - stepStart
|
|
1393
|
+
},
|
|
1394
|
+
actionId: `action-${i + 1}`,
|
|
1395
|
+
stepIndex: i,
|
|
1396
|
+
selector: step.selector,
|
|
1397
|
+
selectorUsed: result.selectorUsed,
|
|
1398
|
+
url: step.url
|
|
1399
|
+
})
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
684
1402
|
results.push(stepResult);
|
|
685
1403
|
succeeded = true;
|
|
686
1404
|
break;
|
|
@@ -718,6 +1436,28 @@ var BatchExecutor = class {
|
|
|
718
1436
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
719
1437
|
await this.captureRecordingFrame(step, failedResult, recording);
|
|
720
1438
|
}
|
|
1439
|
+
if (recording) {
|
|
1440
|
+
recording.traceEvents.push(
|
|
1441
|
+
normalizeTraceEvent({
|
|
1442
|
+
traceId: createTraceId("action"),
|
|
1443
|
+
elapsedMs: Date.now() - startTime,
|
|
1444
|
+
channel: "action",
|
|
1445
|
+
event: "action.failed",
|
|
1446
|
+
severity: "error",
|
|
1447
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
1448
|
+
data: {
|
|
1449
|
+
action: step.action,
|
|
1450
|
+
selector: step.selector ?? null,
|
|
1451
|
+
error: errorMessage,
|
|
1452
|
+
reason
|
|
1453
|
+
},
|
|
1454
|
+
actionId: `action-${i + 1}`,
|
|
1455
|
+
stepIndex: i,
|
|
1456
|
+
selector: step.selector,
|
|
1457
|
+
url: step.url
|
|
1458
|
+
})
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
721
1461
|
results.push(failedResult);
|
|
722
1462
|
if (onFail === "stop" && !step.optional) {
|
|
723
1463
|
stoppedAtIndex = i;
|
|
@@ -733,7 +1473,8 @@ var BatchExecutor = class {
|
|
|
733
1473
|
recording,
|
|
734
1474
|
startTime,
|
|
735
1475
|
startUrl,
|
|
736
|
-
allSuccess
|
|
1476
|
+
allSuccess,
|
|
1477
|
+
steps
|
|
737
1478
|
);
|
|
738
1479
|
}
|
|
739
1480
|
return {
|
|
@@ -748,20 +1489,14 @@ var BatchExecutor = class {
|
|
|
748
1489
|
const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
|
|
749
1490
|
const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
|
|
750
1491
|
const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
|
|
751
|
-
|
|
752
|
-
try {
|
|
753
|
-
const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
754
|
-
if (existing.frames && Array.isArray(existing.frames)) {
|
|
755
|
-
existingFrames = existing.frames;
|
|
756
|
-
}
|
|
757
|
-
} catch {
|
|
758
|
-
}
|
|
1492
|
+
const existing = loadExistingRecording(manifestPath);
|
|
759
1493
|
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
760
1494
|
return {
|
|
761
1495
|
baseDir,
|
|
762
1496
|
screenshotDir,
|
|
763
1497
|
sessionId: record.sessionId ?? this.page.targetId,
|
|
764
|
-
frames:
|
|
1498
|
+
frames: existing.frames,
|
|
1499
|
+
traceEvents: existing.traceEvents,
|
|
765
1500
|
format: record.format ?? "webp",
|
|
766
1501
|
quality: Math.max(0, Math.min(100, record.quality ?? 40)),
|
|
767
1502
|
highlights: record.highlights !== false,
|
|
@@ -817,6 +1552,7 @@ var BatchExecutor = class {
|
|
|
817
1552
|
timestamp: ts,
|
|
818
1553
|
action: stepResult.action,
|
|
819
1554
|
selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
|
|
1555
|
+
selectorUsed: stepResult.selectorUsed,
|
|
820
1556
|
value: redactValueForRecording(
|
|
821
1557
|
typeof step.value === "string" ? step.value : void 0,
|
|
822
1558
|
targetMetadata
|
|
@@ -829,7 +1565,9 @@ var BatchExecutor = class {
|
|
|
829
1565
|
error: stepResult.error,
|
|
830
1566
|
screenshot: filename,
|
|
831
1567
|
pageUrl,
|
|
832
|
-
pageTitle
|
|
1568
|
+
pageTitle,
|
|
1569
|
+
stepIndex: stepResult.index,
|
|
1570
|
+
actionId: `action-${stepResult.index + 1}`
|
|
833
1571
|
});
|
|
834
1572
|
} catch {
|
|
835
1573
|
} finally {
|
|
@@ -841,45 +1579,31 @@ var BatchExecutor = class {
|
|
|
841
1579
|
/**
|
|
842
1580
|
* Write recording manifest to disk
|
|
843
1581
|
*/
|
|
844
|
-
async writeRecordingManifest(recording, startTime, startUrl, success) {
|
|
1582
|
+
async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
|
|
845
1583
|
let endUrl = startUrl;
|
|
846
|
-
let viewport = { width: 1280, height: 720 };
|
|
847
1584
|
try {
|
|
848
1585
|
endUrl = await this.page.url();
|
|
849
1586
|
} catch {
|
|
850
1587
|
}
|
|
851
|
-
try {
|
|
852
|
-
const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
|
|
853
|
-
viewport = {
|
|
854
|
-
width: metrics.cssVisualViewport.clientWidth,
|
|
855
|
-
height: metrics.cssVisualViewport.clientHeight
|
|
856
|
-
};
|
|
857
|
-
} catch {
|
|
858
|
-
}
|
|
859
1588
|
const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
|
|
860
1589
|
let recordedAt = new Date(startTime).toISOString();
|
|
861
1590
|
let originalStartUrl = startUrl;
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
} catch {
|
|
867
|
-
}
|
|
868
|
-
const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
|
|
869
|
-
const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
|
|
870
|
-
const manifest = {
|
|
871
|
-
version: 1,
|
|
1591
|
+
const existing = loadExistingRecording(manifestPath);
|
|
1592
|
+
if (existing.recordedAt) recordedAt = existing.recordedAt;
|
|
1593
|
+
if (existing.startUrl) originalStartUrl = existing.startUrl;
|
|
1594
|
+
const manifest = createRecordingManifest({
|
|
872
1595
|
recordedAt,
|
|
873
1596
|
sessionId: recording.sessionId,
|
|
874
1597
|
startUrl: originalStartUrl,
|
|
875
1598
|
endUrl,
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
success,
|
|
881
|
-
|
|
882
|
-
|
|
1599
|
+
targetId: this.page.targetId,
|
|
1600
|
+
steps,
|
|
1601
|
+
frames: recording.frames,
|
|
1602
|
+
traceEvents: recording.traceEvents,
|
|
1603
|
+
notes: success ? [] : ["Replay ended with at least one failed action."],
|
|
1604
|
+
recordingManifest: "recording.json",
|
|
1605
|
+
screenshotDir: "screenshots/"
|
|
1606
|
+
});
|
|
883
1607
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
884
1608
|
return manifestPath;
|
|
885
1609
|
}
|
|
@@ -1162,6 +1886,39 @@ var BatchExecutor = class {
|
|
|
1162
1886
|
}
|
|
1163
1887
|
return { selectorUsed: usedSelector, value: actual };
|
|
1164
1888
|
}
|
|
1889
|
+
case "waitForWsMessage": {
|
|
1890
|
+
if (typeof step.match !== "string") {
|
|
1891
|
+
throw new Error("waitForWsMessage requires match");
|
|
1892
|
+
}
|
|
1893
|
+
const message = await this.waitForWsMessage(step.match, step.where, timeout);
|
|
1894
|
+
return { value: message };
|
|
1895
|
+
}
|
|
1896
|
+
case "assertNoConsoleErrors": {
|
|
1897
|
+
await this.assertNoConsoleErrors(step.windowMs ?? timeout);
|
|
1898
|
+
return {};
|
|
1899
|
+
}
|
|
1900
|
+
case "assertTextChanged": {
|
|
1901
|
+
const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
|
|
1902
|
+
if (typeof step.to !== "string") {
|
|
1903
|
+
throw new Error("assertTextChanged requires to");
|
|
1904
|
+
}
|
|
1905
|
+
const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
|
|
1906
|
+
return { selectorUsed: selector, text };
|
|
1907
|
+
}
|
|
1908
|
+
case "assertPermission": {
|
|
1909
|
+
if (!step.name || !step.state) {
|
|
1910
|
+
throw new Error("assertPermission requires name and state");
|
|
1911
|
+
}
|
|
1912
|
+
const permission = await this.assertPermission(step.name, step.state);
|
|
1913
|
+
return { value: permission };
|
|
1914
|
+
}
|
|
1915
|
+
case "assertMediaTrackLive": {
|
|
1916
|
+
if (!step.kind) {
|
|
1917
|
+
throw new Error("assertMediaTrackLive requires kind");
|
|
1918
|
+
}
|
|
1919
|
+
const media = await this.assertMediaTrackLive(step.kind);
|
|
1920
|
+
return { value: media };
|
|
1921
|
+
}
|
|
1165
1922
|
default: {
|
|
1166
1923
|
const action = step.action;
|
|
1167
1924
|
const aliases = {
|
|
@@ -1215,7 +1972,7 @@ var BatchExecutor = class {
|
|
|
1215
1972
|
};
|
|
1216
1973
|
const suggestion = aliases[action.toLowerCase()];
|
|
1217
1974
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
1218
|
-
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";
|
|
1975
|
+
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";
|
|
1219
1976
|
throw new Error(`Unknown action "${action}".${hint}
|
|
1220
1977
|
|
|
1221
1978
|
Valid actions: ${valid}`);
|
|
@@ -1231,6 +1988,237 @@ Valid actions: ${valid}`);
|
|
|
1231
1988
|
if (matched) return matched;
|
|
1232
1989
|
return Array.isArray(selector) ? selector[0] : selector;
|
|
1233
1990
|
}
|
|
1991
|
+
async ensureTraceHooks() {
|
|
1992
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
1993
|
+
await this.page.cdpClient.send("Page.enable");
|
|
1994
|
+
await this.page.cdpClient.send("Network.enable");
|
|
1995
|
+
try {
|
|
1996
|
+
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
1997
|
+
} catch {
|
|
1998
|
+
}
|
|
1999
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2000
|
+
source: TRACE_SCRIPT
|
|
2001
|
+
});
|
|
2002
|
+
await this.page.cdpClient.send("Runtime.evaluate", {
|
|
2003
|
+
expression: TRACE_SCRIPT,
|
|
2004
|
+
awaitPromise: false
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
async waitForWsMessage(match, where, timeout) {
|
|
2008
|
+
await this.ensureTraceHooks();
|
|
2009
|
+
const regex = globToRegex(match);
|
|
2010
|
+
const wsUrls = /* @__PURE__ */ new Map();
|
|
2011
|
+
const recentMatch = await this.findRecentWsMessage(regex, where);
|
|
2012
|
+
if (recentMatch) {
|
|
2013
|
+
return recentMatch;
|
|
2014
|
+
}
|
|
2015
|
+
return new Promise((resolve, reject) => {
|
|
2016
|
+
const cleanup = () => {
|
|
2017
|
+
this.page.cdpClient.off("Network.webSocketCreated", onCreated);
|
|
2018
|
+
this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
|
|
2019
|
+
this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
|
|
2020
|
+
clearTimeout(timer);
|
|
2021
|
+
};
|
|
2022
|
+
const onCreated = (params) => {
|
|
2023
|
+
wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
|
|
2024
|
+
};
|
|
2025
|
+
const onFrame = (params) => {
|
|
2026
|
+
const requestId = readStringOr(params["requestId"]);
|
|
2027
|
+
const response = params["response"] ?? {};
|
|
2028
|
+
const payload = response.payloadData ?? "";
|
|
2029
|
+
const url = wsUrls.get(requestId) ?? "";
|
|
2030
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
cleanup();
|
|
2037
|
+
resolve({ requestId, url, payload });
|
|
2038
|
+
};
|
|
2039
|
+
const onBinding = (params) => {
|
|
2040
|
+
if (params["name"] !== TRACE_BINDING_NAME) {
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
try {
|
|
2044
|
+
const parsed = JSON.parse(readStringOr(params["payload"]));
|
|
2045
|
+
if (parsed.event !== "ws.frame.received") {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
const data = parsed.data ?? {};
|
|
2049
|
+
const payload = readStringOr(data["payload"]);
|
|
2050
|
+
const url = readStringOr(data["url"]);
|
|
2051
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
cleanup();
|
|
2058
|
+
resolve({
|
|
2059
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2060
|
+
url,
|
|
2061
|
+
payload
|
|
2062
|
+
});
|
|
2063
|
+
} catch {
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
const timer = setTimeout(() => {
|
|
2067
|
+
cleanup();
|
|
2068
|
+
reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
|
|
2069
|
+
}, timeout);
|
|
2070
|
+
this.page.cdpClient.on("Network.webSocketCreated", onCreated);
|
|
2071
|
+
this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
|
|
2072
|
+
this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
payloadMatchesWhere(payload, where) {
|
|
2076
|
+
try {
|
|
2077
|
+
const parsed = JSON.parse(payload);
|
|
2078
|
+
return Object.entries(where).every(([key, expected]) => {
|
|
2079
|
+
const actual = key.split(".").reduce((current, part) => {
|
|
2080
|
+
if (!current || typeof current !== "object") {
|
|
2081
|
+
return void 0;
|
|
2082
|
+
}
|
|
2083
|
+
return current[part];
|
|
2084
|
+
}, parsed);
|
|
2085
|
+
return actual === expected;
|
|
2086
|
+
});
|
|
2087
|
+
} catch {
|
|
2088
|
+
return false;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
async findRecentWsMessage(regex, where) {
|
|
2092
|
+
const recent = await this.page.evaluate(
|
|
2093
|
+
"(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
|
|
2094
|
+
);
|
|
2095
|
+
if (!Array.isArray(recent)) {
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
for (let i = recent.length - 1; i >= 0; i--) {
|
|
2099
|
+
const entry = recent[i];
|
|
2100
|
+
if (!entry || typeof entry !== "object") {
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
const record = entry;
|
|
2104
|
+
const event = readStringOr(record["event"]);
|
|
2105
|
+
if (event !== "ws.frame.received") {
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
const data = record["data"] ?? {};
|
|
2109
|
+
const payload = readStringOr(data["payload"]);
|
|
2110
|
+
const url = readStringOr(data["url"]);
|
|
2111
|
+
if (!regex.test(url) && !regex.test(payload)) {
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
if (where && !this.payloadMatchesWhere(payload, where)) {
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
return {
|
|
2118
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2119
|
+
url,
|
|
2120
|
+
payload
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
2125
|
+
async assertNoConsoleErrors(windowMs) {
|
|
2126
|
+
await this.page.cdpClient.send("Runtime.enable");
|
|
2127
|
+
return new Promise((resolve, reject) => {
|
|
2128
|
+
const errors = [];
|
|
2129
|
+
const cleanup = () => {
|
|
2130
|
+
this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
|
|
2131
|
+
this.page.cdpClient.off("Runtime.exceptionThrown", onException);
|
|
2132
|
+
clearTimeout(timer);
|
|
2133
|
+
};
|
|
2134
|
+
const onConsole = (params) => {
|
|
2135
|
+
if (params["type"] !== "error") {
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2139
|
+
errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
|
|
2140
|
+
};
|
|
2141
|
+
const onException = (params) => {
|
|
2142
|
+
const details = params["exceptionDetails"] ?? {};
|
|
2143
|
+
errors.push(readString(details["text"]) ?? "Runtime exception");
|
|
2144
|
+
};
|
|
2145
|
+
const timer = setTimeout(() => {
|
|
2146
|
+
cleanup();
|
|
2147
|
+
if (errors.length > 0) {
|
|
2148
|
+
reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
resolve();
|
|
2152
|
+
}, windowMs);
|
|
2153
|
+
this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
|
|
2154
|
+
this.page.cdpClient.on("Runtime.exceptionThrown", onException);
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
async assertTextChanged(selector, from, to, timeout) {
|
|
2158
|
+
const initialText = from ?? await this.page.text(selector);
|
|
2159
|
+
const deadline = Date.now() + timeout;
|
|
2160
|
+
while (Date.now() < deadline) {
|
|
2161
|
+
const text = await this.page.text(selector);
|
|
2162
|
+
if (text !== initialText && text.includes(to)) {
|
|
2163
|
+
return text;
|
|
2164
|
+
}
|
|
2165
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2166
|
+
}
|
|
2167
|
+
throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
|
|
2168
|
+
}
|
|
2169
|
+
async assertPermission(name, state) {
|
|
2170
|
+
const result = await this.page.evaluate(
|
|
2171
|
+
`(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
|
|
2172
|
+
);
|
|
2173
|
+
if (!result || typeof result !== "object" || result.state !== state) {
|
|
2174
|
+
throw new Error(`Permission ${name} is not ${state}`);
|
|
2175
|
+
}
|
|
2176
|
+
return result;
|
|
2177
|
+
}
|
|
2178
|
+
async assertMediaTrackLive(kind) {
|
|
2179
|
+
const result = await this.page.evaluate(
|
|
2180
|
+
`(() => {
|
|
2181
|
+
const requestedKind = ${JSON.stringify(kind)};
|
|
2182
|
+
const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
|
|
2183
|
+
const tracks = [];
|
|
2184
|
+
if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
|
|
2185
|
+
tracks.push(...el.srcObject.getTracks());
|
|
2186
|
+
}
|
|
2187
|
+
return {
|
|
2188
|
+
tag: el.tagName.toLowerCase(),
|
|
2189
|
+
paused: !!el.paused,
|
|
2190
|
+
tracks: tracks.map((track) => ({
|
|
2191
|
+
kind: track.kind,
|
|
2192
|
+
readyState: track.readyState,
|
|
2193
|
+
enabled: track.enabled,
|
|
2194
|
+
label: track.label,
|
|
2195
|
+
})),
|
|
2196
|
+
};
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
const globalTracks =
|
|
2200
|
+
window.__bpStream && typeof window.__bpStream.getTracks === 'function'
|
|
2201
|
+
? window.__bpStream.getTracks().map((track) => ({
|
|
2202
|
+
kind: track.kind,
|
|
2203
|
+
readyState: track.readyState,
|
|
2204
|
+
enabled: track.enabled,
|
|
2205
|
+
label: track.label,
|
|
2206
|
+
}))
|
|
2207
|
+
: [];
|
|
2208
|
+
|
|
2209
|
+
const liveTracks = mediaElements
|
|
2210
|
+
.flatMap((entry) => entry.tracks)
|
|
2211
|
+
.concat(globalTracks)
|
|
2212
|
+
.filter((track) => track.kind === requestedKind && track.readyState === 'live');
|
|
2213
|
+
|
|
2214
|
+
return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
|
|
2215
|
+
})()`
|
|
2216
|
+
);
|
|
2217
|
+
if (!result || typeof result !== "object" || !result.live) {
|
|
2218
|
+
throw new Error(`No live ${kind} media track detected`);
|
|
2219
|
+
}
|
|
2220
|
+
return result;
|
|
2221
|
+
}
|
|
1234
2222
|
};
|
|
1235
2223
|
function addBatchToPage(page) {
|
|
1236
2224
|
const executor = new BatchExecutor(page);
|
|
@@ -1361,7 +2349,7 @@ var ACTION_RULES = {
|
|
|
1361
2349
|
value: { type: "string|string[]" },
|
|
1362
2350
|
trigger: { type: "string|string[]" },
|
|
1363
2351
|
option: { type: "string|string[]" },
|
|
1364
|
-
match: { type: "string"
|
|
2352
|
+
match: { type: "string" }
|
|
1365
2353
|
}
|
|
1366
2354
|
},
|
|
1367
2355
|
check: {
|
|
@@ -1492,6 +2480,38 @@ var ACTION_RULES = {
|
|
|
1492
2480
|
expect: { type: "string" },
|
|
1493
2481
|
value: { type: "string" }
|
|
1494
2482
|
}
|
|
2483
|
+
},
|
|
2484
|
+
waitForWsMessage: {
|
|
2485
|
+
required: { match: { type: "string" } },
|
|
2486
|
+
optional: {
|
|
2487
|
+
where: { type: "object" }
|
|
2488
|
+
}
|
|
2489
|
+
},
|
|
2490
|
+
assertNoConsoleErrors: {
|
|
2491
|
+
required: {},
|
|
2492
|
+
optional: {
|
|
2493
|
+
windowMs: { type: "number" }
|
|
2494
|
+
}
|
|
2495
|
+
},
|
|
2496
|
+
assertTextChanged: {
|
|
2497
|
+
required: { to: { type: "string" } },
|
|
2498
|
+
optional: {
|
|
2499
|
+
selector: { type: "string|string[]" },
|
|
2500
|
+
from: { type: "string" }
|
|
2501
|
+
}
|
|
2502
|
+
},
|
|
2503
|
+
assertPermission: {
|
|
2504
|
+
required: {
|
|
2505
|
+
name: { type: "string" },
|
|
2506
|
+
state: { type: "string" }
|
|
2507
|
+
},
|
|
2508
|
+
optional: {}
|
|
2509
|
+
},
|
|
2510
|
+
assertMediaTrackLive: {
|
|
2511
|
+
required: {
|
|
2512
|
+
kind: { type: "string", enum: ["audio", "video"] }
|
|
2513
|
+
},
|
|
2514
|
+
optional: {}
|
|
1495
2515
|
}
|
|
1496
2516
|
};
|
|
1497
2517
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -1515,6 +2535,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1515
2535
|
"trigger",
|
|
1516
2536
|
"option",
|
|
1517
2537
|
"match",
|
|
2538
|
+
"where",
|
|
1518
2539
|
"x",
|
|
1519
2540
|
"y",
|
|
1520
2541
|
"direction",
|
|
@@ -1524,7 +2545,13 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1524
2545
|
"fullPage",
|
|
1525
2546
|
"expect",
|
|
1526
2547
|
"retry",
|
|
1527
|
-
"retryDelay"
|
|
2548
|
+
"retryDelay",
|
|
2549
|
+
"from",
|
|
2550
|
+
"to",
|
|
2551
|
+
"name",
|
|
2552
|
+
"state",
|
|
2553
|
+
"kind",
|
|
2554
|
+
"windowMs"
|
|
1528
2555
|
]);
|
|
1529
2556
|
function resolveAction(name) {
|
|
1530
2557
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -1597,6 +2624,11 @@ function checkFieldType(value, rule) {
|
|
|
1597
2624
|
return `expected boolean or "auto", got ${typeof value}`;
|
|
1598
2625
|
}
|
|
1599
2626
|
return null;
|
|
2627
|
+
case "object":
|
|
2628
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2629
|
+
return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
|
|
2630
|
+
}
|
|
2631
|
+
return null;
|
|
1600
2632
|
default: {
|
|
1601
2633
|
const _exhaustive = rule.type;
|
|
1602
2634
|
return `unknown type: ${_exhaustive}`;
|