browser-pilot 0.0.15 → 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 +38 -3
- package/dist/actions.cjs +848 -105
- package/dist/actions.d.cts +101 -4
- package/dist/actions.d.ts +101 -4
- package/dist/actions.mjs +17 -1
- package/dist/{browser-MEWT75IB.mjs → browser-4ZHNAQR5.mjs} +2 -2
- package/dist/browser.cjs +1684 -130
- 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 +563 -0
- package/dist/{chunk-ZAXQ5OTV.mjs → chunk-FEEGNSHB.mjs} +606 -12
- package/dist/{chunk-WPNW23CE.mjs → chunk-IRLHCVNH.mjs} +345 -7
- package/dist/chunk-MIJ7UIKB.mjs +96 -0
- package/dist/{chunk-USYSHCI3.mjs → chunk-MRY3HRFJ.mjs} +841 -370
- package/dist/chunk-OIHU7OFY.mjs +91 -0
- package/dist/{chunk-7YVCOL2W.mjs → chunk-ZDODXEBD.mjs} +637 -105
- package/dist/cli.mjs +1280 -549
- package/dist/combobox-RAKBA2BW.mjs +6 -0
- package/dist/index.cjs +1976 -144
- package/dist/index.d.cts +57 -6
- package/dist/index.d.ts +57 -6
- package/dist/index.mjs +206 -7
- package/dist/{page-XPS6IC6V.mjs → page-SD64DY3F.mjs} +1 -1
- package/dist/providers.cjs +637 -2
- package/dist/providers.d.cts +2 -2
- package/dist/providers.d.ts +2 -2
- package/dist/providers.mjs +17 -3
- package/dist/{types-Cvvf0oGu.d.ts → types-B_v62K7C.d.ts} +147 -3
- package/dist/types-DeVSWhXj.d.cts +142 -0
- package/dist/types-DeVSWhXj.d.ts +142 -0
- package/dist/{types-C9ySEdOX.d.cts → types-Yuybzq53.d.cts} +147 -3
- package/dist/upload-E6MCC2OF.mjs +6 -0
- package/package.json +10 -3
- package/dist/chunk-BRAFQUMG.mjs +0 -229
- package/dist/types--wXNHUwt.d.cts +0 -56
- package/dist/types--wXNHUwt.d.ts +0 -56
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, {
|
|
@@ -35,25 +238,48 @@ __export(src_exports, {
|
|
|
35
238
|
BatchExecutor: () => BatchExecutor,
|
|
36
239
|
Browser: () => Browser,
|
|
37
240
|
BrowserBaseProvider: () => BrowserBaseProvider,
|
|
241
|
+
BrowserEndpointResolutionError: () => BrowserEndpointResolutionError,
|
|
38
242
|
BrowserlessProvider: () => BrowserlessProvider,
|
|
39
243
|
CDPError: () => CDPError,
|
|
40
244
|
ElementNotFoundError: () => ElementNotFoundError,
|
|
41
245
|
GenericProvider: () => GenericProvider,
|
|
42
246
|
NavigationError: () => NavigationError,
|
|
247
|
+
NetworkResponseTracker: () => NetworkResponseTracker,
|
|
43
248
|
Page: () => Page,
|
|
44
249
|
RequestInterceptor: () => RequestInterceptor,
|
|
45
250
|
TimeoutError: () => TimeoutError,
|
|
46
251
|
Tracer: () => Tracer,
|
|
47
252
|
addBatchToPage: () => addBatchToPage,
|
|
48
253
|
bufferToBase64: () => bufferToBase64,
|
|
254
|
+
buildFingerprintMap: () => buildFingerprintMap,
|
|
255
|
+
buildLocalBrowserScanTargets: () => buildLocalBrowserScanTargets,
|
|
256
|
+
buildWorkflowSummary: () => buildWorkflowSummary,
|
|
49
257
|
calculateRMS: () => calculateRMS,
|
|
258
|
+
captureStateSignature: () => captureStateSignature,
|
|
259
|
+
chooseOption: () => chooseOption,
|
|
260
|
+
computeDelta: () => computeDelta,
|
|
261
|
+
conditionAll: () => conditionAll,
|
|
262
|
+
conditionAny: () => conditionAny,
|
|
263
|
+
conditionNot: () => conditionNot,
|
|
264
|
+
conditionRace: () => conditionRace,
|
|
50
265
|
connect: () => connect,
|
|
51
266
|
createCDPClient: () => createCDPClient,
|
|
267
|
+
createFingerprint: () => createFingerprint,
|
|
52
268
|
createProvider: () => createProvider,
|
|
269
|
+
createTargetFingerprint: () => createTargetFingerprint,
|
|
270
|
+
detectOverlay: () => detectOverlay,
|
|
53
271
|
devices: () => devices,
|
|
54
272
|
disableTracing: () => disableTracing,
|
|
273
|
+
discoverLocalBrowsers: () => discoverLocalBrowsers,
|
|
55
274
|
discoverTargets: () => discoverTargets,
|
|
56
275
|
enableTracing: () => enableTracing,
|
|
276
|
+
evaluateCondition: () => evaluateCondition,
|
|
277
|
+
evaluateOutcome: () => evaluateOutcome,
|
|
278
|
+
extractPageState: () => extractPageState,
|
|
279
|
+
extractReview: () => extractReview,
|
|
280
|
+
fingerprintKey: () => fingerprintKey,
|
|
281
|
+
fingerprintSimilarity: () => fingerprintSimilarity,
|
|
282
|
+
formatWorkflowSummary: () => formatWorkflowSummary,
|
|
57
283
|
generateSilence: () => generateSilence,
|
|
58
284
|
generateTone: () => generateTone,
|
|
59
285
|
getAudioChromeFlags: () => getAudioChromeFlags,
|
|
@@ -61,9 +287,16 @@ __export(src_exports, {
|
|
|
61
287
|
getTracer: () => getTracer,
|
|
62
288
|
grantAudioPermissions: () => grantAudioPermissions,
|
|
63
289
|
isTranscriptionAvailable: () => isTranscriptionAvailable,
|
|
290
|
+
parseDevToolsActivePortFile: () => parseDevToolsActivePortFile,
|
|
64
291
|
parseWavHeader: () => parseWavHeader,
|
|
65
292
|
pcmToWav: () => pcmToWav,
|
|
293
|
+
recoverPinnedTarget: () => recoverPinnedTarget,
|
|
294
|
+
recoverStaleRef: () => recoverStaleRef,
|
|
295
|
+
resolveBrowserEndpoint: () => resolveBrowserEndpoint,
|
|
296
|
+
resolveChromeUserDataDirs: () => resolveChromeUserDataDirs,
|
|
297
|
+
submitAndVerify: () => submitAndVerify,
|
|
66
298
|
transcribe: () => transcribe,
|
|
299
|
+
uploadFiles: () => uploadFiles,
|
|
67
300
|
validateSteps: () => validateSteps,
|
|
68
301
|
waitForAnyElement: () => waitForAnyElement,
|
|
69
302
|
waitForElement: () => waitForElement,
|
|
@@ -72,6 +305,333 @@ __export(src_exports, {
|
|
|
72
305
|
});
|
|
73
306
|
module.exports = __toCommonJS(src_exports);
|
|
74
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
|
+
|
|
75
635
|
// src/actions/executor.ts
|
|
76
636
|
var fs = __toESM(require("fs"), 1);
|
|
77
637
|
var import_node_path = require("path");
|
|
@@ -942,7 +1502,9 @@ function buildTraceSummaries(events) {
|
|
|
942
1502
|
};
|
|
943
1503
|
}
|
|
944
1504
|
function summarizeWs(events) {
|
|
945
|
-
const relevant = events.filter(
|
|
1505
|
+
const relevant = events.filter(
|
|
1506
|
+
(event) => event.channel === "ws" || event.event.startsWith("ws.")
|
|
1507
|
+
);
|
|
946
1508
|
const connections = /* @__PURE__ */ new Map();
|
|
947
1509
|
for (const event of relevant) {
|
|
948
1510
|
const id = event.connectionId ?? event.requestId ?? event.traceId;
|
|
@@ -972,7 +1534,7 @@ function summarizeWs(events) {
|
|
|
972
1534
|
}
|
|
973
1535
|
const values = [...connections.values()];
|
|
974
1536
|
const reconnects = values.reduce((count, connection) => {
|
|
975
|
-
return connection.closedAt && !connection.createdAt ? count : count;
|
|
1537
|
+
return connection.closedAt && !connection.createdAt ? count + 1 : count;
|
|
976
1538
|
}, 0);
|
|
977
1539
|
return {
|
|
978
1540
|
view: "ws",
|
|
@@ -1225,6 +1787,31 @@ function frameToStep(frame) {
|
|
|
1225
1787
|
}
|
|
1226
1788
|
}
|
|
1227
1789
|
|
|
1790
|
+
// src/trace/model.ts
|
|
1791
|
+
function createTraceId(prefix = "evt") {
|
|
1792
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1793
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1794
|
+
}
|
|
1795
|
+
function normalizeTraceEvent(event) {
|
|
1796
|
+
return {
|
|
1797
|
+
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1798
|
+
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1799
|
+
elapsedMs: event.elapsedMs ?? 0,
|
|
1800
|
+
severity: event.severity ?? inferSeverity(event.event),
|
|
1801
|
+
data: event.data ?? {},
|
|
1802
|
+
...event
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
function inferSeverity(eventName) {
|
|
1806
|
+
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1807
|
+
return "error";
|
|
1808
|
+
}
|
|
1809
|
+
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1810
|
+
return "warn";
|
|
1811
|
+
}
|
|
1812
|
+
return "info";
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1228
1815
|
// src/trace/script.ts
|
|
1229
1816
|
var TRACE_BINDING_NAME = "__bpTraceBinding";
|
|
1230
1817
|
var TRACE_SCRIPT = `
|
|
@@ -1504,38 +2091,6 @@ var TRACE_SCRIPT = `
|
|
|
1504
2091
|
})();
|
|
1505
2092
|
`;
|
|
1506
2093
|
|
|
1507
|
-
// src/trace/model.ts
|
|
1508
|
-
function createTraceId(prefix = "evt") {
|
|
1509
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
1510
|
-
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1511
|
-
}
|
|
1512
|
-
function normalizeTraceEvent(event) {
|
|
1513
|
-
return {
|
|
1514
|
-
traceId: event.traceId ?? createTraceId(event.channel),
|
|
1515
|
-
ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1516
|
-
elapsedMs: event.elapsedMs ?? 0,
|
|
1517
|
-
severity: event.severity ?? inferSeverity(event.event),
|
|
1518
|
-
data: event.data ?? {},
|
|
1519
|
-
...event
|
|
1520
|
-
};
|
|
1521
|
-
}
|
|
1522
|
-
function inferSeverity(eventName) {
|
|
1523
|
-
if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
|
|
1524
|
-
return "error";
|
|
1525
|
-
}
|
|
1526
|
-
if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
|
|
1527
|
-
return "warn";
|
|
1528
|
-
}
|
|
1529
|
-
return "info";
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// src/trace/live.ts
|
|
1533
|
-
function globToRegex(pattern) {
|
|
1534
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1535
|
-
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
1536
|
-
return new RegExp(`^${withWildcards}$`);
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
2094
|
// src/actions/executor.ts
|
|
1540
2095
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1541
2096
|
var DEFAULT_RECORDING_SKIP_ACTIONS = [
|
|
@@ -1658,6 +2213,25 @@ function getSuggestion(reason) {
|
|
|
1658
2213
|
}
|
|
1659
2214
|
}
|
|
1660
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
|
+
}
|
|
1661
2235
|
var BatchExecutor = class {
|
|
1662
2236
|
page;
|
|
1663
2237
|
constructor(page) {
|
|
@@ -1703,9 +2277,25 @@ var BatchExecutor = class {
|
|
|
1703
2277
|
})
|
|
1704
2278
|
);
|
|
1705
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
|
+
}
|
|
1706
2292
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1707
2293
|
if (attempt > 0) {
|
|
1708
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
|
+
}
|
|
1709
2299
|
}
|
|
1710
2300
|
try {
|
|
1711
2301
|
this.page.resetLastActionPosition();
|
|
@@ -1723,6 +2313,28 @@ var BatchExecutor = class {
|
|
|
1723
2313
|
coordinates: this.page.getLastActionCoordinates() ?? void 0,
|
|
1724
2314
|
boundingBox: this.page.getLastActionBoundingBox() ?? void 0
|
|
1725
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
|
+
}
|
|
1726
2338
|
if (recording && !recording.skipActions.has(step.action)) {
|
|
1727
2339
|
await this.captureRecordingFrame(step, stepResult, recording);
|
|
1728
2340
|
}
|
|
@@ -1732,13 +2344,14 @@ var BatchExecutor = class {
|
|
|
1732
2344
|
traceId: createTraceId("action"),
|
|
1733
2345
|
elapsedMs: Date.now() - startTime,
|
|
1734
2346
|
channel: "action",
|
|
1735
|
-
event: "action.succeeded",
|
|
1736
|
-
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}`,
|
|
1737
2349
|
data: {
|
|
1738
2350
|
action: step.action,
|
|
1739
2351
|
selector: step.selector ?? null,
|
|
1740
2352
|
selectorUsed: result.selectorUsed ?? null,
|
|
1741
|
-
durationMs: Date.now() - stepStart
|
|
2353
|
+
durationMs: Date.now() - stepStart,
|
|
2354
|
+
outcomeStatus: stepResult.outcomeStatus ?? null
|
|
1742
2355
|
},
|
|
1743
2356
|
actionId: `action-${i + 1}`,
|
|
1744
2357
|
stepIndex: i,
|
|
@@ -1748,6 +2361,18 @@ var BatchExecutor = class {
|
|
|
1748
2361
|
})
|
|
1749
2362
|
);
|
|
1750
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
|
+
}
|
|
1751
2376
|
results.push(stepResult);
|
|
1752
2377
|
succeeded = true;
|
|
1753
2378
|
break;
|
|
@@ -1755,59 +2380,63 @@ var BatchExecutor = class {
|
|
|
1755
2380
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1756
2381
|
}
|
|
1757
2382
|
}
|
|
2383
|
+
if (networkTracker) networkTracker.stop(this.page.cdpClient);
|
|
1758
2384
|
if (!succeeded) {
|
|
1759
|
-
const
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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 {
|
|
1768
2398
|
}
|
|
1769
|
-
} catch {
|
|
1770
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);
|
|
1771
2439
|
}
|
|
1772
|
-
const failedResult = {
|
|
1773
|
-
index: i,
|
|
1774
|
-
action: step.action,
|
|
1775
|
-
selector: step.selector,
|
|
1776
|
-
success: false,
|
|
1777
|
-
durationMs: Date.now() - stepStart,
|
|
1778
|
-
error: errorMessage,
|
|
1779
|
-
hints,
|
|
1780
|
-
failureReason: reason,
|
|
1781
|
-
coveringElement,
|
|
1782
|
-
suggestion: getSuggestion(reason),
|
|
1783
|
-
timestamp: Date.now()
|
|
1784
|
-
};
|
|
1785
|
-
if (recording && !recording.skipActions.has(step.action)) {
|
|
1786
|
-
await this.captureRecordingFrame(step, failedResult, recording);
|
|
1787
|
-
}
|
|
1788
|
-
if (recording) {
|
|
1789
|
-
recording.traceEvents.push(
|
|
1790
|
-
normalizeTraceEvent({
|
|
1791
|
-
traceId: createTraceId("action"),
|
|
1792
|
-
elapsedMs: Date.now() - startTime,
|
|
1793
|
-
channel: "action",
|
|
1794
|
-
event: "action.failed",
|
|
1795
|
-
severity: "error",
|
|
1796
|
-
summary: `${step.action} failed: ${errorMessage}`,
|
|
1797
|
-
data: {
|
|
1798
|
-
action: step.action,
|
|
1799
|
-
selector: step.selector ?? null,
|
|
1800
|
-
error: errorMessage,
|
|
1801
|
-
reason
|
|
1802
|
-
},
|
|
1803
|
-
actionId: `action-${i + 1}`,
|
|
1804
|
-
stepIndex: i,
|
|
1805
|
-
selector: step.selector,
|
|
1806
|
-
url: step.url
|
|
1807
|
-
})
|
|
1808
|
-
);
|
|
1809
|
-
}
|
|
1810
|
-
results.push(failedResult);
|
|
1811
2440
|
if (onFail === "stop" && !step.optional) {
|
|
1812
2441
|
stoppedAtIndex = i;
|
|
1813
2442
|
break;
|
|
@@ -2118,6 +2747,14 @@ var BatchExecutor = class {
|
|
|
2118
2747
|
case "forms": {
|
|
2119
2748
|
return { value: await this.page.forms() };
|
|
2120
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
|
+
}
|
|
2121
2758
|
case "screenshot": {
|
|
2122
2759
|
const data = await this.page.screenshot({
|
|
2123
2760
|
format: step.format,
|
|
@@ -2268,6 +2905,35 @@ var BatchExecutor = class {
|
|
|
2268
2905
|
const media = await this.assertMediaTrackLive(step.kind);
|
|
2269
2906
|
return { value: media };
|
|
2270
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
|
+
}
|
|
2271
2937
|
default: {
|
|
2272
2938
|
const action = step.action;
|
|
2273
2939
|
const aliases = {
|
|
@@ -2345,8 +3011,13 @@ Valid actions: ${valid}`);
|
|
|
2345
3011
|
await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
|
|
2346
3012
|
} catch {
|
|
2347
3013
|
}
|
|
2348
|
-
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
2349
|
-
|
|
3014
|
+
await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
3015
|
+
source: TRACE_SCRIPT
|
|
3016
|
+
});
|
|
3017
|
+
await this.page.cdpClient.send("Runtime.evaluate", {
|
|
3018
|
+
expression: TRACE_SCRIPT,
|
|
3019
|
+
awaitPromise: false
|
|
3020
|
+
});
|
|
2350
3021
|
}
|
|
2351
3022
|
async waitForWsMessage(match, where, timeout) {
|
|
2352
3023
|
await this.ensureTraceHooks();
|
|
@@ -2364,12 +3035,12 @@ Valid actions: ${valid}`);
|
|
|
2364
3035
|
clearTimeout(timer);
|
|
2365
3036
|
};
|
|
2366
3037
|
const onCreated = (params) => {
|
|
2367
|
-
wsUrls.set(
|
|
3038
|
+
wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
|
|
2368
3039
|
};
|
|
2369
3040
|
const onFrame = (params) => {
|
|
2370
|
-
const requestId =
|
|
3041
|
+
const requestId = readStringOr(params["requestId"]);
|
|
2371
3042
|
const response = params["response"] ?? {};
|
|
2372
|
-
const payload =
|
|
3043
|
+
const payload = response.payloadData ?? "";
|
|
2373
3044
|
const url = wsUrls.get(requestId) ?? "";
|
|
2374
3045
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2375
3046
|
return;
|
|
@@ -2385,13 +3056,13 @@ Valid actions: ${valid}`);
|
|
|
2385
3056
|
return;
|
|
2386
3057
|
}
|
|
2387
3058
|
try {
|
|
2388
|
-
const parsed = JSON.parse(
|
|
3059
|
+
const parsed = JSON.parse(readStringOr(params["payload"]));
|
|
2389
3060
|
if (parsed.event !== "ws.frame.received") {
|
|
2390
3061
|
return;
|
|
2391
3062
|
}
|
|
2392
3063
|
const data = parsed.data ?? {};
|
|
2393
|
-
const payload =
|
|
2394
|
-
const url =
|
|
3064
|
+
const payload = readStringOr(data["payload"]);
|
|
3065
|
+
const url = readStringOr(data["url"]);
|
|
2395
3066
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2396
3067
|
return;
|
|
2397
3068
|
}
|
|
@@ -2400,7 +3071,7 @@ Valid actions: ${valid}`);
|
|
|
2400
3071
|
}
|
|
2401
3072
|
cleanup();
|
|
2402
3073
|
resolve({
|
|
2403
|
-
requestId:
|
|
3074
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2404
3075
|
url,
|
|
2405
3076
|
payload
|
|
2406
3077
|
});
|
|
@@ -2444,13 +3115,14 @@ Valid actions: ${valid}`);
|
|
|
2444
3115
|
if (!entry || typeof entry !== "object") {
|
|
2445
3116
|
continue;
|
|
2446
3117
|
}
|
|
2447
|
-
const
|
|
3118
|
+
const record = entry;
|
|
3119
|
+
const event = readStringOr(record["event"]);
|
|
2448
3120
|
if (event !== "ws.frame.received") {
|
|
2449
3121
|
continue;
|
|
2450
3122
|
}
|
|
2451
|
-
const data =
|
|
2452
|
-
const payload =
|
|
2453
|
-
const url =
|
|
3123
|
+
const data = record["data"] ?? {};
|
|
3124
|
+
const payload = readStringOr(data["payload"]);
|
|
3125
|
+
const url = readStringOr(data["url"]);
|
|
2454
3126
|
if (!regex.test(url) && !regex.test(payload)) {
|
|
2455
3127
|
continue;
|
|
2456
3128
|
}
|
|
@@ -2458,7 +3130,7 @@ Valid actions: ${valid}`);
|
|
|
2458
3130
|
continue;
|
|
2459
3131
|
}
|
|
2460
3132
|
return {
|
|
2461
|
-
requestId:
|
|
3133
|
+
requestId: readStringOr(data["connectionId"]),
|
|
2462
3134
|
url,
|
|
2463
3135
|
payload
|
|
2464
3136
|
};
|
|
@@ -2479,13 +3151,11 @@ Valid actions: ${valid}`);
|
|
|
2479
3151
|
return;
|
|
2480
3152
|
}
|
|
2481
3153
|
const args = Array.isArray(params["args"]) ? params["args"] : [];
|
|
2482
|
-
errors.push(
|
|
2483
|
-
args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
|
|
2484
|
-
);
|
|
3154
|
+
errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
|
|
2485
3155
|
};
|
|
2486
3156
|
const onException = (params) => {
|
|
2487
3157
|
const details = params["exceptionDetails"] ?? {};
|
|
2488
|
-
errors.push(
|
|
3158
|
+
errors.push(readString(details["text"]) ?? "Runtime exception");
|
|
2489
3159
|
};
|
|
2490
3160
|
const timer = setTimeout(() => {
|
|
2491
3161
|
cleanup();
|
|
@@ -2857,6 +3527,30 @@ var ACTION_RULES = {
|
|
|
2857
3527
|
kind: { type: "string", enum: ["audio", "video"] }
|
|
2858
3528
|
},
|
|
2859
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: {}
|
|
2860
3554
|
}
|
|
2861
3555
|
};
|
|
2862
3556
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -2896,7 +3590,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
2896
3590
|
"name",
|
|
2897
3591
|
"state",
|
|
2898
3592
|
"kind",
|
|
2899
|
-
"windowMs"
|
|
3593
|
+
"windowMs",
|
|
3594
|
+
"expectAny",
|
|
3595
|
+
"expectAll",
|
|
3596
|
+
"failIf",
|
|
3597
|
+
"dangerous",
|
|
3598
|
+
"files"
|
|
2900
3599
|
]);
|
|
2901
3600
|
function resolveAction(name) {
|
|
2902
3601
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -3116,6 +3815,64 @@ function validateSteps(steps) {
|
|
|
3116
3815
|
});
|
|
3117
3816
|
}
|
|
3118
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
|
+
}
|
|
3119
3876
|
if (action === "assertText") {
|
|
3120
3877
|
if (!("expect" in obj) && !("value" in obj)) {
|
|
3121
3878
|
errors.push({
|
|
@@ -3258,12 +4015,12 @@ function parseWavHeader(data) {
|
|
|
3258
4015
|
if (data.byteLength < 44) {
|
|
3259
4016
|
throw new Error("Invalid WAV: file too small");
|
|
3260
4017
|
}
|
|
3261
|
-
const riff =
|
|
3262
|
-
const wave =
|
|
4018
|
+
const riff = readString2(view, 0, 4);
|
|
4019
|
+
const wave = readString2(view, 8, 4);
|
|
3263
4020
|
if (riff !== "RIFF" || wave !== "WAVE") {
|
|
3264
4021
|
throw new Error("Invalid WAV: missing RIFF/WAVE header");
|
|
3265
4022
|
}
|
|
3266
|
-
const fmt =
|
|
4023
|
+
const fmt = readString2(view, 12, 4);
|
|
3267
4024
|
if (fmt !== "fmt ") {
|
|
3268
4025
|
throw new Error("Invalid WAV: missing fmt chunk");
|
|
3269
4026
|
}
|
|
@@ -3272,7 +4029,7 @@ function parseWavHeader(data) {
|
|
|
3272
4029
|
const bitsPerSample = view.getUint16(34, true);
|
|
3273
4030
|
let dataOffset = 36;
|
|
3274
4031
|
while (dataOffset < data.byteLength - 8) {
|
|
3275
|
-
const chunkId =
|
|
4032
|
+
const chunkId = readString2(view, dataOffset, 4);
|
|
3276
4033
|
const chunkSize = view.getUint32(dataOffset + 4, true);
|
|
3277
4034
|
if (chunkId === "data") {
|
|
3278
4035
|
return {
|
|
@@ -3303,7 +4060,7 @@ function writeString(view, offset, str) {
|
|
|
3303
4060
|
view.setUint8(offset + i, str.charCodeAt(i));
|
|
3304
4061
|
}
|
|
3305
4062
|
}
|
|
3306
|
-
function
|
|
4063
|
+
function readString2(view, offset, length) {
|
|
3307
4064
|
let str = "";
|
|
3308
4065
|
for (let i = 0; i < length; i++) {
|
|
3309
4066
|
str += String.fromCharCode(view.getUint8(offset + i));
|
|
@@ -5056,40 +5813,364 @@ async function fetchDevToolsJson(host, path, errorPrefix, options = {}) {
|
|
|
5056
5813
|
`${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`
|
|
5057
5814
|
);
|
|
5058
5815
|
}
|
|
5059
|
-
if (attempt < attempts) {
|
|
5060
|
-
await sleep3(delayMs);
|
|
5061
|
-
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
5816
|
+
if (attempt < attempts) {
|
|
5817
|
+
await sleep3(delayMs);
|
|
5818
|
+
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
throw lastError ?? new Error(errorPrefix);
|
|
5822
|
+
}
|
|
5823
|
+
var GenericProvider = class {
|
|
5824
|
+
name = "generic";
|
|
5825
|
+
wsUrl;
|
|
5826
|
+
constructor(options) {
|
|
5827
|
+
this.wsUrl = options.wsUrl;
|
|
5828
|
+
}
|
|
5829
|
+
async createSession(_options = {}) {
|
|
5830
|
+
return {
|
|
5831
|
+
wsUrl: this.wsUrl,
|
|
5832
|
+
metadata: {
|
|
5833
|
+
provider: "generic"
|
|
5834
|
+
},
|
|
5835
|
+
close: async () => {
|
|
5836
|
+
}
|
|
5837
|
+
};
|
|
5838
|
+
}
|
|
5839
|
+
};
|
|
5840
|
+
async function discoverTargets(host = "localhost:9222") {
|
|
5841
|
+
return fetchDevToolsJson(host, "/json/list", "Failed to discover targets");
|
|
5842
|
+
}
|
|
5843
|
+
async function getBrowserWebSocketUrl(host = "localhost:9222") {
|
|
5844
|
+
const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
|
|
5845
|
+
attempts: 10,
|
|
5846
|
+
initialDelayMs: 50,
|
|
5847
|
+
maxDelayMs: 250
|
|
5848
|
+
});
|
|
5849
|
+
return info.webSocketDebuggerUrl;
|
|
5850
|
+
}
|
|
5851
|
+
|
|
5852
|
+
// src/providers/local-discovery.ts
|
|
5853
|
+
var CHANNEL_ORDER = ["stable", "beta", "dev", "canary"];
|
|
5854
|
+
var DEFAULT_PROBE_TIMEOUT_MS = 1e3;
|
|
5855
|
+
var DevToolsActivePortParseError = class extends Error {
|
|
5856
|
+
constructor(message, reason) {
|
|
5857
|
+
super(message);
|
|
5858
|
+
this.reason = reason;
|
|
5859
|
+
this.name = "DevToolsActivePortParseError";
|
|
5860
|
+
}
|
|
5861
|
+
};
|
|
5862
|
+
function getRuntimeEnv() {
|
|
5863
|
+
if (typeof process === "undefined") {
|
|
5864
|
+
return {};
|
|
5865
|
+
}
|
|
5866
|
+
return process.env;
|
|
5867
|
+
}
|
|
5868
|
+
function getRuntimePlatform() {
|
|
5869
|
+
if (typeof process === "undefined") {
|
|
5870
|
+
return void 0;
|
|
5871
|
+
}
|
|
5872
|
+
return process.platform;
|
|
5873
|
+
}
|
|
5874
|
+
function normalizePlatform(platform) {
|
|
5875
|
+
if (platform === "darwin" || platform === "linux" || platform === "win32") {
|
|
5876
|
+
return platform;
|
|
5877
|
+
}
|
|
5878
|
+
throw new Error(`Unsupported platform: ${platform ?? "unknown"}`);
|
|
5879
|
+
}
|
|
5880
|
+
function trimTrailingSeparator(path) {
|
|
5881
|
+
return path.replace(/[\\/]+$/, "");
|
|
5882
|
+
}
|
|
5883
|
+
function joinPath(platform, ...parts) {
|
|
5884
|
+
const separator = platform === "win32" ? "\\" : "/";
|
|
5885
|
+
const cleaned = parts.map((part, index) => {
|
|
5886
|
+
if (index === 0) return trimTrailingSeparator(part);
|
|
5887
|
+
return part.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
5888
|
+
}).filter((part) => part.length > 0);
|
|
5889
|
+
return cleaned.join(separator);
|
|
5890
|
+
}
|
|
5891
|
+
function resolveHomeDir(platform, env, explicitHomeDir) {
|
|
5892
|
+
if (explicitHomeDir) {
|
|
5893
|
+
return explicitHomeDir;
|
|
5894
|
+
}
|
|
5895
|
+
if (platform === "win32") {
|
|
5896
|
+
return env["USERPROFILE"] ?? env["HOME"] ?? "";
|
|
5897
|
+
}
|
|
5898
|
+
return env["HOME"] ?? env["USERPROFILE"] ?? "";
|
|
5899
|
+
}
|
|
5900
|
+
function toFileFailure(target, error) {
|
|
5901
|
+
const errno = error?.code;
|
|
5902
|
+
if (errno === "ENOENT") {
|
|
5903
|
+
return {
|
|
5904
|
+
...target,
|
|
5905
|
+
reason: "missing-file",
|
|
5906
|
+
message: `DevToolsActivePort not found at ${target.portFile}`
|
|
5907
|
+
};
|
|
5908
|
+
}
|
|
5909
|
+
return {
|
|
5910
|
+
...target,
|
|
5911
|
+
reason: "unreadable-file",
|
|
5912
|
+
message: error instanceof Error ? error.message : `Could not read DevToolsActivePort at ${target.portFile}`
|
|
5913
|
+
};
|
|
5914
|
+
}
|
|
5915
|
+
function toProbeFailure(target, wsUrl, error) {
|
|
5916
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5917
|
+
const lowerMessage = message.toLowerCase();
|
|
5918
|
+
let reason = "connection-error";
|
|
5919
|
+
if (lowerMessage.includes("refused") || lowerMessage.includes("econnrefused")) {
|
|
5920
|
+
reason = "connection-refused";
|
|
5921
|
+
} else if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
|
|
5922
|
+
reason = "connection-timeout";
|
|
5923
|
+
} else if (lowerMessage.includes("closed")) {
|
|
5924
|
+
reason = "unexpected-close";
|
|
5925
|
+
} else if (lowerMessage.includes("browser.getversion") || lowerMessage.includes("cdp") || lowerMessage.includes("protocol")) {
|
|
5926
|
+
reason = "cdp-error";
|
|
5927
|
+
}
|
|
5928
|
+
return {
|
|
5929
|
+
...target,
|
|
5930
|
+
wsUrl,
|
|
5931
|
+
reason,
|
|
5932
|
+
message
|
|
5933
|
+
};
|
|
5934
|
+
}
|
|
5935
|
+
async function readTextFile(path) {
|
|
5936
|
+
const fs2 = await import("fs/promises");
|
|
5937
|
+
return fs2.readFile(path, "utf-8");
|
|
5938
|
+
}
|
|
5939
|
+
async function probeBrowserWebSocket(wsUrl, timeoutMs) {
|
|
5940
|
+
let client;
|
|
5941
|
+
try {
|
|
5942
|
+
client = await createCDPClient(wsUrl, { timeout: timeoutMs });
|
|
5943
|
+
const version = await client.send("Browser.getVersion", void 0, null);
|
|
5944
|
+
return { browserVersion: version.product };
|
|
5945
|
+
} finally {
|
|
5946
|
+
await client?.close().catch(() => {
|
|
5947
|
+
});
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
var defaultDependencies = {
|
|
5951
|
+
readTextFile,
|
|
5952
|
+
probeBrowserWebSocket,
|
|
5953
|
+
getLegacyBrowserWebSocketUrl: getBrowserWebSocketUrl
|
|
5954
|
+
};
|
|
5955
|
+
function resolveChromeUserDataDirs(options = {}) {
|
|
5956
|
+
const env = options.env ?? getRuntimeEnv();
|
|
5957
|
+
const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
|
|
5958
|
+
const homeDir = resolveHomeDir(platform, env, options.homeDir);
|
|
5959
|
+
if (!homeDir) {
|
|
5960
|
+
throw new Error("Could not determine home directory for local Chrome discovery");
|
|
5961
|
+
}
|
|
5962
|
+
switch (platform) {
|
|
5963
|
+
case "darwin": {
|
|
5964
|
+
const base = joinPath(platform, homeDir, "Library", "Application Support", "Google");
|
|
5965
|
+
return {
|
|
5966
|
+
stable: joinPath(platform, base, "Chrome"),
|
|
5967
|
+
beta: joinPath(platform, base, "Chrome Beta"),
|
|
5968
|
+
dev: joinPath(platform, base, "Chrome Dev"),
|
|
5969
|
+
canary: joinPath(platform, base, "Chrome Canary")
|
|
5970
|
+
};
|
|
5971
|
+
}
|
|
5972
|
+
case "linux": {
|
|
5973
|
+
const configHome = env["CHROME_CONFIG_HOME"] ?? env["XDG_CONFIG_HOME"] ?? joinPath(platform, homeDir, ".config");
|
|
5974
|
+
return {
|
|
5975
|
+
stable: joinPath(platform, configHome, "google-chrome"),
|
|
5976
|
+
beta: joinPath(platform, configHome, "google-chrome-beta"),
|
|
5977
|
+
dev: joinPath(platform, configHome, "google-chrome-dev"),
|
|
5978
|
+
canary: joinPath(platform, configHome, "google-chrome-canary")
|
|
5979
|
+
};
|
|
5980
|
+
}
|
|
5981
|
+
case "win32": {
|
|
5982
|
+
const localAppData = env["LOCALAPPDATA"] ?? joinPath(platform, homeDir, "AppData", "Local");
|
|
5983
|
+
const base = joinPath(platform, localAppData, "Google");
|
|
5984
|
+
return {
|
|
5985
|
+
stable: joinPath(platform, base, "Chrome", "User Data"),
|
|
5986
|
+
beta: joinPath(platform, base, "Chrome Beta", "User Data"),
|
|
5987
|
+
dev: joinPath(platform, base, "Chrome Dev", "User Data"),
|
|
5988
|
+
canary: joinPath(platform, base, "Chrome SxS", "User Data")
|
|
5989
|
+
};
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
throw new Error(`Unsupported platform for local Chrome discovery: ${platform}`);
|
|
5993
|
+
}
|
|
5994
|
+
function buildLocalBrowserScanTargets(options = {}) {
|
|
5995
|
+
const env = options.env ?? getRuntimeEnv();
|
|
5996
|
+
const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
|
|
5997
|
+
if (options.userDataDir) {
|
|
5998
|
+
return [
|
|
5999
|
+
{
|
|
6000
|
+
channel: options.channel ?? "custom",
|
|
6001
|
+
userDataDir: options.userDataDir,
|
|
6002
|
+
portFile: joinPath(platform, options.userDataDir, "DevToolsActivePort")
|
|
6003
|
+
}
|
|
6004
|
+
];
|
|
6005
|
+
}
|
|
6006
|
+
const dirs = resolveChromeUserDataDirs({
|
|
6007
|
+
platform,
|
|
6008
|
+
env,
|
|
6009
|
+
homeDir: options.homeDir
|
|
6010
|
+
});
|
|
6011
|
+
const channels = options.channel ? [options.channel] : CHANNEL_ORDER;
|
|
6012
|
+
return channels.map((channel) => ({
|
|
6013
|
+
channel,
|
|
6014
|
+
userDataDir: dirs[channel],
|
|
6015
|
+
portFile: joinPath(platform, dirs[channel], "DevToolsActivePort")
|
|
6016
|
+
}));
|
|
6017
|
+
}
|
|
6018
|
+
function parseDevToolsActivePortFile(content) {
|
|
6019
|
+
const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
6020
|
+
if (lines.length !== 2) {
|
|
6021
|
+
throw new DevToolsActivePortParseError(
|
|
6022
|
+
`Expected exactly 2 non-empty lines in DevToolsActivePort, got ${lines.length}`,
|
|
6023
|
+
"malformed-file"
|
|
6024
|
+
);
|
|
6025
|
+
}
|
|
6026
|
+
const portText = lines[0];
|
|
6027
|
+
const browserPath = lines[1];
|
|
6028
|
+
const port = Number.parseInt(portText, 10);
|
|
6029
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
6030
|
+
throw new DevToolsActivePortParseError(
|
|
6031
|
+
`Invalid DevToolsActivePort port: ${portText}`,
|
|
6032
|
+
"invalid-port"
|
|
6033
|
+
);
|
|
6034
|
+
}
|
|
6035
|
+
if (!browserPath.startsWith("/devtools/browser/") || browserPath.includes("..") || /[?#\s\\]/u.test(browserPath)) {
|
|
6036
|
+
throw new DevToolsActivePortParseError(
|
|
6037
|
+
`Invalid DevToolsActivePort browser path: ${browserPath}`,
|
|
6038
|
+
"invalid-path"
|
|
6039
|
+
);
|
|
6040
|
+
}
|
|
6041
|
+
return {
|
|
6042
|
+
port,
|
|
6043
|
+
browserPath,
|
|
6044
|
+
wsUrl: `ws://127.0.0.1:${port}${browserPath}`
|
|
6045
|
+
};
|
|
6046
|
+
}
|
|
6047
|
+
async function inspectScanTarget(target, options, deps) {
|
|
6048
|
+
let content;
|
|
6049
|
+
try {
|
|
6050
|
+
content = await deps.readTextFile(target.portFile);
|
|
6051
|
+
} catch (error) {
|
|
6052
|
+
return { kind: "failure", failure: toFileFailure(target, error) };
|
|
6053
|
+
}
|
|
6054
|
+
let parsed;
|
|
6055
|
+
try {
|
|
6056
|
+
parsed = parseDevToolsActivePortFile(content);
|
|
6057
|
+
} catch (error) {
|
|
6058
|
+
if (error instanceof DevToolsActivePortParseError) {
|
|
6059
|
+
return {
|
|
6060
|
+
kind: "failure",
|
|
6061
|
+
failure: {
|
|
6062
|
+
...target,
|
|
6063
|
+
reason: error.reason,
|
|
6064
|
+
message: error.message
|
|
6065
|
+
}
|
|
6066
|
+
};
|
|
6067
|
+
}
|
|
6068
|
+
throw error;
|
|
6069
|
+
}
|
|
6070
|
+
try {
|
|
6071
|
+
const probe = await deps.probeBrowserWebSocket(
|
|
6072
|
+
parsed.wsUrl,
|
|
6073
|
+
options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
|
|
6074
|
+
);
|
|
6075
|
+
return {
|
|
6076
|
+
kind: "candidate",
|
|
6077
|
+
candidate: {
|
|
6078
|
+
...target,
|
|
6079
|
+
port: parsed.port,
|
|
6080
|
+
browserPath: parsed.browserPath,
|
|
6081
|
+
wsUrl: parsed.wsUrl,
|
|
6082
|
+
browserVersion: probe.browserVersion
|
|
6083
|
+
}
|
|
6084
|
+
};
|
|
6085
|
+
} catch (error) {
|
|
6086
|
+
return {
|
|
6087
|
+
kind: "failure",
|
|
6088
|
+
failure: toProbeFailure(target, parsed.wsUrl, error)
|
|
6089
|
+
};
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
async function discoverLocalBrowsers(options = {}, deps = defaultDependencies) {
|
|
6093
|
+
const scanTargets = buildLocalBrowserScanTargets(options);
|
|
6094
|
+
const outcomes = await Promise.all(
|
|
6095
|
+
scanTargets.map((target) => inspectScanTarget(target, options, deps))
|
|
6096
|
+
);
|
|
6097
|
+
const candidates = [];
|
|
6098
|
+
const failures = [];
|
|
6099
|
+
for (const outcome of outcomes) {
|
|
6100
|
+
if (outcome.kind === "candidate") {
|
|
6101
|
+
candidates.push(outcome.candidate);
|
|
6102
|
+
} else {
|
|
6103
|
+
failures.push(outcome.failure);
|
|
5062
6104
|
}
|
|
5063
6105
|
}
|
|
5064
|
-
|
|
6106
|
+
return { candidates, failures };
|
|
5065
6107
|
}
|
|
5066
|
-
var
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
this.
|
|
6108
|
+
var BrowserEndpointResolutionError = class extends Error {
|
|
6109
|
+
constructor(code, message, details = {}) {
|
|
6110
|
+
super(message);
|
|
6111
|
+
this.code = code;
|
|
6112
|
+
this.details = details;
|
|
5071
6113
|
}
|
|
5072
|
-
|
|
6114
|
+
name = "BrowserEndpointResolutionError";
|
|
6115
|
+
};
|
|
6116
|
+
async function resolveBrowserEndpoint(options = {}, deps = defaultDependencies) {
|
|
6117
|
+
if (options.explicitWsUrl) {
|
|
5073
6118
|
return {
|
|
5074
|
-
wsUrl:
|
|
5075
|
-
|
|
5076
|
-
provider: "generic"
|
|
5077
|
-
},
|
|
5078
|
-
close: async () => {
|
|
5079
|
-
}
|
|
6119
|
+
wsUrl: options.explicitWsUrl,
|
|
6120
|
+
source: "explicit-ws"
|
|
5080
6121
|
};
|
|
5081
6122
|
}
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
6123
|
+
let localDiscovery;
|
|
6124
|
+
if (options.allowLocalDiscovery ?? true) {
|
|
6125
|
+
localDiscovery = await discoverLocalBrowsers(options, deps);
|
|
6126
|
+
if (localDiscovery.candidates.length === 1) {
|
|
6127
|
+
const candidate = localDiscovery.candidates[0];
|
|
6128
|
+
return {
|
|
6129
|
+
wsUrl: candidate.wsUrl,
|
|
6130
|
+
source: "devtools-active-port",
|
|
6131
|
+
channel: candidate.channel,
|
|
6132
|
+
userDataDir: candidate.userDataDir
|
|
6133
|
+
};
|
|
6134
|
+
}
|
|
6135
|
+
if (localDiscovery.candidates.length > 1) {
|
|
6136
|
+
throw new BrowserEndpointResolutionError(
|
|
6137
|
+
"multiple-local-browsers",
|
|
6138
|
+
"Multiple local Chrome profiles are available for auto-discovery",
|
|
6139
|
+
{
|
|
6140
|
+
candidates: localDiscovery.candidates,
|
|
6141
|
+
failures: localDiscovery.failures
|
|
6142
|
+
}
|
|
6143
|
+
);
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
if (options.allowLegacyHostFallback ?? true) {
|
|
6147
|
+
const legacyHost = options.legacyHost ?? "localhost:9222";
|
|
6148
|
+
try {
|
|
6149
|
+
return {
|
|
6150
|
+
wsUrl: await deps.getLegacyBrowserWebSocketUrl(legacyHost),
|
|
6151
|
+
source: "json-version"
|
|
6152
|
+
};
|
|
6153
|
+
} catch (error) {
|
|
6154
|
+
throw new BrowserEndpointResolutionError(
|
|
6155
|
+
"browser-not-found",
|
|
6156
|
+
"Could not resolve a browser endpoint",
|
|
6157
|
+
{
|
|
6158
|
+
candidates: localDiscovery?.candidates,
|
|
6159
|
+
failures: localDiscovery?.failures,
|
|
6160
|
+
legacyError: error instanceof Error ? error : new Error(String(error)),
|
|
6161
|
+
legacyHost
|
|
6162
|
+
}
|
|
6163
|
+
);
|
|
6164
|
+
}
|
|
6165
|
+
}
|
|
6166
|
+
throw new BrowserEndpointResolutionError(
|
|
6167
|
+
"browser-not-found",
|
|
6168
|
+
"Could not resolve a browser endpoint",
|
|
6169
|
+
{
|
|
6170
|
+
candidates: localDiscovery?.candidates,
|
|
6171
|
+
failures: localDiscovery?.failures
|
|
6172
|
+
}
|
|
6173
|
+
);
|
|
5093
6174
|
}
|
|
5094
6175
|
|
|
5095
6176
|
// src/providers/index.ts
|
|
@@ -5892,6 +6973,114 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
5892
6973
|
});
|
|
5893
6974
|
}
|
|
5894
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
|
+
|
|
5895
7084
|
// src/browser/keyboard.ts
|
|
5896
7085
|
var US_KEYBOARD = {
|
|
5897
7086
|
// Letters (lowercase)
|
|
@@ -6051,8 +7240,118 @@ function parseShortcut(combo) {
|
|
|
6051
7240
|
return { modifiers, key };
|
|
6052
7241
|
}
|
|
6053
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
|
+
|
|
6054
7343
|
// src/browser/page.ts
|
|
6055
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
|
+
}
|
|
6056
7355
|
var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
|
|
6057
7356
|
if (globalThis.__bpEventListenerTrackerInstalled) return;
|
|
6058
7357
|
Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
|
|
@@ -7838,7 +9137,9 @@ var Page = class {
|
|
|
7838
9137
|
}
|
|
7839
9138
|
}
|
|
7840
9139
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
7841
|
-
const checked =
|
|
9140
|
+
const checked = normalizeAXCheckedValue(
|
|
9141
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
9142
|
+
);
|
|
7842
9143
|
return {
|
|
7843
9144
|
role,
|
|
7844
9145
|
name,
|
|
@@ -7894,7 +9195,9 @@ var Page = class {
|
|
|
7894
9195
|
const ref = nodeRefs.get(node.nodeId);
|
|
7895
9196
|
const name = node.name?.value ?? "";
|
|
7896
9197
|
const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
|
|
7897
|
-
const checked =
|
|
9198
|
+
const checked = normalizeAXCheckedValue(
|
|
9199
|
+
node.properties?.find((p) => p.name === "checked")?.value.value
|
|
9200
|
+
);
|
|
7898
9201
|
const value = node.value?.value;
|
|
7899
9202
|
const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
|
|
7900
9203
|
interactiveElements.push({
|
|
@@ -7961,6 +9264,45 @@ var Page = class {
|
|
|
7961
9264
|
}
|
|
7962
9265
|
}
|
|
7963
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
|
+
}
|
|
7964
9306
|
// ============ Batch Execution ============
|
|
7965
9307
|
/**
|
|
7966
9308
|
* Execute a batch of steps
|
|
@@ -9186,13 +10528,26 @@ var Browser = class _Browser {
|
|
|
9186
10528
|
* Connect to a browser instance
|
|
9187
10529
|
*/
|
|
9188
10530
|
static async connect(options) {
|
|
9189
|
-
|
|
9190
|
-
|
|
10531
|
+
let connectOptions = options;
|
|
10532
|
+
if (options.provider === "generic" && !options.wsUrl) {
|
|
10533
|
+
const endpoint = await resolveBrowserEndpoint({
|
|
10534
|
+
channel: options.channel,
|
|
10535
|
+
userDataDir: options.userDataDir,
|
|
10536
|
+
allowLocalDiscovery: true,
|
|
10537
|
+
allowLegacyHostFallback: true
|
|
10538
|
+
});
|
|
10539
|
+
connectOptions = {
|
|
10540
|
+
...options,
|
|
10541
|
+
wsUrl: endpoint.wsUrl
|
|
10542
|
+
};
|
|
10543
|
+
}
|
|
10544
|
+
const provider = createProvider(connectOptions);
|
|
10545
|
+
const session = await provider.createSession(connectOptions.session);
|
|
9191
10546
|
const cdp = await createCDPClient(session.wsUrl, {
|
|
9192
|
-
debug:
|
|
9193
|
-
timeout:
|
|
10547
|
+
debug: connectOptions.debug,
|
|
10548
|
+
timeout: connectOptions.timeout
|
|
9194
10549
|
});
|
|
9195
|
-
return new _Browser(cdp, provider, session,
|
|
10550
|
+
return new _Browser(cdp, provider, session, connectOptions);
|
|
9196
10551
|
}
|
|
9197
10552
|
/**
|
|
9198
10553
|
* Get or create a page by name.
|
|
@@ -9373,6 +10728,316 @@ function connect(options) {
|
|
|
9373
10728
|
return Browser.connect(options);
|
|
9374
10729
|
}
|
|
9375
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
|
+
|
|
9376
11041
|
// src/emulation/devices.ts
|
|
9377
11042
|
var devices = {
|
|
9378
11043
|
"iPhone 14": {
|
|
@@ -9591,6 +11256,143 @@ function disableTracing() {
|
|
|
9591
11256
|
globalTracer.disable();
|
|
9592
11257
|
}
|
|
9593
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
|
+
}
|
|
9594
11396
|
// Annotate the CommonJS export names for ESM import in node:
|
|
9595
11397
|
0 && (module.exports = {
|
|
9596
11398
|
AudioInput,
|
|
@@ -9598,25 +11400,48 @@ function disableTracing() {
|
|
|
9598
11400
|
BatchExecutor,
|
|
9599
11401
|
Browser,
|
|
9600
11402
|
BrowserBaseProvider,
|
|
11403
|
+
BrowserEndpointResolutionError,
|
|
9601
11404
|
BrowserlessProvider,
|
|
9602
11405
|
CDPError,
|
|
9603
11406
|
ElementNotFoundError,
|
|
9604
11407
|
GenericProvider,
|
|
9605
11408
|
NavigationError,
|
|
11409
|
+
NetworkResponseTracker,
|
|
9606
11410
|
Page,
|
|
9607
11411
|
RequestInterceptor,
|
|
9608
11412
|
TimeoutError,
|
|
9609
11413
|
Tracer,
|
|
9610
11414
|
addBatchToPage,
|
|
9611
11415
|
bufferToBase64,
|
|
11416
|
+
buildFingerprintMap,
|
|
11417
|
+
buildLocalBrowserScanTargets,
|
|
11418
|
+
buildWorkflowSummary,
|
|
9612
11419
|
calculateRMS,
|
|
11420
|
+
captureStateSignature,
|
|
11421
|
+
chooseOption,
|
|
11422
|
+
computeDelta,
|
|
11423
|
+
conditionAll,
|
|
11424
|
+
conditionAny,
|
|
11425
|
+
conditionNot,
|
|
11426
|
+
conditionRace,
|
|
9613
11427
|
connect,
|
|
9614
11428
|
createCDPClient,
|
|
11429
|
+
createFingerprint,
|
|
9615
11430
|
createProvider,
|
|
11431
|
+
createTargetFingerprint,
|
|
11432
|
+
detectOverlay,
|
|
9616
11433
|
devices,
|
|
9617
11434
|
disableTracing,
|
|
11435
|
+
discoverLocalBrowsers,
|
|
9618
11436
|
discoverTargets,
|
|
9619
11437
|
enableTracing,
|
|
11438
|
+
evaluateCondition,
|
|
11439
|
+
evaluateOutcome,
|
|
11440
|
+
extractPageState,
|
|
11441
|
+
extractReview,
|
|
11442
|
+
fingerprintKey,
|
|
11443
|
+
fingerprintSimilarity,
|
|
11444
|
+
formatWorkflowSummary,
|
|
9620
11445
|
generateSilence,
|
|
9621
11446
|
generateTone,
|
|
9622
11447
|
getAudioChromeFlags,
|
|
@@ -9624,9 +11449,16 @@ function disableTracing() {
|
|
|
9624
11449
|
getTracer,
|
|
9625
11450
|
grantAudioPermissions,
|
|
9626
11451
|
isTranscriptionAvailable,
|
|
11452
|
+
parseDevToolsActivePortFile,
|
|
9627
11453
|
parseWavHeader,
|
|
9628
11454
|
pcmToWav,
|
|
11455
|
+
recoverPinnedTarget,
|
|
11456
|
+
recoverStaleRef,
|
|
11457
|
+
resolveBrowserEndpoint,
|
|
11458
|
+
resolveChromeUserDataDirs,
|
|
11459
|
+
submitAndVerify,
|
|
9629
11460
|
transcribe,
|
|
11461
|
+
uploadFiles,
|
|
9630
11462
|
validateSteps,
|
|
9631
11463
|
waitForAnyElement,
|
|
9632
11464
|
waitForElement,
|