browser-pilot 0.0.13 → 0.0.14
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 +59 -3
- package/dist/actions.cjs +418 -14
- package/dist/actions.d.cts +13 -3
- package/dist/actions.d.ts +13 -3
- package/dist/actions.mjs +1 -1
- package/dist/browser-LZTEHUDI.mjs +9 -0
- package/dist/browser.cjs +600 -20
- package/dist/browser.d.cts +12 -3
- package/dist/browser.d.ts +12 -3
- package/dist/browser.mjs +3 -3
- package/dist/cdp.cjs +31 -2
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +3 -1
- package/dist/chunk-7NDR6V7S.mjs +7788 -0
- package/dist/{chunk-VDAMDOS6.mjs → chunk-IN5HPAPB.mjs} +147 -7
- package/dist/{chunk-HP6R3W32.mjs → chunk-KIFB526Y.mjs} +44 -2
- package/dist/chunk-LUGLEMVR.mjs +11 -0
- package/dist/chunk-SPSZZH22.mjs +308 -0
- package/dist/{chunk-A2ZRAEO3.mjs → chunk-XMJABKCF.mjs} +408 -14
- package/dist/cli.mjs +1063 -7746
- package/dist/client-3AFV2IAF.mjs +10 -0
- package/dist/{client-DRqxBdHv.d.ts → client-Ck2nQksT.d.cts} +8 -6
- package/dist/{client-DRqxBdHv.d.cts → client-Ck2nQksT.d.ts} +8 -6
- package/dist/index.cjs +600 -20
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/transport-WHEBAZUP.mjs +83 -0
- package/dist/{types-CzgQjai9.d.ts → types-BSoh5v1Y.d.cts} +62 -2
- package/dist/{types-BXMGFtnB.d.cts → types-CjT0vClo.d.ts} +62 -2
- package/package.json +2 -2
|
@@ -2,6 +2,230 @@ import {
|
|
|
2
2
|
CDPError
|
|
3
3
|
} from "./chunk-JXAUPHZM.mjs";
|
|
4
4
|
|
|
5
|
+
// src/actions/executor.ts
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
|
|
9
|
+
// src/recording/redaction.ts
|
|
10
|
+
var REDACTED_VALUE = "[REDACTED]";
|
|
11
|
+
var SENSITIVE_AUTOCOMPLETE_TOKENS = [
|
|
12
|
+
"current-password",
|
|
13
|
+
"new-password",
|
|
14
|
+
"one-time-code",
|
|
15
|
+
"cc-number",
|
|
16
|
+
"cc-csc",
|
|
17
|
+
"cc-exp",
|
|
18
|
+
"cc-exp-month",
|
|
19
|
+
"cc-exp-year"
|
|
20
|
+
];
|
|
21
|
+
function autocompleteTokens(autocomplete) {
|
|
22
|
+
if (!autocomplete) return [];
|
|
23
|
+
return autocomplete.toLowerCase().split(/\s+/).map((token) => token.trim()).filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
function isSensitiveFieldMetadata(metadata) {
|
|
26
|
+
if (!metadata) return false;
|
|
27
|
+
if (metadata.sensitiveValue) return true;
|
|
28
|
+
const inputType = metadata.inputType?.toLowerCase();
|
|
29
|
+
if (inputType === "password" || inputType === "hidden") {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const sensitiveAutocompleteTokens = new Set(SENSITIVE_AUTOCOMPLETE_TOKENS);
|
|
33
|
+
return autocompleteTokens(metadata.autocomplete).some(
|
|
34
|
+
(token) => sensitiveAutocompleteTokens.has(token)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function redactValueForRecording(value, metadata) {
|
|
38
|
+
if (value === void 0) return void 0;
|
|
39
|
+
return isSensitiveFieldMetadata(metadata) ? REDACTED_VALUE : value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/browser/action-highlight.ts
|
|
43
|
+
var HIGHLIGHT_STYLES = {
|
|
44
|
+
click: { outline: "3px solid rgba(229,57,53,0.8)", badge: "#e53935", marker: "crosshair" },
|
|
45
|
+
fill: { outline: "3px solid rgba(33,150,243,0.8)", badge: "#2196f3" },
|
|
46
|
+
type: { outline: "3px solid rgba(33,150,243,0.6)", badge: "#2196f3" },
|
|
47
|
+
select: { outline: "3px solid rgba(156,39,176,0.8)", badge: "#9c27b0" },
|
|
48
|
+
hover: { outline: "2px dashed rgba(158,158,158,0.5)", badge: "#9e9e9e" },
|
|
49
|
+
scroll: { outline: "none", badge: "#607d8b", marker: "arrow" },
|
|
50
|
+
navigate: { outline: "none", badge: "#4caf50" },
|
|
51
|
+
submit: { outline: "3px solid rgba(255,152,0,0.8)", badge: "#ff9800" },
|
|
52
|
+
"assert-pass": { outline: "3px solid rgba(76,175,80,0.8)", badge: "#4caf50", marker: "check" },
|
|
53
|
+
"assert-fail": { outline: "3px solid rgba(244,67,54,0.8)", badge: "#f44336", marker: "cross" },
|
|
54
|
+
evaluate: { outline: "none", badge: "#ffc107" },
|
|
55
|
+
focus: { outline: "3px dotted rgba(33,150,243,0.6)", badge: "#2196f3" }
|
|
56
|
+
};
|
|
57
|
+
function buildHighlightScript(options) {
|
|
58
|
+
const style = HIGHLIGHT_STYLES[options.kind];
|
|
59
|
+
const label = options.label ? options.label.slice(0, 80) : void 0;
|
|
60
|
+
const escapedLabel = label ? label.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n") : "";
|
|
61
|
+
return `(function() {
|
|
62
|
+
// Remove any existing highlight
|
|
63
|
+
var existing = document.getElementById('__bp-action-highlight');
|
|
64
|
+
if (existing) existing.remove();
|
|
65
|
+
|
|
66
|
+
var container = document.createElement('div');
|
|
67
|
+
container.id = '__bp-action-highlight';
|
|
68
|
+
container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
|
|
69
|
+
|
|
70
|
+
${options.bbox ? `
|
|
71
|
+
// Element outline
|
|
72
|
+
var outline = document.createElement('div');
|
|
73
|
+
outline.style.cssText = 'position:fixed;' +
|
|
74
|
+
'left:${options.bbox.x}px;top:${options.bbox.y}px;' +
|
|
75
|
+
'width:${options.bbox.width}px;height:${options.bbox.height}px;' +
|
|
76
|
+
'${style.outline !== "none" ? `outline:${style.outline};outline-offset:-1px;` : ""}' +
|
|
77
|
+
'pointer-events:none;box-sizing:border-box;';
|
|
78
|
+
container.appendChild(outline);
|
|
79
|
+
` : ""}
|
|
80
|
+
|
|
81
|
+
${options.point && style.marker === "crosshair" ? `
|
|
82
|
+
// Crosshair at click point
|
|
83
|
+
var hLine = document.createElement('div');
|
|
84
|
+
hLine.style.cssText = 'position:fixed;left:${options.point.x - 12}px;top:${options.point.y}px;' +
|
|
85
|
+
'width:24px;height:2px;background:${style.badge};pointer-events:none;';
|
|
86
|
+
var vLine = document.createElement('div');
|
|
87
|
+
vLine.style.cssText = 'position:fixed;left:${options.point.x}px;top:${options.point.y - 12}px;' +
|
|
88
|
+
'width:2px;height:24px;background:${style.badge};pointer-events:none;';
|
|
89
|
+
// Dot at center
|
|
90
|
+
var dot = document.createElement('div');
|
|
91
|
+
dot.style.cssText = 'position:fixed;left:${options.point.x - 4}px;top:${options.point.y - 4}px;' +
|
|
92
|
+
'width:8px;height:8px;border-radius:50%;background:${style.badge};pointer-events:none;';
|
|
93
|
+
container.appendChild(hLine);
|
|
94
|
+
container.appendChild(vLine);
|
|
95
|
+
container.appendChild(dot);
|
|
96
|
+
` : ""}
|
|
97
|
+
|
|
98
|
+
${label ? `
|
|
99
|
+
// Badge with label
|
|
100
|
+
var badge = document.createElement('div');
|
|
101
|
+
badge.style.cssText = 'position:fixed;' +
|
|
102
|
+
${options.bbox ? `'left:${options.bbox.x}px;top:${Math.max(0, options.bbox.y - 28)}px;'` : options.kind === "navigate" ? "'left:50%;top:8px;transform:translateX(-50%);'" : "'right:8px;top:8px;'"} +
|
|
103
|
+
'background:${style.badge};color:white;padding:4px 8px;' +
|
|
104
|
+
'font-family:monospace;font-size:12px;font-weight:bold;' +
|
|
105
|
+
'border-radius:3px;white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis;' +
|
|
106
|
+
'pointer-events:none;';
|
|
107
|
+
badge.textContent = '${escapedLabel}';
|
|
108
|
+
container.appendChild(badge);
|
|
109
|
+
` : ""}
|
|
110
|
+
|
|
111
|
+
${style.marker === "check" && options.bbox ? `
|
|
112
|
+
// Checkmark
|
|
113
|
+
var check = document.createElement('div');
|
|
114
|
+
check.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
|
|
115
|
+
'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
|
|
116
|
+
'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;';
|
|
117
|
+
check.textContent = '\\u2713';
|
|
118
|
+
container.appendChild(check);
|
|
119
|
+
` : ""}
|
|
120
|
+
|
|
121
|
+
${style.marker === "cross" && options.bbox ? `
|
|
122
|
+
// Cross mark
|
|
123
|
+
var cross = document.createElement('div');
|
|
124
|
+
cross.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
|
|
125
|
+
'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
|
|
126
|
+
'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;font-weight:bold;';
|
|
127
|
+
cross.textContent = '\\u2717';
|
|
128
|
+
container.appendChild(cross);
|
|
129
|
+
` : ""}
|
|
130
|
+
|
|
131
|
+
document.body.appendChild(container);
|
|
132
|
+
window.__bpRemoveActionHighlight = function() {
|
|
133
|
+
var el = document.getElementById('__bp-action-highlight');
|
|
134
|
+
if (el) el.remove();
|
|
135
|
+
delete window.__bpRemoveActionHighlight;
|
|
136
|
+
};
|
|
137
|
+
})();`;
|
|
138
|
+
}
|
|
139
|
+
async function injectActionHighlight(page, options) {
|
|
140
|
+
try {
|
|
141
|
+
await page.evaluate(buildHighlightScript(options));
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function removeActionHighlight(page) {
|
|
146
|
+
try {
|
|
147
|
+
await page.evaluate(`(function() {
|
|
148
|
+
if (window.__bpRemoveActionHighlight) {
|
|
149
|
+
window.__bpRemoveActionHighlight();
|
|
150
|
+
}
|
|
151
|
+
})()`);
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function stepToHighlightKind(step) {
|
|
156
|
+
switch (step.action) {
|
|
157
|
+
case "click":
|
|
158
|
+
return "click";
|
|
159
|
+
case "fill":
|
|
160
|
+
return "fill";
|
|
161
|
+
case "type":
|
|
162
|
+
return "type";
|
|
163
|
+
case "select":
|
|
164
|
+
return "select";
|
|
165
|
+
case "hover":
|
|
166
|
+
return "hover";
|
|
167
|
+
case "scroll":
|
|
168
|
+
return "scroll";
|
|
169
|
+
case "goto":
|
|
170
|
+
return "navigate";
|
|
171
|
+
case "submit":
|
|
172
|
+
return "submit";
|
|
173
|
+
case "focus":
|
|
174
|
+
return "focus";
|
|
175
|
+
case "evaluate":
|
|
176
|
+
case "press":
|
|
177
|
+
case "shortcut":
|
|
178
|
+
return "evaluate";
|
|
179
|
+
case "assertVisible":
|
|
180
|
+
case "assertExists":
|
|
181
|
+
case "assertText":
|
|
182
|
+
case "assertUrl":
|
|
183
|
+
case "assertValue":
|
|
184
|
+
return step.success ? "assert-pass" : "assert-fail";
|
|
185
|
+
// Observation-only actions — no highlight
|
|
186
|
+
case "wait":
|
|
187
|
+
case "snapshot":
|
|
188
|
+
case "forms":
|
|
189
|
+
case "text":
|
|
190
|
+
case "screenshot":
|
|
191
|
+
case "newTab":
|
|
192
|
+
case "closeTab":
|
|
193
|
+
case "switchFrame":
|
|
194
|
+
case "switchToMain":
|
|
195
|
+
return null;
|
|
196
|
+
default:
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function getHighlightLabel(step, result, targetMetadata) {
|
|
201
|
+
switch (step.action) {
|
|
202
|
+
case "fill":
|
|
203
|
+
case "type":
|
|
204
|
+
return typeof step.value === "string" ? `"${redactValueForRecording(step.value, targetMetadata)}"` : void 0;
|
|
205
|
+
case "select":
|
|
206
|
+
return redactValueForRecording(
|
|
207
|
+
typeof step.value === "string" ? step.value : void 0,
|
|
208
|
+
targetMetadata
|
|
209
|
+
);
|
|
210
|
+
case "goto":
|
|
211
|
+
return step.url;
|
|
212
|
+
case "evaluate":
|
|
213
|
+
return "JS";
|
|
214
|
+
case "press":
|
|
215
|
+
return step.key;
|
|
216
|
+
case "shortcut":
|
|
217
|
+
return step.combo;
|
|
218
|
+
case "assertText":
|
|
219
|
+
case "assertUrl":
|
|
220
|
+
case "assertValue":
|
|
221
|
+
case "assertVisible":
|
|
222
|
+
case "assertExists":
|
|
223
|
+
return result.success ? "\u2713" : "\u2717";
|
|
224
|
+
default:
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
5
229
|
// src/browser/actionability.ts
|
|
6
230
|
var ActionabilityError = class extends Error {
|
|
7
231
|
failureType;
|
|
@@ -616,6 +840,13 @@ var NavigationError = class extends Error {
|
|
|
616
840
|
|
|
617
841
|
// src/actions/executor.ts
|
|
618
842
|
var DEFAULT_TIMEOUT = 3e4;
|
|
843
|
+
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
844
|
+
"wait",
|
|
845
|
+
"snapshot",
|
|
846
|
+
"forms",
|
|
847
|
+
"text",
|
|
848
|
+
"screenshot"
|
|
849
|
+
];
|
|
619
850
|
function classifyFailure(error) {
|
|
620
851
|
if (error instanceof ElementNotFoundError) {
|
|
621
852
|
return { reason: "missing" };
|
|
@@ -695,6 +926,9 @@ var BatchExecutor = class {
|
|
|
695
926
|
const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
|
|
696
927
|
const results = [];
|
|
697
928
|
const startTime = Date.now();
|
|
929
|
+
const recording = options.record ? this.createRecordingContext(options.record) : null;
|
|
930
|
+
const startUrl = recording ? await this.getPageUrlSafe() : "";
|
|
931
|
+
let stoppedAtIndex;
|
|
698
932
|
for (let i = 0; i < steps.length; i++) {
|
|
699
933
|
const step = steps[i];
|
|
700
934
|
const stepStart = Date.now();
|
|
@@ -707,8 +941,9 @@ var BatchExecutor = class {
|
|
|
707
941
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
708
942
|
}
|
|
709
943
|
try {
|
|
944
|
+
this.page.resetLastActionPosition();
|
|
710
945
|
const result = await this.executeStep(step, timeout);
|
|
711
|
-
|
|
946
|
+
const stepResult = {
|
|
712
947
|
index: i,
|
|
713
948
|
action: step.action,
|
|
714
949
|
selector: step.selector,
|
|
@@ -716,8 +951,15 @@ var BatchExecutor = class {
|
|
|
716
951
|
success: true,
|
|
717
952
|
durationMs: Date.now() - stepStart,
|
|
718
953
|
result: result.value,
|
|
719
|
-
text: result.text
|
|
720
|
-
|
|
954
|
+
text: result.text,
|
|
955
|
+
timestamp: Date.now(),
|
|
956
|
+
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
957
|
+
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
958
|
+
};
|
|
959
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
960
|
+
await this.captureRecordingFrame(step, stepResult, recording);
|
|
961
|
+
}
|
|
962
|
+
results.push(stepResult);
|
|
721
963
|
succeeded = true;
|
|
722
964
|
break;
|
|
723
965
|
} catch (error) {
|
|
@@ -738,7 +980,7 @@ var BatchExecutor = class {
|
|
|
738
980
|
} catch {
|
|
739
981
|
}
|
|
740
982
|
}
|
|
741
|
-
|
|
983
|
+
const failedResult = {
|
|
742
984
|
index: i,
|
|
743
985
|
action: step.action,
|
|
744
986
|
selector: step.selector,
|
|
@@ -748,24 +990,176 @@ var BatchExecutor = class {
|
|
|
748
990
|
hints,
|
|
749
991
|
failureReason: reason,
|
|
750
992
|
coveringElement,
|
|
751
|
-
suggestion: getSuggestion(reason)
|
|
752
|
-
|
|
993
|
+
suggestion: getSuggestion(reason),
|
|
994
|
+
timestamp: Date.now()
|
|
995
|
+
};
|
|
996
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
997
|
+
await this.captureRecordingFrame(step, failedResult, recording);
|
|
998
|
+
}
|
|
999
|
+
results.push(failedResult);
|
|
753
1000
|
if (onFail === "stop" && !step.optional) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
stoppedAtIndex: i,
|
|
757
|
-
steps: results,
|
|
758
|
-
totalDurationMs: Date.now() - startTime
|
|
759
|
-
};
|
|
1001
|
+
stoppedAtIndex = i;
|
|
1002
|
+
break;
|
|
760
1003
|
}
|
|
761
1004
|
}
|
|
762
1005
|
}
|
|
763
|
-
const
|
|
1006
|
+
const totalDurationMs = Date.now() - startTime;
|
|
1007
|
+
const allSuccess = stoppedAtIndex === void 0 && results.every((result) => result.success || steps[result.index]?.optional);
|
|
1008
|
+
let recordingManifest;
|
|
1009
|
+
if (recording) {
|
|
1010
|
+
recordingManifest = await this.writeRecordingManifest(
|
|
1011
|
+
recording,
|
|
1012
|
+
startTime,
|
|
1013
|
+
startUrl,
|
|
1014
|
+
allSuccess
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
764
1017
|
return {
|
|
765
1018
|
success: allSuccess,
|
|
1019
|
+
stoppedAtIndex,
|
|
766
1020
|
steps: results,
|
|
767
|
-
totalDurationMs
|
|
1021
|
+
totalDurationMs,
|
|
1022
|
+
recordingManifest
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
createRecordingContext(record) {
|
|
1026
|
+
const baseDir = record.outputDir ?? join(process.cwd(), ".browser-pilot");
|
|
1027
|
+
const screenshotDir = join(baseDir, "screenshots");
|
|
1028
|
+
const manifestPath = join(baseDir, "recording.json");
|
|
1029
|
+
let existingFrames = [];
|
|
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
|
+
}
|
|
1037
|
+
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
1038
|
+
return {
|
|
1039
|
+
baseDir,
|
|
1040
|
+
screenshotDir,
|
|
1041
|
+
sessionId: record.sessionId ?? this.page.targetId,
|
|
1042
|
+
frames: existingFrames,
|
|
1043
|
+
format: record.format ?? "webp",
|
|
1044
|
+
quality: Math.max(0, Math.min(100, record.quality ?? 40)),
|
|
1045
|
+
highlights: record.highlights !== false,
|
|
1046
|
+
skipActions: new Set(record.skipActions ?? DEFAULT_RECORDING_SKIP_ACTIONS)
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
async getPageUrlSafe() {
|
|
1050
|
+
try {
|
|
1051
|
+
return await this.page.url();
|
|
1052
|
+
} catch {
|
|
1053
|
+
return "";
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Capture a recording screenshot frame with optional highlight overlay
|
|
1058
|
+
*/
|
|
1059
|
+
async captureRecordingFrame(step, stepResult, recording) {
|
|
1060
|
+
const targetMetadata = this.page.getLastActionTargetMetadata();
|
|
1061
|
+
let highlightInjected = false;
|
|
1062
|
+
try {
|
|
1063
|
+
const ts = Date.now();
|
|
1064
|
+
const seq = String(recording.frames.length + 1).padStart(4, "0");
|
|
1065
|
+
const filename = `${seq}-${ts}-${stepResult.action}.${recording.format}`;
|
|
1066
|
+
const filepath = join(recording.screenshotDir, filename);
|
|
1067
|
+
if (recording.highlights) {
|
|
1068
|
+
const kind = stepToHighlightKind(stepResult);
|
|
1069
|
+
if (kind) {
|
|
1070
|
+
await injectActionHighlight(this.page, {
|
|
1071
|
+
kind,
|
|
1072
|
+
bbox: stepResult.boundingBox,
|
|
1073
|
+
point: stepResult.coordinates,
|
|
1074
|
+
label: getHighlightLabel(step, stepResult, targetMetadata)
|
|
1075
|
+
});
|
|
1076
|
+
highlightInjected = true;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const base64 = await this.page.screenshot({
|
|
1080
|
+
format: recording.format,
|
|
1081
|
+
quality: recording.quality
|
|
1082
|
+
});
|
|
1083
|
+
const buffer = Buffer.from(base64, "base64");
|
|
1084
|
+
fs.writeFileSync(filepath, buffer);
|
|
1085
|
+
stepResult.screenshotPath = filepath;
|
|
1086
|
+
let pageUrl;
|
|
1087
|
+
let pageTitle;
|
|
1088
|
+
try {
|
|
1089
|
+
pageUrl = await this.page.url();
|
|
1090
|
+
pageTitle = await this.page.title();
|
|
1091
|
+
} catch {
|
|
1092
|
+
}
|
|
1093
|
+
recording.frames.push({
|
|
1094
|
+
seq: recording.frames.length + 1,
|
|
1095
|
+
timestamp: ts,
|
|
1096
|
+
action: stepResult.action,
|
|
1097
|
+
selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
|
|
1098
|
+
value: redactValueForRecording(
|
|
1099
|
+
typeof step.value === "string" ? step.value : void 0,
|
|
1100
|
+
targetMetadata
|
|
1101
|
+
),
|
|
1102
|
+
url: step.url,
|
|
1103
|
+
coordinates: stepResult.coordinates,
|
|
1104
|
+
boundingBox: stepResult.boundingBox,
|
|
1105
|
+
success: stepResult.success,
|
|
1106
|
+
durationMs: stepResult.durationMs,
|
|
1107
|
+
error: stepResult.error,
|
|
1108
|
+
screenshot: filename,
|
|
1109
|
+
pageUrl,
|
|
1110
|
+
pageTitle
|
|
1111
|
+
});
|
|
1112
|
+
} catch {
|
|
1113
|
+
} finally {
|
|
1114
|
+
if (recording.highlights || highlightInjected) {
|
|
1115
|
+
await removeActionHighlight(this.page);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Write recording manifest to disk
|
|
1121
|
+
*/
|
|
1122
|
+
async writeRecordingManifest(recording, startTime, startUrl, success) {
|
|
1123
|
+
let endUrl = startUrl;
|
|
1124
|
+
let viewport = { width: 1280, height: 720 };
|
|
1125
|
+
try {
|
|
1126
|
+
endUrl = await this.page.url();
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
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
|
+
const manifestPath = join(recording.baseDir, "recording.json");
|
|
1138
|
+
let recordedAt = new Date(startTime).toISOString();
|
|
1139
|
+
let originalStartUrl = startUrl;
|
|
1140
|
+
try {
|
|
1141
|
+
const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1142
|
+
if (existing.recordedAt) recordedAt = existing.recordedAt;
|
|
1143
|
+
if (existing.startUrl) originalStartUrl = existing.startUrl;
|
|
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,
|
|
1150
|
+
recordedAt,
|
|
1151
|
+
sessionId: recording.sessionId,
|
|
1152
|
+
startUrl: originalStartUrl,
|
|
1153
|
+
endUrl,
|
|
1154
|
+
viewport,
|
|
1155
|
+
format: recording.format,
|
|
1156
|
+
quality: recording.quality,
|
|
1157
|
+
totalDurationMs,
|
|
1158
|
+
success,
|
|
1159
|
+
frames: recording.frames
|
|
768
1160
|
};
|
|
1161
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1162
|
+
return manifestPath;
|
|
769
1163
|
}
|
|
770
1164
|
/**
|
|
771
1165
|
* Execute a single step
|