browser-pilot 0.0.12 → 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 +72 -10
- package/dist/actions.cjs +481 -24
- 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 +1227 -75
- package/dist/browser.d.cts +18 -3
- package/dist/browser.d.ts +18 -3
- package/dist/browser.mjs +3 -3
- package/dist/cdp.cjs +32 -3
- 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-RUWAXHDX.mjs → chunk-IN5HPAPB.mjs} +749 -60
- package/dist/{chunk-4MBSALQL.mjs → chunk-KIFB526Y.mjs} +45 -3
- package/dist/chunk-LUGLEMVR.mjs +11 -0
- package/dist/chunk-SPSZZH22.mjs +308 -0
- package/dist/{chunk-NLIARNEE.mjs → chunk-XMJABKCF.mjs} +471 -24
- package/dist/cli.mjs +1427 -7092
- package/dist/client-3AFV2IAF.mjs +10 -0
- package/dist/{client-7Nqka5MV.d.ts → client-Ck2nQksT.d.cts} +9 -7
- package/dist/{client-7Nqka5MV.d.cts → client-Ck2nQksT.d.ts} +9 -7
- package/dist/index.cjs +1266 -84
- 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-j23Iqo2L.d.ts → types-BSoh5v1Y.d.cts} +106 -5
- package/dist/{types-BOPu0OQZ.d.cts → types-CjT0vClo.d.ts} +106 -5
- 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
|
|
@@ -926,6 +1320,9 @@ var BatchExecutor = class {
|
|
|
926
1320
|
const snapshot = await this.page.snapshot();
|
|
927
1321
|
return { value: snapshot };
|
|
928
1322
|
}
|
|
1323
|
+
case "forms": {
|
|
1324
|
+
return { value: await this.page.forms() };
|
|
1325
|
+
}
|
|
929
1326
|
case "screenshot": {
|
|
930
1327
|
const data = await this.page.screenshot({
|
|
931
1328
|
format: step.format,
|
|
@@ -945,6 +1342,21 @@ var BatchExecutor = class {
|
|
|
945
1342
|
const text = await this.page.text(selector);
|
|
946
1343
|
return { text, selectorUsed: selector };
|
|
947
1344
|
}
|
|
1345
|
+
case "newTab": {
|
|
1346
|
+
const { targetId } = await this.page.cdpClient.send(
|
|
1347
|
+
"Target.createTarget",
|
|
1348
|
+
{
|
|
1349
|
+
url: step.url ?? "about:blank"
|
|
1350
|
+
},
|
|
1351
|
+
null
|
|
1352
|
+
);
|
|
1353
|
+
return { value: { targetId } };
|
|
1354
|
+
}
|
|
1355
|
+
case "closeTab": {
|
|
1356
|
+
const targetId = step.targetId ?? this.page.targetId;
|
|
1357
|
+
await this.page.cdpClient.send("Target.closeTarget", { targetId }, null);
|
|
1358
|
+
return { value: { targetId, closedCurrent: targetId === this.page.targetId } };
|
|
1359
|
+
}
|
|
948
1360
|
case "switchFrame": {
|
|
949
1361
|
if (!step.selector) throw new Error("switchFrame requires selector");
|
|
950
1362
|
await this.page.switchToFrame(step.selector, { timeout, optional });
|
|
@@ -1059,10 +1471,15 @@ var BatchExecutor = class {
|
|
|
1059
1471
|
snap: "snapshot",
|
|
1060
1472
|
accessibility: "snapshot",
|
|
1061
1473
|
a11y: "snapshot",
|
|
1474
|
+
formslist: "forms",
|
|
1062
1475
|
image: "screenshot",
|
|
1063
1476
|
pic: "screenshot",
|
|
1064
1477
|
frame: "switchFrame",
|
|
1065
1478
|
iframe: "switchFrame",
|
|
1479
|
+
newtab: "newTab",
|
|
1480
|
+
opentab: "newTab",
|
|
1481
|
+
createtab: "newTab",
|
|
1482
|
+
closetab: "closeTab",
|
|
1066
1483
|
assert_visible: "assertVisible",
|
|
1067
1484
|
assert_exists: "assertExists",
|
|
1068
1485
|
assert_text: "assertText",
|
|
@@ -1076,7 +1493,7 @@ var BatchExecutor = class {
|
|
|
1076
1493
|
};
|
|
1077
1494
|
const suggestion = aliases[action.toLowerCase()];
|
|
1078
1495
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
1079
|
-
const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, screenshot, evaluate, text, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
|
|
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";
|
|
1080
1497
|
throw new Error(`Unknown action "${action}".${hint}
|
|
1081
1498
|
|
|
1082
1499
|
Valid actions: ${valid}`);
|
|
@@ -1147,6 +1564,11 @@ var ACTION_ALIASES = {
|
|
|
1147
1564
|
pic: "screenshot",
|
|
1148
1565
|
frame: "switchFrame",
|
|
1149
1566
|
iframe: "switchFrame",
|
|
1567
|
+
formslist: "forms",
|
|
1568
|
+
newtab: "newTab",
|
|
1569
|
+
opentab: "newTab",
|
|
1570
|
+
createtab: "newTab",
|
|
1571
|
+
closetab: "closeTab",
|
|
1150
1572
|
assert_visible: "assertVisible",
|
|
1151
1573
|
assert_exists: "assertExists",
|
|
1152
1574
|
assert_text: "assertText",
|
|
@@ -1183,7 +1605,8 @@ var PROPERTY_ALIASES = {
|
|
|
1183
1605
|
button: "key",
|
|
1184
1606
|
address: "url",
|
|
1185
1607
|
page: "url",
|
|
1186
|
-
path: "url"
|
|
1608
|
+
path: "url",
|
|
1609
|
+
tabId: "targetId"
|
|
1187
1610
|
};
|
|
1188
1611
|
var ACTION_RULES = {
|
|
1189
1612
|
goto: {
|
|
@@ -1284,6 +1707,10 @@ var ACTION_RULES = {
|
|
|
1284
1707
|
fullPage: { type: "boolean" }
|
|
1285
1708
|
}
|
|
1286
1709
|
},
|
|
1710
|
+
forms: {
|
|
1711
|
+
required: {},
|
|
1712
|
+
optional: {}
|
|
1713
|
+
},
|
|
1287
1714
|
evaluate: {
|
|
1288
1715
|
required: { value: { type: "string" } },
|
|
1289
1716
|
optional: {}
|
|
@@ -1298,6 +1725,18 @@ var ACTION_RULES = {
|
|
|
1298
1725
|
required: { selector: { type: "string|string[]" } },
|
|
1299
1726
|
optional: {}
|
|
1300
1727
|
},
|
|
1728
|
+
newTab: {
|
|
1729
|
+
required: {},
|
|
1730
|
+
optional: {
|
|
1731
|
+
url: { type: "string" }
|
|
1732
|
+
}
|
|
1733
|
+
},
|
|
1734
|
+
closeTab: {
|
|
1735
|
+
required: {},
|
|
1736
|
+
optional: {
|
|
1737
|
+
targetId: { type: "string" }
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1301
1740
|
switchToMain: {
|
|
1302
1741
|
required: {},
|
|
1303
1742
|
optional: {}
|
|
@@ -1340,6 +1779,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1340
1779
|
"selector",
|
|
1341
1780
|
"url",
|
|
1342
1781
|
"value",
|
|
1782
|
+
"targetId",
|
|
1343
1783
|
"key",
|
|
1344
1784
|
"combo",
|
|
1345
1785
|
"modifiers",
|
|
@@ -1494,15 +1934,22 @@ function validateSteps(steps) {
|
|
|
1494
1934
|
const rule = ACTION_RULES[action];
|
|
1495
1935
|
for (const key of Object.keys(obj)) {
|
|
1496
1936
|
if (key === "action") continue;
|
|
1497
|
-
if (
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1937
|
+
if (KNOWN_STEP_FIELDS.has(key)) continue;
|
|
1938
|
+
const canonical = PROPERTY_ALIASES[key];
|
|
1939
|
+
if (canonical) {
|
|
1940
|
+
if (!(canonical in obj)) {
|
|
1941
|
+
obj[canonical] = obj[key];
|
|
1942
|
+
}
|
|
1943
|
+
delete obj[key];
|
|
1944
|
+
continue;
|
|
1505
1945
|
}
|
|
1946
|
+
const suggestion = suggestProperty(key);
|
|
1947
|
+
errors.push({
|
|
1948
|
+
stepIndex: i,
|
|
1949
|
+
field: key,
|
|
1950
|
+
message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
|
|
1951
|
+
suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
|
|
1952
|
+
});
|
|
1506
1953
|
}
|
|
1507
1954
|
for (const [field, fieldRule] of Object.entries(rule.required)) {
|
|
1508
1955
|
if (!(field in obj) || obj[field] === void 0) {
|