browser-pilot 0.0.11 → 0.0.13
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 +44 -8
- package/dist/actions.cjs +686 -32
- package/dist/actions.d.cts +3 -3
- package/dist/actions.d.ts +3 -3
- package/dist/actions.mjs +2 -1
- package/dist/browser.cjs +3415 -2324
- package/dist/browser.d.cts +9 -3
- package/dist/browser.d.ts +9 -3
- package/dist/browser.mjs +4 -3
- package/dist/cdp.cjs +19 -4
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +4 -2
- package/dist/chunk-A2ZRAEO3.mjs +1711 -0
- package/dist/{chunk-BCOZUKWS.mjs → chunk-HP6R3W32.mjs} +22 -16
- package/dist/chunk-JXAUPHZM.mjs +15 -0
- package/dist/{chunk-JHAF52FA.mjs → chunk-VDAMDOS6.mjs} +1014 -738
- package/dist/cli.mjs +4998 -3259
- package/dist/{client-7Nqka5MV.d.ts → client-DRqxBdHv.d.cts} +1 -1
- package/dist/{client-7Nqka5MV.d.cts → client-DRqxBdHv.d.ts} +1 -1
- package/dist/index.cjs +4555 -3314
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +6 -4
- package/dist/{types-GWuQJs_e.d.cts → types-BXMGFtnB.d.cts} +96 -9
- package/dist/{types-DtGF3yGl.d.ts → types-CzgQjai9.d.ts} +96 -9
- package/package.json +6 -2
- package/dist/chunk-FAUNIZR7.mjs +0 -751
package/dist/actions.cjs
CHANGED
|
@@ -26,6 +26,263 @@ __export(actions_exports, {
|
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(actions_exports);
|
|
28
28
|
|
|
29
|
+
// src/browser/actionability.ts
|
|
30
|
+
var ActionabilityError = class extends Error {
|
|
31
|
+
failureType;
|
|
32
|
+
coveringElement;
|
|
33
|
+
constructor(message, failureType, coveringElement) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "ActionabilityError";
|
|
36
|
+
this.failureType = failureType;
|
|
37
|
+
this.coveringElement = coveringElement;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/browser/fuzzy-match.ts
|
|
42
|
+
function jaroWinkler(a, b) {
|
|
43
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
44
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
45
|
+
if (a === b) return 1;
|
|
46
|
+
const s1 = a.toLowerCase();
|
|
47
|
+
const s2 = b.toLowerCase();
|
|
48
|
+
const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
|
|
49
|
+
const s1Matches = Array.from({ length: s1.length }, () => false);
|
|
50
|
+
const s2Matches = Array.from({ length: s2.length }, () => false);
|
|
51
|
+
let matches = 0;
|
|
52
|
+
let transpositions = 0;
|
|
53
|
+
for (let i = 0; i < s1.length; i++) {
|
|
54
|
+
const start = Math.max(0, i - matchWindow);
|
|
55
|
+
const end = Math.min(i + matchWindow + 1, s2.length);
|
|
56
|
+
for (let j = start; j < end; j++) {
|
|
57
|
+
if (s2Matches[j] || s1[i] !== s2[j]) continue;
|
|
58
|
+
s1Matches[i] = true;
|
|
59
|
+
s2Matches[j] = true;
|
|
60
|
+
matches++;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (matches === 0) return 0;
|
|
65
|
+
let k = 0;
|
|
66
|
+
for (let i = 0; i < s1.length; i++) {
|
|
67
|
+
if (!s1Matches[i]) continue;
|
|
68
|
+
while (!s2Matches[k]) k++;
|
|
69
|
+
if (s1[i] !== s2[k]) transpositions++;
|
|
70
|
+
k++;
|
|
71
|
+
}
|
|
72
|
+
const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
|
|
73
|
+
let prefix = 0;
|
|
74
|
+
for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
|
|
75
|
+
if (s1[i] === s2[i]) {
|
|
76
|
+
prefix++;
|
|
77
|
+
} else {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const WINKLER_SCALING = 0.1;
|
|
82
|
+
return jaro + prefix * WINKLER_SCALING * (1 - jaro);
|
|
83
|
+
}
|
|
84
|
+
function stringSimilarity(a, b) {
|
|
85
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
86
|
+
const lowerA = a.toLowerCase();
|
|
87
|
+
const lowerB = b.toLowerCase();
|
|
88
|
+
if (lowerA === lowerB) return 1;
|
|
89
|
+
const jw = jaroWinkler(a, b);
|
|
90
|
+
let containsBonus = 0;
|
|
91
|
+
if (lowerB.includes(lowerA)) {
|
|
92
|
+
containsBonus = 0.2;
|
|
93
|
+
} else if (lowerA.includes(lowerB)) {
|
|
94
|
+
containsBonus = 0.1;
|
|
95
|
+
}
|
|
96
|
+
return Math.min(1, jw + containsBonus);
|
|
97
|
+
}
|
|
98
|
+
function scoreElement(query, element) {
|
|
99
|
+
const lowerQuery = query.toLowerCase();
|
|
100
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
101
|
+
let nameScore = 0;
|
|
102
|
+
if (element.name) {
|
|
103
|
+
const lowerName = element.name.toLowerCase();
|
|
104
|
+
if (lowerName === lowerQuery) {
|
|
105
|
+
nameScore = 1;
|
|
106
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
107
|
+
nameScore = 0.8;
|
|
108
|
+
} else if (words.length > 0) {
|
|
109
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
110
|
+
nameScore = matchedWords.length / words.length * 0.7;
|
|
111
|
+
} else {
|
|
112
|
+
nameScore = stringSimilarity(query, element.name) * 0.6;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let roleScore = 0;
|
|
116
|
+
const lowerRole = element.role.toLowerCase();
|
|
117
|
+
if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
|
|
118
|
+
roleScore = 0.3;
|
|
119
|
+
} else if (words.some((w) => lowerRole.includes(w))) {
|
|
120
|
+
roleScore = 0.2;
|
|
121
|
+
}
|
|
122
|
+
let selectorScore = 0;
|
|
123
|
+
const lowerSelector = element.selector.toLowerCase();
|
|
124
|
+
if (words.some((w) => lowerSelector.includes(w))) {
|
|
125
|
+
selectorScore = 0.2;
|
|
126
|
+
}
|
|
127
|
+
const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
|
|
128
|
+
return totalScore;
|
|
129
|
+
}
|
|
130
|
+
function explainMatch(query, element, score) {
|
|
131
|
+
const reasons = [];
|
|
132
|
+
const lowerQuery = query.toLowerCase();
|
|
133
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
134
|
+
if (element.name) {
|
|
135
|
+
const lowerName = element.name.toLowerCase();
|
|
136
|
+
if (lowerName === lowerQuery) {
|
|
137
|
+
reasons.push("exact name match");
|
|
138
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
139
|
+
reasons.push("name contains query");
|
|
140
|
+
} else if (words.some((w) => lowerName.includes(w))) {
|
|
141
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
142
|
+
reasons.push(`name contains: ${matchedWords.join(", ")}`);
|
|
143
|
+
} else if (stringSimilarity(query, element.name) > 0.5) {
|
|
144
|
+
reasons.push("similar name");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const lowerRole = element.role.toLowerCase();
|
|
148
|
+
if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
|
|
149
|
+
reasons.push(`role: ${element.role}`);
|
|
150
|
+
}
|
|
151
|
+
if (words.some((w) => element.selector.toLowerCase().includes(w))) {
|
|
152
|
+
reasons.push("selector match");
|
|
153
|
+
}
|
|
154
|
+
if (reasons.length === 0) {
|
|
155
|
+
reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
|
|
156
|
+
}
|
|
157
|
+
return reasons.join(", ");
|
|
158
|
+
}
|
|
159
|
+
function fuzzyMatchElements(query, elements, maxResults = 5) {
|
|
160
|
+
if (!query || query.length === 0) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const THRESHOLD = 0.3;
|
|
164
|
+
const scored = elements.map((element) => ({
|
|
165
|
+
element,
|
|
166
|
+
score: scoreElement(query, element)
|
|
167
|
+
}));
|
|
168
|
+
return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
|
|
169
|
+
element: s.element,
|
|
170
|
+
score: s.score,
|
|
171
|
+
matchReason: explainMatch(query, s.element, s.score)
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/browser/hint-generator.ts
|
|
176
|
+
var ACTION_ROLE_MAP = {
|
|
177
|
+
click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
|
|
178
|
+
fill: ["textbox", "searchbox", "textarea"],
|
|
179
|
+
type: ["textbox", "searchbox", "textarea"],
|
|
180
|
+
submit: ["button", "form"],
|
|
181
|
+
select: ["combobox", "listbox", "option"],
|
|
182
|
+
check: ["checkbox", "radio", "switch"],
|
|
183
|
+
uncheck: ["checkbox", "switch"],
|
|
184
|
+
focus: [],
|
|
185
|
+
// Any focusable element
|
|
186
|
+
hover: [],
|
|
187
|
+
// Any element
|
|
188
|
+
clear: ["textbox", "searchbox", "textarea"]
|
|
189
|
+
};
|
|
190
|
+
function extractIntent(selectors) {
|
|
191
|
+
const patterns = [];
|
|
192
|
+
let text = "";
|
|
193
|
+
for (const selector of selectors) {
|
|
194
|
+
if (selector.startsWith("ref:")) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
|
|
198
|
+
if (idMatch) {
|
|
199
|
+
patterns.push(idMatch[1]);
|
|
200
|
+
}
|
|
201
|
+
const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
|
|
202
|
+
if (ariaMatch) {
|
|
203
|
+
patterns.push(ariaMatch[1]);
|
|
204
|
+
}
|
|
205
|
+
const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
|
|
206
|
+
if (testidMatch) {
|
|
207
|
+
patterns.push(testidMatch[1]);
|
|
208
|
+
}
|
|
209
|
+
const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
|
|
210
|
+
if (classMatch) {
|
|
211
|
+
patterns.push(classMatch[1]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
patterns.sort((a, b) => b.length - a.length);
|
|
215
|
+
text = patterns[0] ?? selectors[0] ?? "";
|
|
216
|
+
return { text, patterns };
|
|
217
|
+
}
|
|
218
|
+
function getHintType(selector) {
|
|
219
|
+
if (selector.startsWith("ref:")) return "ref";
|
|
220
|
+
if (selector.includes("data-testid")) return "testid";
|
|
221
|
+
if (selector.includes("aria-label")) return "aria";
|
|
222
|
+
if (selector.startsWith("#")) return "id";
|
|
223
|
+
return "css";
|
|
224
|
+
}
|
|
225
|
+
function getConfidence(score) {
|
|
226
|
+
if (score >= 0.8) return "high";
|
|
227
|
+
if (score >= 0.5) return "medium";
|
|
228
|
+
return "low";
|
|
229
|
+
}
|
|
230
|
+
function diversifyHints(candidates, maxHints) {
|
|
231
|
+
const hints = [];
|
|
232
|
+
const usedTypes = /* @__PURE__ */ new Set();
|
|
233
|
+
for (const candidate of candidates) {
|
|
234
|
+
if (hints.length >= maxHints) break;
|
|
235
|
+
const refSelector = `ref:${candidate.element.ref}`;
|
|
236
|
+
const hintType = getHintType(refSelector);
|
|
237
|
+
if (!usedTypes.has(hintType)) {
|
|
238
|
+
hints.push({
|
|
239
|
+
selector: refSelector,
|
|
240
|
+
reason: candidate.matchReason,
|
|
241
|
+
confidence: getConfidence(candidate.score),
|
|
242
|
+
element: {
|
|
243
|
+
ref: candidate.element.ref,
|
|
244
|
+
role: candidate.element.role,
|
|
245
|
+
name: candidate.element.name,
|
|
246
|
+
disabled: candidate.element.disabled
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
usedTypes.add(hintType);
|
|
250
|
+
} else if (hints.length < maxHints) {
|
|
251
|
+
hints.push({
|
|
252
|
+
selector: refSelector,
|
|
253
|
+
reason: candidate.matchReason,
|
|
254
|
+
confidence: getConfidence(candidate.score),
|
|
255
|
+
element: {
|
|
256
|
+
ref: candidate.element.ref,
|
|
257
|
+
role: candidate.element.role,
|
|
258
|
+
name: candidate.element.name,
|
|
259
|
+
disabled: candidate.element.disabled
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return hints;
|
|
265
|
+
}
|
|
266
|
+
async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
|
|
267
|
+
let snapshot;
|
|
268
|
+
try {
|
|
269
|
+
snapshot = await page.snapshot();
|
|
270
|
+
} catch {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
const intent = extractIntent(failedSelectors);
|
|
274
|
+
const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
|
|
275
|
+
let candidates = snapshot.interactiveElements;
|
|
276
|
+
if (roleFilter.length > 0) {
|
|
277
|
+
candidates = candidates.filter((el) => roleFilter.includes(el.role));
|
|
278
|
+
}
|
|
279
|
+
const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
|
|
280
|
+
if (matches.length === 0) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
return diversifyHints(matches, maxHints);
|
|
284
|
+
}
|
|
285
|
+
|
|
29
286
|
// src/browser/types.ts
|
|
30
287
|
var ElementNotFoundError = class extends Error {
|
|
31
288
|
selectors;
|
|
@@ -43,9 +300,101 @@ var ElementNotFoundError = class extends Error {
|
|
|
43
300
|
this.hints = hints;
|
|
44
301
|
}
|
|
45
302
|
};
|
|
303
|
+
var TimeoutError = class extends Error {
|
|
304
|
+
constructor(message = "Operation timed out") {
|
|
305
|
+
const msg = message.includes("bp snapshot") ? message : `${message}. Run 'bp snapshot' to check current page state.`;
|
|
306
|
+
super(msg);
|
|
307
|
+
this.name = "TimeoutError";
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var NavigationError = class extends Error {
|
|
311
|
+
constructor(message) {
|
|
312
|
+
super(message);
|
|
313
|
+
this.name = "NavigationError";
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// src/cdp/protocol.ts
|
|
318
|
+
var CDPError = class extends Error {
|
|
319
|
+
code;
|
|
320
|
+
data;
|
|
321
|
+
constructor(error) {
|
|
322
|
+
super(error.message);
|
|
323
|
+
this.name = "CDPError";
|
|
324
|
+
this.code = error.code;
|
|
325
|
+
this.data = error.data;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
46
328
|
|
|
47
329
|
// src/actions/executor.ts
|
|
48
330
|
var DEFAULT_TIMEOUT = 3e4;
|
|
331
|
+
function classifyFailure(error) {
|
|
332
|
+
if (error instanceof ElementNotFoundError) {
|
|
333
|
+
return { reason: "missing" };
|
|
334
|
+
}
|
|
335
|
+
if (error instanceof ActionabilityError) {
|
|
336
|
+
switch (error.failureType) {
|
|
337
|
+
case "visible":
|
|
338
|
+
return { reason: "hidden" };
|
|
339
|
+
case "hitTarget":
|
|
340
|
+
return { reason: "covered", coveringElement: error.coveringElement };
|
|
341
|
+
case "enabled":
|
|
342
|
+
return { reason: "disabled" };
|
|
343
|
+
case "editable":
|
|
344
|
+
return { reason: error.message?.includes("readonly") ? "readonly" : "notEditable" };
|
|
345
|
+
case "stable":
|
|
346
|
+
return { reason: "replaced" };
|
|
347
|
+
default:
|
|
348
|
+
return { reason: "unknown" };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (error instanceof TimeoutError) {
|
|
352
|
+
return { reason: "timeout" };
|
|
353
|
+
}
|
|
354
|
+
if (error instanceof NavigationError) {
|
|
355
|
+
return { reason: "navigation" };
|
|
356
|
+
}
|
|
357
|
+
if (error instanceof CDPError) {
|
|
358
|
+
return { reason: "cdpError" };
|
|
359
|
+
}
|
|
360
|
+
const msg = String(error?.message ?? error);
|
|
361
|
+
if (msg.includes("Could not find node") || msg.includes("does not belong to the document")) {
|
|
362
|
+
return { reason: "detached" };
|
|
363
|
+
}
|
|
364
|
+
return { reason: "unknown" };
|
|
365
|
+
}
|
|
366
|
+
function getSuggestion(reason) {
|
|
367
|
+
switch (reason) {
|
|
368
|
+
case "missing":
|
|
369
|
+
return "Element not found. Run 'snapshot' to see available elements, or try alternative selectors.";
|
|
370
|
+
case "hidden":
|
|
371
|
+
return "Element exists but is not visible. Try 'scroll' or wait for it to appear.";
|
|
372
|
+
case "covered":
|
|
373
|
+
return "Element is blocked by another element. Dismiss the covering element first.";
|
|
374
|
+
case "disabled":
|
|
375
|
+
return "Element is disabled. Complete prerequisite steps to enable it.";
|
|
376
|
+
case "readonly":
|
|
377
|
+
return "Element is readonly and cannot be edited directly.";
|
|
378
|
+
case "detached":
|
|
379
|
+
return "Element was removed from the DOM. Run 'snapshot' for fresh element refs.";
|
|
380
|
+
case "replaced":
|
|
381
|
+
return "Element was replaced in the DOM. Run 'snapshot' to get updated refs.";
|
|
382
|
+
case "notEditable":
|
|
383
|
+
return "Element is not an editable field. Try a different selector targeting an input or textarea.";
|
|
384
|
+
case "timeout":
|
|
385
|
+
return "Timed out waiting. The page may still be loading. Try increasing timeout.";
|
|
386
|
+
case "navigation":
|
|
387
|
+
return "Navigation failed. Check the URL and network connectivity.";
|
|
388
|
+
case "cdpError":
|
|
389
|
+
return "Browser connection error. Try 'bp connect' again.";
|
|
390
|
+
case "unknown":
|
|
391
|
+
return "Unexpected error. Run 'snapshot' to check page state.";
|
|
392
|
+
default: {
|
|
393
|
+
const _exhaustive = reason;
|
|
394
|
+
return `Unknown failure: ${_exhaustive}`;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
49
398
|
var BatchExecutor = class {
|
|
50
399
|
page;
|
|
51
400
|
constructor(page) {
|
|
@@ -61,21 +410,46 @@ var BatchExecutor = class {
|
|
|
61
410
|
for (let i = 0; i < steps.length; i++) {
|
|
62
411
|
const step = steps[i];
|
|
63
412
|
const stepStart = Date.now();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
result
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
413
|
+
const maxAttempts = (step.retry ?? 0) + 1;
|
|
414
|
+
const retryDelay = step.retryDelay ?? 500;
|
|
415
|
+
let lastError;
|
|
416
|
+
let succeeded = false;
|
|
417
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
418
|
+
if (attempt > 0) {
|
|
419
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
const result = await this.executeStep(step, timeout);
|
|
423
|
+
results.push({
|
|
424
|
+
index: i,
|
|
425
|
+
action: step.action,
|
|
426
|
+
selector: step.selector,
|
|
427
|
+
selectorUsed: result.selectorUsed,
|
|
428
|
+
success: true,
|
|
429
|
+
durationMs: Date.now() - stepStart,
|
|
430
|
+
result: result.value,
|
|
431
|
+
text: result.text
|
|
432
|
+
});
|
|
433
|
+
succeeded = true;
|
|
434
|
+
break;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (!succeeded) {
|
|
440
|
+
const errorMessage = lastError?.message ?? "Unknown error";
|
|
441
|
+
let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
|
|
442
|
+
const { reason, coveringElement } = classifyFailure(lastError);
|
|
443
|
+
if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
|
|
444
|
+
try {
|
|
445
|
+
const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
|
|
446
|
+
const autoHints = await generateHints(this.page, selectors, step.action, 3);
|
|
447
|
+
if (autoHints.length > 0) {
|
|
448
|
+
hints = autoHints;
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
79
453
|
results.push({
|
|
80
454
|
index: i,
|
|
81
455
|
action: step.action,
|
|
@@ -83,7 +457,10 @@ var BatchExecutor = class {
|
|
|
83
457
|
success: false,
|
|
84
458
|
durationMs: Date.now() - stepStart,
|
|
85
459
|
error: errorMessage,
|
|
86
|
-
hints
|
|
460
|
+
hints,
|
|
461
|
+
failureReason: reason,
|
|
462
|
+
coveringElement,
|
|
463
|
+
suggestion: getSuggestion(reason)
|
|
87
464
|
});
|
|
88
465
|
if (onFail === "stop" && !step.optional) {
|
|
89
466
|
return {
|
|
@@ -185,7 +562,24 @@ var BatchExecutor = class {
|
|
|
185
562
|
}
|
|
186
563
|
case "press": {
|
|
187
564
|
if (!step.key) throw new Error("press requires key");
|
|
188
|
-
|
|
565
|
+
try {
|
|
566
|
+
await this.page.press(step.key, {
|
|
567
|
+
modifiers: step.modifiers
|
|
568
|
+
});
|
|
569
|
+
} catch (e) {
|
|
570
|
+
if (optional) return {};
|
|
571
|
+
throw e;
|
|
572
|
+
}
|
|
573
|
+
return {};
|
|
574
|
+
}
|
|
575
|
+
case "shortcut": {
|
|
576
|
+
if (!step.combo) throw new Error("shortcut requires combo");
|
|
577
|
+
try {
|
|
578
|
+
await this.page.shortcut(step.combo);
|
|
579
|
+
} catch (e) {
|
|
580
|
+
if (optional) return {};
|
|
581
|
+
throw e;
|
|
582
|
+
}
|
|
189
583
|
return {};
|
|
190
584
|
}
|
|
191
585
|
case "focus": {
|
|
@@ -244,6 +638,9 @@ var BatchExecutor = class {
|
|
|
244
638
|
const snapshot = await this.page.snapshot();
|
|
245
639
|
return { value: snapshot };
|
|
246
640
|
}
|
|
641
|
+
case "forms": {
|
|
642
|
+
return { value: await this.page.forms() };
|
|
643
|
+
}
|
|
247
644
|
case "screenshot": {
|
|
248
645
|
const data = await this.page.screenshot({
|
|
249
646
|
format: step.format,
|
|
@@ -263,6 +660,21 @@ var BatchExecutor = class {
|
|
|
263
660
|
const text = await this.page.text(selector);
|
|
264
661
|
return { text, selectorUsed: selector };
|
|
265
662
|
}
|
|
663
|
+
case "newTab": {
|
|
664
|
+
const { targetId } = await this.page.cdpClient.send(
|
|
665
|
+
"Target.createTarget",
|
|
666
|
+
{
|
|
667
|
+
url: step.url ?? "about:blank"
|
|
668
|
+
},
|
|
669
|
+
null
|
|
670
|
+
);
|
|
671
|
+
return { value: { targetId } };
|
|
672
|
+
}
|
|
673
|
+
case "closeTab": {
|
|
674
|
+
const targetId = step.targetId ?? this.page.targetId;
|
|
675
|
+
await this.page.cdpClient.send("Target.closeTarget", { targetId }, null);
|
|
676
|
+
return { value: { targetId, closedCurrent: targetId === this.page.targetId } };
|
|
677
|
+
}
|
|
266
678
|
case "switchFrame": {
|
|
267
679
|
if (!step.selector) throw new Error("switchFrame requires selector");
|
|
268
680
|
await this.page.switchToFrame(step.selector, { timeout, optional });
|
|
@@ -272,6 +684,80 @@ var BatchExecutor = class {
|
|
|
272
684
|
await this.page.switchToMain();
|
|
273
685
|
return {};
|
|
274
686
|
}
|
|
687
|
+
case "assertVisible": {
|
|
688
|
+
if (!step.selector) throw new Error("assertVisible requires selector");
|
|
689
|
+
const el = await this.page.waitFor(step.selector, {
|
|
690
|
+
timeout,
|
|
691
|
+
optional: true,
|
|
692
|
+
state: "visible"
|
|
693
|
+
});
|
|
694
|
+
if (!el) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Assertion failed: selector ${JSON.stringify(step.selector)} is not visible`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
700
|
+
}
|
|
701
|
+
case "assertExists": {
|
|
702
|
+
if (!step.selector) throw new Error("assertExists requires selector");
|
|
703
|
+
const el = await this.page.waitFor(step.selector, {
|
|
704
|
+
timeout,
|
|
705
|
+
optional: true,
|
|
706
|
+
state: "attached"
|
|
707
|
+
});
|
|
708
|
+
if (!el) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
`Assertion failed: selector ${JSON.stringify(step.selector)} does not exist`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
714
|
+
}
|
|
715
|
+
case "assertText": {
|
|
716
|
+
const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
|
|
717
|
+
const text = await this.page.text(selector);
|
|
718
|
+
const expected = step.expect ?? step.value;
|
|
719
|
+
if (typeof expected !== "string") throw new Error("assertText requires expect or value");
|
|
720
|
+
if (!text.includes(expected)) {
|
|
721
|
+
throw new Error(
|
|
722
|
+
`Assertion failed: text does not contain ${JSON.stringify(expected)}. Got: ${JSON.stringify(text.slice(0, 200))}`
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
return { selectorUsed: selector, text };
|
|
726
|
+
}
|
|
727
|
+
case "assertUrl": {
|
|
728
|
+
const currentUrl = await this.page.url();
|
|
729
|
+
const expected = step.expect ?? step.url;
|
|
730
|
+
if (typeof expected !== "string") throw new Error("assertUrl requires expect or url");
|
|
731
|
+
if (!currentUrl.includes(expected)) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
`Assertion failed: URL does not contain ${JSON.stringify(expected)}. Got: ${JSON.stringify(currentUrl)}`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
return { value: currentUrl };
|
|
737
|
+
}
|
|
738
|
+
case "assertValue": {
|
|
739
|
+
if (!step.selector) throw new Error("assertValue requires selector");
|
|
740
|
+
const expected = step.expect ?? step.value;
|
|
741
|
+
if (typeof expected !== "string") throw new Error("assertValue requires expect or value");
|
|
742
|
+
const found = await this.page.waitFor(step.selector, {
|
|
743
|
+
timeout,
|
|
744
|
+
optional: true,
|
|
745
|
+
state: "attached"
|
|
746
|
+
});
|
|
747
|
+
if (!found) {
|
|
748
|
+
throw new Error(`Assertion failed: selector ${JSON.stringify(step.selector)} not found`);
|
|
749
|
+
}
|
|
750
|
+
const usedSelector = this.getUsedSelector(step.selector);
|
|
751
|
+
const actual = await this.page.evaluate(
|
|
752
|
+
`(function() { var el = document.querySelector(${JSON.stringify(usedSelector)}); return el ? el.value : null; })()`
|
|
753
|
+
);
|
|
754
|
+
if (actual !== expected) {
|
|
755
|
+
throw new Error(
|
|
756
|
+
`Assertion failed: value of ${JSON.stringify(usedSelector)} is ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return { selectorUsed: usedSelector, value: actual };
|
|
760
|
+
}
|
|
275
761
|
default: {
|
|
276
762
|
const action = step.action;
|
|
277
763
|
const aliases = {
|
|
@@ -284,16 +770,48 @@ var BatchExecutor = class {
|
|
|
284
770
|
capture: "screenshot",
|
|
285
771
|
inspect: "snapshot",
|
|
286
772
|
enter: "press",
|
|
773
|
+
keypress: "press",
|
|
774
|
+
hotkey: "shortcut",
|
|
775
|
+
keybinding: "shortcut",
|
|
776
|
+
nav: "goto",
|
|
287
777
|
open: "goto",
|
|
288
778
|
visit: "goto",
|
|
779
|
+
browse: "goto",
|
|
780
|
+
load: "goto",
|
|
781
|
+
write: "fill",
|
|
782
|
+
set: "fill",
|
|
783
|
+
pick: "select",
|
|
784
|
+
choose: "select",
|
|
785
|
+
send: "press",
|
|
289
786
|
eval: "evaluate",
|
|
290
787
|
js: "evaluate",
|
|
788
|
+
script: "evaluate",
|
|
291
789
|
snap: "snapshot",
|
|
292
|
-
|
|
790
|
+
accessibility: "snapshot",
|
|
791
|
+
a11y: "snapshot",
|
|
792
|
+
formslist: "forms",
|
|
793
|
+
image: "screenshot",
|
|
794
|
+
pic: "screenshot",
|
|
795
|
+
frame: "switchFrame",
|
|
796
|
+
iframe: "switchFrame",
|
|
797
|
+
newtab: "newTab",
|
|
798
|
+
opentab: "newTab",
|
|
799
|
+
createtab: "newTab",
|
|
800
|
+
closetab: "closeTab",
|
|
801
|
+
assert_visible: "assertVisible",
|
|
802
|
+
assert_exists: "assertExists",
|
|
803
|
+
assert_text: "assertText",
|
|
804
|
+
assert_url: "assertUrl",
|
|
805
|
+
assert_value: "assertValue",
|
|
806
|
+
checkvisible: "assertVisible",
|
|
807
|
+
checkexists: "assertExists",
|
|
808
|
+
checktext: "assertText",
|
|
809
|
+
checkurl: "assertUrl",
|
|
810
|
+
checkvalue: "assertValue"
|
|
293
811
|
};
|
|
294
812
|
const suggestion = aliases[action.toLowerCase()];
|
|
295
813
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
296
|
-
const valid = "goto, click, fill, type, select, check, uncheck, submit, press, focus, hover, scroll, wait, snapshot, screenshot, evaluate, text, switchFrame, switchToMain";
|
|
814
|
+
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";
|
|
297
815
|
throw new Error(`Unknown action "${action}".${hint}
|
|
298
816
|
|
|
299
817
|
Valid actions: ${valid}`);
|
|
@@ -342,6 +860,8 @@ var ACTION_ALIASES = {
|
|
|
342
860
|
inspect: "snapshot",
|
|
343
861
|
enter: "press",
|
|
344
862
|
keypress: "press",
|
|
863
|
+
hotkey: "shortcut",
|
|
864
|
+
keybinding: "shortcut",
|
|
345
865
|
nav: "goto",
|
|
346
866
|
open: "goto",
|
|
347
867
|
visit: "goto",
|
|
@@ -361,7 +881,22 @@ var ACTION_ALIASES = {
|
|
|
361
881
|
image: "screenshot",
|
|
362
882
|
pic: "screenshot",
|
|
363
883
|
frame: "switchFrame",
|
|
364
|
-
iframe: "switchFrame"
|
|
884
|
+
iframe: "switchFrame",
|
|
885
|
+
formslist: "forms",
|
|
886
|
+
newtab: "newTab",
|
|
887
|
+
opentab: "newTab",
|
|
888
|
+
createtab: "newTab",
|
|
889
|
+
closetab: "closeTab",
|
|
890
|
+
assert_visible: "assertVisible",
|
|
891
|
+
assert_exists: "assertExists",
|
|
892
|
+
assert_text: "assertText",
|
|
893
|
+
assert_url: "assertUrl",
|
|
894
|
+
assert_value: "assertValue",
|
|
895
|
+
checkvisible: "assertVisible",
|
|
896
|
+
checkexists: "assertExists",
|
|
897
|
+
checktext: "assertText",
|
|
898
|
+
checkurl: "assertUrl",
|
|
899
|
+
checkvalue: "assertValue"
|
|
365
900
|
};
|
|
366
901
|
var PROPERTY_ALIASES = {
|
|
367
902
|
expression: "value",
|
|
@@ -382,10 +917,14 @@ var PROPERTY_ALIASES = {
|
|
|
382
917
|
input: "value",
|
|
383
918
|
content: "value",
|
|
384
919
|
keys: "key",
|
|
920
|
+
shortcutKey: "combo",
|
|
921
|
+
hotkey: "combo",
|
|
922
|
+
keybinding: "combo",
|
|
385
923
|
button: "key",
|
|
386
924
|
address: "url",
|
|
387
925
|
page: "url",
|
|
388
|
-
path: "url"
|
|
926
|
+
path: "url",
|
|
927
|
+
tabId: "targetId"
|
|
389
928
|
};
|
|
390
929
|
var ACTION_RULES = {
|
|
391
930
|
goto: {
|
|
@@ -395,7 +934,7 @@ var ACTION_RULES = {
|
|
|
395
934
|
click: {
|
|
396
935
|
required: { selector: { type: "string|string[]" } },
|
|
397
936
|
optional: {
|
|
398
|
-
waitForNavigation: { type: "boolean" }
|
|
937
|
+
waitForNavigation: { type: "boolean|auto" }
|
|
399
938
|
}
|
|
400
939
|
},
|
|
401
940
|
fill: {
|
|
@@ -407,7 +946,8 @@ var ACTION_RULES = {
|
|
|
407
946
|
type: {
|
|
408
947
|
required: { selector: { type: "string|string[]" }, value: { type: "string" } },
|
|
409
948
|
optional: {
|
|
410
|
-
delay: { type: "number" }
|
|
949
|
+
delay: { type: "number" },
|
|
950
|
+
blur: { type: "boolean" }
|
|
411
951
|
}
|
|
412
952
|
},
|
|
413
953
|
select: {
|
|
@@ -437,6 +977,12 @@ var ACTION_RULES = {
|
|
|
437
977
|
},
|
|
438
978
|
press: {
|
|
439
979
|
required: { key: { type: "string" } },
|
|
980
|
+
optional: {
|
|
981
|
+
modifiers: { type: "string|string[]" }
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
shortcut: {
|
|
985
|
+
required: { combo: { type: "string" } },
|
|
440
986
|
optional: {}
|
|
441
987
|
},
|
|
442
988
|
focus: {
|
|
@@ -479,6 +1025,10 @@ var ACTION_RULES = {
|
|
|
479
1025
|
fullPage: { type: "boolean" }
|
|
480
1026
|
}
|
|
481
1027
|
},
|
|
1028
|
+
forms: {
|
|
1029
|
+
required: {},
|
|
1030
|
+
optional: {}
|
|
1031
|
+
},
|
|
482
1032
|
evaluate: {
|
|
483
1033
|
required: { value: { type: "string" } },
|
|
484
1034
|
optional: {}
|
|
@@ -493,9 +1043,51 @@ var ACTION_RULES = {
|
|
|
493
1043
|
required: { selector: { type: "string|string[]" } },
|
|
494
1044
|
optional: {}
|
|
495
1045
|
},
|
|
1046
|
+
newTab: {
|
|
1047
|
+
required: {},
|
|
1048
|
+
optional: {
|
|
1049
|
+
url: { type: "string" }
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
closeTab: {
|
|
1053
|
+
required: {},
|
|
1054
|
+
optional: {
|
|
1055
|
+
targetId: { type: "string" }
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
496
1058
|
switchToMain: {
|
|
497
1059
|
required: {},
|
|
498
1060
|
optional: {}
|
|
1061
|
+
},
|
|
1062
|
+
assertVisible: {
|
|
1063
|
+
required: { selector: { type: "string|string[]" } },
|
|
1064
|
+
optional: {}
|
|
1065
|
+
},
|
|
1066
|
+
assertExists: {
|
|
1067
|
+
required: { selector: { type: "string|string[]" } },
|
|
1068
|
+
optional: {}
|
|
1069
|
+
},
|
|
1070
|
+
assertText: {
|
|
1071
|
+
required: {},
|
|
1072
|
+
optional: {
|
|
1073
|
+
selector: { type: "string|string[]" },
|
|
1074
|
+
expect: { type: "string" },
|
|
1075
|
+
value: { type: "string" }
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1078
|
+
assertUrl: {
|
|
1079
|
+
required: {},
|
|
1080
|
+
optional: {
|
|
1081
|
+
expect: { type: "string" },
|
|
1082
|
+
url: { type: "string" }
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
assertValue: {
|
|
1086
|
+
required: { selector: { type: "string|string[]" } },
|
|
1087
|
+
optional: {
|
|
1088
|
+
expect: { type: "string" },
|
|
1089
|
+
value: { type: "string" }
|
|
1090
|
+
}
|
|
499
1091
|
}
|
|
500
1092
|
};
|
|
501
1093
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -505,7 +1097,10 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
505
1097
|
"selector",
|
|
506
1098
|
"url",
|
|
507
1099
|
"value",
|
|
1100
|
+
"targetId",
|
|
508
1101
|
"key",
|
|
1102
|
+
"combo",
|
|
1103
|
+
"modifiers",
|
|
509
1104
|
"waitFor",
|
|
510
1105
|
"timeout",
|
|
511
1106
|
"optional",
|
|
@@ -522,7 +1117,10 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
522
1117
|
"amount",
|
|
523
1118
|
"format",
|
|
524
1119
|
"quality",
|
|
525
|
-
"fullPage"
|
|
1120
|
+
"fullPage",
|
|
1121
|
+
"expect",
|
|
1122
|
+
"retry",
|
|
1123
|
+
"retryDelay"
|
|
526
1124
|
]);
|
|
527
1125
|
function resolveAction(name) {
|
|
528
1126
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -595,6 +1193,10 @@ function checkFieldType(value, rule) {
|
|
|
595
1193
|
return `expected boolean or "auto", got ${typeof value}`;
|
|
596
1194
|
}
|
|
597
1195
|
return null;
|
|
1196
|
+
default: {
|
|
1197
|
+
const _exhaustive = rule.type;
|
|
1198
|
+
return `unknown type: ${_exhaustive}`;
|
|
1199
|
+
}
|
|
598
1200
|
}
|
|
599
1201
|
}
|
|
600
1202
|
function validateSteps(steps) {
|
|
@@ -650,15 +1252,22 @@ function validateSteps(steps) {
|
|
|
650
1252
|
const rule = ACTION_RULES[action];
|
|
651
1253
|
for (const key of Object.keys(obj)) {
|
|
652
1254
|
if (key === "action") continue;
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1255
|
+
if (KNOWN_STEP_FIELDS.has(key)) continue;
|
|
1256
|
+
const canonical = PROPERTY_ALIASES[key];
|
|
1257
|
+
if (canonical) {
|
|
1258
|
+
if (!(canonical in obj)) {
|
|
1259
|
+
obj[canonical] = obj[key];
|
|
1260
|
+
}
|
|
1261
|
+
delete obj[key];
|
|
1262
|
+
continue;
|
|
661
1263
|
}
|
|
1264
|
+
const suggestion = suggestProperty(key);
|
|
1265
|
+
errors.push({
|
|
1266
|
+
stepIndex: i,
|
|
1267
|
+
field: key,
|
|
1268
|
+
message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
|
|
1269
|
+
suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
|
|
1270
|
+
});
|
|
662
1271
|
}
|
|
663
1272
|
for (const [field, fieldRule] of Object.entries(rule.required)) {
|
|
664
1273
|
if (!(field in obj) || obj[field] === void 0) {
|
|
@@ -708,6 +1317,51 @@ function validateSteps(steps) {
|
|
|
708
1317
|
});
|
|
709
1318
|
}
|
|
710
1319
|
}
|
|
1320
|
+
if ("retry" in obj && obj["retry"] !== void 0) {
|
|
1321
|
+
if (typeof obj["retry"] !== "number") {
|
|
1322
|
+
errors.push({
|
|
1323
|
+
stepIndex: i,
|
|
1324
|
+
field: "retry",
|
|
1325
|
+
message: `"retry" expected number, got ${typeof obj["retry"]}.`
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if ("retryDelay" in obj && obj["retryDelay"] !== void 0) {
|
|
1330
|
+
if (typeof obj["retryDelay"] !== "number") {
|
|
1331
|
+
errors.push({
|
|
1332
|
+
stepIndex: i,
|
|
1333
|
+
field: "retryDelay",
|
|
1334
|
+
message: `"retryDelay" expected number, got ${typeof obj["retryDelay"]}.`
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (action === "assertText") {
|
|
1339
|
+
if (!("expect" in obj) && !("value" in obj)) {
|
|
1340
|
+
errors.push({
|
|
1341
|
+
stepIndex: i,
|
|
1342
|
+
field: "expect",
|
|
1343
|
+
message: 'assertText requires "expect" or "value" containing the expected text.'
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
if (action === "assertUrl") {
|
|
1348
|
+
if (!("expect" in obj) && !("url" in obj)) {
|
|
1349
|
+
errors.push({
|
|
1350
|
+
stepIndex: i,
|
|
1351
|
+
field: "expect",
|
|
1352
|
+
message: 'assertUrl requires "expect" or "url" containing the expected URL substring.'
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (action === "assertValue") {
|
|
1357
|
+
if (!("expect" in obj) && !("value" in obj)) {
|
|
1358
|
+
errors.push({
|
|
1359
|
+
stepIndex: i,
|
|
1360
|
+
field: "expect",
|
|
1361
|
+
message: 'assertValue requires "expect" or "value" containing the expected value.'
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
711
1365
|
if (action === "select") {
|
|
712
1366
|
const hasNative = "selector" in obj && "value" in obj;
|
|
713
1367
|
const hasCustom = "trigger" in obj && "option" in obj && "value" in obj;
|