browser-pilot 0.0.16 → 0.0.17
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 +22 -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-4ZHNAQR5.mjs} +2 -2
- package/dist/browser.cjs +1238 -72
- package/dist/browser.d.cts +229 -5
- package/dist/browser.d.ts +229 -5
- package/dist/browser.mjs +36 -4
- package/dist/{chunk-NNEHWWHL.mjs → chunk-FEEGNSHB.mjs} +584 -4
- package/dist/{chunk-TJ5B56NV.mjs → chunk-IRLHCVNH.mjs} +1 -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-V3VLBQAM.mjs → chunk-ZDODXEBD.mjs} +586 -69
- package/dist/cli.mjs +756 -174
- package/dist/combobox-RAKBA2BW.mjs +6 -0
- package/dist/index.cjs +1539 -71
- package/dist/index.d.cts +56 -5
- package/dist/index.d.ts +56 -5
- package/dist/index.mjs +189 -2
- package/dist/{page-IUUTJ3SW.mjs → page-SD64DY3F.mjs} +1 -1
- package/dist/{types-BzM-IfsL.d.ts → types-B_v62K7C.d.ts} +146 -2
- 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/index.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,6 +30,206 @@ 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/index.ts
|
|
31
234
|
var src_exports = {};
|
|
32
235
|
__export(src_exports, {
|
|
@@ -41,22 +244,42 @@ __export(src_exports, {
|
|
|
41
244
|
ElementNotFoundError: () => ElementNotFoundError,
|
|
42
245
|
GenericProvider: () => GenericProvider,
|
|
43
246
|
NavigationError: () => NavigationError,
|
|
247
|
+
NetworkResponseTracker: () => NetworkResponseTracker,
|
|
44
248
|
Page: () => Page,
|
|
45
249
|
RequestInterceptor: () => RequestInterceptor,
|
|
46
250
|
TimeoutError: () => TimeoutError,
|
|
47
251
|
Tracer: () => Tracer,
|
|
48
252
|
addBatchToPage: () => addBatchToPage,
|
|
49
253
|
bufferToBase64: () => bufferToBase64,
|
|
254
|
+
buildFingerprintMap: () => buildFingerprintMap,
|
|
50
255
|
buildLocalBrowserScanTargets: () => buildLocalBrowserScanTargets,
|
|
256
|
+
buildWorkflowSummary: () => buildWorkflowSummary,
|
|
51
257
|
calculateRMS: () => calculateRMS,
|
|
258
|
+
captureStateSignature: () => captureStateSignature,
|
|
259
|
+
chooseOption: () => chooseOption,
|
|
260
|
+
computeDelta: () => computeDelta,
|
|
261
|
+
conditionAll: () => conditionAll,
|
|
262
|
+
conditionAny: () => conditionAny,
|
|
263
|
+
conditionNot: () => conditionNot,
|
|
264
|
+
conditionRace: () => conditionRace,
|
|
52
265
|
connect: () => connect,
|
|
53
266
|
createCDPClient: () => createCDPClient,
|
|
267
|
+
createFingerprint: () => createFingerprint,
|
|
54
268
|
createProvider: () => createProvider,
|
|
269
|
+
createTargetFingerprint: () => createTargetFingerprint,
|
|
270
|
+
detectOverlay: () => detectOverlay,
|
|
55
271
|
devices: () => devices,
|
|
56
272
|
disableTracing: () => disableTracing,
|
|
57
273
|
discoverLocalBrowsers: () => discoverLocalBrowsers,
|
|
58
274
|
discoverTargets: () => discoverTargets,
|
|
59
275
|
enableTracing: () => enableTracing,
|
|
276
|
+
evaluateCondition: () => evaluateCondition,
|
|
277
|
+
evaluateOutcome: () => evaluateOutcome,
|
|
278
|
+
extractPageState: () => extractPageState,
|
|
279
|
+
extractReview: () => extractReview,
|
|
280
|
+
fingerprintKey: () => fingerprintKey,
|
|
281
|
+
fingerprintSimilarity: () => fingerprintSimilarity,
|
|
282
|
+
formatWorkflowSummary: () => formatWorkflowSummary,
|
|
60
283
|
generateSilence: () => generateSilence,
|
|
61
284
|
generateTone: () => generateTone,
|
|
62
285
|
getAudioChromeFlags: () => getAudioChromeFlags,
|
|
@@ -67,9 +290,13 @@ __export(src_exports, {
|
|
|
67
290
|
parseDevToolsActivePortFile: () => parseDevToolsActivePortFile,
|
|
68
291
|
parseWavHeader: () => parseWavHeader,
|
|
69
292
|
pcmToWav: () => pcmToWav,
|
|
293
|
+
recoverPinnedTarget: () => recoverPinnedTarget,
|
|
294
|
+
recoverStaleRef: () => recoverStaleRef,
|
|
70
295
|
resolveBrowserEndpoint: () => resolveBrowserEndpoint,
|
|
71
296
|
resolveChromeUserDataDirs: () => resolveChromeUserDataDirs,
|
|
297
|
+
submitAndVerify: () => submitAndVerify,
|
|
72
298
|
transcribe: () => transcribe,
|
|
299
|
+
uploadFiles: () => uploadFiles,
|
|
73
300
|
validateSteps: () => validateSteps,
|
|
74
301
|
waitForAnyElement: () => waitForAnyElement,
|
|
75
302
|
waitForElement: () => waitForElement,
|
|
@@ -78,6 +305,333 @@ __export(src_exports, {
|
|
|
78
305
|
});
|
|
79
306
|
module.exports = __toCommonJS(src_exports);
|
|
80
307
|
|
|
308
|
+
// src/utils/strings.ts
|
|
309
|
+
function readString(value) {
|
|
310
|
+
return typeof value === "string" ? value : void 0;
|
|
311
|
+
}
|
|
312
|
+
function readStringOr(value, fallback = "") {
|
|
313
|
+
return readString(value) ?? fallback;
|
|
314
|
+
}
|
|
315
|
+
function formatConsoleArg(entry) {
|
|
316
|
+
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
317
|
+
}
|
|
318
|
+
function globToRegex(pattern) {
|
|
319
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
320
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
321
|
+
return new RegExp(`^${withWildcards}$`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/actions/conditions.ts
|
|
325
|
+
var NetworkResponseTracker = class {
|
|
326
|
+
responses = [];
|
|
327
|
+
listening = false;
|
|
328
|
+
handler = null;
|
|
329
|
+
start(cdp) {
|
|
330
|
+
if (this.listening) return;
|
|
331
|
+
this.listening = true;
|
|
332
|
+
this.handler = (params) => {
|
|
333
|
+
const response = params["response"];
|
|
334
|
+
if (response) {
|
|
335
|
+
this.responses.push({ url: response.url, status: response.status });
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
cdp.on("Network.responseReceived", this.handler);
|
|
339
|
+
}
|
|
340
|
+
stop(cdp) {
|
|
341
|
+
if (this.handler) {
|
|
342
|
+
cdp.off("Network.responseReceived", this.handler);
|
|
343
|
+
this.handler = null;
|
|
344
|
+
}
|
|
345
|
+
this.listening = false;
|
|
346
|
+
}
|
|
347
|
+
getResponses() {
|
|
348
|
+
return this.responses;
|
|
349
|
+
}
|
|
350
|
+
reset() {
|
|
351
|
+
this.responses = [];
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
async function captureStateSignature(page) {
|
|
355
|
+
try {
|
|
356
|
+
const url = await page.url();
|
|
357
|
+
const text = await page.text();
|
|
358
|
+
const truncated = text.slice(0, 2e3);
|
|
359
|
+
return `${url}|${simpleHash(truncated)}`;
|
|
360
|
+
} catch {
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function simpleHash(str) {
|
|
365
|
+
let hash = 0;
|
|
366
|
+
for (let i = 0; i < str.length; i++) {
|
|
367
|
+
const char = str.charCodeAt(i);
|
|
368
|
+
hash = (hash << 5) - hash + char | 0;
|
|
369
|
+
}
|
|
370
|
+
return hash.toString(36);
|
|
371
|
+
}
|
|
372
|
+
async function evaluateCondition(condition, page, context = {}) {
|
|
373
|
+
switch (condition.kind) {
|
|
374
|
+
case "urlMatches": {
|
|
375
|
+
try {
|
|
376
|
+
const currentUrl = await page.url();
|
|
377
|
+
const regex = globToRegex(condition.pattern);
|
|
378
|
+
const matched = regex.test(currentUrl);
|
|
379
|
+
return {
|
|
380
|
+
condition,
|
|
381
|
+
matched,
|
|
382
|
+
detail: matched ? `URL "${currentUrl}" matches "${condition.pattern}"` : `URL "${currentUrl}" does not match "${condition.pattern}"`
|
|
383
|
+
};
|
|
384
|
+
} catch {
|
|
385
|
+
return { condition, matched: false, detail: "Failed to get current URL" };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
case "elementVisible": {
|
|
389
|
+
try {
|
|
390
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
391
|
+
for (const sel of selectors) {
|
|
392
|
+
const visible = await page.waitFor(sel, {
|
|
393
|
+
timeout: 2e3,
|
|
394
|
+
optional: true,
|
|
395
|
+
state: "visible"
|
|
396
|
+
});
|
|
397
|
+
if (visible) {
|
|
398
|
+
return { condition, matched: true, detail: `Element "${sel}" is visible` };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return { condition, matched: false, detail: "No matching visible element found" };
|
|
402
|
+
} catch {
|
|
403
|
+
return { condition, matched: false, detail: "Visibility check failed" };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
case "elementHidden": {
|
|
407
|
+
try {
|
|
408
|
+
const selectors = Array.isArray(condition.selector) ? condition.selector : [condition.selector];
|
|
409
|
+
for (const sel of selectors) {
|
|
410
|
+
const visible = await page.waitFor(sel, {
|
|
411
|
+
timeout: 500,
|
|
412
|
+
optional: true,
|
|
413
|
+
state: "visible"
|
|
414
|
+
});
|
|
415
|
+
if (visible) {
|
|
416
|
+
return { condition, matched: false, detail: `Element "${sel}" is still visible` };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { condition, matched: true, detail: "Element is hidden or not found" };
|
|
420
|
+
} catch {
|
|
421
|
+
return { condition, matched: true, detail: "Element is hidden (check threw)" };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
case "textAppears": {
|
|
425
|
+
try {
|
|
426
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
427
|
+
const text = await page.text(selector);
|
|
428
|
+
const matched = text.includes(condition.text);
|
|
429
|
+
return {
|
|
430
|
+
condition,
|
|
431
|
+
matched,
|
|
432
|
+
detail: matched ? `Text "${condition.text}" found` : `Text "${condition.text}" not found in page content`
|
|
433
|
+
};
|
|
434
|
+
} catch {
|
|
435
|
+
return { condition, matched: false, detail: "Failed to get page text" };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
case "textChanges": {
|
|
439
|
+
try {
|
|
440
|
+
const selector = Array.isArray(condition.selector) ? condition.selector[0] : condition.selector;
|
|
441
|
+
const text = await page.text(selector);
|
|
442
|
+
if (condition.to !== void 0) {
|
|
443
|
+
const matched = text.includes(condition.to);
|
|
444
|
+
return {
|
|
445
|
+
condition,
|
|
446
|
+
matched,
|
|
447
|
+
detail: matched ? `Text changed to include "${condition.to}"` : `Text does not include "${condition.to}"`
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return { condition, matched: true, detail: "textChanges without `to` defaults to true" };
|
|
451
|
+
} catch {
|
|
452
|
+
return { condition, matched: false, detail: "Failed to get text for change detection" };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
case "networkResponse": {
|
|
456
|
+
const tracker = context.networkTracker;
|
|
457
|
+
if (!tracker) {
|
|
458
|
+
return { condition, matched: false, detail: "No network tracker active" };
|
|
459
|
+
}
|
|
460
|
+
const regex = globToRegex(condition.urlPattern);
|
|
461
|
+
const responses = tracker.getResponses();
|
|
462
|
+
for (const resp of responses) {
|
|
463
|
+
if (regex.test(resp.url)) {
|
|
464
|
+
if (condition.status !== void 0 && resp.status !== condition.status) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
condition,
|
|
469
|
+
matched: true,
|
|
470
|
+
detail: `Network response ${resp.url} (${resp.status}) matches pattern "${condition.urlPattern}"`
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
condition,
|
|
476
|
+
matched: false,
|
|
477
|
+
detail: `No network response matching "${condition.urlPattern}" (saw ${responses.length} responses)`
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
case "stateSignatureChanges": {
|
|
481
|
+
if (!context.beforeSignature) {
|
|
482
|
+
return { condition, matched: false, detail: "No before-signature captured" };
|
|
483
|
+
}
|
|
484
|
+
const afterSignature = await captureStateSignature(page);
|
|
485
|
+
const matched = afterSignature !== context.beforeSignature;
|
|
486
|
+
return {
|
|
487
|
+
condition,
|
|
488
|
+
matched,
|
|
489
|
+
detail: matched ? "Page state changed" : "Page state unchanged"
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
default: {
|
|
493
|
+
const _exhaustive = condition;
|
|
494
|
+
return { condition: _exhaustive, matched: false, detail: "Unknown condition kind" };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function evaluateOutcome(page, options) {
|
|
499
|
+
const {
|
|
500
|
+
expectAny,
|
|
501
|
+
expectAll,
|
|
502
|
+
failIf,
|
|
503
|
+
dangerous = false,
|
|
504
|
+
networkTracker,
|
|
505
|
+
beforeSignature
|
|
506
|
+
} = options;
|
|
507
|
+
const allMatched = [];
|
|
508
|
+
const context = { networkTracker, beforeSignature };
|
|
509
|
+
if (failIf && failIf.length > 0) {
|
|
510
|
+
for (const condition of failIf) {
|
|
511
|
+
const result = await evaluateCondition(condition, page, context);
|
|
512
|
+
allMatched.push(result);
|
|
513
|
+
if (result.matched) {
|
|
514
|
+
return {
|
|
515
|
+
outcomeStatus: "failed",
|
|
516
|
+
matchedConditions: allMatched,
|
|
517
|
+
retrySafe: !dangerous
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (expectAll && expectAll.length > 0) {
|
|
523
|
+
let allPassed = true;
|
|
524
|
+
for (const condition of expectAll) {
|
|
525
|
+
const result = await evaluateCondition(condition, page, context);
|
|
526
|
+
allMatched.push(result);
|
|
527
|
+
if (!result.matched) {
|
|
528
|
+
allPassed = false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (!allPassed) {
|
|
532
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
533
|
+
return {
|
|
534
|
+
outcomeStatus: status,
|
|
535
|
+
matchedConditions: allMatched,
|
|
536
|
+
retrySafe: !dangerous
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
if (!expectAny || expectAny.length === 0) {
|
|
540
|
+
return {
|
|
541
|
+
outcomeStatus: "success",
|
|
542
|
+
matchedConditions: allMatched,
|
|
543
|
+
retrySafe: true
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (expectAny && expectAny.length > 0) {
|
|
548
|
+
let anyPassed = false;
|
|
549
|
+
for (const condition of expectAny) {
|
|
550
|
+
const result = await evaluateCondition(condition, page, context);
|
|
551
|
+
allMatched.push(result);
|
|
552
|
+
if (result.matched) {
|
|
553
|
+
anyPassed = true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (anyPassed) {
|
|
557
|
+
return {
|
|
558
|
+
outcomeStatus: "success",
|
|
559
|
+
matchedConditions: allMatched,
|
|
560
|
+
retrySafe: true
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
const status = dangerous ? "unsafe_to_retry" : "ambiguous";
|
|
564
|
+
return {
|
|
565
|
+
outcomeStatus: status,
|
|
566
|
+
matchedConditions: allMatched,
|
|
567
|
+
retrySafe: !dangerous
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
outcomeStatus: "success",
|
|
572
|
+
matchedConditions: allMatched,
|
|
573
|
+
retrySafe: true
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/actions/combinators.ts
|
|
578
|
+
async function conditionAny(conditions, page, context) {
|
|
579
|
+
const results = [];
|
|
580
|
+
let winnerIndex;
|
|
581
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
582
|
+
const result = await evaluateCondition(conditions[i], page, context);
|
|
583
|
+
results.push(result);
|
|
584
|
+
if (result.matched && winnerIndex === void 0) {
|
|
585
|
+
winnerIndex = i;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
matched: winnerIndex !== void 0,
|
|
590
|
+
matchedConditions: results,
|
|
591
|
+
winnerIndex
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
async function conditionAll(conditions, page, context) {
|
|
595
|
+
const results = [];
|
|
596
|
+
let allMatched = true;
|
|
597
|
+
for (const condition of conditions) {
|
|
598
|
+
const result = await evaluateCondition(condition, page, context);
|
|
599
|
+
results.push(result);
|
|
600
|
+
if (!result.matched) allMatched = false;
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
matched: allMatched,
|
|
604
|
+
matchedConditions: results
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
async function conditionNot(condition, page, context) {
|
|
608
|
+
const result = await evaluateCondition(condition, page, context);
|
|
609
|
+
return {
|
|
610
|
+
matched: !result.matched,
|
|
611
|
+
matchedConditions: [
|
|
612
|
+
{
|
|
613
|
+
condition: result.condition,
|
|
614
|
+
matched: !result.matched,
|
|
615
|
+
detail: result.matched ? `NOT: condition was true (inverted to false): ${result.detail}` : `NOT: condition was false (inverted to true): ${result.detail}`
|
|
616
|
+
}
|
|
617
|
+
]
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
async function conditionRace(conditions, page, options = {}) {
|
|
621
|
+
const { timeout = 1e4, pollInterval = 200, networkTracker, beforeSignature } = options;
|
|
622
|
+
const context = { networkTracker, beforeSignature };
|
|
623
|
+
const startTime = Date.now();
|
|
624
|
+
const deadline = startTime + timeout;
|
|
625
|
+
const immediate = await conditionAny(conditions, page, context);
|
|
626
|
+
if (immediate.matched) return immediate;
|
|
627
|
+
while (Date.now() < deadline) {
|
|
628
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
629
|
+
const result = await conditionAny(conditions, page, context);
|
|
630
|
+
if (result.matched) return result;
|
|
631
|
+
}
|
|
632
|
+
return await conditionAny(conditions, page, context);
|
|
633
|
+
}
|
|
634
|
+
|
|
81
635
|
// src/actions/executor.ts
|
|
82
636
|
var fs = __toESM(require("fs"), 1);
|
|
83
637
|
var import_node_path = require("path");
|
|
@@ -1537,13 +2091,6 @@ var TRACE_SCRIPT = `
|
|
|
1537
2091
|
})();
|
|
1538
2092
|
`;
|
|
1539
2093
|
|
|
1540
|
-
// src/trace/live.ts
|
|
1541
|
-
function globToRegex(pattern) {
|
|
1542
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1543
|
-
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1544
|
-
return new RegExp(`^${withWildcards}$`);
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
2094
|
// src/actions/executor.ts
|
|
1548
2095
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1549
2096
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -1553,15 +2100,6 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
|
1553
2100
|
"text",
|
|
1554
2101
|
"screenshot"
|
|
1555
2102
|
];
|
|
1556
|
-
function readString(value) {
|
|
1557
|
-
return typeof value === "string" ? value : void 0;
|
|
1558
|
-
}
|
|
1559
|
-
function readStringOr(value, fallback = "") {
|
|
1560
|
-
return readString(value) ?? fallback;
|
|
1561
|
-
}
|
|
1562
|
-
function formatConsoleArg(entry) {
|
|
1563
|
-
return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
|
|
1564
|
-
}
|
|
1565
2103
|
function loadExistingRecording(manifestPath) {
|
|
1566
2104
|
try {
|
|
1567
2105
|
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
@@ -1675,6 +2213,25 @@ function getSuggestion(reason) {
|
|
|
1675
2213
|
}
|
|
1676
2214
|
}
|
|
1677
2215
|
}
|
|
2216
|
+
function hasOutcomeConditions(step) {
|
|
2217
|
+
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;
|
|
2218
|
+
}
|
|
2219
|
+
function needsNetworkTracking(step) {
|
|
2220
|
+
const allConditions = [
|
|
2221
|
+
...step.expectAny ?? [],
|
|
2222
|
+
...step.expectAll ?? [],
|
|
2223
|
+
...step.failIf ?? []
|
|
2224
|
+
];
|
|
2225
|
+
return allConditions.some((c) => c.kind === "networkResponse");
|
|
2226
|
+
}
|
|
2227
|
+
function needsStateSignature(step) {
|
|
2228
|
+
const allConditions = [
|
|
2229
|
+
...step.expectAny ?? [],
|
|
2230
|
+
...step.expectAll ?? [],
|
|
2231
|
+
...step.failIf ?? []
|
|
2232
|
+
];
|
|
2233
|
+
return allConditions.some((c) => c.kind === "stateSignatureChanges");
|
|
2234
|
+
}
|
|
1678
2235
|
var BatchExecutor = class {
|
|
1679
2236
|
page;
|
|
1680
2237
|
constructor(page) {
|
|
@@ -1720,9 +2277,25 @@ var BatchExecutor = class {
|
|
|
1720
2277
|
})
|
|
1721
2278
|
);
|
|
1722
2279
|
}
|
|
2280
|
+
const hasOutcome = hasOutcomeConditions(step);
|
|
2281
|
+
let networkTracker;
|
|
2282
|
+
let beforeSignature;
|
|
2283
|
+
if (hasOutcome) {
|
|
2284
|
+
if (needsNetworkTracking(step)) {
|
|
2285
|
+
networkTracker = new NetworkResponseTracker();
|
|
2286
|
+
networkTracker.start(this.page.cdpClient);
|
|
2287
|
+
}
|
|
2288
|
+
if (needsStateSignature(step)) {
|
|
2289
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
1723
2292
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1724
2293
|
if (attempt > 0) {
|
|
1725
2294
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
2295
|
+
if (networkTracker) networkTracker.reset();
|
|
2296
|
+
if (hasOutcome && needsStateSignature(step)) {
|
|
2297
|
+
beforeSignature = await captureStateSignature(this.page);
|
|
2298
|
+
}
|
|
1726
2299
|
}
|
|
1727
2300
|
try {
|
|
1728
2301
|
this.page.resetLastActionPosition();
|
|
@@ -1740,6 +2313,28 @@ var BatchExecutor = class {
|
|
|
1740
2313
|
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
1741
2314
|
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
1742
2315
|
};
|
|
2316
|
+
if (hasOutcome) {
|
|
2317
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
2318
|
+
const outcome = await evaluateOutcome(this.page, {
|
|
2319
|
+
expectAny: step.expectAny,
|
|
2320
|
+
expectAll: step.expectAll,
|
|
2321
|
+
failIf: step.failIf,
|
|
2322
|
+
dangerous: step.dangerous,
|
|
2323
|
+
networkTracker,
|
|
2324
|
+
beforeSignature
|
|
2325
|
+
});
|
|
2326
|
+
stepResult.outcomeStatus = outcome.outcomeStatus;
|
|
2327
|
+
stepResult.matchedConditions = outcome.matchedConditions;
|
|
2328
|
+
stepResult.retrySafe = outcome.retrySafe;
|
|
2329
|
+
if (outcome.outcomeStatus !== "success") {
|
|
2330
|
+
stepResult.success = false;
|
|
2331
|
+
stepResult.error = `Outcome: ${outcome.outcomeStatus}`;
|
|
2332
|
+
const failedDetails = outcome.matchedConditions.filter((mc) => outcome.outcomeStatus === "failed" ? mc.matched : !mc.matched).map((mc) => mc.detail).filter(Boolean);
|
|
2333
|
+
if (failedDetails.length > 0) {
|
|
2334
|
+
stepResult.suggestion = failedDetails.join("; ");
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
1743
2338
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1744
2339
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1745
2340
|
}
|
|
@@ -1749,13 +2344,14 @@ var BatchExecutor = class {
|
|
|
1749
2344
|
traceId: createTraceId("action"),
|
|
1750
2345
|
elapsedMs: Date.now() - startTime,
|
|
1751
2346
|
channel: "action",
|
|
1752
|
-
event: "action.succeeded",
|
|
1753
|
-
summary: `${step.action} succeeded`,
|
|
2347
|
+
event: stepResult.success ? "action.succeeded" : "action.outcome_failed",
|
|
2348
|
+
summary: stepResult.success ? `${step.action} succeeded` : `${step.action} outcome: ${stepResult.outcomeStatus}`,
|
|
1754
2349
|
data: {
|
|
1755
2350
|
action: step.action,
|
|
1756
2351
|
selector: step.selector ?? null,
|
|
1757
2352
|
selectorUsed: result.selectorUsed ?? null,
|
|
1758
|
-
durationMs: Date.now() - stepStart
|
|
2353
|
+
durationMs: Date.now() - stepStart,
|
|
2354
|
+
outcomeStatus: stepResult.outcomeStatus ?? null
|
|
1759
2355
|
},
|
|
1760
2356
|
actionId: `action-${i + 1}`,
|
|
1761
2357
|
stepIndex: i,
|
|
@@ -1765,6 +2361,18 @@ var BatchExecutor = class {
|
|
|
1765
2361
|
})
|
|
1766
2362
|
);
|
|
1767
2363
|
}
|
|
2364
|
+
if (hasOutcome && !stepResult.success) {
|
|
2365
|
+
if (step.dangerous) {
|
|
2366
|
+
results.push(stepResult);
|
|
2367
|
+
break;
|
|
2368
|
+
}
|
|
2369
|
+
if (attempt < maxAttempts - 1) {
|
|
2370
|
+
lastError = new Error(stepResult.error ?? "Outcome failed");
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
results.push(stepResult);
|
|
2374
|
+
break;
|
|
2375
|
+
}
|
|
1768
2376
|
results.push(stepResult);
|
|
1769
2377
|
succeeded = true;
|
|
1770
2378
|
break;
|
|
@@ -1772,59 +2380,63 @@ var BatchExecutor = class {
|
|
|
1772
2380
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1773
2381
|
}
|
|
1774
2382
|
}
|
|
2383
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
1775
2384
|
if (!succeeded) {
|
|
1776
|
-
const
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2385
|
+
const resultAlreadyPushed = results.length > 0 && results[results.length - 1].index === i;
|
|
2386
|
+
if (!resultAlreadyPushed) {
|
|
2387
|
+
const errorMessage = lastError?.message ?? "Unknown error";
|
|
2388
|
+
let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
|
|
2389
|
+
const { reason, coveringElement } = classifyFailure(lastError);
|
|
2390
|
+
if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
|
|
2391
|
+
try {
|
|
2392
|
+
const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
|
|
2393
|
+
const autoHints = await generateHints(this.page, selectors, step.action, 3);
|
|
2394
|
+
if (autoHints.length > 0) {
|
|
2395
|
+
hints = autoHints;
|
|
2396
|
+
}
|
|
2397
|
+
} catch {
|
|
1785
2398
|
}
|
|
1786
|
-
} catch {
|
|
1787
2399
|
}
|
|
2400
|
+
const failedResult = {
|
|
2401
|
+
index: i,
|
|
2402
|
+
action: step.action,
|
|
2403
|
+
selector: step.selector,
|
|
2404
|
+
success: false,
|
|
2405
|
+
durationMs: Date.now() - stepStart,
|
|
2406
|
+
error: errorMessage,
|
|
2407
|
+
hints,
|
|
2408
|
+
failureReason: reason,
|
|
2409
|
+
coveringElement,
|
|
2410
|
+
suggestion: getSuggestion(reason),
|
|
2411
|
+
timestamp: Date.now()
|
|
2412
|
+
};
|
|
2413
|
+
if (recording && !recording.skipActions.has(step.action)) {
|
|
2414
|
+
await this.captureRecordingFrame(step, failedResult, recording);
|
|
2415
|
+
}
|
|
2416
|
+
if (recording) {
|
|
2417
|
+
recording.traceEvents.push(
|
|
2418
|
+
normalizeTraceEvent({
|
|
2419
|
+
traceId: createTraceId("action"),
|
|
2420
|
+
elapsedMs: Date.now() - startTime,
|
|
2421
|
+
channel: "action",
|
|
2422
|
+
event: "action.failed",
|
|
2423
|
+
severity: "error",
|
|
2424
|
+
summary: `${step.action} failed: ${errorMessage}`,
|
|
2425
|
+
data: {
|
|
2426
|
+
action: step.action,
|
|
2427
|
+
selector: step.selector ?? null,
|
|
2428
|
+
error: errorMessage,
|
|
2429
|
+
reason
|
|
2430
|
+
},
|
|
2431
|
+
actionId: `action-${i + 1}`,
|
|
2432
|
+
stepIndex: i,
|
|
2433
|
+
selector: step.selector,
|
|
2434
|
+
url: step.url
|
|
2435
|
+
})
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
results.push(failedResult);
|
|
1788
2439
|
}
|
|
1789
|
-
const failedResult = {
|
|
1790
|
-
index: i,
|
|
1791
|
-
action: step.action,
|
|
1792
|
-
selector: step.selector,
|
|
1793
|
-
success: false,
|
|
1794
|
-
durationMs: Date.now() - stepStart,
|
|
1795
|
-
error: errorMessage,
|
|
1796
|
-
hints,
|
|
1797
|
-
failureReason: reason,
|
|
1798
|
-
coveringElement,
|
|
1799
|
-
suggestion: getSuggestion(reason),
|
|
1800
|
-
timestamp: Date.now()
|
|
1801
|
-
};
|
|
1802
|
-
if (recording && !recording.skipActions.has(step.action)) {
|
|
1803
|
-
await this.captureRecordingFrame(step, failedResult, recording);
|
|
1804
|
-
}
|
|
1805
|
-
if (recording) {
|
|
1806
|
-
recording.traceEvents.push(
|
|
1807
|
-
normalizeTraceEvent({
|
|
1808
|
-
traceId: createTraceId("action"),
|
|
1809
|
-
elapsedMs: Date.now() - startTime,
|
|
1810
|
-
channel: "action",
|
|
1811
|
-
event: "action.failed",
|
|
1812
|
-
severity: "error",
|
|
1813
|
-
summary: `${step.action} failed: ${errorMessage}`,
|
|
1814
|
-
data: {
|
|
1815
|
-
action: step.action,
|
|
1816
|
-
selector: step.selector ?? null,
|
|
1817
|
-
error: errorMessage,
|
|
1818
|
-
reason
|
|
1819
|
-
},
|
|
1820
|
-
actionId: `action-${i + 1}`,
|
|
1821
|
-
stepIndex: i,
|
|
1822
|
-
selector: step.selector,
|
|
1823
|
-
url: step.url
|
|
1824
|
-
})
|
|
1825
|
-
);
|
|
1826
|
-
}
|
|
1827
|
-
results.push(failedResult);
|
|
1828
2440
|
if (onFail === "stop" && !step.optional) {
|
|
1829
2441
|
stoppedAtIndex = i;
|
|
1830
2442
|
break;
|
|
@@ -2135,6 +2747,14 @@ var BatchExecutor = class {
|
|
|
2135
2747
|
case "forms": {
|
|
2136
2748
|
return { value: await this.page.forms() };
|
|
2137
2749
|
}
|
|
2750
|
+
case "delta": {
|
|
2751
|
+
const review = await this.page.review();
|
|
2752
|
+
return { value: review };
|
|
2753
|
+
}
|
|
2754
|
+
case "review": {
|
|
2755
|
+
const review = await this.page.review();
|
|
2756
|
+
return { value: review };
|
|
2757
|
+
}
|
|
2138
2758
|
case "screenshot": {
|
|
2139
2759
|
const data = await this.page.screenshot({
|
|
2140
2760
|
format: step.format,
|
|
@@ -2285,6 +2905,35 @@ var BatchExecutor = class {
|
|
|
2285
2905
|
const media = await this.assertMediaTrackLive(step.kind);
|
|
2286
2906
|
return { value: media };
|
|
2287
2907
|
}
|
|
2908
|
+
case "chooseOption": {
|
|
2909
|
+
const { chooseOption: chooseOption2 } = await Promise.resolve().then(() => (init_combobox(), combobox_exports));
|
|
2910
|
+
if (!step.value) throw new Error("chooseOption requires value");
|
|
2911
|
+
const result = await chooseOption2(this.page, {
|
|
2912
|
+
trigger: step.trigger ?? step.selector ?? "",
|
|
2913
|
+
listbox: step.option ? Array.isArray(step.option) ? step.option : [step.option] : void 0,
|
|
2914
|
+
value: typeof step.value === "string" ? step.value : step.value[0] ?? "",
|
|
2915
|
+
match: step.match,
|
|
2916
|
+
timeout: step.timeout ?? timeout
|
|
2917
|
+
});
|
|
2918
|
+
if (!result.success) {
|
|
2919
|
+
throw new Error(result.error ?? `chooseOption failed at ${result.failedAt}`);
|
|
2920
|
+
}
|
|
2921
|
+
return { value: result };
|
|
2922
|
+
}
|
|
2923
|
+
case "upload": {
|
|
2924
|
+
const { uploadFiles: uploadFiles2 } = await Promise.resolve().then(() => (init_upload(), upload_exports));
|
|
2925
|
+
if (!step.selector) throw new Error("upload requires selector");
|
|
2926
|
+
if (!step.files || step.files.length === 0) throw new Error("upload requires files");
|
|
2927
|
+
const result = await uploadFiles2(this.page, {
|
|
2928
|
+
selector: step.selector,
|
|
2929
|
+
files: step.files,
|
|
2930
|
+
timeout: step.timeout ?? timeout
|
|
2931
|
+
});
|
|
2932
|
+
if (!result.accepted) {
|
|
2933
|
+
throw new Error(result.error ?? "Upload was not accepted");
|
|
2934
|
+
}
|
|
2935
|
+
return { value: result };
|
|
2936
|
+
}
|
|
2288
2937
|
default: {
|
|
2289
2938
|
const action = step.action;
|
|
2290
2939
|
const aliases = {
|
|
@@ -2878,6 +3527,30 @@ var ACTION_RULES = {
|
|
|
2878
3527
|
kind: { type: "string", enum: ["audio", "video"] }
|
|
2879
3528
|
},
|
|
2880
3529
|
optional: {}
|
|
3530
|
+
},
|
|
3531
|
+
delta: {
|
|
3532
|
+
required: {},
|
|
3533
|
+
optional: {}
|
|
3534
|
+
},
|
|
3535
|
+
review: {
|
|
3536
|
+
required: {},
|
|
3537
|
+
optional: {}
|
|
3538
|
+
},
|
|
3539
|
+
chooseOption: {
|
|
3540
|
+
required: { value: { type: "string|string[]" } },
|
|
3541
|
+
optional: {
|
|
3542
|
+
trigger: { type: "string|string[]" },
|
|
3543
|
+
selector: { type: "string|string[]" },
|
|
3544
|
+
option: { type: "string|string[]" },
|
|
3545
|
+
match: { type: "string", enum: ["exact", "contains", "startsWith"] }
|
|
3546
|
+
}
|
|
3547
|
+
},
|
|
3548
|
+
upload: {
|
|
3549
|
+
required: {
|
|
3550
|
+
selector: { type: "string|string[]" },
|
|
3551
|
+
files: { type: "string|string[]" }
|
|
3552
|
+
},
|
|
3553
|
+
optional: {}
|
|
2881
3554
|
}
|
|
2882
3555
|
};
|
|
2883
3556
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -2917,7 +3590,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2917
3590
|
"name",
|
|
2918
3591
|
"state",
|
|
2919
3592
|
"kind",
|
|
2920
|
-
"windowMs"
|
|
3593
|
+
"windowMs",
|
|
3594
|
+
"expectAny",
|
|
3595
|
+
"expectAll",
|
|
3596
|
+
"failIf",
|
|
3597
|
+
"dangerous",
|
|
3598
|
+
"files"
|
|
2921
3599
|
]);
|
|
2922
3600
|
function resolveAction(name) {
|
|
2923
3601
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -3137,6 +3815,64 @@ function validateSteps(steps) {
|
|
|
3137
3815
|
});
|
|
3138
3816
|
}
|
|
3139
3817
|
}
|
|
3818
|
+
if ("dangerous" in obj && obj["dangerous"] !== void 0) {
|
|
3819
|
+
if (typeof obj["dangerous"] !== "boolean") {
|
|
3820
|
+
errors.push({
|
|
3821
|
+
stepIndex: i,
|
|
3822
|
+
field: "dangerous",
|
|
3823
|
+
message: `"dangerous" expected boolean, got ${typeof obj["dangerous"]}.`
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
for (const condField of ["expectAny", "expectAll", "failIf"]) {
|
|
3828
|
+
if (condField in obj && obj[condField] !== void 0) {
|
|
3829
|
+
if (!Array.isArray(obj[condField])) {
|
|
3830
|
+
errors.push({
|
|
3831
|
+
stepIndex: i,
|
|
3832
|
+
field: condField,
|
|
3833
|
+
message: `"${condField}" expected array, got ${typeof obj[condField]}.`
|
|
3834
|
+
});
|
|
3835
|
+
} else {
|
|
3836
|
+
const conditions = obj[condField];
|
|
3837
|
+
for (let ci = 0; ci < conditions.length; ci++) {
|
|
3838
|
+
const cond = conditions[ci];
|
|
3839
|
+
if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
|
|
3840
|
+
errors.push({
|
|
3841
|
+
stepIndex: i,
|
|
3842
|
+
field: condField,
|
|
3843
|
+
message: `"${condField}[${ci}]" must be a condition object.`
|
|
3844
|
+
});
|
|
3845
|
+
continue;
|
|
3846
|
+
}
|
|
3847
|
+
const condObj = cond;
|
|
3848
|
+
if (!("kind" in condObj) || typeof condObj["kind"] !== "string") {
|
|
3849
|
+
errors.push({
|
|
3850
|
+
stepIndex: i,
|
|
3851
|
+
field: condField,
|
|
3852
|
+
message: `"${condField}[${ci}]" missing required "kind" field.`
|
|
3853
|
+
});
|
|
3854
|
+
} else {
|
|
3855
|
+
const validKinds = [
|
|
3856
|
+
"urlMatches",
|
|
3857
|
+
"elementVisible",
|
|
3858
|
+
"elementHidden",
|
|
3859
|
+
"textAppears",
|
|
3860
|
+
"textChanges",
|
|
3861
|
+
"networkResponse",
|
|
3862
|
+
"stateSignatureChanges"
|
|
3863
|
+
];
|
|
3864
|
+
if (!validKinds.includes(condObj["kind"])) {
|
|
3865
|
+
errors.push({
|
|
3866
|
+
stepIndex: i,
|
|
3867
|
+
field: condField,
|
|
3868
|
+
message: `"${condField}[${ci}].kind" must be one of: ${validKinds.join(", ")}. Got "${condObj["kind"]}".`
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3140
3876
|
if (action === "assertText") {
|
|
3141
3877
|
if (!("expect" in obj) && !("value" in obj)) {
|
|
3142
3878
|
errors.push({
|
|
@@ -6237,6 +6973,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
6237
6973
|
});
|
|
6238
6974
|
}
|
|
6239
6975
|
|
|
6976
|
+
// src/browser/delta.ts
|
|
6977
|
+
function extractPageState(url, title, snapshot, forms, pageText) {
|
|
6978
|
+
const headings = [];
|
|
6979
|
+
const buttons = [];
|
|
6980
|
+
const alerts = [];
|
|
6981
|
+
function walkNodes(nodes) {
|
|
6982
|
+
for (const node of nodes) {
|
|
6983
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
6984
|
+
if (role === "heading" && node.name) {
|
|
6985
|
+
headings.push(node.name);
|
|
6986
|
+
}
|
|
6987
|
+
if ((role === "button" || role === "link") && node.name) {
|
|
6988
|
+
const disabled = node.disabled ?? false;
|
|
6989
|
+
buttons.push({ text: node.name, disabled, ref: node.ref });
|
|
6990
|
+
}
|
|
6991
|
+
if (role === "alert" && node.name) {
|
|
6992
|
+
alerts.push(node.name);
|
|
6993
|
+
}
|
|
6994
|
+
if (node.children) {
|
|
6995
|
+
walkNodes(node.children);
|
|
6996
|
+
}
|
|
6997
|
+
}
|
|
6998
|
+
}
|
|
6999
|
+
walkNodes(snapshot.accessibilityTree);
|
|
7000
|
+
const formFields = forms.map((f) => ({
|
|
7001
|
+
label: f.label,
|
|
7002
|
+
name: f.name,
|
|
7003
|
+
id: f.id,
|
|
7004
|
+
value: f.value,
|
|
7005
|
+
type: f.type
|
|
7006
|
+
}));
|
|
7007
|
+
return {
|
|
7008
|
+
url,
|
|
7009
|
+
title,
|
|
7010
|
+
headings,
|
|
7011
|
+
formFields,
|
|
7012
|
+
buttons,
|
|
7013
|
+
alerts,
|
|
7014
|
+
visibleText: pageText.slice(0, 3e3)
|
|
7015
|
+
};
|
|
7016
|
+
}
|
|
7017
|
+
function computeDelta(before, after) {
|
|
7018
|
+
const changes = [];
|
|
7019
|
+
if (before.url !== after.url) {
|
|
7020
|
+
changes.push({ kind: "url", before: before.url, after: after.url });
|
|
7021
|
+
}
|
|
7022
|
+
if (before.title !== after.title) {
|
|
7023
|
+
changes.push({ kind: "title", before: before.title, after: after.title });
|
|
7024
|
+
}
|
|
7025
|
+
const beforeHeadings = new Set(before.headings);
|
|
7026
|
+
const afterHeadings = new Set(after.headings);
|
|
7027
|
+
for (const h of after.headings) {
|
|
7028
|
+
if (!beforeHeadings.has(h)) {
|
|
7029
|
+
changes.push({ kind: "heading_added", after: h });
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
for (const h of before.headings) {
|
|
7033
|
+
if (!afterHeadings.has(h)) {
|
|
7034
|
+
changes.push({ kind: "heading_removed", before: h });
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
const beforeFieldMap = new Map(
|
|
7038
|
+
before.formFields.map((f) => [f.id ?? f.name ?? f.label ?? "", f])
|
|
7039
|
+
);
|
|
7040
|
+
for (const af of after.formFields) {
|
|
7041
|
+
const key = af.id ?? af.name ?? af.label ?? "";
|
|
7042
|
+
const bf = beforeFieldMap.get(key);
|
|
7043
|
+
if (bf && JSON.stringify(bf.value) !== JSON.stringify(af.value)) {
|
|
7044
|
+
changes.push({
|
|
7045
|
+
kind: "field_changed",
|
|
7046
|
+
before: String(bf.value ?? ""),
|
|
7047
|
+
after: String(af.value ?? ""),
|
|
7048
|
+
detail: af.label ?? af.name ?? af.id ?? key
|
|
7049
|
+
});
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
7052
|
+
const beforeBtnMap = new Map(before.buttons.map((b) => [b.text, b]));
|
|
7053
|
+
for (const ab of after.buttons) {
|
|
7054
|
+
const bb = beforeBtnMap.get(ab.text);
|
|
7055
|
+
if (bb && bb.disabled !== ab.disabled) {
|
|
7056
|
+
changes.push({
|
|
7057
|
+
kind: "button_changed",
|
|
7058
|
+
detail: ab.text,
|
|
7059
|
+
before: bb.disabled ? "disabled" : "enabled",
|
|
7060
|
+
after: ab.disabled ? "disabled" : "enabled"
|
|
7061
|
+
});
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
const beforeAlerts = new Set(before.alerts);
|
|
7065
|
+
const afterAlerts = new Set(after.alerts);
|
|
7066
|
+
for (const a of after.alerts) {
|
|
7067
|
+
if (!beforeAlerts.has(a)) {
|
|
7068
|
+
changes.push({ kind: "alert_added", after: a });
|
|
7069
|
+
}
|
|
7070
|
+
}
|
|
7071
|
+
for (const a of before.alerts) {
|
|
7072
|
+
if (!afterAlerts.has(a)) {
|
|
7073
|
+
changes.push({ kind: "alert_removed", before: a });
|
|
7074
|
+
}
|
|
7075
|
+
}
|
|
7076
|
+
return {
|
|
7077
|
+
changes,
|
|
7078
|
+
before,
|
|
7079
|
+
after,
|
|
7080
|
+
hasChanges: changes.length > 0
|
|
7081
|
+
};
|
|
7082
|
+
}
|
|
7083
|
+
|
|
6240
7084
|
// src/browser/keyboard.ts
|
|
6241
7085
|
var US_KEYBOARD = {
|
|
6242
7086
|
// Letters (lowercase)
|
|
@@ -6396,8 +7240,118 @@ function parseShortcut(combo) {
|
|
|
6396
7240
|
return { modifiers, key };
|
|
6397
7241
|
}
|
|
6398
7242
|
|
|
7243
|
+
// src/browser/review.ts
|
|
7244
|
+
function extractReview(url, title, snapshot, forms, pageText) {
|
|
7245
|
+
const headings = [];
|
|
7246
|
+
const alerts = [];
|
|
7247
|
+
const statusLabels = [];
|
|
7248
|
+
const keyValues = [];
|
|
7249
|
+
const tables = [];
|
|
7250
|
+
const summaryCards = [];
|
|
7251
|
+
function walkNodes(nodes, parentHeading) {
|
|
7252
|
+
let currentHeading = parentHeading;
|
|
7253
|
+
for (const node of nodes) {
|
|
7254
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
7255
|
+
if (role === "heading" && node.name) {
|
|
7256
|
+
headings.push(node.name);
|
|
7257
|
+
currentHeading = node.name;
|
|
7258
|
+
}
|
|
7259
|
+
if (role === "alert" && node.name) {
|
|
7260
|
+
alerts.push(node.name);
|
|
7261
|
+
}
|
|
7262
|
+
if (role === "status" && node.name) {
|
|
7263
|
+
statusLabels.push(node.name);
|
|
7264
|
+
}
|
|
7265
|
+
if (role === "table" || role === "grid") {
|
|
7266
|
+
const table = extractTableFromNode(node);
|
|
7267
|
+
if (table) tables.push(table);
|
|
7268
|
+
}
|
|
7269
|
+
if ((role === "definition" || role === "term") && node.name) {
|
|
7270
|
+
if (role === "term") {
|
|
7271
|
+
keyValues.push({ key: node.name, value: "" });
|
|
7272
|
+
} else if (role === "definition" && keyValues.length > 0) {
|
|
7273
|
+
const last = keyValues[keyValues.length - 1];
|
|
7274
|
+
if (!last.value) last.value = node.name;
|
|
7275
|
+
}
|
|
7276
|
+
}
|
|
7277
|
+
if (node.children) {
|
|
7278
|
+
walkNodes(node.children, currentHeading);
|
|
7279
|
+
}
|
|
7280
|
+
}
|
|
7281
|
+
}
|
|
7282
|
+
walkNodes(snapshot.accessibilityTree);
|
|
7283
|
+
const textKvPairs = extractKeyValueFromText(pageText);
|
|
7284
|
+
keyValues.push(...textKvPairs);
|
|
7285
|
+
const formEntries = forms.map((f) => ({
|
|
7286
|
+
label: f.label,
|
|
7287
|
+
value: f.value,
|
|
7288
|
+
type: f.type,
|
|
7289
|
+
disabled: f.disabled
|
|
7290
|
+
}));
|
|
7291
|
+
return {
|
|
7292
|
+
url,
|
|
7293
|
+
title,
|
|
7294
|
+
headings,
|
|
7295
|
+
forms: formEntries,
|
|
7296
|
+
alerts,
|
|
7297
|
+
summaryCards,
|
|
7298
|
+
tables,
|
|
7299
|
+
keyValues,
|
|
7300
|
+
statusLabels
|
|
7301
|
+
};
|
|
7302
|
+
}
|
|
7303
|
+
function extractTableFromNode(node) {
|
|
7304
|
+
const headers = [];
|
|
7305
|
+
const rows = [];
|
|
7306
|
+
function findRows(n) {
|
|
7307
|
+
const role = n.role?.toLowerCase() ?? "";
|
|
7308
|
+
if (role === "columnheader" && n.name) {
|
|
7309
|
+
headers.push(n.name);
|
|
7310
|
+
}
|
|
7311
|
+
if (role === "row") {
|
|
7312
|
+
const cells = [];
|
|
7313
|
+
if (n.children) {
|
|
7314
|
+
for (const child of n.children) {
|
|
7315
|
+
const childRole = child.role?.toLowerCase() ?? "";
|
|
7316
|
+
if ((childRole === "cell" || childRole === "gridcell") && child.name) {
|
|
7317
|
+
cells.push(child.name);
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
if (cells.length > 0) rows.push(cells);
|
|
7322
|
+
}
|
|
7323
|
+
if (n.children) {
|
|
7324
|
+
for (const child of n.children) findRows(child);
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
findRows(node);
|
|
7328
|
+
if (rows.length === 0) return null;
|
|
7329
|
+
return { headers, rows };
|
|
7330
|
+
}
|
|
7331
|
+
function extractKeyValueFromText(text) {
|
|
7332
|
+
const pairs = [];
|
|
7333
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
7334
|
+
for (const line of lines) {
|
|
7335
|
+
const match = line.match(/^([A-Z][A-Za-z0-9 ]{1,30})[:—]\s+(.+)$/);
|
|
7336
|
+
if (match) {
|
|
7337
|
+
pairs.push({ key: match[1].trim(), value: match[2].trim() });
|
|
7338
|
+
}
|
|
7339
|
+
}
|
|
7340
|
+
return pairs.slice(0, 20);
|
|
7341
|
+
}
|
|
7342
|
+
|
|
6399
7343
|
// src/browser/page.ts
|
|
6400
7344
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
7345
|
+
function normalizeAXCheckedValue(value) {
|
|
7346
|
+
if (typeof value === "boolean") {
|
|
7347
|
+
return value;
|
|
7348
|
+
}
|
|
7349
|
+
if (typeof value === "string") {
|
|
7350
|
+
if (value === "true") return true;
|
|
7351
|
+
if (value === "false") return false;
|
|
7352
|
+
}
|
|
7353
|
+
return void 0;
|
|
7354
|
+
}
|
|
6401
7355
|
var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
|
|
6402
7356
|
if (globalThis.__bpEventListenerTrackerInstalled) return;
|
|
6403
7357
|
Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
|
|
@@ -8183,7 +9137,9 @@ var Page = class {
|
|
|
8183
9137
|
}
|
|
8184
9138
|
}
|
|
8185
9139
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
8186
|
-
const checked =
|
|
9140
|
+
const checked = normalizeAXCheckedValue(
|
|
9141
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
9142
|
+
);
|
|
8187
9143
|
return {
|
|
8188
9144
|
role,
|
|
8189
9145
|
name,
|
|
@@ -8239,7 +9195,9 @@ var Page = class {
|
|
|
8239
9195
|
const ref = nodeRefs.get(node.nodeId);
|
|
8240
9196
|
const name = node.name?.value ?? "";
|
|
8241
9197
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
8242
|
-
const checked =
|
|
9198
|
+
const checked = normalizeAXCheckedValue(
|
|
9199
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
9200
|
+
);
|
|
8243
9201
|
const value = node.value?.value;
|
|
8244
9202
|
const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
|
|
8245
9203
|
interactiveElements.push({
|
|
@@ -8306,6 +9264,45 @@ var Page = class {
|
|
|
8306
9264
|
}
|
|
8307
9265
|
}
|
|
8308
9266
|
}
|
|
9267
|
+
// ============ Delta & Review ============
|
|
9268
|
+
/**
|
|
9269
|
+
* Capture current page state for delta comparison.
|
|
9270
|
+
* Call before an action, then call delta() again after and use computeDelta().
|
|
9271
|
+
*/
|
|
9272
|
+
async captureState() {
|
|
9273
|
+
const [url, title, snapshot, forms, text] = await Promise.all([
|
|
9274
|
+
this.url(),
|
|
9275
|
+
this.title(),
|
|
9276
|
+
this.snapshot(),
|
|
9277
|
+
this.forms(),
|
|
9278
|
+
this.text()
|
|
9279
|
+
]);
|
|
9280
|
+
return extractPageState(url, title, snapshot, forms, text);
|
|
9281
|
+
}
|
|
9282
|
+
/**
|
|
9283
|
+
* Compute what changed between two page states.
|
|
9284
|
+
* If no arguments: captures current state and returns it (for use as "before").
|
|
9285
|
+
* If one argument (before state): captures current state and computes delta.
|
|
9286
|
+
*/
|
|
9287
|
+
async delta(before) {
|
|
9288
|
+
const currentState = await this.captureState();
|
|
9289
|
+
if (!before) return currentState;
|
|
9290
|
+
return computeDelta(before, currentState);
|
|
9291
|
+
}
|
|
9292
|
+
/**
|
|
9293
|
+
* Extract structured review surface from the current page.
|
|
9294
|
+
* Returns headings, form values, alerts, key-value pairs, tables, and status labels.
|
|
9295
|
+
*/
|
|
9296
|
+
async review() {
|
|
9297
|
+
const [url, title, snapshot, forms, text] = await Promise.all([
|
|
9298
|
+
this.url(),
|
|
9299
|
+
this.title(),
|
|
9300
|
+
this.snapshot(),
|
|
9301
|
+
this.forms(),
|
|
9302
|
+
this.text()
|
|
9303
|
+
]);
|
|
9304
|
+
return extractReview(url, title, snapshot, forms, text);
|
|
9305
|
+
}
|
|
8309
9306
|
// ============ Batch Execution ============
|
|
8310
9307
|
/**
|
|
8311
9308
|
* Execute a batch of steps
|
|
@@ -9731,6 +10728,316 @@ function connect(options) {
|
|
|
9731
10728
|
return Browser.connect(options);
|
|
9732
10729
|
}
|
|
9733
10730
|
|
|
10731
|
+
// src/browser/index.ts
|
|
10732
|
+
init_combobox();
|
|
10733
|
+
|
|
10734
|
+
// src/browser/fingerprint.ts
|
|
10735
|
+
function createFingerprint(node, context) {
|
|
10736
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
10737
|
+
const name = node.name ?? "";
|
|
10738
|
+
let valueShape = "";
|
|
10739
|
+
if (node.value !== void 0) {
|
|
10740
|
+
valueShape = typeof node.value === "string" ? "text" : typeof node.value === "number" ? "number" : typeof node.value === "boolean" ? "boolean" : "other";
|
|
10741
|
+
}
|
|
10742
|
+
const stableAttrs = {};
|
|
10743
|
+
if (node.properties) {
|
|
10744
|
+
for (const key of ["id", "name", "type", "aria-label"]) {
|
|
10745
|
+
const val = node.properties[key];
|
|
10746
|
+
if (val !== void 0 && val !== null) {
|
|
10747
|
+
stableAttrs[key] = String(val);
|
|
10748
|
+
}
|
|
10749
|
+
}
|
|
10750
|
+
}
|
|
10751
|
+
return {
|
|
10752
|
+
role,
|
|
10753
|
+
name,
|
|
10754
|
+
valueShape,
|
|
10755
|
+
label: name,
|
|
10756
|
+
// label is typically the accessible name
|
|
10757
|
+
stableAttrs,
|
|
10758
|
+
nearestHeading: context.nearestHeading,
|
|
10759
|
+
siblingIndex: context.siblingIndex,
|
|
10760
|
+
sectionPath: [...context.headingTrail]
|
|
10761
|
+
};
|
|
10762
|
+
}
|
|
10763
|
+
function fingerprintKey(fp) {
|
|
10764
|
+
const parts = [fp.role, fp.name, fp.sectionPath.join(">")];
|
|
10765
|
+
if (fp.stableAttrs["id"]) parts.push(`id=${fp.stableAttrs["id"]}`);
|
|
10766
|
+
if (fp.stableAttrs["name"]) parts.push(`name=${fp.stableAttrs["name"]}`);
|
|
10767
|
+
return parts.join("|");
|
|
10768
|
+
}
|
|
10769
|
+
function fingerprintSimilarity(a, b) {
|
|
10770
|
+
let score = 0;
|
|
10771
|
+
let weight = 0;
|
|
10772
|
+
weight += 3;
|
|
10773
|
+
if (a.role === b.role) score += 3;
|
|
10774
|
+
else return 0;
|
|
10775
|
+
weight += 5;
|
|
10776
|
+
if (a.name && b.name && a.name === b.name) score += 5;
|
|
10777
|
+
else if (a.name && b.name && a.name.toLowerCase() === b.name.toLowerCase()) score += 4;
|
|
10778
|
+
weight += 3;
|
|
10779
|
+
const pathA = a.sectionPath.join(">");
|
|
10780
|
+
const pathB = b.sectionPath.join(">");
|
|
10781
|
+
if (pathA === pathB) score += 3;
|
|
10782
|
+
else if (pathA && pathB && (pathA.includes(pathB) || pathB.includes(pathA))) score += 1;
|
|
10783
|
+
const attrKeys = /* @__PURE__ */ new Set([...Object.keys(a.stableAttrs), ...Object.keys(b.stableAttrs)]);
|
|
10784
|
+
for (const key of attrKeys) {
|
|
10785
|
+
weight += 2;
|
|
10786
|
+
if (a.stableAttrs[key] && b.stableAttrs[key] && a.stableAttrs[key] === b.stableAttrs[key]) {
|
|
10787
|
+
score += 2;
|
|
10788
|
+
}
|
|
10789
|
+
}
|
|
10790
|
+
weight += 1;
|
|
10791
|
+
if (a.siblingIndex === b.siblingIndex) score += 1;
|
|
10792
|
+
return score / weight;
|
|
10793
|
+
}
|
|
10794
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
10795
|
+
"button",
|
|
10796
|
+
"link",
|
|
10797
|
+
"textbox",
|
|
10798
|
+
"checkbox",
|
|
10799
|
+
"radio",
|
|
10800
|
+
"combobox",
|
|
10801
|
+
"listbox",
|
|
10802
|
+
"menuitem",
|
|
10803
|
+
"tab",
|
|
10804
|
+
"switch",
|
|
10805
|
+
"searchbox",
|
|
10806
|
+
"spinbutton",
|
|
10807
|
+
"slider"
|
|
10808
|
+
]);
|
|
10809
|
+
function buildFingerprintMap(nodes) {
|
|
10810
|
+
const map = /* @__PURE__ */ new Map();
|
|
10811
|
+
function walk(nodeList, headingTrail, nearestHeading) {
|
|
10812
|
+
const roleCounts = /* @__PURE__ */ new Map();
|
|
10813
|
+
for (const node of nodeList) {
|
|
10814
|
+
const role = node.role?.toLowerCase() ?? "";
|
|
10815
|
+
let currentHeadingTrail = headingTrail;
|
|
10816
|
+
let currentNearestHeading = nearestHeading;
|
|
10817
|
+
if (role === "heading" && node.name) {
|
|
10818
|
+
currentHeadingTrail = [...headingTrail, node.name];
|
|
10819
|
+
currentNearestHeading = node.name;
|
|
10820
|
+
}
|
|
10821
|
+
if (INTERACTIVE_ROLES.has(role) && node.ref) {
|
|
10822
|
+
const siblingCount = roleCounts.get(role) ?? 0;
|
|
10823
|
+
roleCounts.set(role, siblingCount + 1);
|
|
10824
|
+
const fp = createFingerprint(node, {
|
|
10825
|
+
headingTrail: currentHeadingTrail,
|
|
10826
|
+
siblingIndex: siblingCount,
|
|
10827
|
+
nearestHeading: currentNearestHeading
|
|
10828
|
+
});
|
|
10829
|
+
map.set(node.ref, fp);
|
|
10830
|
+
}
|
|
10831
|
+
if (node.children) {
|
|
10832
|
+
walk(node.children, currentHeadingTrail, currentNearestHeading);
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
}
|
|
10836
|
+
walk(nodes, [], "");
|
|
10837
|
+
return map;
|
|
10838
|
+
}
|
|
10839
|
+
function recoverStaleRef(staleFingerprint, currentFingerprints, threshold = 0.7) {
|
|
10840
|
+
let bestRef = null;
|
|
10841
|
+
let bestScore = 0;
|
|
10842
|
+
let secondBestScore = 0;
|
|
10843
|
+
for (const [ref, fp] of currentFingerprints) {
|
|
10844
|
+
const similarity = fingerprintSimilarity(staleFingerprint, fp);
|
|
10845
|
+
if (similarity > bestScore) {
|
|
10846
|
+
secondBestScore = bestScore;
|
|
10847
|
+
bestScore = similarity;
|
|
10848
|
+
bestRef = ref;
|
|
10849
|
+
} else if (similarity > secondBestScore) {
|
|
10850
|
+
secondBestScore = similarity;
|
|
10851
|
+
}
|
|
10852
|
+
}
|
|
10853
|
+
if (!bestRef || bestScore < threshold) return null;
|
|
10854
|
+
if (secondBestScore > 0 && bestScore - secondBestScore < 0.15) return null;
|
|
10855
|
+
return { ref: bestRef, confidence: bestScore };
|
|
10856
|
+
}
|
|
10857
|
+
|
|
10858
|
+
// src/browser/overlay-detect.ts
|
|
10859
|
+
async function detectOverlay(page) {
|
|
10860
|
+
const result = await page.evaluate(`(() => {
|
|
10861
|
+
// Check for role="dialog" or role="alertdialog"
|
|
10862
|
+
const dialogs = document.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog[open]');
|
|
10863
|
+
for (const d of dialogs) {
|
|
10864
|
+
if (d.offsetParent !== null || getComputedStyle(d).display !== 'none') {
|
|
10865
|
+
return {
|
|
10866
|
+
hasOverlay: true,
|
|
10867
|
+
overlaySelector: d.id ? '#' + d.id : (d.getAttribute('role') ? '[role="' + d.getAttribute('role') + '"]' : 'dialog'),
|
|
10868
|
+
overlayText: (d.textContent || '').trim().slice(0, 200),
|
|
10869
|
+
};
|
|
10870
|
+
}
|
|
10871
|
+
}
|
|
10872
|
+
|
|
10873
|
+
// Check for fixed/absolute positioned elements with high z-index that look like modals
|
|
10874
|
+
const allElements = document.querySelectorAll('*');
|
|
10875
|
+
for (const el of allElements) {
|
|
10876
|
+
const style = getComputedStyle(el);
|
|
10877
|
+
if (
|
|
10878
|
+
(style.position === 'fixed' || style.position === 'absolute') &&
|
|
10879
|
+
parseInt(style.zIndex || '0', 10) > 999 &&
|
|
10880
|
+
el.offsetWidth > 100 &&
|
|
10881
|
+
el.offsetHeight > 100 &&
|
|
10882
|
+
style.display !== 'none' &&
|
|
10883
|
+
style.visibility !== 'hidden'
|
|
10884
|
+
) {
|
|
10885
|
+
const text = (el.textContent || '').trim();
|
|
10886
|
+
if (text.length > 10) {
|
|
10887
|
+
return {
|
|
10888
|
+
hasOverlay: true,
|
|
10889
|
+
overlaySelector: el.id ? '#' + el.id : null,
|
|
10890
|
+
overlayText: text.slice(0, 200),
|
|
10891
|
+
};
|
|
10892
|
+
}
|
|
10893
|
+
}
|
|
10894
|
+
}
|
|
10895
|
+
|
|
10896
|
+
return { hasOverlay: false };
|
|
10897
|
+
})()`);
|
|
10898
|
+
return result ?? { hasOverlay: false };
|
|
10899
|
+
}
|
|
10900
|
+
|
|
10901
|
+
// src/browser/safe-submit.ts
|
|
10902
|
+
async function submitAndVerify(page, options) {
|
|
10903
|
+
const {
|
|
10904
|
+
selector,
|
|
10905
|
+
method = "enter+click",
|
|
10906
|
+
expectAny,
|
|
10907
|
+
expectAll,
|
|
10908
|
+
failIf,
|
|
10909
|
+
dangerous = false,
|
|
10910
|
+
timeout = 3e4,
|
|
10911
|
+
waitForNavigation: waitForNavigation2 = "auto"
|
|
10912
|
+
} = options;
|
|
10913
|
+
const startTime = Date.now();
|
|
10914
|
+
const allConditions = [...expectAny ?? [], ...expectAll ?? [], ...failIf ?? []];
|
|
10915
|
+
const needsNetwork = allConditions.some((c) => c.kind === "networkResponse");
|
|
10916
|
+
const needsSignature = allConditions.some((c) => c.kind === "stateSignatureChanges");
|
|
10917
|
+
let networkTracker;
|
|
10918
|
+
let beforeSignature;
|
|
10919
|
+
if (needsNetwork) {
|
|
10920
|
+
networkTracker = new NetworkResponseTracker();
|
|
10921
|
+
networkTracker.start(page.cdpClient);
|
|
10922
|
+
}
|
|
10923
|
+
if (needsSignature) {
|
|
10924
|
+
beforeSignature = await captureStateSignature(page);
|
|
10925
|
+
}
|
|
10926
|
+
try {
|
|
10927
|
+
await page.submit(selector, {
|
|
10928
|
+
timeout,
|
|
10929
|
+
method,
|
|
10930
|
+
waitForNavigation: waitForNavigation2
|
|
10931
|
+
});
|
|
10932
|
+
if (networkTracker) networkTracker.stop(page.cdpClient);
|
|
10933
|
+
if (allConditions.length === 0) {
|
|
10934
|
+
return {
|
|
10935
|
+
submitted: true,
|
|
10936
|
+
outcomeStatus: "success",
|
|
10937
|
+
matchedConditions: [],
|
|
10938
|
+
retrySafe: !dangerous,
|
|
10939
|
+
durationMs: Date.now() - startTime
|
|
10940
|
+
};
|
|
10941
|
+
}
|
|
10942
|
+
const outcome = await evaluateOutcome(page, {
|
|
10943
|
+
expectAny,
|
|
10944
|
+
expectAll,
|
|
10945
|
+
failIf,
|
|
10946
|
+
dangerous,
|
|
10947
|
+
networkTracker,
|
|
10948
|
+
beforeSignature
|
|
10949
|
+
});
|
|
10950
|
+
return {
|
|
10951
|
+
submitted: true,
|
|
10952
|
+
outcomeStatus: outcome.outcomeStatus,
|
|
10953
|
+
matchedConditions: outcome.matchedConditions,
|
|
10954
|
+
retrySafe: outcome.retrySafe,
|
|
10955
|
+
durationMs: Date.now() - startTime
|
|
10956
|
+
};
|
|
10957
|
+
} catch (error) {
|
|
10958
|
+
if (networkTracker) networkTracker.stop(page.cdpClient);
|
|
10959
|
+
return {
|
|
10960
|
+
submitted: false,
|
|
10961
|
+
outcomeStatus: "failed",
|
|
10962
|
+
matchedConditions: [],
|
|
10963
|
+
retrySafe: !dangerous,
|
|
10964
|
+
durationMs: Date.now() - startTime,
|
|
10965
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10966
|
+
};
|
|
10967
|
+
}
|
|
10968
|
+
}
|
|
10969
|
+
|
|
10970
|
+
// src/runtime/clock.ts
|
|
10971
|
+
function now() {
|
|
10972
|
+
return Date.now();
|
|
10973
|
+
}
|
|
10974
|
+
|
|
10975
|
+
// src/browser/target-pin.ts
|
|
10976
|
+
function createTargetFingerprint(targetId, url, title) {
|
|
10977
|
+
return {
|
|
10978
|
+
url,
|
|
10979
|
+
title,
|
|
10980
|
+
originalTargetId: targetId,
|
|
10981
|
+
pinnedAt: now()
|
|
10982
|
+
};
|
|
10983
|
+
}
|
|
10984
|
+
function scoreCandidate(candidate, pin) {
|
|
10985
|
+
if (candidate.targetId === pin.originalTargetId) return 1;
|
|
10986
|
+
let score = 0;
|
|
10987
|
+
if (candidate.url && pin.url) {
|
|
10988
|
+
if (candidate.url === pin.url) {
|
|
10989
|
+
score += 0.6;
|
|
10990
|
+
} else {
|
|
10991
|
+
try {
|
|
10992
|
+
const candidateOrigin = new URL(candidate.url).origin;
|
|
10993
|
+
const pinOrigin = new URL(pin.url).origin;
|
|
10994
|
+
if (candidateOrigin === pinOrigin) score += 0.3;
|
|
10995
|
+
} catch {
|
|
10996
|
+
}
|
|
10997
|
+
}
|
|
10998
|
+
}
|
|
10999
|
+
if (candidate.title && pin.title) {
|
|
11000
|
+
if (candidate.title === pin.title) {
|
|
11001
|
+
score += 0.3;
|
|
11002
|
+
} else if (candidate.title.includes(pin.title) || pin.title.includes(candidate.title)) {
|
|
11003
|
+
score += 0.15;
|
|
11004
|
+
}
|
|
11005
|
+
}
|
|
11006
|
+
if (candidate.type !== "page") score *= 0.5;
|
|
11007
|
+
return Math.min(score, 0.95);
|
|
11008
|
+
}
|
|
11009
|
+
function recoverPinnedTarget(pin, targets, threshold = 0.4) {
|
|
11010
|
+
if (targets.length === 0) return null;
|
|
11011
|
+
let bestTarget = null;
|
|
11012
|
+
let bestScore = 0;
|
|
11013
|
+
for (const target of targets) {
|
|
11014
|
+
const score = scoreCandidate(target, pin);
|
|
11015
|
+
if (score > bestScore) {
|
|
11016
|
+
bestScore = score;
|
|
11017
|
+
bestTarget = target;
|
|
11018
|
+
}
|
|
11019
|
+
}
|
|
11020
|
+
if (!bestTarget || bestScore < threshold) return null;
|
|
11021
|
+
let method;
|
|
11022
|
+
if (bestTarget.targetId === pin.originalTargetId) {
|
|
11023
|
+
method = "exact";
|
|
11024
|
+
} else if (bestTarget.url === pin.url) {
|
|
11025
|
+
method = "url_match";
|
|
11026
|
+
} else if (bestTarget.title === pin.title) {
|
|
11027
|
+
method = "title_match";
|
|
11028
|
+
} else {
|
|
11029
|
+
method = "best_guess";
|
|
11030
|
+
}
|
|
11031
|
+
return {
|
|
11032
|
+
targetId: bestTarget.targetId,
|
|
11033
|
+
method,
|
|
11034
|
+
confidence: bestScore
|
|
11035
|
+
};
|
|
11036
|
+
}
|
|
11037
|
+
|
|
11038
|
+
// src/browser/index.ts
|
|
11039
|
+
init_upload();
|
|
11040
|
+
|
|
9734
11041
|
// src/emulation/devices.ts
|
|
9735
11042
|
var devices = {
|
|
9736
11043
|
"iPhone 14": {
|
|
@@ -9949,6 +11256,143 @@ function disableTracing() {
|
|
|
9949
11256
|
globalTracer.disable();
|
|
9950
11257
|
}
|
|
9951
11258
|
}
|
|
11259
|
+
|
|
11260
|
+
// src/trace/workflow-summary.ts
|
|
11261
|
+
function describeStep(result) {
|
|
11262
|
+
const action = result.action;
|
|
11263
|
+
const selector = result.selectorUsed ?? (Array.isArray(result.selector) ? result.selector[0] : result.selector);
|
|
11264
|
+
switch (action) {
|
|
11265
|
+
case "goto":
|
|
11266
|
+
return "Navigate to page";
|
|
11267
|
+
case "click":
|
|
11268
|
+
return `Click ${selector ? `"${selector}"` : "element"}`;
|
|
11269
|
+
case "fill":
|
|
11270
|
+
return `Fill ${selector ? `"${selector}"` : "field"}`;
|
|
11271
|
+
case "type":
|
|
11272
|
+
return `Type into ${selector ? `"${selector}"` : "field"}`;
|
|
11273
|
+
case "select":
|
|
11274
|
+
return `Select option in ${selector ? `"${selector}"` : "dropdown"}`;
|
|
11275
|
+
case "submit":
|
|
11276
|
+
return `Submit ${selector ? `"${selector}"` : "form"}`;
|
|
11277
|
+
case "check":
|
|
11278
|
+
return `Check ${selector ? `"${selector}"` : "checkbox"}`;
|
|
11279
|
+
case "uncheck":
|
|
11280
|
+
return `Uncheck ${selector ? `"${selector}"` : "checkbox"}`;
|
|
11281
|
+
case "press":
|
|
11282
|
+
return "Press key";
|
|
11283
|
+
case "shortcut":
|
|
11284
|
+
return "Keyboard shortcut";
|
|
11285
|
+
case "hover":
|
|
11286
|
+
return `Hover over ${selector ? `"${selector}"` : "element"}`;
|
|
11287
|
+
case "scroll":
|
|
11288
|
+
return `Scroll ${selector ? `"${selector}"` : "page"}`;
|
|
11289
|
+
case "wait":
|
|
11290
|
+
return `Wait for ${selector ? `"${selector}"` : "condition"}`;
|
|
11291
|
+
case "snapshot":
|
|
11292
|
+
return "Capture accessibility snapshot";
|
|
11293
|
+
case "screenshot":
|
|
11294
|
+
return "Take screenshot";
|
|
11295
|
+
case "forms":
|
|
11296
|
+
return "Enumerate form fields";
|
|
11297
|
+
case "evaluate":
|
|
11298
|
+
return "Execute JavaScript";
|
|
11299
|
+
case "text":
|
|
11300
|
+
return "Extract text content";
|
|
11301
|
+
case "review":
|
|
11302
|
+
return "Extract page review";
|
|
11303
|
+
case "delta":
|
|
11304
|
+
return "Capture page delta";
|
|
11305
|
+
default:
|
|
11306
|
+
return `${action}${selector ? ` "${selector}"` : ""}`;
|
|
11307
|
+
}
|
|
11308
|
+
}
|
|
11309
|
+
function summarizeConditions(conditions) {
|
|
11310
|
+
return conditions.filter((c) => c.detail).map((c) => {
|
|
11311
|
+
const status = c.matched ? "\u2713" : "\u2717";
|
|
11312
|
+
return `${status} ${c.detail}`;
|
|
11313
|
+
});
|
|
11314
|
+
}
|
|
11315
|
+
function buildWorkflowSummary(result) {
|
|
11316
|
+
const steps = result.steps.map((s) => {
|
|
11317
|
+
const step = {
|
|
11318
|
+
step: s.index + 1,
|
|
11319
|
+
description: describeStep(s),
|
|
11320
|
+
success: s.success,
|
|
11321
|
+
durationMs: s.durationMs
|
|
11322
|
+
};
|
|
11323
|
+
if (s.outcomeStatus) {
|
|
11324
|
+
step.outcomeStatus = s.outcomeStatus;
|
|
11325
|
+
step.retrySafe = s.retrySafe;
|
|
11326
|
+
}
|
|
11327
|
+
if (s.matchedConditions && s.matchedConditions.length > 0) {
|
|
11328
|
+
step.outcomeEvidence = summarizeConditions(s.matchedConditions);
|
|
11329
|
+
}
|
|
11330
|
+
if (s.error) step.error = s.error;
|
|
11331
|
+
if (s.suggestion) step.suggestion = s.suggestion;
|
|
11332
|
+
return step;
|
|
11333
|
+
});
|
|
11334
|
+
const succeededSteps = steps.filter((s) => s.success).length;
|
|
11335
|
+
const failedSteps = steps.filter((s) => !s.success).length;
|
|
11336
|
+
const hasUnsafeStep = steps.some(
|
|
11337
|
+
(s) => s.retrySafe === false || s.outcomeStatus === "unsafe_to_retry"
|
|
11338
|
+
);
|
|
11339
|
+
const workflowRetrySafe = !hasUnsafeStep;
|
|
11340
|
+
let verdict;
|
|
11341
|
+
if (result.success) {
|
|
11342
|
+
verdict = `Workflow completed successfully (${succeededSteps}/${steps.length} steps)`;
|
|
11343
|
+
} else if (result.stoppedAtIndex !== void 0) {
|
|
11344
|
+
const failedStep = steps[result.stoppedAtIndex];
|
|
11345
|
+
verdict = `Workflow stopped at step ${result.stoppedAtIndex + 1}: ${failedStep?.description ?? "unknown"}`;
|
|
11346
|
+
if (failedStep?.outcomeStatus) {
|
|
11347
|
+
verdict += ` (outcome: ${failedStep.outcomeStatus})`;
|
|
11348
|
+
}
|
|
11349
|
+
} else {
|
|
11350
|
+
verdict = `Workflow completed with ${failedSteps} failure(s)`;
|
|
11351
|
+
}
|
|
11352
|
+
return {
|
|
11353
|
+
success: result.success,
|
|
11354
|
+
totalSteps: steps.length,
|
|
11355
|
+
succeededSteps,
|
|
11356
|
+
failedSteps,
|
|
11357
|
+
totalDurationMs: result.totalDurationMs,
|
|
11358
|
+
steps,
|
|
11359
|
+
verdict,
|
|
11360
|
+
workflowRetrySafe
|
|
11361
|
+
};
|
|
11362
|
+
}
|
|
11363
|
+
function formatWorkflowSummary(summary) {
|
|
11364
|
+
const lines = [];
|
|
11365
|
+
lines.push(`## Workflow ${summary.success ? "Succeeded" : "Failed"}`);
|
|
11366
|
+
lines.push(`${summary.verdict}`);
|
|
11367
|
+
lines.push(
|
|
11368
|
+
`Duration: ${summary.totalDurationMs}ms | Steps: ${summary.succeededSteps}/${summary.totalSteps} passed`
|
|
11369
|
+
);
|
|
11370
|
+
if (!summary.workflowRetrySafe) {
|
|
11371
|
+
lines.push("\u26A0 Contains unsafe-to-retry steps");
|
|
11372
|
+
}
|
|
11373
|
+
lines.push("");
|
|
11374
|
+
for (const step of summary.steps) {
|
|
11375
|
+
const icon = step.success ? "\u2713" : "\u2717";
|
|
11376
|
+
lines.push(`${icon} Step ${step.step}: ${step.description} (${step.durationMs}ms)`);
|
|
11377
|
+
if (step.outcomeStatus) {
|
|
11378
|
+
lines.push(
|
|
11379
|
+
` Outcome: ${step.outcomeStatus}${step.retrySafe === false ? " (unsafe to retry)" : ""}`
|
|
11380
|
+
);
|
|
11381
|
+
}
|
|
11382
|
+
if (step.outcomeEvidence) {
|
|
11383
|
+
for (const evidence of step.outcomeEvidence) {
|
|
11384
|
+
lines.push(` ${evidence}`);
|
|
11385
|
+
}
|
|
11386
|
+
}
|
|
11387
|
+
if (step.error) {
|
|
11388
|
+
lines.push(` Error: ${step.error}`);
|
|
11389
|
+
}
|
|
11390
|
+
if (step.suggestion) {
|
|
11391
|
+
lines.push(` \u2192 ${step.suggestion}`);
|
|
11392
|
+
}
|
|
11393
|
+
}
|
|
11394
|
+
return lines.join("\n");
|
|
11395
|
+
}
|
|
9952
11396
|
// Annotate the CommonJS export names for ESM import in node:
|
|
9953
11397
|
0 && (module.exports = {
|
|
9954
11398
|
AudioInput,
|
|
@@ -9962,22 +11406,42 @@ function disableTracing() {
|
|
|
9962
11406
|
ElementNotFoundError,
|
|
9963
11407
|
GenericProvider,
|
|
9964
11408
|
NavigationError,
|
|
11409
|
+
NetworkResponseTracker,
|
|
9965
11410
|
Page,
|
|
9966
11411
|
RequestInterceptor,
|
|
9967
11412
|
TimeoutError,
|
|
9968
11413
|
Tracer,
|
|
9969
11414
|
addBatchToPage,
|
|
9970
11415
|
bufferToBase64,
|
|
11416
|
+
buildFingerprintMap,
|
|
9971
11417
|
buildLocalBrowserScanTargets,
|
|
11418
|
+
buildWorkflowSummary,
|
|
9972
11419
|
calculateRMS,
|
|
11420
|
+
captureStateSignature,
|
|
11421
|
+
chooseOption,
|
|
11422
|
+
computeDelta,
|
|
11423
|
+
conditionAll,
|
|
11424
|
+
conditionAny,
|
|
11425
|
+
conditionNot,
|
|
11426
|
+
conditionRace,
|
|
9973
11427
|
connect,
|
|
9974
11428
|
createCDPClient,
|
|
11429
|
+
createFingerprint,
|
|
9975
11430
|
createProvider,
|
|
11431
|
+
createTargetFingerprint,
|
|
11432
|
+
detectOverlay,
|
|
9976
11433
|
devices,
|
|
9977
11434
|
disableTracing,
|
|
9978
11435
|
discoverLocalBrowsers,
|
|
9979
11436
|
discoverTargets,
|
|
9980
11437
|
enableTracing,
|
|
11438
|
+
evaluateCondition,
|
|
11439
|
+
evaluateOutcome,
|
|
11440
|
+
extractPageState,
|
|
11441
|
+
extractReview,
|
|
11442
|
+
fingerprintKey,
|
|
11443
|
+
fingerprintSimilarity,
|
|
11444
|
+
formatWorkflowSummary,
|
|
9981
11445
|
generateSilence,
|
|
9982
11446
|
generateTone,
|
|
9983
11447
|
getAudioChromeFlags,
|
|
@@ -9988,9 +11452,13 @@ function disableTracing() {
|
|
|
9988
11452
|
parseDevToolsActivePortFile,
|
|
9989
11453
|
parseWavHeader,
|
|
9990
11454
|
pcmToWav,
|
|
11455
|
+
recoverPinnedTarget,
|
|
11456
|
+
recoverStaleRef,
|
|
9991
11457
|
resolveBrowserEndpoint,
|
|
9992
11458
|
resolveChromeUserDataDirs,
|
|
11459
|
+
submitAndVerify,
|
|
9993
11460
|
transcribe,
|
|
11461
|
+
uploadFiles,
|
|
9994
11462
|
validateSteps,
|
|
9995
11463
|
waitForAnyElement,
|
|
9996
11464
|
waitForElement,
|