browser-pilot 0.0.16 → 0.0.18
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 +39 -0
- package/dist/actions.cjs +797 -69
- package/dist/actions.d.cts +101 -4
- package/dist/actions.d.ts +101 -4
- package/dist/actions.mjs +17 -1
- package/dist/{browser-ZCR6AA4D.mjs → browser-GHQRYU4R.mjs} +2 -2
- package/dist/browser.cjs +1366 -72
- package/dist/browser.d.cts +230 -6
- package/dist/browser.d.ts +230 -6
- package/dist/browser.mjs +37 -5
- package/dist/{chunk-EZNZ72VA.mjs → chunk-ASEIFKKV.mjs} +126 -0
- package/dist/{chunk-TJ5B56NV.mjs → chunk-FSB25GRR.mjs} +129 -1
- package/dist/chunk-MIJ7UIKB.mjs +96 -0
- package/dist/{chunk-6GBYX7C2.mjs → chunk-MRY3HRFJ.mjs} +799 -353
- package/dist/chunk-OIHU7OFY.mjs +91 -0
- package/dist/{chunk-NNEHWWHL.mjs → chunk-SW52ALBD.mjs} +588 -5
- package/dist/{chunk-V3VLBQAM.mjs → chunk-ZDODXEBD.mjs} +586 -69
- package/dist/cli.mjs +784 -176
- package/dist/combobox-RAKBA2BW.mjs +6 -0
- package/dist/index.cjs +1669 -71
- package/dist/index.d.cts +58 -7
- package/dist/index.d.ts +58 -7
- package/dist/index.mjs +192 -3
- package/dist/{page-IUUTJ3SW.mjs → page-SD64DY3F.mjs} +1 -1
- package/dist/providers.cjs +127 -0
- package/dist/providers.d.cts +38 -3
- package/dist/providers.d.ts +38 -3
- package/dist/providers.mjs +3 -1
- package/dist/{types-BzM-IfsL.d.ts → types-B_v62K7C.d.ts} +146 -2
- package/dist/{types-DeVSWhXj.d.cts → types-D2pJQpWs.d.cts} +7 -1
- package/dist/{types-DeVSWhXj.d.ts → types-D2pJQpWs.d.ts} +7 -1
- package/dist/{types-BflRmiDz.d.cts → types-Yuybzq53.d.cts} +146 -2
- package/dist/upload-E6MCC2OF.mjs +6 -0
- package/package.json +10 -3
package/dist/actions.cjs
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,15 +30,550 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// src/browser/combobox.ts
|
|
34
|
+
var combobox_exports = {};
|
|
35
|
+
__export(combobox_exports, {
|
|
36
|
+
chooseOption: () => chooseOption
|
|
37
|
+
});
|
|
38
|
+
async function chooseOption(page, config) {
|
|
39
|
+
const {
|
|
40
|
+
trigger,
|
|
41
|
+
listbox,
|
|
42
|
+
optionSelector,
|
|
43
|
+
searchText,
|
|
44
|
+
value,
|
|
45
|
+
match = "contains",
|
|
46
|
+
timeout = 1e4
|
|
47
|
+
} = config;
|
|
48
|
+
try {
|
|
49
|
+
await page.click(trigger, { timeout });
|
|
50
|
+
const listboxSelectors = listbox ? Array.isArray(listbox) ? listbox : [listbox] : DEFAULT_LISTBOX_SELECTORS;
|
|
51
|
+
const listboxFound = await page.waitFor(listboxSelectors, {
|
|
52
|
+
timeout: Math.min(timeout, 3e3),
|
|
53
|
+
optional: true,
|
|
54
|
+
state: "visible"
|
|
55
|
+
});
|
|
56
|
+
if (!listboxFound) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
failedAt: "open",
|
|
60
|
+
error: "Listbox did not appear after clicking trigger"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (searchText) {
|
|
64
|
+
try {
|
|
65
|
+
const triggerSel = Array.isArray(trigger) ? trigger[0] : trigger;
|
|
66
|
+
await page.type(triggerSel, searchText, {
|
|
67
|
+
delay: 30,
|
|
68
|
+
timeout: Math.min(timeout, 3e3)
|
|
69
|
+
});
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
71
|
+
} catch {
|
|
72
|
+
return { success: false, failedAt: "search", error: "Failed to type search text" };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const optionSelectors = optionSelector ? [optionSelector] : DEFAULT_OPTION_SELECTORS;
|
|
76
|
+
const matchFn = match === "exact" ? "exact" : match === "startsWith" ? "startsWith" : "contains";
|
|
77
|
+
const clickedOption = await page.evaluate(`(() => {
|
|
78
|
+
const selectors = ${JSON.stringify(optionSelectors)};
|
|
79
|
+
const targetValue = ${JSON.stringify(value)};
|
|
80
|
+
const matchMode = ${JSON.stringify(matchFn)};
|
|
81
|
+
|
|
82
|
+
for (const sel of selectors) {
|
|
83
|
+
const options = document.querySelectorAll(sel);
|
|
84
|
+
for (const opt of options) {
|
|
85
|
+
const text = (opt.textContent || '').trim();
|
|
86
|
+
let matches = false;
|
|
87
|
+
if (matchMode === 'exact') matches = text === targetValue;
|
|
88
|
+
else if (matchMode === 'startsWith') matches = text.startsWith(targetValue);
|
|
89
|
+
else matches = text.includes(targetValue);
|
|
90
|
+
|
|
91
|
+
if (matches) {
|
|
92
|
+
opt.click();
|
|
93
|
+
return text;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
})()`);
|
|
99
|
+
if (!clickedOption) {
|
|
100
|
+
return { success: false, failedAt: "select", error: `No option matching "${value}" found` };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
selectedText: String(clickedOption)
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: error instanceof Error ? error.message : String(error)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
var DEFAULT_LISTBOX_SELECTORS, DEFAULT_OPTION_SELECTORS;
|
|
114
|
+
var init_combobox = __esm({
|
|
115
|
+
"src/browser/combobox.ts"() {
|
|
116
|
+
"use strict";
|
|
117
|
+
DEFAULT_LISTBOX_SELECTORS = [
|
|
118
|
+
'[role="listbox"]',
|
|
119
|
+
'[role="menu"]',
|
|
120
|
+
'[role="tree"]',
|
|
121
|
+
'ul[class*="dropdown"]',
|
|
122
|
+
'ul[class*="option"]',
|
|
123
|
+
'ul[class*="list"]',
|
|
124
|
+
'div[class*="dropdown"]',
|
|
125
|
+
'div[class*="menu"]'
|
|
126
|
+
];
|
|
127
|
+
DEFAULT_OPTION_SELECTORS = [
|
|
128
|
+
'[role="option"]',
|
|
129
|
+
'[role="menuitem"]',
|
|
130
|
+
'[role="treeitem"]',
|
|
131
|
+
"li"
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// src/browser/upload.ts
|
|
137
|
+
var upload_exports = {};
|
|
138
|
+
__export(upload_exports, {
|
|
139
|
+
uploadFiles: () => uploadFiles
|
|
140
|
+
});
|
|
141
|
+
async function uploadFiles(page, config) {
|
|
142
|
+
const { selector, files, timeout = 1e4 } = config;
|
|
143
|
+
const fileNames = files.map((f) => f.split("/").pop() ?? f);
|
|
144
|
+
try {
|
|
145
|
+
const selectors = Array.isArray(selector) ? selector : [selector];
|
|
146
|
+
let nodeId;
|
|
147
|
+
for (const sel of selectors) {
|
|
148
|
+
try {
|
|
149
|
+
const found = await page.waitFor(sel, {
|
|
150
|
+
timeout: Math.min(timeout, 5e3),
|
|
151
|
+
optional: true,
|
|
152
|
+
state: "attached"
|
|
153
|
+
});
|
|
154
|
+
if (found) {
|
|
155
|
+
const result = await page.evaluate(`(() => {
|
|
156
|
+
const el = document.querySelector(${JSON.stringify(sel)});
|
|
157
|
+
if (!el) return null;
|
|
158
|
+
return el.tagName.toLowerCase() === 'input' && el.type === 'file' ? 'file-input' : 'not-file-input';
|
|
159
|
+
})()`);
|
|
160
|
+
if (result === "file-input") {
|
|
161
|
+
const doc = await page.cdpClient.send("DOM.getDocument");
|
|
162
|
+
const queryResult = await page.cdpClient.send("DOM.querySelector", {
|
|
163
|
+
nodeId: doc.root.nodeId,
|
|
164
|
+
selector: sel
|
|
165
|
+
});
|
|
166
|
+
nodeId = queryResult.nodeId;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!nodeId) {
|
|
174
|
+
return {
|
|
175
|
+
accepted: false,
|
|
176
|
+
fileCount: 0,
|
|
177
|
+
fileNames,
|
|
178
|
+
error: "No file input element found"
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
await page.cdpClient.send("DOM.setFileInputFiles", {
|
|
182
|
+
files,
|
|
183
|
+
nodeId
|
|
184
|
+
});
|
|
185
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
186
|
+
let validationError;
|
|
187
|
+
try {
|
|
188
|
+
const errorText = await page.evaluate(`(() => {
|
|
189
|
+
const errorSelectors = ['.error', '.validation-error', '[class*="error"]', '[role="alert"]'];
|
|
190
|
+
for (const sel of errorSelectors) {
|
|
191
|
+
const el = document.querySelector(sel);
|
|
192
|
+
if (el && el.offsetParent !== null && el.textContent.trim()) {
|
|
193
|
+
return el.textContent.trim();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
})()`);
|
|
198
|
+
if (errorText) validationError = String(errorText);
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
let visibleInUI;
|
|
202
|
+
try {
|
|
203
|
+
const visible = await page.evaluate(`(() => {
|
|
204
|
+
const text = document.body.innerText;
|
|
205
|
+
const fileNames = ${JSON.stringify(fileNames)};
|
|
206
|
+
return fileNames.some(name => text.includes(name));
|
|
207
|
+
})()`);
|
|
208
|
+
visibleInUI = visible === true;
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
accepted: true,
|
|
213
|
+
fileCount: files.length,
|
|
214
|
+
fileNames,
|
|
215
|
+
visibleInUI,
|
|
216
|
+
validationError
|
|
217
|
+
};
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
accepted: false,
|
|
221
|
+
fileCount: 0,
|
|
222
|
+
fileNames,
|
|
223
|
+
error: error instanceof Error ? error.message : String(error)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
var init_upload = __esm({
|
|
228
|
+
"src/browser/upload.ts"() {
|
|
229
|
+
"use strict";
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
30
233
|
// src/actions/index.ts
|
|
31
234
|
var actions_exports = {};
|
|
32
235
|
__export(actions_exports, {
|
|
33
236
|
BatchExecutor: () => BatchExecutor,
|
|
237
|
+
NetworkResponseTracker: () => NetworkResponseTracker,
|
|
34
238
|
addBatchToPage: () => addBatchToPage,
|
|
239
|
+
captureStateSignature: () => captureStateSignature,
|
|
240
|
+
conditionAll: () => conditionAll,
|
|
241
|
+
conditionAny: () => conditionAny,
|
|
242
|
+
conditionNot: () => conditionNot,
|
|
243
|
+
conditionRace: () => conditionRace,
|
|
244
|
+
evaluateCondition: () => evaluateCondition,
|
|
245
|
+
evaluateOutcome: () => evaluateOutcome,
|
|
35
246
|
validateSteps: () => validateSteps
|
|
36
247
|
});
|
|
37
248
|
module.exports = __toCommonJS(actions_exports);
|
|
38
249
|
|
|
250
|
+
// src/utils/strings.ts
|
|
251
|
+
function readString(value) {
|
|
252
|
+
return typeof value === "string" ? value : void 0;
|
|
253
|
+
}
|
|
254
|
+
function readStringOr(value, fallback = "") {
|
|
255
|
+
return readString(value) ?? fallback;
|
|
256
|
+
}
|
|
257
|
+
function formatConsoleArg(entry) {
|
|
258
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
259
|
+
}
|
|
260
|
+
function globToRegex(pattern) {
|
|
261
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
262
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
263
|
+
return new RegExp(`^${withWildcards}$`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/actions/conditions.ts
|
|
267
|
+
var NetworkResponseTracker = class {
|
|
268
|
+
responses = [];
|
|
269
|
+
listening = false;
|
|
270
|
+
handler = null;
|
|
271
|
+
start(cdp) {
|
|
272
|
+
if (this.listening) return;
|
|
273
|
+
this.listening = true;
|
|
274
|
+
this.handler = (params) => {
|
|
275
|
+
const response = params["response"];
|
|
276
|
+
if (response) {
|
|
277
|
+
this.responses.push({ url: response.url, status: response.status });
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
cdp.on("Network.responseReceived", this.handler);
|
|
281
|
+
}
|
|
282
|
+
stop(cdp) {
|
|
283
|
+
if (this.handler) {
|
|
284
|
+
cdp.off("Network.responseReceived", this.handler);
|
|
285
|
+
this.handler = null;
|
|
286
|
+
}
|
|
287
|
+
this.listening = false;
|
|
288
|
+
}
|
|
289
|
+
getResponses() {
|
|
290
|
+
return this.responses;
|
|
291
|
+
}
|
|
292
|
+
reset() {
|
|
293
|
+
this.responses = [];
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
async function captureStateSignature(page) {
|
|
297
|
+
try {
|
|
298
|
+
const url = await page.url();
|
|
299
|
+
const text = await page.text();
|
|
300
|
+
const truncated = text.slice(0, 2e3);
|
|
301
|
+
return `${url}|${simpleHash(truncated)}`;
|
|
302
|
+
} catch {
|
|
303
|
+
return "";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function simpleHash(str) {
|
|
307
|
+
let hash = 0;
|
|
308
|
+
for (let i = 0; i < str.length; i++) {
|
|
309
|
+
const char = str.charCodeAt(i);
|
|
310
|
+
hash = (hash << 5) - hash + char | 0;
|
|
311
|
+
}
|
|
312
|
+
return hash.toString(36);
|
|
313
|
+
}
|
|
314
|
+
async function evaluateCondition(condition, page, context = {}) {
|
|
315
|
+
switch (condition.kind) {
|
|
316
|
+
case "urlMatches": {
|
|
317
|
+
try {
|
|
318
|
+
const currentUrl = await page.url();
|
|
319
|
+
const regex = globToRegex(condition.pattern);
|
|
320
|
+
const matched = regex.test(currentUrl);
|
|
321
|
+
return {
|
|
322
|
+
condition,
|
|
323
|
+
matched,
|
|
324
|
+
detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
|
|
325
|
+
};
|
|
326
|
+
} catch {
|
|
327
|
+
return { condition, matched: false, detail: "Failed to get current URL" };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
case "elementVisible": {
|
|
331
|
+
try {
|
|
332
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
333
|
+
for (const sel of selectors) {
|
|
334
|
+
const visible = await page.waitFor(sel, {
|
|
335
|
+
timeout: 2e3,
|
|
336
|
+
optional: true,
|
|
337
|
+
state: "visible"
|
|
338
|
+
});
|
|
339
|
+
if (visible) {
|
|
340
|
+
return { condition, matched: true, detail: `Element "${sel}" is visible` };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { condition, matched: false, detail: "No matching visible element found" };
|
|
344
|
+
} catch {
|
|
345
|
+
return { condition, matched: false, detail: "Visibility check failed" };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
case "elementHidden": {
|
|
349
|
+
try {
|
|
350
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
351
|
+
for (const sel of selectors) {
|
|
352
|
+
const visible = await page.waitFor(sel, {
|
|
353
|
+
timeout: 500,
|
|
354
|
+
optional: true,
|
|
355
|
+
state: "visible"
|
|
356
|
+
});
|
|
357
|
+
if (visible) {
|
|
358
|
+
return { condition, matched: false, detail: `Element "${sel}" is still visible` };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { condition, matched: true, detail: "Element is hidden or not found" };
|
|
362
|
+
} catch {
|
|
363
|
+
return { condition, matched: true, detail: "Element is hidden (check threw)" };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
case "textAppears": {
|
|
367
|
+
try {
|
|
368
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
369
|
+
const text = await page.text(selector);
|
|
370
|
+
const matched = text.includes(condition.text);
|
|
371
|
+
return {
|
|
372
|
+
condition,
|
|
373
|
+
matched,
|
|
374
|
+
detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
|
|
375
|
+
};
|
|
376
|
+
} catch {
|
|
377
|
+
return { condition, matched: false, detail: "Failed to get page text" };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
case "textChanges": {
|
|
381
|
+
try {
|
|
382
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
383
|
+
const text = await page.text(selector);
|
|
384
|
+
if (condition.to !== void 0) {
|
|
385
|
+
const matched = text.includes(condition.to);
|
|
386
|
+
return {
|
|
387
|
+
condition,
|
|
388
|
+
matched,
|
|
389
|
+
detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
|
|
393
|
+
} catch {
|
|
394
|
+
return { condition, matched: false, detail: "Failed to get text for change detection" };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
case "networkResponse": {
|
|
398
|
+
const tracker = context.networkTracker;
|
|
399
|
+
if (!tracker) {
|
|
400
|
+
return { condition, matched: false, detail: "No network tracker active" };
|
|
401
|
+
}
|
|
402
|
+
const regex = globToRegex(condition.urlPattern);
|
|
403
|
+
const responses = tracker.getResponses();
|
|
404
|
+
for (const resp of responses) {
|
|
405
|
+
if (regex.test(resp.url)) {
|
|
406
|
+
if (condition.status !== void 0 && resp.status !== condition.status) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
condition,
|
|
411
|
+
matched: true,
|
|
412
|
+
detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
condition,
|
|
418
|
+
matched: false,
|
|
419
|
+
detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
case "stateSignatureChanges": {
|
|
423
|
+
if (!context.beforeSignature) {
|
|
424
|
+
return { condition, matched: false, detail: "No before-signature captured" };
|
|
425
|
+
}
|
|
426
|
+
const afterSignature = await captureStateSignature(page);
|
|
427
|
+
const matched = afterSignature !== context.beforeSignature;
|
|
428
|
+
return {
|
|
429
|
+
condition,
|
|
430
|
+
matched,
|
|
431
|
+
detail: matched ? "Page state changed" : "Page state unchanged"
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
default: {
|
|
435
|
+
const _exhaustive = condition;
|
|
436
|
+
return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function evaluateOutcome(page, options) {
|
|
441
|
+
const {
|
|
442
|
+
expectAny,
|
|
443
|
+
expectAll,
|
|
444
|
+
failIf,
|
|
445
|
+
dangerous = false,
|
|
446
|
+
networkTracker,
|
|
447
|
+
beforeSignature
|
|
448
|
+
} = options;
|
|
449
|
+
const allMatched = [];
|
|
450
|
+
const context = { networkTracker, beforeSignature };
|
|
451
|
+
if (failIf && failIf.length > 0) {
|
|
452
|
+
for (const condition of failIf) {
|
|
453
|
+
const result = await evaluateCondition(condition, page, context);
|
|
454
|
+
allMatched.push(result);
|
|
455
|
+
if (result.matched) {
|
|
456
|
+
return {
|
|
457
|
+
outcomeStatus: "failed",
|
|
458
|
+
matchedConditions: allMatched,
|
|
459
|
+
retrySafe: !dangerous
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (expectAll && expectAll.length > 0) {
|
|
465
|
+
let allPassed = true;
|
|
466
|
+
for (const condition of expectAll) {
|
|
467
|
+
const result = await evaluateCondition(condition, page, context);
|
|
468
|
+
allMatched.push(result);
|
|
469
|
+
if (!result.matched) {
|
|
470
|
+
allPassed = false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (!allPassed) {
|
|
474
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
475
|
+
return {
|
|
476
|
+
outcomeStatus: status,
|
|
477
|
+
matchedConditions: allMatched,
|
|
478
|
+
retrySafe: !dangerous
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (!expectAny || expectAny.length === 0) {
|
|
482
|
+
return {
|
|
483
|
+
outcomeStatus: "success",
|
|
484
|
+
matchedConditions: allMatched,
|
|
485
|
+
retrySafe: true
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (expectAny && expectAny.length > 0) {
|
|
490
|
+
let anyPassed = false;
|
|
491
|
+
for (const condition of expectAny) {
|
|
492
|
+
const result = await evaluateCondition(condition, page, context);
|
|
493
|
+
allMatched.push(result);
|
|
494
|
+
if (result.matched) {
|
|
495
|
+
anyPassed = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (anyPassed) {
|
|
499
|
+
return {
|
|
500
|
+
outcomeStatus: "success",
|
|
501
|
+
matchedConditions: allMatched,
|
|
502
|
+
retrySafe: true
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
506
|
+
return {
|
|
507
|
+
outcomeStatus: status,
|
|
508
|
+
matchedConditions: allMatched,
|
|
509
|
+
retrySafe: !dangerous
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
outcomeStatus: "success",
|
|
514
|
+
matchedConditions: allMatched,
|
|
515
|
+
retrySafe: true
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/actions/combinators.ts
|
|
520
|
+
async function conditionAny(conditions, page, context) {
|
|
521
|
+
const results = [];
|
|
522
|
+
let winnerIndex;
|
|
523
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
524
|
+
const result = await evaluateCondition(conditions[i], page, context);
|
|
525
|
+
results.push(result);
|
|
526
|
+
if (result.matched && winnerIndex === void 0) {
|
|
527
|
+
winnerIndex = i;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
matched: winnerIndex !== void 0,
|
|
532
|
+
matchedConditions: results,
|
|
533
|
+
winnerIndex
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
async function conditionAll(conditions, page, context) {
|
|
537
|
+
const results = [];
|
|
538
|
+
let allMatched = true;
|
|
539
|
+
for (const condition of conditions) {
|
|
540
|
+
const result = await evaluateCondition(condition, page, context);
|
|
541
|
+
results.push(result);
|
|
542
|
+
if (!result.matched) allMatched = false;
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
matched: allMatched,
|
|
546
|
+
matchedConditions: results
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
async function conditionNot(condition, page, context) {
|
|
550
|
+
const result = await evaluateCondition(condition, page, context);
|
|
551
|
+
return {
|
|
552
|
+
matched: !result.matched,
|
|
553
|
+
matchedConditions: [
|
|
554
|
+
{
|
|
555
|
+
condition: result.condition,
|
|
556
|
+
matched: !result.matched,
|
|
557
|
+
detail: result.matched ? `NOT: condition was true (inverted to false): ${result.detail}` : `NOT: condition was false (inverted to true): ${result.detail}`
|
|
558
|
+
}
|
|
559
|
+
]
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
async function conditionRace(conditions, page, options = {}) {
|
|
563
|
+
const { timeout = 1e4, pollInterval = 200, networkTracker, beforeSignature } = options;
|
|
564
|
+
const context = { networkTracker, beforeSignature };
|
|
565
|
+
const startTime = Date.now();
|
|
566
|
+
const deadline = startTime + timeout;
|
|
567
|
+
const immediate = await conditionAny(conditions, page, context);
|
|
568
|
+
if (immediate.matched) return immediate;
|
|
569
|
+
while (Date.now() < deadline) {
|
|
570
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
571
|
+
const result = await conditionAny(conditions, page, context);
|
|
572
|
+
if (result.matched) return result;
|
|
573
|
+
}
|
|
574
|
+
return await conditionAny(conditions, page, context);
|
|
575
|
+
}
|
|
576
|
+
|
|
39
577
|
// src/actions/executor.ts
|
|
40
578
|
var fs = __toESM(require("fs"), 1);
|
|
41
579
|
var import_node_path = require("path");
|
|
@@ -1171,13 +1709,6 @@ var TRACE_SCRIPT = `
|
|
|
1171
1709
|
})();
|
|
1172
1710
|
`;
|
|
1173
1711
|
|
|
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
|
-
|
|
1181
1712
|
// src/actions/executor.ts
|
|
1182
1713
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1183
1714
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -1187,15 +1718,6 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
|
1187
1718
|
"text",
|
|
1188
1719
|
"screenshot"
|
|
1189
1720
|
];
|
|
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
1721
|
function loadExistingRecording(manifestPath) {
|
|
1200
1722
|
try {
|
|
1201
1723
|
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
@@ -1309,6 +1831,25 @@ function getSuggestion(reason) {
|
|
|
1309
1831
|
}
|
|
1310
1832
|
}
|
|
1311
1833
|
}
|
|
1834
|
+
function hasOutcomeConditions(step) {
|
|
1835
|
+
return step.expectAny !== void 0 && step.expectAny.length > 0 || step.expectAll !== void 0 && step.expectAll.length > 0 || step.failIf !== void 0 && step.failIf.length > 0;
|
|
1836
|
+
}
|
|
1837
|
+
function needsNetworkTracking(step) {
|
|
1838
|
+
const allConditions = [
|
|
1839
|
+
...step.expectAny ?? [],
|
|
1840
|
+
...step.expectAll ?? [],
|
|
1841
|
+
...step.failIf ?? []
|
|
1842
|
+
];
|
|
1843
|
+
return allConditions.some((c) => c.kind === "networkResponse");
|
|
1844
|
+
}
|
|
1845
|
+
function needsStateSignature(step) {
|
|
1846
|
+
const allConditions = [
|
|
1847
|
+
...step.expectAny ?? [],
|
|
1848
|
+
...step.expectAll ?? [],
|
|
1849
|
+
...step.failIf ?? []
|
|
1850
|
+
];
|
|
1851
|
+
return allConditions.some((c) => c.kind === "stateSignatureChanges");
|
|
1852
|
+
}
|
|
1312
1853
|
var BatchExecutor = class {
|
|
1313
1854
|
page;
|
|
1314
1855
|
constructor(page) {
|
|
@@ -1354,9 +1895,25 @@ var BatchExecutor = class {
|
|
|
1354
1895
|
})
|
|
1355
1896
|
);
|
|
1356
1897
|
}
|
|
1898
|
+
const hasOutcome = hasOutcomeConditions(step);
|
|
1899
|
+
let networkTracker;
|
|
1900
|
+
let beforeSignature;
|
|
1901
|
+
if (hasOutcome) {
|
|
1902
|
+
if (needsNetworkTracking(step)) {
|
|
1903
|
+
networkTracker = new NetworkResponseTracker();
|
|
1904
|
+
networkTracker.start(this.page.cdpClient);
|
|
1905
|
+
}
|
|
1906
|
+
if (needsStateSignature(step)) {
|
|
1907
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1357
1910
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1358
1911
|
if (attempt > 0) {
|
|
1359
1912
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1913
|
+
if (networkTracker) networkTracker.reset();
|
|
1914
|
+
if (hasOutcome && needsStateSignature(step)) {
|
|
1915
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
1916
|
+
}
|
|
1360
1917
|
}
|
|
1361
1918
|
try {
|
|
1362
1919
|
this.page.resetLastActionPosition();
|
|
@@ -1374,6 +1931,28 @@ var BatchExecutor = class {
|
|
|
1374
1931
|
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
1375
1932
|
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
1376
1933
|
};
|
|
1934
|
+
if (hasOutcome) {
|
|
1935
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
1936
|
+
const outcome = await evaluateOutcome(this.page, {
|
|
1937
|
+
expectAny: step.expectAny,
|
|
1938
|
+
expectAll: step.expectAll,
|
|
1939
|
+
failIf: step.failIf,
|
|
1940
|
+
dangerous: step.dangerous,
|
|
1941
|
+
networkTracker,
|
|
1942
|
+
beforeSignature
|
|
1943
|
+
});
|
|
1944
|
+
stepResult.outcomeStatus = outcome.outcomeStatus;
|
|
1945
|
+
stepResult.matchedConditions = outcome.matchedConditions;
|
|
1946
|
+
stepResult.retrySafe = outcome.retrySafe;
|
|
1947
|
+
if (outcome.outcomeStatus !== "success") {
|
|
1948
|
+
stepResult.success = false;
|
|
1949
|
+
stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
|
|
1950
|
+
const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
|
|
1951
|
+
if (failedDetails.length > 0) {
|
|
1952
|
+
stepResult.suggestion = failedDetails.join("; ");
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1377
1956
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1378
1957
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1379
1958
|
}
|
|
@@ -1383,13 +1962,14 @@ var BatchExecutor = class {
|
|
|
1383
1962
|
traceId: createTraceId("action"),
|
|
1384
1963
|
elapsedMs: Date.now() - startTime,
|
|
1385
1964
|
channel: "action",
|
|
1386
|
-
event: "action.succeeded",
|
|
1387
|
-
summary: `${step.action} succeeded`,
|
|
1965
|
+
event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
|
|
1966
|
+
summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
|
|
1388
1967
|
data: {
|
|
1389
1968
|
action: step.action,
|
|
1390
1969
|
selector: step.selector ?? null,
|
|
1391
1970
|
selectorUsed: result.selectorUsed ?? null,
|
|
1392
|
-
durationMs: Date.now() - stepStart
|
|
1971
|
+
durationMs: Date.now() - stepStart,
|
|
1972
|
+
outcomeStatus: stepResult.outcomeStatus ?? null
|
|
1393
1973
|
},
|
|
1394
1974
|
actionId: `action-${i + 1}`,
|
|
1395
1975
|
stepIndex: i,
|
|
@@ -1399,6 +1979,18 @@ var BatchExecutor = class {
|
|
|
1399
1979
|
})
|
|
1400
1980
|
);
|
|
1401
1981
|
}
|
|
1982
|
+
if (hasOutcome && !stepResult.success) {
|
|
1983
|
+
if (step.dangerous) {
|
|
1984
|
+
results.push(stepResult);
|
|
1985
|
+
break;
|
|
1986
|
+
}
|
|
1987
|
+
if (attempt < maxAttempts - 1) {
|
|
1988
|
+
lastError = new Error(stepResult.error ?? "Outcome failed");
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
results.push(stepResult);
|
|
1992
|
+
break;
|
|
1993
|
+
}
|
|
1402
1994
|
results.push(stepResult);
|
|
1403
1995
|
succeeded = true;
|
|
1404
1996
|
break;
|
|
@@ -1406,59 +1998,63 @@ var BatchExecutor = class {
|
|
|
1406
1998
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1407
1999
|
}
|
|
1408
2000
|
}
|
|
2001
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
1409
2002
|
if (!succeeded) {
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
2003
|
+
const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
|
|
2004
|
+
if (!resultAlreadyPushed) {
|
|
2005
|
+
const errorMessage = lastError?.message ?? "Unknown error";
|
|
2006
|
+
let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
|
|
2007
|
+
const { reason, coveringElement } = classifyFailure(lastError);
|
|
2008
|
+
if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
|
|
2009
|
+
try {
|
|
2010
|
+
const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
|
|
2011
|
+
const autoHints = await generateHints(this.page, selectors, step.action, 3);
|
|
2012
|
+
if (autoHints.length > 0) {
|
|
2013
|
+
hints = autoHints;
|
|
2014
|
+
}
|
|
2015
|
+
} catch {
|
|
1419
2016
|
}
|
|
1420
|
-
} catch {
|
|
1421
2017
|
}
|
|
2018
|
+
const failedResult = {
|
|
2019
|
+
index: i,
|
|
2020
|
+
action: step.action,
|
|
2021
|
+
selector: step.selector,
|
|
2022
|
+
success: false,
|
|
2023
|
+
durationMs: Date.now() - stepStart,
|
|
2024
|
+
error: errorMessage,
|
|
2025
|
+
hints,
|
|
2026
|
+
failureReason: reason,
|
|
2027
|
+
coveringElement,
|
|
2028
|
+
suggestion: getSuggestion(reason),
|
|
2029
|
+
timestamp: Date.now()
|
|
2030
|
+
};
|
|
2031
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
2032
|
+
await this.captureRecordingFrame(step, failedResult, recording);
|
|
2033
|
+
}
|
|
2034
|
+
if (recording) {
|
|
2035
|
+
recording.traceEvents.push(
|
|
2036
|
+
normalizeTraceEvent({
|
|
2037
|
+
traceId: createTraceId("action"),
|
|
2038
|
+
elapsedMs: Date.now() - startTime,
|
|
2039
|
+
channel: "action",
|
|
2040
|
+
event: "action.failed",
|
|
2041
|
+
severity: "error",
|
|
2042
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
2043
|
+
data: {
|
|
2044
|
+
action: step.action,
|
|
2045
|
+
selector: step.selector ?? null,
|
|
2046
|
+
error: errorMessage,
|
|
2047
|
+
reason
|
|
2048
|
+
},
|
|
2049
|
+
actionId: `action-${i + 1}`,
|
|
2050
|
+
stepIndex: i,
|
|
2051
|
+
selector: step.selector,
|
|
2052
|
+
url: step.url
|
|
2053
|
+
})
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
results.push(failedResult);
|
|
1422
2057
|
}
|
|
1423
|
-
const failedResult = {
|
|
1424
|
-
index: i,
|
|
1425
|
-
action: step.action,
|
|
1426
|
-
selector: step.selector,
|
|
1427
|
-
success: false,
|
|
1428
|
-
durationMs: Date.now() - stepStart,
|
|
1429
|
-
error: errorMessage,
|
|
1430
|
-
hints,
|
|
1431
|
-
failureReason: reason,
|
|
1432
|
-
coveringElement,
|
|
1433
|
-
suggestion: getSuggestion(reason),
|
|
1434
|
-
timestamp: Date.now()
|
|
1435
|
-
};
|
|
1436
|
-
if (recording && !recording.skipActions.has(step.action)) {
|
|
1437
|
-
await this.captureRecordingFrame(step, failedResult, recording);
|
|
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
|
-
}
|
|
1461
|
-
results.push(failedResult);
|
|
1462
2058
|
if (onFail === "stop" && !step.optional) {
|
|
1463
2059
|
stoppedAtIndex = i;
|
|
1464
2060
|
break;
|
|
@@ -1769,6 +2365,14 @@ var BatchExecutor = class {
|
|
|
1769
2365
|
case "forms": {
|
|
1770
2366
|
return { value: await this.page.forms() };
|
|
1771
2367
|
}
|
|
2368
|
+
case "delta": {
|
|
2369
|
+
const review = await this.page.review();
|
|
2370
|
+
return { value: review };
|
|
2371
|
+
}
|
|
2372
|
+
case "review": {
|
|
2373
|
+
const review = await this.page.review();
|
|
2374
|
+
return { value: review };
|
|
2375
|
+
}
|
|
1772
2376
|
case "screenshot": {
|
|
1773
2377
|
const data = await this.page.screenshot({
|
|
1774
2378
|
format: step.format,
|
|
@@ -1919,6 +2523,35 @@ var BatchExecutor = class {
|
|
|
1919
2523
|
const media = await this.assertMediaTrackLive(step.kind);
|
|
1920
2524
|
return { value: media };
|
|
1921
2525
|
}
|
|
2526
|
+
case "chooseOption": {
|
|
2527
|
+
const { chooseOption: chooseOption2 } = await Promise.resolve().then(() => (init_combobox(), combobox_exports));
|
|
2528
|
+
if (!step.value) throw new Error("chooseOption requires value");
|
|
2529
|
+
const result = await chooseOption2(this.page, {
|
|
2530
|
+
trigger: step.trigger ?? step.selector ?? "",
|
|
2531
|
+
listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
|
|
2532
|
+
value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
|
|
2533
|
+
match: step.match,
|
|
2534
|
+
timeout: step.timeout ?? timeout
|
|
2535
|
+
});
|
|
2536
|
+
if (!result.success) {
|
|
2537
|
+
throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
|
|
2538
|
+
}
|
|
2539
|
+
return { value: result };
|
|
2540
|
+
}
|
|
2541
|
+
case "upload": {
|
|
2542
|
+
const { uploadFiles: uploadFiles2 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
|
|
2543
|
+
if (!step.selector) throw new Error("upload requires selector");
|
|
2544
|
+
if (!step.files || step.files.length === 0) throw new Error("upload requires files");
|
|
2545
|
+
const result = await uploadFiles2(this.page, {
|
|
2546
|
+
selector: step.selector,
|
|
2547
|
+
files: step.files,
|
|
2548
|
+
timeout: step.timeout ?? timeout
|
|
2549
|
+
});
|
|
2550
|
+
if (!result.accepted) {
|
|
2551
|
+
throw new Error(result.error ?? "Upload was not accepted");
|
|
2552
|
+
}
|
|
2553
|
+
return { value: result };
|
|
2554
|
+
}
|
|
1922
2555
|
default: {
|
|
1923
2556
|
const action = step.action;
|
|
1924
2557
|
const aliases = {
|
|
@@ -2512,6 +3145,30 @@ var ACTION_RULES = {
|
|
|
2512
3145
|
kind: { type: "string", enum: ["audio", "video"] }
|
|
2513
3146
|
},
|
|
2514
3147
|
optional: {}
|
|
3148
|
+
},
|
|
3149
|
+
delta: {
|
|
3150
|
+
required: {},
|
|
3151
|
+
optional: {}
|
|
3152
|
+
},
|
|
3153
|
+
review: {
|
|
3154
|
+
required: {},
|
|
3155
|
+
optional: {}
|
|
3156
|
+
},
|
|
3157
|
+
chooseOption: {
|
|
3158
|
+
required: { value: { type: "string|string[]" } },
|
|
3159
|
+
optional: {
|
|
3160
|
+
trigger: { type: "string|string[]" },
|
|
3161
|
+
selector: { type: "string|string[]" },
|
|
3162
|
+
option: { type: "string|string[]" },
|
|
3163
|
+
match: { type: "string", enum: ["exact", "contains", "startsWith"] }
|
|
3164
|
+
}
|
|
3165
|
+
},
|
|
3166
|
+
upload: {
|
|
3167
|
+
required: {
|
|
3168
|
+
selector: { type: "string|string[]" },
|
|
3169
|
+
files: { type: "string|string[]" }
|
|
3170
|
+
},
|
|
3171
|
+
optional: {}
|
|
2515
3172
|
}
|
|
2516
3173
|
};
|
|
2517
3174
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -2551,7 +3208,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2551
3208
|
"name",
|
|
2552
3209
|
"state",
|
|
2553
3210
|
"kind",
|
|
2554
|
-
"windowMs"
|
|
3211
|
+
"windowMs",
|
|
3212
|
+
"expectAny",
|
|
3213
|
+
"expectAll",
|
|
3214
|
+
"failIf",
|
|
3215
|
+
"dangerous",
|
|
3216
|
+
"files"
|
|
2555
3217
|
]);
|
|
2556
3218
|
function resolveAction(name) {
|
|
2557
3219
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -2771,6 +3433,64 @@ function validateSteps(steps) {
|
|
|
2771
3433
|
});
|
|
2772
3434
|
}
|
|
2773
3435
|
}
|
|
3436
|
+
if ("dangerous" in obj && obj["dangerous"] !== void 0) {
|
|
3437
|
+
if (typeof obj["dangerous"] !== "boolean") {
|
|
3438
|
+
errors.push({
|
|
3439
|
+
stepIndex: i,
|
|
3440
|
+
field: "dangerous",
|
|
3441
|
+
message: `"dangerous" expected boolean, got ${typeof obj["dangerous"]}.`
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
for (const condField of ["expectAny", "expectAll", "failIf"]) {
|
|
3446
|
+
if (condField in obj && obj[condField] !== void 0) {
|
|
3447
|
+
if (!Array.isArray(obj[condField])) {
|
|
3448
|
+
errors.push({
|
|
3449
|
+
stepIndex: i,
|
|
3450
|
+
field: condField,
|
|
3451
|
+
message: `"${condField}" expected array, got ${typeof obj[condField]}.`
|
|
3452
|
+
});
|
|
3453
|
+
} else {
|
|
3454
|
+
const conditions = obj[condField];
|
|
3455
|
+
for (let ci = 0; ci < conditions.length; ci++) {
|
|
3456
|
+
const cond = conditions[ci];
|
|
3457
|
+
if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
|
|
3458
|
+
errors.push({
|
|
3459
|
+
stepIndex: i,
|
|
3460
|
+
field: condField,
|
|
3461
|
+
message: `"${condField}[${ci}]" must be a condition object.`
|
|
3462
|
+
});
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
const condObj = cond;
|
|
3466
|
+
if (!("kind" in condObj) || typeof condObj["kind"] !== "string") {
|
|
3467
|
+
errors.push({
|
|
3468
|
+
stepIndex: i,
|
|
3469
|
+
field: condField,
|
|
3470
|
+
message: `"${condField}[${ci}]" missing required "kind" field.`
|
|
3471
|
+
});
|
|
3472
|
+
} else {
|
|
3473
|
+
const validKinds = [
|
|
3474
|
+
"urlMatches",
|
|
3475
|
+
"elementVisible",
|
|
3476
|
+
"elementHidden",
|
|
3477
|
+
"textAppears",
|
|
3478
|
+
"textChanges",
|
|
3479
|
+
"networkResponse",
|
|
3480
|
+
"stateSignatureChanges"
|
|
3481
|
+
];
|
|
3482
|
+
if (!validKinds.includes(condObj["kind"])) {
|
|
3483
|
+
errors.push({
|
|
3484
|
+
stepIndex: i,
|
|
3485
|
+
field: condField,
|
|
3486
|
+
message: `"${condField}[${ci}].kind" must be one of: ${validKinds.join(", ")}. Got "${condObj["kind"]}".`
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
2774
3494
|
if (action === "assertText") {
|
|
2775
3495
|
if (!("expect" in obj) && !("value" in obj)) {
|
|
2776
3496
|
errors.push({
|
|
@@ -2848,6 +3568,14 @@ function validateSteps(steps) {
|
|
|
2848
3568
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2849
3569
|
0 && (module.exports = {
|
|
2850
3570
|
BatchExecutor,
|
|
3571
|
+
NetworkResponseTracker,
|
|
2851
3572
|
addBatchToPage,
|
|
3573
|
+
captureStateSignature,
|
|
3574
|
+
conditionAll,
|
|
3575
|
+
conditionAny,
|
|
3576
|
+
conditionNot,
|
|
3577
|
+
conditionRace,
|
|
3578
|
+
evaluateCondition,
|
|
3579
|
+
evaluateOutcome,
|
|
2852
3580
|
validateSteps
|
|
2853
3581
|
});
|