browser-pilot 0.0.10 → 0.0.12
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 +31 -1
- package/dist/actions.cjs +640 -30
- package/dist/actions.d.cts +2 -2
- package/dist/actions.d.ts +2 -2
- package/dist/actions.mjs +2 -1
- package/dist/browser.cjs +2736 -901
- package/dist/browser.d.cts +22 -5
- package/dist/browser.d.ts +22 -5
- package/dist/browser.mjs +5 -4
- package/dist/cdp.cjs +18 -3
- package/dist/cdp.mjs +4 -2
- package/dist/{chunk-BCOZUKWS.mjs → chunk-4MBSALQL.mjs} +21 -15
- package/dist/{chunk-R3PS4PCM.mjs → chunk-BRAFQUMG.mjs} +34 -12
- package/dist/chunk-JXAUPHZM.mjs +15 -0
- package/dist/chunk-NLIARNEE.mjs +1658 -0
- package/dist/{chunk-7OSR2CAE.mjs → chunk-RUWAXHDX.mjs} +1704 -667
- package/dist/cli.mjs +3759 -1395
- package/dist/index.cjs +2841 -860
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +7 -5
- package/dist/providers.cjs +34 -12
- package/dist/providers.mjs +1 -1
- package/dist/{types-CYw-7vx1.d.cts → types-BOPu0OQZ.d.cts} +91 -17
- package/dist/{types-DOGsEYQa.d.ts → types-j23Iqo2L.d.ts} +91 -17
- package/package.json +6 -2
- package/dist/chunk-KKW2SZLV.mjs +0 -741
package/dist/index.cjs
CHANGED
|
@@ -62,13 +62,599 @@ __export(src_exports, {
|
|
|
62
62
|
});
|
|
63
63
|
module.exports = __toCommonJS(src_exports);
|
|
64
64
|
|
|
65
|
+
// src/browser/actionability.ts
|
|
66
|
+
var ActionabilityError = class extends Error {
|
|
67
|
+
failureType;
|
|
68
|
+
coveringElement;
|
|
69
|
+
constructor(message, failureType, coveringElement) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "ActionabilityError";
|
|
72
|
+
this.failureType = failureType;
|
|
73
|
+
this.coveringElement = coveringElement;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var CHECK_VISIBLE = `function() {
|
|
77
|
+
// checkVisibility handles display:none, visibility:hidden, content-visibility up the tree
|
|
78
|
+
if (typeof this.checkVisibility === 'function' && !this.checkVisibility()) {
|
|
79
|
+
return { actionable: false, reason: 'Element is not visible (checkVisibility failed). Try scrolling or check if a prior action is needed to reveal it.' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
var style = getComputedStyle(this);
|
|
83
|
+
|
|
84
|
+
if (style.visibility !== 'visible') {
|
|
85
|
+
return { actionable: false, reason: 'Element has visibility: ' + style.visibility + '. Try scrolling or check if a prior action is needed to reveal it.' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// display:contents elements have no box themselves \u2014 check children
|
|
89
|
+
if (style.display === 'contents') {
|
|
90
|
+
var children = this.children;
|
|
91
|
+
if (children.length === 0) {
|
|
92
|
+
return { actionable: false, reason: 'Element has display:contents with no children. Try scrolling or check if a prior action is needed to reveal it.' };
|
|
93
|
+
}
|
|
94
|
+
for (var i = 0; i < children.length; i++) {
|
|
95
|
+
var childRect = children[i].getBoundingClientRect();
|
|
96
|
+
if (childRect.width > 0 && childRect.height > 0) {
|
|
97
|
+
return { actionable: true };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { actionable: false, reason: 'Element has display:contents but no visible children. Try scrolling or check if a prior action is needed to reveal it.' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
var rect = this.getBoundingClientRect();
|
|
104
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
105
|
+
return { actionable: false, reason: 'Element has zero size (' + rect.width + 'x' + rect.height + '). Try scrolling or check if a prior action is needed to reveal it.' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { actionable: true };
|
|
109
|
+
}`;
|
|
110
|
+
var CHECK_ENABLED = `function() {
|
|
111
|
+
// Native disabled property
|
|
112
|
+
var disableable = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'];
|
|
113
|
+
if (disableable.indexOf(this.tagName) !== -1 && this.disabled) {
|
|
114
|
+
return { actionable: false, reason: 'Element is disabled. Check if a prerequisite field needs to be filled first.' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check ancestor FIELDSET[disabled]
|
|
118
|
+
var parent = this.parentElement;
|
|
119
|
+
while (parent) {
|
|
120
|
+
if (parent.tagName === 'FIELDSET' && parent.disabled) {
|
|
121
|
+
// Exception: elements inside the first <legend> of a disabled fieldset are NOT disabled
|
|
122
|
+
var legend = parent.querySelector(':scope > legend');
|
|
123
|
+
if (!legend || !legend.contains(this)) {
|
|
124
|
+
return { actionable: false, reason: 'Element is inside a disabled fieldset. Check if a prerequisite field needs to be filled first.' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
parent = parent.parentElement;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// aria-disabled="true" walking up ancestor chain (crosses shadow DOM)
|
|
131
|
+
var node = this;
|
|
132
|
+
while (node) {
|
|
133
|
+
if (node.nodeType === 1 && node.getAttribute && node.getAttribute('aria-disabled') === 'true') {
|
|
134
|
+
return { actionable: false, reason: 'Element or ancestor has aria-disabled="true". Check if a prerequisite field needs to be filled first.' };
|
|
135
|
+
}
|
|
136
|
+
if (node.parentElement) {
|
|
137
|
+
node = node.parentElement;
|
|
138
|
+
} else if (node.getRootNode && node.getRootNode() !== node) {
|
|
139
|
+
// Cross shadow DOM boundary
|
|
140
|
+
var root = node.getRootNode();
|
|
141
|
+
node = root.host || null;
|
|
142
|
+
} else {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { actionable: true };
|
|
148
|
+
}`;
|
|
149
|
+
var CHECK_STABLE = `function() {
|
|
150
|
+
var self = this;
|
|
151
|
+
return new Promise(function(resolve) {
|
|
152
|
+
// If tab is backgrounded, RAF won't fire reliably \u2014 skip stability check
|
|
153
|
+
if (document.visibilityState === 'hidden') {
|
|
154
|
+
var rect = self.getBoundingClientRect();
|
|
155
|
+
resolve({ actionable: rect.width > 0 && rect.height > 0 });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
var maxFrames = 30;
|
|
160
|
+
var prev = null;
|
|
161
|
+
var frame = 0;
|
|
162
|
+
var resolved = false;
|
|
163
|
+
|
|
164
|
+
var fallbackTimer = setTimeout(function() {
|
|
165
|
+
if (!resolved) {
|
|
166
|
+
resolved = true;
|
|
167
|
+
resolve({ actionable: false, reason: 'Element stability check timed out (tab may be backgrounded)' });
|
|
168
|
+
}
|
|
169
|
+
}, 2000);
|
|
170
|
+
|
|
171
|
+
function check() {
|
|
172
|
+
if (resolved) return;
|
|
173
|
+
frame++;
|
|
174
|
+
if (frame > maxFrames) {
|
|
175
|
+
resolved = true;
|
|
176
|
+
clearTimeout(fallbackTimer);
|
|
177
|
+
resolve({ actionable: false, reason: 'Element position not stable after ' + maxFrames + ' frames' });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
var rect = self.getBoundingClientRect();
|
|
182
|
+
var cur = { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
|
|
183
|
+
|
|
184
|
+
if (prev !== null &&
|
|
185
|
+
prev.x === cur.x && prev.y === cur.y &&
|
|
186
|
+
prev.w === cur.w && prev.h === cur.h) {
|
|
187
|
+
resolved = true;
|
|
188
|
+
clearTimeout(fallbackTimer);
|
|
189
|
+
resolve({ actionable: true });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
prev = cur;
|
|
194
|
+
requestAnimationFrame(check);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
requestAnimationFrame(check);
|
|
198
|
+
});
|
|
199
|
+
}`;
|
|
200
|
+
var CHECK_HIT_TARGET = `function(x, y) {
|
|
201
|
+
// Compute click center if coordinates not provided
|
|
202
|
+
if (x === undefined || y === undefined) {
|
|
203
|
+
var rect = this.getBoundingClientRect();
|
|
204
|
+
x = rect.x + rect.width / 2;
|
|
205
|
+
y = rect.y + rect.height / 2;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function checkPoint(root, px, py) {
|
|
209
|
+
var method = root.elementsFromPoint || root.msElementsFromPoint;
|
|
210
|
+
if (!method) return [];
|
|
211
|
+
return method.call(root, px, py) || [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Follow only the top-most hit through nested shadow roots.
|
|
215
|
+
// Accepting any hit in the stack creates false positives for covered elements.
|
|
216
|
+
var root = document;
|
|
217
|
+
var topHits = [];
|
|
218
|
+
var seenRoots = [];
|
|
219
|
+
while (root && seenRoots.indexOf(root) === -1) {
|
|
220
|
+
seenRoots.push(root);
|
|
221
|
+
var hits = checkPoint(root, x, y);
|
|
222
|
+
if (!hits.length) break;
|
|
223
|
+
var top = hits[0];
|
|
224
|
+
topHits.push(top);
|
|
225
|
+
if (top && top.shadowRoot) {
|
|
226
|
+
root = top.shadowRoot;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Target must be the top-most hit element or an ancestor/descendant
|
|
233
|
+
for (var j = 0; j < topHits.length; j++) {
|
|
234
|
+
var hit = topHits[j];
|
|
235
|
+
if (hit === this || this.contains(hit) || hit.contains(this)) {
|
|
236
|
+
return { actionable: true };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Report the covering element
|
|
241
|
+
var top = topHits.length > 0 ? topHits[topHits.length - 1] : null;
|
|
242
|
+
if (top) {
|
|
243
|
+
return {
|
|
244
|
+
actionable: false,
|
|
245
|
+
reason: 'Element is covered by <' + top.tagName.toLowerCase() + '>' +
|
|
246
|
+
(top.id ? '#' + top.id : '') +
|
|
247
|
+
(top.className && typeof top.className === 'string' ? '.' + top.className.split(' ').join('.') : '') +
|
|
248
|
+
'. Try dismissing overlays first.',
|
|
249
|
+
coveringElement: {
|
|
250
|
+
tag: top.tagName.toLowerCase(),
|
|
251
|
+
id: top.id || undefined,
|
|
252
|
+
className: (typeof top.className === 'string' && top.className) || undefined
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { actionable: false, reason: 'No element found at click point (' + x + ', ' + y + '). Try scrolling the element into view first.' };
|
|
258
|
+
}`;
|
|
259
|
+
var CHECK_EDITABLE = `function() {
|
|
260
|
+
// Must be an editable element type
|
|
261
|
+
var tag = this.tagName;
|
|
262
|
+
var isEditable = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' ||
|
|
263
|
+
this.isContentEditable;
|
|
264
|
+
if (!isEditable) {
|
|
265
|
+
return { actionable: false, reason: 'Element is not an editable type (<' + tag.toLowerCase() + '>). Target an <input>, <textarea>, <select>, or [contenteditable] element instead.' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check disabled
|
|
269
|
+
var disableable = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'];
|
|
270
|
+
if (disableable.indexOf(tag) !== -1 && this.disabled) {
|
|
271
|
+
return { actionable: false, reason: 'Element is disabled. Check if a prerequisite field needs to be filled first.' };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check ancestor FIELDSET[disabled]
|
|
275
|
+
var parent = this.parentElement;
|
|
276
|
+
while (parent) {
|
|
277
|
+
if (parent.tagName === 'FIELDSET' && parent.disabled) {
|
|
278
|
+
var legend = parent.querySelector(':scope > legend');
|
|
279
|
+
if (!legend || !legend.contains(this)) {
|
|
280
|
+
return { actionable: false, reason: 'Element is inside a disabled fieldset. Check if a prerequisite field needs to be filled first.' };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
parent = parent.parentElement;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// aria-disabled walking up (crosses shadow DOM)
|
|
287
|
+
var node = this;
|
|
288
|
+
while (node) {
|
|
289
|
+
if (node.nodeType === 1 && node.getAttribute && node.getAttribute('aria-disabled') === 'true') {
|
|
290
|
+
return { actionable: false, reason: 'Element or ancestor has aria-disabled="true". Check if a prerequisite field needs to be filled first.' };
|
|
291
|
+
}
|
|
292
|
+
if (node.parentElement) {
|
|
293
|
+
node = node.parentElement;
|
|
294
|
+
} else if (node.getRootNode && node.getRootNode() !== node) {
|
|
295
|
+
var root = node.getRootNode();
|
|
296
|
+
node = root.host || null;
|
|
297
|
+
} else {
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check readonly
|
|
303
|
+
if (this.hasAttribute && this.hasAttribute('readonly')) {
|
|
304
|
+
return { actionable: false, reason: 'Cannot fill a readonly input. Remove the readonly attribute or target a different element.' };
|
|
305
|
+
}
|
|
306
|
+
if (this.getAttribute && this.getAttribute('aria-readonly') === 'true') {
|
|
307
|
+
return { actionable: false, reason: 'Cannot fill a readonly input (aria-readonly="true"). Remove the attribute or target a different element.' };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { actionable: true };
|
|
311
|
+
}`;
|
|
312
|
+
function sleep(ms) {
|
|
313
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
314
|
+
}
|
|
315
|
+
var BACKOFF = [0, 20, 100, 100];
|
|
316
|
+
async function runCheck(cdp, objectId, check, options) {
|
|
317
|
+
let script;
|
|
318
|
+
let awaitPromise = false;
|
|
319
|
+
const args = [];
|
|
320
|
+
switch (check) {
|
|
321
|
+
case "visible":
|
|
322
|
+
script = CHECK_VISIBLE;
|
|
323
|
+
break;
|
|
324
|
+
case "enabled":
|
|
325
|
+
script = CHECK_ENABLED;
|
|
326
|
+
break;
|
|
327
|
+
case "stable":
|
|
328
|
+
script = CHECK_STABLE;
|
|
329
|
+
awaitPromise = true;
|
|
330
|
+
break;
|
|
331
|
+
case "hitTarget":
|
|
332
|
+
script = CHECK_HIT_TARGET;
|
|
333
|
+
if (options?.coordinates) {
|
|
334
|
+
args.push({ value: options.coordinates.x });
|
|
335
|
+
args.push({ value: options.coordinates.y });
|
|
336
|
+
} else {
|
|
337
|
+
args.push({ value: void 0 });
|
|
338
|
+
args.push({ value: void 0 });
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
case "editable":
|
|
342
|
+
script = CHECK_EDITABLE;
|
|
343
|
+
break;
|
|
344
|
+
default: {
|
|
345
|
+
const _exhaustive = check;
|
|
346
|
+
throw new Error(`Unknown actionability check: ${_exhaustive}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const params = {
|
|
350
|
+
functionDeclaration: script,
|
|
351
|
+
objectId,
|
|
352
|
+
returnByValue: true,
|
|
353
|
+
arguments: args
|
|
354
|
+
};
|
|
355
|
+
if (awaitPromise) {
|
|
356
|
+
params["awaitPromise"] = true;
|
|
357
|
+
}
|
|
358
|
+
const response = await cdp.send("Runtime.callFunctionOn", params);
|
|
359
|
+
if (response.exceptionDetails) {
|
|
360
|
+
return {
|
|
361
|
+
actionable: false,
|
|
362
|
+
reason: `Check "${check}" threw: ${response.exceptionDetails.text}`,
|
|
363
|
+
failureType: check
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const result = response.result.value;
|
|
367
|
+
if (!result.actionable) {
|
|
368
|
+
result.failureType = check;
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
async function runChecks(cdp, objectId, checks, options) {
|
|
373
|
+
for (const check of checks) {
|
|
374
|
+
const result = await runCheck(cdp, objectId, check, options);
|
|
375
|
+
if (!result.actionable) {
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { actionable: true };
|
|
380
|
+
}
|
|
381
|
+
async function ensureActionable(cdp, objectId, checks, options) {
|
|
382
|
+
const timeout = options?.timeout ?? 3e4;
|
|
383
|
+
const start = Date.now();
|
|
384
|
+
let attempt = 0;
|
|
385
|
+
while (true) {
|
|
386
|
+
const result = await runChecks(cdp, objectId, checks, options);
|
|
387
|
+
if (result.actionable) return;
|
|
388
|
+
if (Date.now() - start >= timeout) {
|
|
389
|
+
throw new ActionabilityError(
|
|
390
|
+
`Element not actionable: ${result.reason}`,
|
|
391
|
+
result.failureType,
|
|
392
|
+
result.coveringElement
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const delay = attempt < BACKOFF.length ? BACKOFF[attempt] ?? 0 : 500;
|
|
396
|
+
if (delay > 0) await sleep(delay);
|
|
397
|
+
attempt++;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/browser/fuzzy-match.ts
|
|
402
|
+
function jaroWinkler(a, b) {
|
|
403
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
404
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
405
|
+
if (a === b) return 1;
|
|
406
|
+
const s1 = a.toLowerCase();
|
|
407
|
+
const s2 = b.toLowerCase();
|
|
408
|
+
const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
|
|
409
|
+
const s1Matches = Array.from({ length: s1.length }, () => false);
|
|
410
|
+
const s2Matches = Array.from({ length: s2.length }, () => false);
|
|
411
|
+
let matches = 0;
|
|
412
|
+
let transpositions = 0;
|
|
413
|
+
for (let i = 0; i < s1.length; i++) {
|
|
414
|
+
const start = Math.max(0, i - matchWindow);
|
|
415
|
+
const end = Math.min(i + matchWindow + 1, s2.length);
|
|
416
|
+
for (let j = start; j < end; j++) {
|
|
417
|
+
if (s2Matches[j] || s1[i] !== s2[j]) continue;
|
|
418
|
+
s1Matches[i] = true;
|
|
419
|
+
s2Matches[j] = true;
|
|
420
|
+
matches++;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (matches === 0) return 0;
|
|
425
|
+
let k = 0;
|
|
426
|
+
for (let i = 0; i < s1.length; i++) {
|
|
427
|
+
if (!s1Matches[i]) continue;
|
|
428
|
+
while (!s2Matches[k]) k++;
|
|
429
|
+
if (s1[i] !== s2[k]) transpositions++;
|
|
430
|
+
k++;
|
|
431
|
+
}
|
|
432
|
+
const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
|
|
433
|
+
let prefix = 0;
|
|
434
|
+
for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
|
|
435
|
+
if (s1[i] === s2[i]) {
|
|
436
|
+
prefix++;
|
|
437
|
+
} else {
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const WINKLER_SCALING = 0.1;
|
|
442
|
+
return jaro + prefix * WINKLER_SCALING * (1 - jaro);
|
|
443
|
+
}
|
|
444
|
+
function stringSimilarity(a, b) {
|
|
445
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
446
|
+
const lowerA = a.toLowerCase();
|
|
447
|
+
const lowerB = b.toLowerCase();
|
|
448
|
+
if (lowerA === lowerB) return 1;
|
|
449
|
+
const jw = jaroWinkler(a, b);
|
|
450
|
+
let containsBonus = 0;
|
|
451
|
+
if (lowerB.includes(lowerA)) {
|
|
452
|
+
containsBonus = 0.2;
|
|
453
|
+
} else if (lowerA.includes(lowerB)) {
|
|
454
|
+
containsBonus = 0.1;
|
|
455
|
+
}
|
|
456
|
+
return Math.min(1, jw + containsBonus);
|
|
457
|
+
}
|
|
458
|
+
function scoreElement(query, element) {
|
|
459
|
+
const lowerQuery = query.toLowerCase();
|
|
460
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
461
|
+
let nameScore = 0;
|
|
462
|
+
if (element.name) {
|
|
463
|
+
const lowerName = element.name.toLowerCase();
|
|
464
|
+
if (lowerName === lowerQuery) {
|
|
465
|
+
nameScore = 1;
|
|
466
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
467
|
+
nameScore = 0.8;
|
|
468
|
+
} else if (words.length > 0) {
|
|
469
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
470
|
+
nameScore = matchedWords.length / words.length * 0.7;
|
|
471
|
+
} else {
|
|
472
|
+
nameScore = stringSimilarity(query, element.name) * 0.6;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
let roleScore = 0;
|
|
476
|
+
const lowerRole = element.role.toLowerCase();
|
|
477
|
+
if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
|
|
478
|
+
roleScore = 0.3;
|
|
479
|
+
} else if (words.some((w) => lowerRole.includes(w))) {
|
|
480
|
+
roleScore = 0.2;
|
|
481
|
+
}
|
|
482
|
+
let selectorScore = 0;
|
|
483
|
+
const lowerSelector = element.selector.toLowerCase();
|
|
484
|
+
if (words.some((w) => lowerSelector.includes(w))) {
|
|
485
|
+
selectorScore = 0.2;
|
|
486
|
+
}
|
|
487
|
+
const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
|
|
488
|
+
return totalScore;
|
|
489
|
+
}
|
|
490
|
+
function explainMatch(query, element, score) {
|
|
491
|
+
const reasons = [];
|
|
492
|
+
const lowerQuery = query.toLowerCase();
|
|
493
|
+
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
494
|
+
if (element.name) {
|
|
495
|
+
const lowerName = element.name.toLowerCase();
|
|
496
|
+
if (lowerName === lowerQuery) {
|
|
497
|
+
reasons.push("exact name match");
|
|
498
|
+
} else if (lowerName.includes(lowerQuery)) {
|
|
499
|
+
reasons.push("name contains query");
|
|
500
|
+
} else if (words.some((w) => lowerName.includes(w))) {
|
|
501
|
+
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
502
|
+
reasons.push(`name contains: ${matchedWords.join(", ")}`);
|
|
503
|
+
} else if (stringSimilarity(query, element.name) > 0.5) {
|
|
504
|
+
reasons.push("similar name");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const lowerRole = element.role.toLowerCase();
|
|
508
|
+
if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
|
|
509
|
+
reasons.push(`role: ${element.role}`);
|
|
510
|
+
}
|
|
511
|
+
if (words.some((w) => element.selector.toLowerCase().includes(w))) {
|
|
512
|
+
reasons.push("selector match");
|
|
513
|
+
}
|
|
514
|
+
if (reasons.length === 0) {
|
|
515
|
+
reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
|
|
516
|
+
}
|
|
517
|
+
return reasons.join(", ");
|
|
518
|
+
}
|
|
519
|
+
function fuzzyMatchElements(query, elements, maxResults = 5) {
|
|
520
|
+
if (!query || query.length === 0) {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
const THRESHOLD = 0.3;
|
|
524
|
+
const scored = elements.map((element) => ({
|
|
525
|
+
element,
|
|
526
|
+
score: scoreElement(query, element)
|
|
527
|
+
}));
|
|
528
|
+
return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
|
|
529
|
+
element: s.element,
|
|
530
|
+
score: s.score,
|
|
531
|
+
matchReason: explainMatch(query, s.element, s.score)
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/browser/hint-generator.ts
|
|
536
|
+
var ACTION_ROLE_MAP = {
|
|
537
|
+
click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
|
|
538
|
+
fill: ["textbox", "searchbox", "textarea"],
|
|
539
|
+
type: ["textbox", "searchbox", "textarea"],
|
|
540
|
+
submit: ["button", "form"],
|
|
541
|
+
select: ["combobox", "listbox", "option"],
|
|
542
|
+
check: ["checkbox", "radio", "switch"],
|
|
543
|
+
uncheck: ["checkbox", "switch"],
|
|
544
|
+
focus: [],
|
|
545
|
+
// Any focusable element
|
|
546
|
+
hover: [],
|
|
547
|
+
// Any element
|
|
548
|
+
clear: ["textbox", "searchbox", "textarea"]
|
|
549
|
+
};
|
|
550
|
+
function extractIntent(selectors) {
|
|
551
|
+
const patterns = [];
|
|
552
|
+
let text = "";
|
|
553
|
+
for (const selector of selectors) {
|
|
554
|
+
if (selector.startsWith("ref:")) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
|
|
558
|
+
if (idMatch) {
|
|
559
|
+
patterns.push(idMatch[1]);
|
|
560
|
+
}
|
|
561
|
+
const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
|
|
562
|
+
if (ariaMatch) {
|
|
563
|
+
patterns.push(ariaMatch[1]);
|
|
564
|
+
}
|
|
565
|
+
const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
|
|
566
|
+
if (testidMatch) {
|
|
567
|
+
patterns.push(testidMatch[1]);
|
|
568
|
+
}
|
|
569
|
+
const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
|
|
570
|
+
if (classMatch) {
|
|
571
|
+
patterns.push(classMatch[1]);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
patterns.sort((a, b) => b.length - a.length);
|
|
575
|
+
text = patterns[0] ?? selectors[0] ?? "";
|
|
576
|
+
return { text, patterns };
|
|
577
|
+
}
|
|
578
|
+
function getHintType(selector) {
|
|
579
|
+
if (selector.startsWith("ref:")) return "ref";
|
|
580
|
+
if (selector.includes("data-testid")) return "testid";
|
|
581
|
+
if (selector.includes("aria-label")) return "aria";
|
|
582
|
+
if (selector.startsWith("#")) return "id";
|
|
583
|
+
return "css";
|
|
584
|
+
}
|
|
585
|
+
function getConfidence(score) {
|
|
586
|
+
if (score >= 0.8) return "high";
|
|
587
|
+
if (score >= 0.5) return "medium";
|
|
588
|
+
return "low";
|
|
589
|
+
}
|
|
590
|
+
function diversifyHints(candidates, maxHints) {
|
|
591
|
+
const hints = [];
|
|
592
|
+
const usedTypes = /* @__PURE__ */ new Set();
|
|
593
|
+
for (const candidate of candidates) {
|
|
594
|
+
if (hints.length >= maxHints) break;
|
|
595
|
+
const refSelector = `ref:${candidate.element.ref}`;
|
|
596
|
+
const hintType = getHintType(refSelector);
|
|
597
|
+
if (!usedTypes.has(hintType)) {
|
|
598
|
+
hints.push({
|
|
599
|
+
selector: refSelector,
|
|
600
|
+
reason: candidate.matchReason,
|
|
601
|
+
confidence: getConfidence(candidate.score),
|
|
602
|
+
element: {
|
|
603
|
+
ref: candidate.element.ref,
|
|
604
|
+
role: candidate.element.role,
|
|
605
|
+
name: candidate.element.name,
|
|
606
|
+
disabled: candidate.element.disabled
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
usedTypes.add(hintType);
|
|
610
|
+
} else if (hints.length < maxHints) {
|
|
611
|
+
hints.push({
|
|
612
|
+
selector: refSelector,
|
|
613
|
+
reason: candidate.matchReason,
|
|
614
|
+
confidence: getConfidence(candidate.score),
|
|
615
|
+
element: {
|
|
616
|
+
ref: candidate.element.ref,
|
|
617
|
+
role: candidate.element.role,
|
|
618
|
+
name: candidate.element.name,
|
|
619
|
+
disabled: candidate.element.disabled
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return hints;
|
|
625
|
+
}
|
|
626
|
+
async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
|
|
627
|
+
let snapshot;
|
|
628
|
+
try {
|
|
629
|
+
snapshot = await page.snapshot();
|
|
630
|
+
} catch {
|
|
631
|
+
return [];
|
|
632
|
+
}
|
|
633
|
+
const intent = extractIntent(failedSelectors);
|
|
634
|
+
const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
|
|
635
|
+
let candidates = snapshot.interactiveElements;
|
|
636
|
+
if (roleFilter.length > 0) {
|
|
637
|
+
candidates = candidates.filter((el) => roleFilter.includes(el.role));
|
|
638
|
+
}
|
|
639
|
+
const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
|
|
640
|
+
if (matches.length === 0) {
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
return diversifyHints(matches, maxHints);
|
|
644
|
+
}
|
|
645
|
+
|
|
65
646
|
// src/browser/types.ts
|
|
66
647
|
var ElementNotFoundError = class extends Error {
|
|
67
648
|
selectors;
|
|
68
649
|
hints;
|
|
69
650
|
constructor(selectors, hints) {
|
|
70
651
|
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
71
|
-
|
|
652
|
+
let msg = `Element not found: ${selectorList.join(", ")}`;
|
|
653
|
+
if (hints?.length) {
|
|
654
|
+
msg += `. Did you mean: ${hints.slice(0, 3).map((h) => `${h.element.ref} (${h.element.role} "${h.element.name}")`).join(", ")}`;
|
|
655
|
+
}
|
|
656
|
+
msg += `. Run 'bp snapshot' to see available elements.`;
|
|
657
|
+
super(msg);
|
|
72
658
|
this.name = "ElementNotFoundError";
|
|
73
659
|
this.selectors = selectorList;
|
|
74
660
|
this.hints = hints;
|
|
@@ -76,7 +662,8 @@ var ElementNotFoundError = class extends Error {
|
|
|
76
662
|
};
|
|
77
663
|
var TimeoutError = class extends Error {
|
|
78
664
|
constructor(message = "Operation timed out") {
|
|
79
|
-
|
|
665
|
+
const msg = message.includes("bp snapshot") ? message : `${message}. Run 'bp snapshot' to check current page state.`;
|
|
666
|
+
super(msg);
|
|
80
667
|
this.name = "TimeoutError";
|
|
81
668
|
}
|
|
82
669
|
};
|
|
@@ -87,8 +674,87 @@ var NavigationError = class extends Error {
|
|
|
87
674
|
}
|
|
88
675
|
};
|
|
89
676
|
|
|
677
|
+
// src/cdp/protocol.ts
|
|
678
|
+
var CDPError = class extends Error {
|
|
679
|
+
code;
|
|
680
|
+
data;
|
|
681
|
+
constructor(error) {
|
|
682
|
+
super(error.message);
|
|
683
|
+
this.name = "CDPError";
|
|
684
|
+
this.code = error.code;
|
|
685
|
+
this.data = error.data;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
90
689
|
// src/actions/executor.ts
|
|
91
690
|
var DEFAULT_TIMEOUT = 3e4;
|
|
691
|
+
function classifyFailure(error) {
|
|
692
|
+
if (error instanceof ElementNotFoundError) {
|
|
693
|
+
return { reason: "missing" };
|
|
694
|
+
}
|
|
695
|
+
if (error instanceof ActionabilityError) {
|
|
696
|
+
switch (error.failureType) {
|
|
697
|
+
case "visible":
|
|
698
|
+
return { reason: "hidden" };
|
|
699
|
+
case "hitTarget":
|
|
700
|
+
return { reason: "covered", coveringElement: error.coveringElement };
|
|
701
|
+
case "enabled":
|
|
702
|
+
return { reason: "disabled" };
|
|
703
|
+
case "editable":
|
|
704
|
+
return { reason: error.message?.includes("readonly") ? "readonly" : "notEditable" };
|
|
705
|
+
case "stable":
|
|
706
|
+
return { reason: "replaced" };
|
|
707
|
+
default:
|
|
708
|
+
return { reason: "unknown" };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (error instanceof TimeoutError) {
|
|
712
|
+
return { reason: "timeout" };
|
|
713
|
+
}
|
|
714
|
+
if (error instanceof NavigationError) {
|
|
715
|
+
return { reason: "navigation" };
|
|
716
|
+
}
|
|
717
|
+
if (error instanceof CDPError) {
|
|
718
|
+
return { reason: "cdpError" };
|
|
719
|
+
}
|
|
720
|
+
const msg = String(error?.message ?? error);
|
|
721
|
+
if (msg.includes("Could not find node") || msg.includes("does not belong to the document")) {
|
|
722
|
+
return { reason: "detached" };
|
|
723
|
+
}
|
|
724
|
+
return { reason: "unknown" };
|
|
725
|
+
}
|
|
726
|
+
function getSuggestion(reason) {
|
|
727
|
+
switch (reason) {
|
|
728
|
+
case "missing":
|
|
729
|
+
return "Element not found. Run 'snapshot' to see available elements, or try alternative selectors.";
|
|
730
|
+
case "hidden":
|
|
731
|
+
return "Element exists but is not visible. Try 'scroll' or wait for it to appear.";
|
|
732
|
+
case "covered":
|
|
733
|
+
return "Element is blocked by another element. Dismiss the covering element first.";
|
|
734
|
+
case "disabled":
|
|
735
|
+
return "Element is disabled. Complete prerequisite steps to enable it.";
|
|
736
|
+
case "readonly":
|
|
737
|
+
return "Element is readonly and cannot be edited directly.";
|
|
738
|
+
case "detached":
|
|
739
|
+
return "Element was removed from the DOM. Run 'snapshot' for fresh element refs.";
|
|
740
|
+
case "replaced":
|
|
741
|
+
return "Element was replaced in the DOM. Run 'snapshot' to get updated refs.";
|
|
742
|
+
case "notEditable":
|
|
743
|
+
return "Element is not an editable field. Try a different selector targeting an input or textarea.";
|
|
744
|
+
case "timeout":
|
|
745
|
+
return "Timed out waiting. The page may still be loading. Try increasing timeout.";
|
|
746
|
+
case "navigation":
|
|
747
|
+
return "Navigation failed. Check the URL and network connectivity.";
|
|
748
|
+
case "cdpError":
|
|
749
|
+
return "Browser connection error. Try 'bp connect' again.";
|
|
750
|
+
case "unknown":
|
|
751
|
+
return "Unexpected error. Run 'snapshot' to check page state.";
|
|
752
|
+
default: {
|
|
753
|
+
const _exhaustive = reason;
|
|
754
|
+
return `Unknown failure: ${_exhaustive}`;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
92
758
|
var BatchExecutor = class {
|
|
93
759
|
page;
|
|
94
760
|
constructor(page) {
|
|
@@ -104,21 +770,46 @@ var BatchExecutor = class {
|
|
|
104
770
|
for (let i = 0; i < steps.length; i++) {
|
|
105
771
|
const step = steps[i];
|
|
106
772
|
const stepStart = Date.now();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
result
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
773
|
+
const maxAttempts = (step.retry ?? 0) + 1;
|
|
774
|
+
const retryDelay = step.retryDelay ?? 500;
|
|
775
|
+
let lastError;
|
|
776
|
+
let succeeded = false;
|
|
777
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
778
|
+
if (attempt > 0) {
|
|
779
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
const result = await this.executeStep(step, timeout);
|
|
783
|
+
results.push({
|
|
784
|
+
index: i,
|
|
785
|
+
action: step.action,
|
|
786
|
+
selector: step.selector,
|
|
787
|
+
selectorUsed: result.selectorUsed,
|
|
788
|
+
success: true,
|
|
789
|
+
durationMs: Date.now() - stepStart,
|
|
790
|
+
result: result.value,
|
|
791
|
+
text: result.text
|
|
792
|
+
});
|
|
793
|
+
succeeded = true;
|
|
794
|
+
break;
|
|
795
|
+
} catch (error) {
|
|
796
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (!succeeded) {
|
|
800
|
+
const errorMessage = lastError?.message ?? "Unknown error";
|
|
801
|
+
let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
|
|
802
|
+
const { reason, coveringElement } = classifyFailure(lastError);
|
|
803
|
+
if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
|
|
804
|
+
try {
|
|
805
|
+
const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
|
|
806
|
+
const autoHints = await generateHints(this.page, selectors, step.action, 3);
|
|
807
|
+
if (autoHints.length > 0) {
|
|
808
|
+
hints = autoHints;
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
}
|
|
122
813
|
results.push({
|
|
123
814
|
index: i,
|
|
124
815
|
action: step.action,
|
|
@@ -126,7 +817,10 @@ var BatchExecutor = class {
|
|
|
126
817
|
success: false,
|
|
127
818
|
durationMs: Date.now() - stepStart,
|
|
128
819
|
error: errorMessage,
|
|
129
|
-
hints
|
|
820
|
+
hints,
|
|
821
|
+
failureReason: reason,
|
|
822
|
+
coveringElement,
|
|
823
|
+
suggestion: getSuggestion(reason)
|
|
130
824
|
});
|
|
131
825
|
if (onFail === "stop" && !step.optional) {
|
|
132
826
|
return {
|
|
@@ -159,7 +853,7 @@ var BatchExecutor = class {
|
|
|
159
853
|
}
|
|
160
854
|
case "click": {
|
|
161
855
|
if (!step.selector) throw new Error("click requires selector");
|
|
162
|
-
if (step.waitForNavigation) {
|
|
856
|
+
if (step.waitForNavigation === true) {
|
|
163
857
|
const navPromise = this.page.waitForNavigation({ timeout, optional });
|
|
164
858
|
await this.page.click(step.selector, { timeout, optional });
|
|
165
859
|
await navPromise;
|
|
@@ -174,7 +868,6 @@ var BatchExecutor = class {
|
|
|
174
868
|
await this.page.fill(step.selector, step.value, {
|
|
175
869
|
timeout,
|
|
176
870
|
optional,
|
|
177
|
-
clear: step.clear ?? true,
|
|
178
871
|
blur: step.blur
|
|
179
872
|
});
|
|
180
873
|
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
@@ -222,13 +915,31 @@ var BatchExecutor = class {
|
|
|
222
915
|
await this.page.submit(step.selector, {
|
|
223
916
|
timeout,
|
|
224
917
|
optional,
|
|
225
|
-
method: step.method ?? "enter+click"
|
|
918
|
+
method: step.method ?? "enter+click",
|
|
919
|
+
waitForNavigation: step.waitForNavigation
|
|
226
920
|
});
|
|
227
921
|
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
228
922
|
}
|
|
229
923
|
case "press": {
|
|
230
924
|
if (!step.key) throw new Error("press requires key");
|
|
231
|
-
|
|
925
|
+
try {
|
|
926
|
+
await this.page.press(step.key, {
|
|
927
|
+
modifiers: step.modifiers
|
|
928
|
+
});
|
|
929
|
+
} catch (e) {
|
|
930
|
+
if (optional) return {};
|
|
931
|
+
throw e;
|
|
932
|
+
}
|
|
933
|
+
return {};
|
|
934
|
+
}
|
|
935
|
+
case "shortcut": {
|
|
936
|
+
if (!step.combo) throw new Error("shortcut requires combo");
|
|
937
|
+
try {
|
|
938
|
+
await this.page.shortcut(step.combo);
|
|
939
|
+
} catch (e) {
|
|
940
|
+
if (optional) return {};
|
|
941
|
+
throw e;
|
|
942
|
+
}
|
|
232
943
|
return {};
|
|
233
944
|
}
|
|
234
945
|
case "focus": {
|
|
@@ -315,6 +1026,80 @@ var BatchExecutor = class {
|
|
|
315
1026
|
await this.page.switchToMain();
|
|
316
1027
|
return {};
|
|
317
1028
|
}
|
|
1029
|
+
case "assertVisible": {
|
|
1030
|
+
if (!step.selector) throw new Error("assertVisible requires selector");
|
|
1031
|
+
const el = await this.page.waitFor(step.selector, {
|
|
1032
|
+
timeout,
|
|
1033
|
+
optional: true,
|
|
1034
|
+
state: "visible"
|
|
1035
|
+
});
|
|
1036
|
+
if (!el) {
|
|
1037
|
+
throw new Error(
|
|
1038
|
+
`Assertion failed: selector ${JSON.stringify(step.selector)} is not visible`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
1042
|
+
}
|
|
1043
|
+
case "assertExists": {
|
|
1044
|
+
if (!step.selector) throw new Error("assertExists requires selector");
|
|
1045
|
+
const el = await this.page.waitFor(step.selector, {
|
|
1046
|
+
timeout,
|
|
1047
|
+
optional: true,
|
|
1048
|
+
state: "attached"
|
|
1049
|
+
});
|
|
1050
|
+
if (!el) {
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
`Assertion failed: selector ${JSON.stringify(step.selector)} does not exist`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
1056
|
+
}
|
|
1057
|
+
case "assertText": {
|
|
1058
|
+
const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
|
|
1059
|
+
const text = await this.page.text(selector);
|
|
1060
|
+
const expected = step.expect ?? step.value;
|
|
1061
|
+
if (typeof expected !== "string") throw new Error("assertText requires expect or value");
|
|
1062
|
+
if (!text.includes(expected)) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`Assertion failed: text does not contain ${JSON.stringify(expected)}. Got: ${JSON.stringify(text.slice(0, 200))}`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
return { selectorUsed: selector, text };
|
|
1068
|
+
}
|
|
1069
|
+
case "assertUrl": {
|
|
1070
|
+
const currentUrl = await this.page.url();
|
|
1071
|
+
const expected = step.expect ?? step.url;
|
|
1072
|
+
if (typeof expected !== "string") throw new Error("assertUrl requires expect or url");
|
|
1073
|
+
if (!currentUrl.includes(expected)) {
|
|
1074
|
+
throw new Error(
|
|
1075
|
+
`Assertion failed: URL does not contain ${JSON.stringify(expected)}. Got: ${JSON.stringify(currentUrl)}`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
return { value: currentUrl };
|
|
1079
|
+
}
|
|
1080
|
+
case "assertValue": {
|
|
1081
|
+
if (!step.selector) throw new Error("assertValue requires selector");
|
|
1082
|
+
const expected = step.expect ?? step.value;
|
|
1083
|
+
if (typeof expected !== "string") throw new Error("assertValue requires expect or value");
|
|
1084
|
+
const found = await this.page.waitFor(step.selector, {
|
|
1085
|
+
timeout,
|
|
1086
|
+
optional: true,
|
|
1087
|
+
state: "attached"
|
|
1088
|
+
});
|
|
1089
|
+
if (!found) {
|
|
1090
|
+
throw new Error(`Assertion failed: selector ${JSON.stringify(step.selector)} not found`);
|
|
1091
|
+
}
|
|
1092
|
+
const usedSelector = this.getUsedSelector(step.selector);
|
|
1093
|
+
const actual = await this.page.evaluate(
|
|
1094
|
+
`(function() { var el = document.querySelector(${JSON.stringify(usedSelector)}); return el ? el.value : null; })()`
|
|
1095
|
+
);
|
|
1096
|
+
if (actual !== expected) {
|
|
1097
|
+
throw new Error(
|
|
1098
|
+
`Assertion failed: value of ${JSON.stringify(usedSelector)} is ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
return { selectorUsed: usedSelector, value: actual };
|
|
1102
|
+
}
|
|
318
1103
|
default: {
|
|
319
1104
|
const action = step.action;
|
|
320
1105
|
const aliases = {
|
|
@@ -327,16 +1112,43 @@ var BatchExecutor = class {
|
|
|
327
1112
|
capture: "screenshot",
|
|
328
1113
|
inspect: "snapshot",
|
|
329
1114
|
enter: "press",
|
|
1115
|
+
keypress: "press",
|
|
1116
|
+
hotkey: "shortcut",
|
|
1117
|
+
keybinding: "shortcut",
|
|
1118
|
+
nav: "goto",
|
|
330
1119
|
open: "goto",
|
|
331
1120
|
visit: "goto",
|
|
1121
|
+
browse: "goto",
|
|
1122
|
+
load: "goto",
|
|
1123
|
+
write: "fill",
|
|
1124
|
+
set: "fill",
|
|
1125
|
+
pick: "select",
|
|
1126
|
+
choose: "select",
|
|
1127
|
+
send: "press",
|
|
332
1128
|
eval: "evaluate",
|
|
333
1129
|
js: "evaluate",
|
|
1130
|
+
script: "evaluate",
|
|
334
1131
|
snap: "snapshot",
|
|
335
|
-
|
|
1132
|
+
accessibility: "snapshot",
|
|
1133
|
+
a11y: "snapshot",
|
|
1134
|
+
image: "screenshot",
|
|
1135
|
+
pic: "screenshot",
|
|
1136
|
+
frame: "switchFrame",
|
|
1137
|
+
iframe: "switchFrame",
|
|
1138
|
+
assert_visible: "assertVisible",
|
|
1139
|
+
assert_exists: "assertExists",
|
|
1140
|
+
assert_text: "assertText",
|
|
1141
|
+
assert_url: "assertUrl",
|
|
1142
|
+
assert_value: "assertValue",
|
|
1143
|
+
checkvisible: "assertVisible",
|
|
1144
|
+
checkexists: "assertExists",
|
|
1145
|
+
checktext: "assertText",
|
|
1146
|
+
checkurl: "assertUrl",
|
|
1147
|
+
checkvalue: "assertValue"
|
|
336
1148
|
};
|
|
337
1149
|
const suggestion = aliases[action.toLowerCase()];
|
|
338
1150
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
339
|
-
const valid = "goto, click, fill, type, select, check, uncheck, submit, press, focus, hover, scroll, wait, snapshot, screenshot, evaluate, text, switchFrame, switchToMain";
|
|
1151
|
+
const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, screenshot, evaluate, text, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
|
|
340
1152
|
throw new Error(`Unknown action "${action}".${hint}
|
|
341
1153
|
|
|
342
1154
|
Valid actions: ${valid}`);
|
|
@@ -385,6 +1197,8 @@ var ACTION_ALIASES = {
|
|
|
385
1197
|
inspect: "snapshot",
|
|
386
1198
|
enter: "press",
|
|
387
1199
|
keypress: "press",
|
|
1200
|
+
hotkey: "shortcut",
|
|
1201
|
+
keybinding: "shortcut",
|
|
388
1202
|
nav: "goto",
|
|
389
1203
|
open: "goto",
|
|
390
1204
|
visit: "goto",
|
|
@@ -404,7 +1218,17 @@ var ACTION_ALIASES = {
|
|
|
404
1218
|
image: "screenshot",
|
|
405
1219
|
pic: "screenshot",
|
|
406
1220
|
frame: "switchFrame",
|
|
407
|
-
iframe: "switchFrame"
|
|
1221
|
+
iframe: "switchFrame",
|
|
1222
|
+
assert_visible: "assertVisible",
|
|
1223
|
+
assert_exists: "assertExists",
|
|
1224
|
+
assert_text: "assertText",
|
|
1225
|
+
assert_url: "assertUrl",
|
|
1226
|
+
assert_value: "assertValue",
|
|
1227
|
+
checkvisible: "assertVisible",
|
|
1228
|
+
checkexists: "assertExists",
|
|
1229
|
+
checktext: "assertText",
|
|
1230
|
+
checkurl: "assertUrl",
|
|
1231
|
+
checkvalue: "assertValue"
|
|
408
1232
|
};
|
|
409
1233
|
var PROPERTY_ALIASES = {
|
|
410
1234
|
expression: "value",
|
|
@@ -425,6 +1249,9 @@ var PROPERTY_ALIASES = {
|
|
|
425
1249
|
input: "value",
|
|
426
1250
|
content: "value",
|
|
427
1251
|
keys: "key",
|
|
1252
|
+
shortcutKey: "combo",
|
|
1253
|
+
hotkey: "combo",
|
|
1254
|
+
keybinding: "combo",
|
|
428
1255
|
button: "key",
|
|
429
1256
|
address: "url",
|
|
430
1257
|
page: "url",
|
|
@@ -438,20 +1265,20 @@ var ACTION_RULES = {
|
|
|
438
1265
|
click: {
|
|
439
1266
|
required: { selector: { type: "string|string[]" } },
|
|
440
1267
|
optional: {
|
|
441
|
-
waitForNavigation: { type: "boolean" }
|
|
1268
|
+
waitForNavigation: { type: "boolean|auto" }
|
|
442
1269
|
}
|
|
443
1270
|
},
|
|
444
1271
|
fill: {
|
|
445
1272
|
required: { selector: { type: "string|string[]" }, value: { type: "string" } },
|
|
446
1273
|
optional: {
|
|
447
|
-
clear: { type: "boolean" },
|
|
448
1274
|
blur: { type: "boolean" }
|
|
449
1275
|
}
|
|
450
1276
|
},
|
|
451
1277
|
type: {
|
|
452
1278
|
required: { selector: { type: "string|string[]" }, value: { type: "string" } },
|
|
453
1279
|
optional: {
|
|
454
|
-
delay: { type: "number" }
|
|
1280
|
+
delay: { type: "number" },
|
|
1281
|
+
blur: { type: "boolean" }
|
|
455
1282
|
}
|
|
456
1283
|
},
|
|
457
1284
|
select: {
|
|
@@ -475,11 +1302,18 @@ var ACTION_RULES = {
|
|
|
475
1302
|
submit: {
|
|
476
1303
|
required: { selector: { type: "string|string[]" } },
|
|
477
1304
|
optional: {
|
|
478
|
-
method: { type: "string", enum: ["enter", "click", "enter+click"] }
|
|
1305
|
+
method: { type: "string", enum: ["enter", "click", "enter+click"] },
|
|
1306
|
+
waitForNavigation: { type: "boolean|auto" }
|
|
479
1307
|
}
|
|
480
1308
|
},
|
|
481
1309
|
press: {
|
|
482
1310
|
required: { key: { type: "string" } },
|
|
1311
|
+
optional: {
|
|
1312
|
+
modifiers: { type: "string|string[]" }
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
shortcut: {
|
|
1316
|
+
required: { combo: { type: "string" } },
|
|
483
1317
|
optional: {}
|
|
484
1318
|
},
|
|
485
1319
|
focus: {
|
|
@@ -539,6 +1373,36 @@ var ACTION_RULES = {
|
|
|
539
1373
|
switchToMain: {
|
|
540
1374
|
required: {},
|
|
541
1375
|
optional: {}
|
|
1376
|
+
},
|
|
1377
|
+
assertVisible: {
|
|
1378
|
+
required: { selector: { type: "string|string[]" } },
|
|
1379
|
+
optional: {}
|
|
1380
|
+
},
|
|
1381
|
+
assertExists: {
|
|
1382
|
+
required: { selector: { type: "string|string[]" } },
|
|
1383
|
+
optional: {}
|
|
1384
|
+
},
|
|
1385
|
+
assertText: {
|
|
1386
|
+
required: {},
|
|
1387
|
+
optional: {
|
|
1388
|
+
selector: { type: "string|string[]" },
|
|
1389
|
+
expect: { type: "string" },
|
|
1390
|
+
value: { type: "string" }
|
|
1391
|
+
}
|
|
1392
|
+
},
|
|
1393
|
+
assertUrl: {
|
|
1394
|
+
required: {},
|
|
1395
|
+
optional: {
|
|
1396
|
+
expect: { type: "string" },
|
|
1397
|
+
url: { type: "string" }
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
assertValue: {
|
|
1401
|
+
required: { selector: { type: "string|string[]" } },
|
|
1402
|
+
optional: {
|
|
1403
|
+
expect: { type: "string" },
|
|
1404
|
+
value: { type: "string" }
|
|
1405
|
+
}
|
|
542
1406
|
}
|
|
543
1407
|
};
|
|
544
1408
|
var VALID_ACTIONS = Object.keys(ACTION_RULES);
|
|
@@ -549,11 +1413,12 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
549
1413
|
"url",
|
|
550
1414
|
"value",
|
|
551
1415
|
"key",
|
|
1416
|
+
"combo",
|
|
1417
|
+
"modifiers",
|
|
552
1418
|
"waitFor",
|
|
553
1419
|
"timeout",
|
|
554
1420
|
"optional",
|
|
555
1421
|
"method",
|
|
556
|
-
"clear",
|
|
557
1422
|
"blur",
|
|
558
1423
|
"delay",
|
|
559
1424
|
"waitForNavigation",
|
|
@@ -566,7 +1431,10 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
566
1431
|
"amount",
|
|
567
1432
|
"format",
|
|
568
1433
|
"quality",
|
|
569
|
-
"fullPage"
|
|
1434
|
+
"fullPage",
|
|
1435
|
+
"expect",
|
|
1436
|
+
"retry",
|
|
1437
|
+
"retryDelay"
|
|
570
1438
|
]);
|
|
571
1439
|
function resolveAction(name) {
|
|
572
1440
|
if (VALID_ACTIONS.includes(name)) {
|
|
@@ -634,6 +1502,15 @@ function checkFieldType(value, rule) {
|
|
|
634
1502
|
case "boolean":
|
|
635
1503
|
if (typeof value !== "boolean") return `expected boolean, got ${typeof value}`;
|
|
636
1504
|
return null;
|
|
1505
|
+
case "boolean|auto":
|
|
1506
|
+
if (typeof value !== "boolean" && value !== "auto") {
|
|
1507
|
+
return `expected boolean or "auto", got ${typeof value}`;
|
|
1508
|
+
}
|
|
1509
|
+
return null;
|
|
1510
|
+
default: {
|
|
1511
|
+
const _exhaustive = rule.type;
|
|
1512
|
+
return `unknown type: ${_exhaustive}`;
|
|
1513
|
+
}
|
|
637
1514
|
}
|
|
638
1515
|
}
|
|
639
1516
|
function validateSteps(steps) {
|
|
@@ -747,6 +1624,51 @@ function validateSteps(steps) {
|
|
|
747
1624
|
});
|
|
748
1625
|
}
|
|
749
1626
|
}
|
|
1627
|
+
if ("retry" in obj && obj["retry"] !== void 0) {
|
|
1628
|
+
if (typeof obj["retry"] !== "number") {
|
|
1629
|
+
errors.push({
|
|
1630
|
+
stepIndex: i,
|
|
1631
|
+
field: "retry",
|
|
1632
|
+
message: `"retry" expected number, got ${typeof obj["retry"]}.`
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if ("retryDelay" in obj && obj["retryDelay"] !== void 0) {
|
|
1637
|
+
if (typeof obj["retryDelay"] !== "number") {
|
|
1638
|
+
errors.push({
|
|
1639
|
+
stepIndex: i,
|
|
1640
|
+
field: "retryDelay",
|
|
1641
|
+
message: `"retryDelay" expected number, got ${typeof obj["retryDelay"]}.`
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (action === "assertText") {
|
|
1646
|
+
if (!("expect" in obj) && !("value" in obj)) {
|
|
1647
|
+
errors.push({
|
|
1648
|
+
stepIndex: i,
|
|
1649
|
+
field: "expect",
|
|
1650
|
+
message: 'assertText requires "expect" or "value" containing the expected text.'
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
if (action === "assertUrl") {
|
|
1655
|
+
if (!("expect" in obj) && !("url" in obj)) {
|
|
1656
|
+
errors.push({
|
|
1657
|
+
stepIndex: i,
|
|
1658
|
+
field: "expect",
|
|
1659
|
+
message: 'assertUrl requires "expect" or "url" containing the expected URL substring.'
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (action === "assertValue") {
|
|
1664
|
+
if (!("expect" in obj) && !("value" in obj)) {
|
|
1665
|
+
errors.push({
|
|
1666
|
+
stepIndex: i,
|
|
1667
|
+
field: "expect",
|
|
1668
|
+
message: 'assertValue requires "expect" or "value" containing the expected value.'
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
750
1672
|
if (action === "select") {
|
|
751
1673
|
const hasNative = "selector" in obj && "value" in obj;
|
|
752
1674
|
const hasCustom = "trigger" in obj && "option" in obj && "value" in obj;
|
|
@@ -1899,7 +2821,7 @@ var AudioOutput = class {
|
|
|
1899
2821
|
awaitPromise: false
|
|
1900
2822
|
});
|
|
1901
2823
|
this.capturing = false;
|
|
1902
|
-
await
|
|
2824
|
+
await sleep2(250);
|
|
1903
2825
|
return this.mergeChunks();
|
|
1904
2826
|
}
|
|
1905
2827
|
/**
|
|
@@ -1924,35 +2846,37 @@ var AudioOutput = class {
|
|
|
1924
2846
|
let heardAudio = false;
|
|
1925
2847
|
let lastSoundTime = 0;
|
|
1926
2848
|
const startTime = Date.now();
|
|
1927
|
-
const checkInterval = setInterval(
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
if (
|
|
1940
|
-
heardAudio
|
|
1941
|
-
|
|
2849
|
+
const checkInterval = setInterval(() => {
|
|
2850
|
+
void (async () => {
|
|
2851
|
+
const elapsed = Date.now() - startTime;
|
|
2852
|
+
if (elapsed > maxDuration) {
|
|
2853
|
+
clearInterval(checkInterval);
|
|
2854
|
+
this.onDiagHandler?.(`max duration reached (${maxDuration}ms), stopping`);
|
|
2855
|
+
resolve(await this.stop());
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const latest = this.chunks[this.chunks.length - 1];
|
|
2859
|
+
if (latest) {
|
|
2860
|
+
const rms = calculateRMS(latest.left);
|
|
2861
|
+
if (rms > silenceThreshold) {
|
|
2862
|
+
if (!heardAudio) {
|
|
2863
|
+
heardAudio = true;
|
|
2864
|
+
this.onDiagHandler?.("first audio detected \u2014 silence countdown begins");
|
|
2865
|
+
}
|
|
2866
|
+
lastSoundTime = Date.now();
|
|
1942
2867
|
}
|
|
1943
|
-
lastSoundTime = Date.now();
|
|
1944
2868
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
}
|
|
2869
|
+
if (!heardAudio && elapsed > noAudioTimeout) {
|
|
2870
|
+
clearInterval(checkInterval);
|
|
2871
|
+
this.onDiagHandler?.(`no audio detected after ${noAudioTimeout}ms, stopping early`);
|
|
2872
|
+
resolve(await this.stop());
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
if (heardAudio && Date.now() - lastSoundTime > silenceTimeout) {
|
|
2876
|
+
clearInterval(checkInterval);
|
|
2877
|
+
resolve(await this.stop());
|
|
2878
|
+
}
|
|
2879
|
+
})();
|
|
1956
2880
|
}, 200);
|
|
1957
2881
|
});
|
|
1958
2882
|
}
|
|
@@ -2113,7 +3037,7 @@ function emptyCaptureResult() {
|
|
|
2113
3037
|
chunkCount: 0
|
|
2114
3038
|
};
|
|
2115
3039
|
}
|
|
2116
|
-
function
|
|
3040
|
+
function sleep2(ms) {
|
|
2117
3041
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2118
3042
|
}
|
|
2119
3043
|
|
|
@@ -2212,18 +3136,6 @@ Content-Type: ${contentType}\r
|
|
|
2212
3136
|
parts.push(data);
|
|
2213
3137
|
}
|
|
2214
3138
|
|
|
2215
|
-
// src/cdp/protocol.ts
|
|
2216
|
-
var CDPError = class extends Error {
|
|
2217
|
-
code;
|
|
2218
|
-
data;
|
|
2219
|
-
constructor(error) {
|
|
2220
|
-
super(error.message);
|
|
2221
|
-
this.name = "CDPError";
|
|
2222
|
-
this.code = error.code;
|
|
2223
|
-
this.data = error.data;
|
|
2224
|
-
}
|
|
2225
|
-
};
|
|
2226
|
-
|
|
2227
3139
|
// src/cdp/transport.ts
|
|
2228
3140
|
function createTransport(wsUrl, options = {}) {
|
|
2229
3141
|
const { timeout = 3e4 } = options;
|
|
@@ -2253,13 +3165,28 @@ function createTransport(wsUrl, options = {}) {
|
|
|
2253
3165
|
resolveClose();
|
|
2254
3166
|
return;
|
|
2255
3167
|
}
|
|
2256
|
-
|
|
3168
|
+
let settled = false;
|
|
3169
|
+
let fallbackTimer;
|
|
3170
|
+
const finish = () => {
|
|
3171
|
+
if (settled) return;
|
|
3172
|
+
settled = true;
|
|
3173
|
+
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
2257
3174
|
ws.removeEventListener("close", onClose);
|
|
2258
3175
|
resolveClose();
|
|
2259
3176
|
};
|
|
3177
|
+
const onClose = () => {
|
|
3178
|
+
finish();
|
|
3179
|
+
};
|
|
2260
3180
|
ws.addEventListener("close", onClose);
|
|
2261
|
-
|
|
2262
|
-
|
|
3181
|
+
try {
|
|
3182
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
3183
|
+
ws.close();
|
|
3184
|
+
}
|
|
3185
|
+
} catch {
|
|
3186
|
+
finish();
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
fallbackTimer = setTimeout(finish, 200);
|
|
2263
3190
|
});
|
|
2264
3191
|
},
|
|
2265
3192
|
onMessage(handler) {
|
|
@@ -2589,6 +3516,34 @@ var BrowserlessProvider = class {
|
|
|
2589
3516
|
};
|
|
2590
3517
|
|
|
2591
3518
|
// src/providers/generic.ts
|
|
3519
|
+
function sleep3(ms) {
|
|
3520
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3521
|
+
}
|
|
3522
|
+
async function fetchDevToolsJson(host, path, errorPrefix, options = {}) {
|
|
3523
|
+
const protocol = host.includes("://") ? "" : "http://";
|
|
3524
|
+
const attempts = options.attempts ?? 1;
|
|
3525
|
+
let delayMs = options.initialDelayMs ?? 50;
|
|
3526
|
+
const maxDelayMs = options.maxDelayMs ?? 250;
|
|
3527
|
+
let lastError;
|
|
3528
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
3529
|
+
try {
|
|
3530
|
+
const response = await fetch(`${protocol}${host}${path}`);
|
|
3531
|
+
if (response.ok) {
|
|
3532
|
+
return await response.json();
|
|
3533
|
+
}
|
|
3534
|
+
lastError = new Error(`${errorPrefix}: ${response.status}`);
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
lastError = new Error(
|
|
3537
|
+
`${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`
|
|
3538
|
+
);
|
|
3539
|
+
}
|
|
3540
|
+
if (attempt < attempts) {
|
|
3541
|
+
await sleep3(delayMs);
|
|
3542
|
+
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
throw lastError ?? new Error(errorPrefix);
|
|
3546
|
+
}
|
|
2592
3547
|
var GenericProvider = class {
|
|
2593
3548
|
name = "generic";
|
|
2594
3549
|
wsUrl;
|
|
@@ -2607,20 +3562,14 @@ var GenericProvider = class {
|
|
|
2607
3562
|
}
|
|
2608
3563
|
};
|
|
2609
3564
|
async function discoverTargets(host = "localhost:9222") {
|
|
2610
|
-
|
|
2611
|
-
const response = await fetch(`${protocol}${host}/json/list`);
|
|
2612
|
-
if (!response.ok) {
|
|
2613
|
-
throw new Error(`Failed to discover targets: ${response.status}`);
|
|
2614
|
-
}
|
|
2615
|
-
return await response.json();
|
|
3565
|
+
return fetchDevToolsJson(host, "/json/list", "Failed to discover targets");
|
|
2616
3566
|
}
|
|
2617
3567
|
async function getBrowserWebSocketUrl(host = "localhost:9222") {
|
|
2618
|
-
const
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
}
|
|
2623
|
-
const info = await response.json();
|
|
3568
|
+
const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
|
|
3569
|
+
attempts: 10,
|
|
3570
|
+
initialDelayMs: 50,
|
|
3571
|
+
maxDelayMs: 250
|
|
3572
|
+
});
|
|
2624
3573
|
return info.webSocketDebuggerUrl;
|
|
2625
3574
|
}
|
|
2626
3575
|
|
|
@@ -2667,8 +3616,12 @@ var RequestInterceptor = class {
|
|
|
2667
3616
|
boundHandleAuthRequired;
|
|
2668
3617
|
constructor(cdp) {
|
|
2669
3618
|
this.cdp = cdp;
|
|
2670
|
-
this.boundHandleRequestPaused =
|
|
2671
|
-
|
|
3619
|
+
this.boundHandleRequestPaused = (params) => {
|
|
3620
|
+
void this.handleRequestPaused(params);
|
|
3621
|
+
};
|
|
3622
|
+
this.boundHandleAuthRequired = (params) => {
|
|
3623
|
+
void this.handleAuthRequired(params);
|
|
3624
|
+
};
|
|
2672
3625
|
}
|
|
2673
3626
|
/**
|
|
2674
3627
|
* Enable request interception with optional patterns
|
|
@@ -2895,429 +3848,463 @@ async function isElementVisible(cdp, selector, contextId) {
|
|
|
2895
3848
|
}
|
|
2896
3849
|
async function isElementAttached(cdp, selector, contextId) {
|
|
2897
3850
|
const params = {
|
|
2898
|
-
expression: `(() => {
|
|
2899
|
-
${DEEP_QUERY_SCRIPT}
|
|
2900
|
-
return deepQuery(${JSON.stringify(selector)}) !== null;
|
|
2901
|
-
})()`,
|
|
2902
|
-
returnByValue: true
|
|
2903
|
-
};
|
|
2904
|
-
if (contextId !== void 0) {
|
|
2905
|
-
params["contextId"] = contextId;
|
|
2906
|
-
}
|
|
2907
|
-
const result = await cdp.send("Runtime.evaluate", params);
|
|
2908
|
-
return result.result.value === true;
|
|
2909
|
-
}
|
|
2910
|
-
function sleep2(ms) {
|
|
2911
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2912
|
-
}
|
|
2913
|
-
async function waitForElement(cdp, selector, options = {}) {
|
|
2914
|
-
const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
|
|
2915
|
-
const startTime = Date.now();
|
|
2916
|
-
const deadline = startTime + timeout;
|
|
2917
|
-
while (Date.now() < deadline) {
|
|
2918
|
-
let conditionMet = false;
|
|
2919
|
-
switch (state) {
|
|
2920
|
-
case "visible":
|
|
2921
|
-
conditionMet = await isElementVisible(cdp, selector, contextId);
|
|
2922
|
-
break;
|
|
2923
|
-
case "hidden":
|
|
2924
|
-
conditionMet = !await isElementVisible(cdp, selector, contextId);
|
|
2925
|
-
break;
|
|
2926
|
-
case "attached":
|
|
2927
|
-
conditionMet = await isElementAttached(cdp, selector, contextId);
|
|
2928
|
-
break;
|
|
2929
|
-
case "detached":
|
|
2930
|
-
conditionMet = !await isElementAttached(cdp, selector, contextId);
|
|
2931
|
-
break;
|
|
2932
|
-
}
|
|
2933
|
-
if (conditionMet) {
|
|
2934
|
-
return { success: true, waitedMs: Date.now() - startTime };
|
|
2935
|
-
}
|
|
2936
|
-
await sleep2(pollInterval);
|
|
2937
|
-
}
|
|
2938
|
-
return { success: false, waitedMs: Date.now() - startTime };
|
|
2939
|
-
}
|
|
2940
|
-
async function waitForAnyElement(cdp, selectors, options = {}) {
|
|
2941
|
-
const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
|
|
2942
|
-
const startTime = Date.now();
|
|
2943
|
-
const deadline = startTime + timeout;
|
|
2944
|
-
while (Date.now() < deadline) {
|
|
2945
|
-
for (const selector of selectors) {
|
|
2946
|
-
let conditionMet = false;
|
|
2947
|
-
switch (state) {
|
|
2948
|
-
case "visible":
|
|
2949
|
-
conditionMet = await isElementVisible(cdp, selector, contextId);
|
|
2950
|
-
break;
|
|
2951
|
-
case "hidden":
|
|
2952
|
-
conditionMet = !await isElementVisible(cdp, selector, contextId);
|
|
2953
|
-
break;
|
|
2954
|
-
case "attached":
|
|
2955
|
-
conditionMet = await isElementAttached(cdp, selector, contextId);
|
|
2956
|
-
break;
|
|
2957
|
-
case "detached":
|
|
2958
|
-
conditionMet = !await isElementAttached(cdp, selector, contextId);
|
|
2959
|
-
break;
|
|
2960
|
-
}
|
|
2961
|
-
if (conditionMet) {
|
|
2962
|
-
return { success: true, selector, waitedMs: Date.now() - startTime };
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
2965
|
-
await sleep2(pollInterval);
|
|
2966
|
-
}
|
|
2967
|
-
return { success: false, waitedMs: Date.now() - startTime };
|
|
2968
|
-
}
|
|
2969
|
-
async function getCurrentUrl(cdp) {
|
|
2970
|
-
const result = await cdp.send("Runtime.evaluate", {
|
|
2971
|
-
expression: "location.href",
|
|
2972
|
-
returnByValue: true
|
|
2973
|
-
});
|
|
2974
|
-
return result.result.value;
|
|
2975
|
-
}
|
|
2976
|
-
async function waitForNavigation(cdp, options = {}) {
|
|
2977
|
-
const { timeout = 3e4, allowSameDocument = true } = options;
|
|
2978
|
-
const startTime = Date.now();
|
|
2979
|
-
let startUrl;
|
|
2980
|
-
try {
|
|
2981
|
-
startUrl = await getCurrentUrl(cdp);
|
|
2982
|
-
} catch {
|
|
2983
|
-
startUrl = "";
|
|
2984
|
-
}
|
|
2985
|
-
return new Promise((resolve) => {
|
|
2986
|
-
let resolved = false;
|
|
2987
|
-
const cleanup = [];
|
|
2988
|
-
const done = (success) => {
|
|
2989
|
-
if (resolved) return;
|
|
2990
|
-
resolved = true;
|
|
2991
|
-
for (const fn of cleanup) fn();
|
|
2992
|
-
resolve({ success, waitedMs: Date.now() - startTime });
|
|
2993
|
-
};
|
|
2994
|
-
const timer = setTimeout(() => done(false), timeout);
|
|
2995
|
-
cleanup.push(() => clearTimeout(timer));
|
|
2996
|
-
const onLoad = () => done(true);
|
|
2997
|
-
cdp.on("Page.loadEventFired", onLoad);
|
|
2998
|
-
cleanup.push(() => cdp.off("Page.loadEventFired", onLoad));
|
|
2999
|
-
const onFrameNavigated = (params) => {
|
|
3000
|
-
const frame = params["frame"];
|
|
3001
|
-
if (frame && !frame.parentId && frame.url !== startUrl) {
|
|
3002
|
-
done(true);
|
|
3003
|
-
}
|
|
3004
|
-
};
|
|
3005
|
-
cdp.on("Page.frameNavigated", onFrameNavigated);
|
|
3006
|
-
cleanup.push(() => cdp.off("Page.frameNavigated", onFrameNavigated));
|
|
3007
|
-
if (allowSameDocument) {
|
|
3008
|
-
const onSameDoc = () => done(true);
|
|
3009
|
-
cdp.on("Page.navigatedWithinDocument", onSameDoc);
|
|
3010
|
-
cleanup.push(() => cdp.off("Page.navigatedWithinDocument", onSameDoc));
|
|
3011
|
-
}
|
|
3012
|
-
const pollUrl = async () => {
|
|
3013
|
-
while (!resolved && Date.now() < startTime + timeout) {
|
|
3014
|
-
await sleep2(100);
|
|
3015
|
-
if (resolved) return;
|
|
3016
|
-
try {
|
|
3017
|
-
const currentUrl = await getCurrentUrl(cdp);
|
|
3018
|
-
if (startUrl && currentUrl !== startUrl) {
|
|
3019
|
-
done(true);
|
|
3020
|
-
return;
|
|
3021
|
-
}
|
|
3022
|
-
} catch {
|
|
3023
|
-
}
|
|
3024
|
-
}
|
|
3025
|
-
};
|
|
3026
|
-
pollUrl();
|
|
3027
|
-
});
|
|
3028
|
-
}
|
|
3029
|
-
async function waitForNetworkIdle(cdp, options = {}) {
|
|
3030
|
-
const { timeout = 3e4, idleTime = 500 } = options;
|
|
3031
|
-
const startTime = Date.now();
|
|
3032
|
-
await cdp.send("Network.enable");
|
|
3033
|
-
return new Promise((resolve) => {
|
|
3034
|
-
let inFlight = 0;
|
|
3035
|
-
let idleTimer = null;
|
|
3036
|
-
const timeoutTimer = setTimeout(() => {
|
|
3037
|
-
cleanup();
|
|
3038
|
-
resolve({ success: false, waitedMs: Date.now() - startTime });
|
|
3039
|
-
}, timeout);
|
|
3040
|
-
const checkIdle = () => {
|
|
3041
|
-
if (inFlight === 0) {
|
|
3042
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
3043
|
-
idleTimer = setTimeout(() => {
|
|
3044
|
-
cleanup();
|
|
3045
|
-
resolve({ success: true, waitedMs: Date.now() - startTime });
|
|
3046
|
-
}, idleTime);
|
|
3047
|
-
}
|
|
3048
|
-
};
|
|
3049
|
-
const onRequestStart = () => {
|
|
3050
|
-
inFlight++;
|
|
3051
|
-
if (idleTimer) {
|
|
3052
|
-
clearTimeout(idleTimer);
|
|
3053
|
-
idleTimer = null;
|
|
3054
|
-
}
|
|
3055
|
-
};
|
|
3056
|
-
const onRequestEnd = () => {
|
|
3057
|
-
inFlight = Math.max(0, inFlight - 1);
|
|
3058
|
-
checkIdle();
|
|
3059
|
-
};
|
|
3060
|
-
const cleanup = () => {
|
|
3061
|
-
clearTimeout(timeoutTimer);
|
|
3062
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
3063
|
-
cdp.off("Network.requestWillBeSent", onRequestStart);
|
|
3064
|
-
cdp.off("Network.loadingFinished", onRequestEnd);
|
|
3065
|
-
cdp.off("Network.loadingFailed", onRequestEnd);
|
|
3066
|
-
};
|
|
3067
|
-
cdp.on("Network.requestWillBeSent", onRequestStart);
|
|
3068
|
-
cdp.on("Network.loadingFinished", onRequestEnd);
|
|
3069
|
-
cdp.on("Network.loadingFailed", onRequestEnd);
|
|
3070
|
-
checkIdle();
|
|
3071
|
-
});
|
|
3072
|
-
}
|
|
3073
|
-
|
|
3074
|
-
// src/browser/fuzzy-match.ts
|
|
3075
|
-
function jaroWinkler(a, b) {
|
|
3076
|
-
if (a.length === 0 && b.length === 0) return 0;
|
|
3077
|
-
if (a.length === 0 || b.length === 0) return 0;
|
|
3078
|
-
if (a === b) return 1;
|
|
3079
|
-
const s1 = a.toLowerCase();
|
|
3080
|
-
const s2 = b.toLowerCase();
|
|
3081
|
-
const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
|
|
3082
|
-
const s1Matches = new Array(s1.length).fill(false);
|
|
3083
|
-
const s2Matches = new Array(s2.length).fill(false);
|
|
3084
|
-
let matches = 0;
|
|
3085
|
-
let transpositions = 0;
|
|
3086
|
-
for (let i = 0; i < s1.length; i++) {
|
|
3087
|
-
const start = Math.max(0, i - matchWindow);
|
|
3088
|
-
const end = Math.min(i + matchWindow + 1, s2.length);
|
|
3089
|
-
for (let j = start; j < end; j++) {
|
|
3090
|
-
if (s2Matches[j] || s1[i] !== s2[j]) continue;
|
|
3091
|
-
s1Matches[i] = true;
|
|
3092
|
-
s2Matches[j] = true;
|
|
3093
|
-
matches++;
|
|
3094
|
-
break;
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
if (matches === 0) return 0;
|
|
3098
|
-
let k = 0;
|
|
3099
|
-
for (let i = 0; i < s1.length; i++) {
|
|
3100
|
-
if (!s1Matches[i]) continue;
|
|
3101
|
-
while (!s2Matches[k]) k++;
|
|
3102
|
-
if (s1[i] !== s2[k]) transpositions++;
|
|
3103
|
-
k++;
|
|
3104
|
-
}
|
|
3105
|
-
const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
|
|
3106
|
-
let prefix = 0;
|
|
3107
|
-
for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
|
|
3108
|
-
if (s1[i] === s2[i]) {
|
|
3109
|
-
prefix++;
|
|
3110
|
-
} else {
|
|
3111
|
-
break;
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
const WINKLER_SCALING = 0.1;
|
|
3115
|
-
return jaro + prefix * WINKLER_SCALING * (1 - jaro);
|
|
3116
|
-
}
|
|
3117
|
-
function stringSimilarity(a, b) {
|
|
3118
|
-
if (a.length === 0 || b.length === 0) return 0;
|
|
3119
|
-
const lowerA = a.toLowerCase();
|
|
3120
|
-
const lowerB = b.toLowerCase();
|
|
3121
|
-
if (lowerA === lowerB) return 1;
|
|
3122
|
-
const jw = jaroWinkler(a, b);
|
|
3123
|
-
let containsBonus = 0;
|
|
3124
|
-
if (lowerB.includes(lowerA)) {
|
|
3125
|
-
containsBonus = 0.2;
|
|
3126
|
-
} else if (lowerA.includes(lowerB)) {
|
|
3127
|
-
containsBonus = 0.1;
|
|
3128
|
-
}
|
|
3129
|
-
return Math.min(1, jw + containsBonus);
|
|
3130
|
-
}
|
|
3131
|
-
function scoreElement(query, element) {
|
|
3132
|
-
const lowerQuery = query.toLowerCase();
|
|
3133
|
-
const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
|
|
3134
|
-
let nameScore = 0;
|
|
3135
|
-
if (element.name) {
|
|
3136
|
-
const lowerName = element.name.toLowerCase();
|
|
3137
|
-
if (lowerName === lowerQuery) {
|
|
3138
|
-
nameScore = 1;
|
|
3139
|
-
} else if (lowerName.includes(lowerQuery)) {
|
|
3140
|
-
nameScore = 0.8;
|
|
3141
|
-
} else if (words.length > 0) {
|
|
3142
|
-
const matchedWords = words.filter((w) => lowerName.includes(w));
|
|
3143
|
-
nameScore = matchedWords.length / words.length * 0.7;
|
|
3144
|
-
} else {
|
|
3145
|
-
nameScore = stringSimilarity(query, element.name) * 0.6;
|
|
3146
|
-
}
|
|
3147
|
-
}
|
|
3148
|
-
let roleScore = 0;
|
|
3149
|
-
const lowerRole = element.role.toLowerCase();
|
|
3150
|
-
if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
|
|
3151
|
-
roleScore = 0.3;
|
|
3152
|
-
} else if (words.some((w) => lowerRole.includes(w))) {
|
|
3153
|
-
roleScore = 0.2;
|
|
3851
|
+
expression: `(() => {
|
|
3852
|
+
${DEEP_QUERY_SCRIPT}
|
|
3853
|
+
return deepQuery(${JSON.stringify(selector)}) !== null;
|
|
3854
|
+
})()`,
|
|
3855
|
+
returnByValue: true
|
|
3856
|
+
};
|
|
3857
|
+
if (contextId !== void 0) {
|
|
3858
|
+
params["contextId"] = contextId;
|
|
3154
3859
|
}
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3860
|
+
const result = await cdp.send("Runtime.evaluate", params);
|
|
3861
|
+
return result.result.value === true;
|
|
3862
|
+
}
|
|
3863
|
+
function sleep4(ms) {
|
|
3864
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3865
|
+
}
|
|
3866
|
+
async function isPageStatic(cdp, windowMs = 200, contextId) {
|
|
3867
|
+
const params = {
|
|
3868
|
+
expression: `new Promise(resolve => {
|
|
3869
|
+
// If page is still loading, it's not static
|
|
3870
|
+
if (document.readyState !== 'complete') { resolve(false); return; }
|
|
3871
|
+
// Check for recent page load (navigationStart within last 1s = page just loaded)
|
|
3872
|
+
try {
|
|
3873
|
+
var nav = performance.getEntriesByType('navigation')[0];
|
|
3874
|
+
if (nav && (performance.now() - nav.loadEventEnd) < 500) { resolve(false); return; }
|
|
3875
|
+
} catch(e) {}
|
|
3876
|
+
// Observe for DOM mutations
|
|
3877
|
+
var seen = false;
|
|
3878
|
+
var obs = new MutationObserver(function() { seen = true; });
|
|
3879
|
+
obs.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
|
|
3880
|
+
setTimeout(function() { obs.disconnect(); resolve(!seen); }, ${windowMs});
|
|
3881
|
+
})`,
|
|
3882
|
+
returnByValue: true,
|
|
3883
|
+
awaitPromise: true
|
|
3884
|
+
};
|
|
3885
|
+
if (contextId !== void 0) params["contextId"] = contextId;
|
|
3886
|
+
try {
|
|
3887
|
+
const result = await cdp.send("Runtime.evaluate", params);
|
|
3888
|
+
return result.result.value === true;
|
|
3889
|
+
} catch {
|
|
3890
|
+
return false;
|
|
3159
3891
|
}
|
|
3160
|
-
const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
|
|
3161
|
-
return totalScore;
|
|
3162
3892
|
}
|
|
3163
|
-
function
|
|
3164
|
-
const
|
|
3165
|
-
const
|
|
3166
|
-
const
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3893
|
+
async function waitForElement(cdp, selector, options = {}) {
|
|
3894
|
+
const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
|
|
3895
|
+
const startTime = Date.now();
|
|
3896
|
+
const deadline = startTime + timeout;
|
|
3897
|
+
const checkCondition = async () => {
|
|
3898
|
+
switch (state) {
|
|
3899
|
+
case "visible":
|
|
3900
|
+
return isElementVisible(cdp, selector, contextId);
|
|
3901
|
+
case "hidden":
|
|
3902
|
+
return !await isElementVisible(cdp, selector, contextId);
|
|
3903
|
+
case "attached":
|
|
3904
|
+
return isElementAttached(cdp, selector, contextId);
|
|
3905
|
+
case "detached":
|
|
3906
|
+
return !await isElementAttached(cdp, selector, contextId);
|
|
3907
|
+
default: {
|
|
3908
|
+
const _exhaustive = state;
|
|
3909
|
+
throw new Error(`Unhandled wait state: ${_exhaustive}`);
|
|
3910
|
+
}
|
|
3178
3911
|
}
|
|
3912
|
+
};
|
|
3913
|
+
if (await checkCondition()) {
|
|
3914
|
+
return { success: true, waitedMs: Date.now() - startTime };
|
|
3179
3915
|
}
|
|
3180
|
-
const
|
|
3181
|
-
if (
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3916
|
+
const waitingForPresence = state === "visible" || state === "attached";
|
|
3917
|
+
if (waitingForPresence && timeout >= 300) {
|
|
3918
|
+
const pageStatic = await isPageStatic(cdp, 200, contextId);
|
|
3919
|
+
if (pageStatic) {
|
|
3920
|
+
if (await checkCondition()) {
|
|
3921
|
+
return { success: true, waitedMs: Date.now() - startTime };
|
|
3922
|
+
}
|
|
3923
|
+
return { success: false, waitedMs: Date.now() - startTime };
|
|
3924
|
+
}
|
|
3189
3925
|
}
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3926
|
+
while (Date.now() < deadline) {
|
|
3927
|
+
await sleep4(pollInterval);
|
|
3928
|
+
if (await checkCondition()) {
|
|
3929
|
+
return { success: true, waitedMs: Date.now() - startTime };
|
|
3930
|
+
}
|
|
3195
3931
|
}
|
|
3196
|
-
|
|
3197
|
-
const scored = elements.map((element) => ({
|
|
3198
|
-
element,
|
|
3199
|
-
score: scoreElement(query, element)
|
|
3200
|
-
}));
|
|
3201
|
-
return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
|
|
3202
|
-
element: s.element,
|
|
3203
|
-
score: s.score,
|
|
3204
|
-
matchReason: explainMatch(query, s.element, s.score)
|
|
3205
|
-
}));
|
|
3932
|
+
return { success: false, waitedMs: Date.now() - startTime };
|
|
3206
3933
|
}
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
let text = "";
|
|
3226
|
-
for (const selector of selectors) {
|
|
3227
|
-
if (selector.startsWith("ref:")) {
|
|
3228
|
-
continue;
|
|
3229
|
-
}
|
|
3230
|
-
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
|
|
3231
|
-
if (idMatch) {
|
|
3232
|
-
patterns.push(idMatch[1]);
|
|
3934
|
+
async function waitForAnyElement(cdp, selectors, options = {}) {
|
|
3935
|
+
const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
|
|
3936
|
+
const startTime = Date.now();
|
|
3937
|
+
const deadline = startTime + timeout;
|
|
3938
|
+
const checkSelector = async (selector) => {
|
|
3939
|
+
switch (state) {
|
|
3940
|
+
case "visible":
|
|
3941
|
+
return isElementVisible(cdp, selector, contextId);
|
|
3942
|
+
case "hidden":
|
|
3943
|
+
return !await isElementVisible(cdp, selector, contextId);
|
|
3944
|
+
case "attached":
|
|
3945
|
+
return isElementAttached(cdp, selector, contextId);
|
|
3946
|
+
case "detached":
|
|
3947
|
+
return !await isElementAttached(cdp, selector, contextId);
|
|
3948
|
+
default: {
|
|
3949
|
+
const _exhaustive = state;
|
|
3950
|
+
throw new Error(`Unhandled wait state: ${_exhaustive}`);
|
|
3951
|
+
}
|
|
3233
3952
|
}
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3953
|
+
};
|
|
3954
|
+
for (const selector of selectors) {
|
|
3955
|
+
if (await checkSelector(selector)) {
|
|
3956
|
+
return { success: true, selector, waitedMs: Date.now() - startTime };
|
|
3237
3957
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3958
|
+
}
|
|
3959
|
+
const waitingForPresence = state === "visible" || state === "attached";
|
|
3960
|
+
if (waitingForPresence && timeout >= 300) {
|
|
3961
|
+
const pageStatic = await isPageStatic(cdp, 200, contextId);
|
|
3962
|
+
if (pageStatic) {
|
|
3963
|
+
for (const selector of selectors) {
|
|
3964
|
+
if (await checkSelector(selector)) {
|
|
3965
|
+
return { success: true, selector, waitedMs: Date.now() - startTime };
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
return { success: false, waitedMs: Date.now() - startTime };
|
|
3241
3969
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3970
|
+
}
|
|
3971
|
+
while (Date.now() < deadline) {
|
|
3972
|
+
await sleep4(pollInterval);
|
|
3973
|
+
for (const selector of selectors) {
|
|
3974
|
+
if (await checkSelector(selector)) {
|
|
3975
|
+
return { success: true, selector, waitedMs: Date.now() - startTime };
|
|
3976
|
+
}
|
|
3245
3977
|
}
|
|
3246
3978
|
}
|
|
3247
|
-
|
|
3248
|
-
text = patterns[0] ?? selectors[0] ?? "";
|
|
3249
|
-
return { text, patterns };
|
|
3250
|
-
}
|
|
3251
|
-
function getHintType(selector) {
|
|
3252
|
-
if (selector.startsWith("ref:")) return "ref";
|
|
3253
|
-
if (selector.includes("data-testid")) return "testid";
|
|
3254
|
-
if (selector.includes("aria-label")) return "aria";
|
|
3255
|
-
if (selector.startsWith("#")) return "id";
|
|
3256
|
-
return "css";
|
|
3979
|
+
return { success: false, waitedMs: Date.now() - startTime };
|
|
3257
3980
|
}
|
|
3258
|
-
function
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3981
|
+
async function getCurrentUrl(cdp) {
|
|
3982
|
+
const result = await cdp.send("Runtime.evaluate", {
|
|
3983
|
+
expression: "location.href",
|
|
3984
|
+
returnByValue: true
|
|
3985
|
+
});
|
|
3986
|
+
return result.result.value;
|
|
3262
3987
|
}
|
|
3263
|
-
function
|
|
3264
|
-
const
|
|
3265
|
-
const
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
hints.push({
|
|
3272
|
-
selector: refSelector,
|
|
3273
|
-
reason: candidate.matchReason,
|
|
3274
|
-
confidence: getConfidence(candidate.score),
|
|
3275
|
-
element: {
|
|
3276
|
-
ref: candidate.element.ref,
|
|
3277
|
-
role: candidate.element.role,
|
|
3278
|
-
name: candidate.element.name,
|
|
3279
|
-
disabled: candidate.element.disabled
|
|
3280
|
-
}
|
|
3281
|
-
});
|
|
3282
|
-
usedTypes.add(hintType);
|
|
3283
|
-
} else if (hints.length < maxHints) {
|
|
3284
|
-
hints.push({
|
|
3285
|
-
selector: refSelector,
|
|
3286
|
-
reason: candidate.matchReason,
|
|
3287
|
-
confidence: getConfidence(candidate.score),
|
|
3288
|
-
element: {
|
|
3289
|
-
ref: candidate.element.ref,
|
|
3290
|
-
role: candidate.element.role,
|
|
3291
|
-
name: candidate.element.name,
|
|
3292
|
-
disabled: candidate.element.disabled
|
|
3293
|
-
}
|
|
3294
|
-
});
|
|
3295
|
-
}
|
|
3988
|
+
async function waitForNavigation(cdp, options = {}) {
|
|
3989
|
+
const { timeout = 3e4, allowSameDocument = true } = options;
|
|
3990
|
+
const startTime = Date.now();
|
|
3991
|
+
let startUrl;
|
|
3992
|
+
try {
|
|
3993
|
+
startUrl = await getCurrentUrl(cdp);
|
|
3994
|
+
} catch {
|
|
3995
|
+
startUrl = "";
|
|
3296
3996
|
}
|
|
3297
|
-
return
|
|
3997
|
+
return new Promise((resolve) => {
|
|
3998
|
+
let resolved = false;
|
|
3999
|
+
const cleanup = [];
|
|
4000
|
+
const done = (success) => {
|
|
4001
|
+
if (resolved) return;
|
|
4002
|
+
resolved = true;
|
|
4003
|
+
for (const fn of cleanup) fn();
|
|
4004
|
+
resolve({ success, waitedMs: Date.now() - startTime });
|
|
4005
|
+
};
|
|
4006
|
+
const timer = setTimeout(() => done(false), timeout);
|
|
4007
|
+
cleanup.push(() => clearTimeout(timer));
|
|
4008
|
+
const onLoad = () => done(true);
|
|
4009
|
+
cdp.on("Page.loadEventFired", onLoad);
|
|
4010
|
+
cleanup.push(() => cdp.off("Page.loadEventFired", onLoad));
|
|
4011
|
+
const onFrameNavigated = (params) => {
|
|
4012
|
+
const frame = params["frame"];
|
|
4013
|
+
if (frame && !frame.parentId && frame.url !== startUrl) {
|
|
4014
|
+
done(true);
|
|
4015
|
+
}
|
|
4016
|
+
};
|
|
4017
|
+
cdp.on("Page.frameNavigated", onFrameNavigated);
|
|
4018
|
+
cleanup.push(() => cdp.off("Page.frameNavigated", onFrameNavigated));
|
|
4019
|
+
if (allowSameDocument) {
|
|
4020
|
+
const onSameDoc = () => done(true);
|
|
4021
|
+
cdp.on("Page.navigatedWithinDocument", onSameDoc);
|
|
4022
|
+
cleanup.push(() => cdp.off("Page.navigatedWithinDocument", onSameDoc));
|
|
4023
|
+
}
|
|
4024
|
+
const onLifecycle = (params) => {
|
|
4025
|
+
if (params["name"] === "networkIdle") {
|
|
4026
|
+
done(true);
|
|
4027
|
+
}
|
|
4028
|
+
};
|
|
4029
|
+
cdp.on("Page.lifecycleEvent", onLifecycle);
|
|
4030
|
+
cleanup.push(() => cdp.off("Page.lifecycleEvent", onLifecycle));
|
|
4031
|
+
const pollUrl = async () => {
|
|
4032
|
+
while (!resolved && Date.now() < startTime + timeout) {
|
|
4033
|
+
await sleep4(100);
|
|
4034
|
+
if (resolved) return;
|
|
4035
|
+
try {
|
|
4036
|
+
const currentUrl = await getCurrentUrl(cdp);
|
|
4037
|
+
if (startUrl && currentUrl !== startUrl) {
|
|
4038
|
+
done(true);
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
} catch {
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
};
|
|
4045
|
+
void pollUrl();
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
async function waitForNetworkIdle(cdp, options = {}) {
|
|
4049
|
+
const { timeout = 3e4, idleTime = 500 } = options;
|
|
4050
|
+
const startTime = Date.now();
|
|
4051
|
+
await cdp.send("Network.enable");
|
|
4052
|
+
return new Promise((resolve) => {
|
|
4053
|
+
let inFlight = 0;
|
|
4054
|
+
let idleTimer = null;
|
|
4055
|
+
const timeoutTimer = setTimeout(() => {
|
|
4056
|
+
cleanup();
|
|
4057
|
+
resolve({ success: false, waitedMs: Date.now() - startTime });
|
|
4058
|
+
}, timeout);
|
|
4059
|
+
const checkIdle = () => {
|
|
4060
|
+
if (inFlight === 0) {
|
|
4061
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
4062
|
+
idleTimer = setTimeout(() => {
|
|
4063
|
+
cleanup();
|
|
4064
|
+
resolve({ success: true, waitedMs: Date.now() - startTime });
|
|
4065
|
+
}, idleTime);
|
|
4066
|
+
}
|
|
4067
|
+
};
|
|
4068
|
+
const onRequestStart = () => {
|
|
4069
|
+
inFlight++;
|
|
4070
|
+
if (idleTimer) {
|
|
4071
|
+
clearTimeout(idleTimer);
|
|
4072
|
+
idleTimer = null;
|
|
4073
|
+
}
|
|
4074
|
+
};
|
|
4075
|
+
const onRequestEnd = () => {
|
|
4076
|
+
inFlight = Math.max(0, inFlight - 1);
|
|
4077
|
+
checkIdle();
|
|
4078
|
+
};
|
|
4079
|
+
const cleanup = () => {
|
|
4080
|
+
clearTimeout(timeoutTimer);
|
|
4081
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
4082
|
+
cdp.off("Network.requestWillBeSent", onRequestStart);
|
|
4083
|
+
cdp.off("Network.loadingFinished", onRequestEnd);
|
|
4084
|
+
cdp.off("Network.loadingFailed", onRequestEnd);
|
|
4085
|
+
};
|
|
4086
|
+
cdp.on("Network.requestWillBeSent", onRequestStart);
|
|
4087
|
+
cdp.on("Network.loadingFinished", onRequestEnd);
|
|
4088
|
+
cdp.on("Network.loadingFailed", onRequestEnd);
|
|
4089
|
+
checkIdle();
|
|
4090
|
+
});
|
|
3298
4091
|
}
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
}
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
4092
|
+
|
|
4093
|
+
// src/browser/keyboard.ts
|
|
4094
|
+
var US_KEYBOARD = {
|
|
4095
|
+
// Letters (lowercase)
|
|
4096
|
+
a: { key: "a", code: "KeyA", keyCode: 65, text: "a" },
|
|
4097
|
+
b: { key: "b", code: "KeyB", keyCode: 66, text: "b" },
|
|
4098
|
+
c: { key: "c", code: "KeyC", keyCode: 67, text: "c" },
|
|
4099
|
+
d: { key: "d", code: "KeyD", keyCode: 68, text: "d" },
|
|
4100
|
+
e: { key: "e", code: "KeyE", keyCode: 69, text: "e" },
|
|
4101
|
+
f: { key: "f", code: "KeyF", keyCode: 70, text: "f" },
|
|
4102
|
+
g: { key: "g", code: "KeyG", keyCode: 71, text: "g" },
|
|
4103
|
+
h: { key: "h", code: "KeyH", keyCode: 72, text: "h" },
|
|
4104
|
+
i: { key: "i", code: "KeyI", keyCode: 73, text: "i" },
|
|
4105
|
+
j: { key: "j", code: "KeyJ", keyCode: 74, text: "j" },
|
|
4106
|
+
k: { key: "k", code: "KeyK", keyCode: 75, text: "k" },
|
|
4107
|
+
l: { key: "l", code: "KeyL", keyCode: 76, text: "l" },
|
|
4108
|
+
m: { key: "m", code: "KeyM", keyCode: 77, text: "m" },
|
|
4109
|
+
n: { key: "n", code: "KeyN", keyCode: 78, text: "n" },
|
|
4110
|
+
o: { key: "o", code: "KeyO", keyCode: 79, text: "o" },
|
|
4111
|
+
p: { key: "p", code: "KeyP", keyCode: 80, text: "p" },
|
|
4112
|
+
q: { key: "q", code: "KeyQ", keyCode: 81, text: "q" },
|
|
4113
|
+
r: { key: "r", code: "KeyR", keyCode: 82, text: "r" },
|
|
4114
|
+
s: { key: "s", code: "KeyS", keyCode: 83, text: "s" },
|
|
4115
|
+
t: { key: "t", code: "KeyT", keyCode: 84, text: "t" },
|
|
4116
|
+
u: { key: "u", code: "KeyU", keyCode: 85, text: "u" },
|
|
4117
|
+
v: { key: "v", code: "KeyV", keyCode: 86, text: "v" },
|
|
4118
|
+
w: { key: "w", code: "KeyW", keyCode: 87, text: "w" },
|
|
4119
|
+
x: { key: "x", code: "KeyX", keyCode: 88, text: "x" },
|
|
4120
|
+
y: { key: "y", code: "KeyY", keyCode: 89, text: "y" },
|
|
4121
|
+
z: { key: "z", code: "KeyZ", keyCode: 90, text: "z" },
|
|
4122
|
+
// Letters (uppercase)
|
|
4123
|
+
A: { key: "A", code: "KeyA", keyCode: 65, text: "A" },
|
|
4124
|
+
B: { key: "B", code: "KeyB", keyCode: 66, text: "B" },
|
|
4125
|
+
C: { key: "C", code: "KeyC", keyCode: 67, text: "C" },
|
|
4126
|
+
D: { key: "D", code: "KeyD", keyCode: 68, text: "D" },
|
|
4127
|
+
E: { key: "E", code: "KeyE", keyCode: 69, text: "E" },
|
|
4128
|
+
F: { key: "F", code: "KeyF", keyCode: 70, text: "F" },
|
|
4129
|
+
G: { key: "G", code: "KeyG", keyCode: 71, text: "G" },
|
|
4130
|
+
H: { key: "H", code: "KeyH", keyCode: 72, text: "H" },
|
|
4131
|
+
I: { key: "I", code: "KeyI", keyCode: 73, text: "I" },
|
|
4132
|
+
J: { key: "J", code: "KeyJ", keyCode: 74, text: "J" },
|
|
4133
|
+
K: { key: "K", code: "KeyK", keyCode: 75, text: "K" },
|
|
4134
|
+
L: { key: "L", code: "KeyL", keyCode: 76, text: "L" },
|
|
4135
|
+
M: { key: "M", code: "KeyM", keyCode: 77, text: "M" },
|
|
4136
|
+
N: { key: "N", code: "KeyN", keyCode: 78, text: "N" },
|
|
4137
|
+
O: { key: "O", code: "KeyO", keyCode: 79, text: "O" },
|
|
4138
|
+
P: { key: "P", code: "KeyP", keyCode: 80, text: "P" },
|
|
4139
|
+
Q: { key: "Q", code: "KeyQ", keyCode: 81, text: "Q" },
|
|
4140
|
+
R: { key: "R", code: "KeyR", keyCode: 82, text: "R" },
|
|
4141
|
+
S: { key: "S", code: "KeyS", keyCode: 83, text: "S" },
|
|
4142
|
+
T: { key: "T", code: "KeyT", keyCode: 84, text: "T" },
|
|
4143
|
+
U: { key: "U", code: "KeyU", keyCode: 85, text: "U" },
|
|
4144
|
+
V: { key: "V", code: "KeyV", keyCode: 86, text: "V" },
|
|
4145
|
+
W: { key: "W", code: "KeyW", keyCode: 87, text: "W" },
|
|
4146
|
+
X: { key: "X", code: "KeyX", keyCode: 88, text: "X" },
|
|
4147
|
+
Y: { key: "Y", code: "KeyY", keyCode: 89, text: "Y" },
|
|
4148
|
+
Z: { key: "Z", code: "KeyZ", keyCode: 90, text: "Z" },
|
|
4149
|
+
// Numbers
|
|
4150
|
+
"0": { key: "0", code: "Digit0", keyCode: 48, text: "0" },
|
|
4151
|
+
"1": { key: "1", code: "Digit1", keyCode: 49, text: "1" },
|
|
4152
|
+
"2": { key: "2", code: "Digit2", keyCode: 50, text: "2" },
|
|
4153
|
+
"3": { key: "3", code: "Digit3", keyCode: 51, text: "3" },
|
|
4154
|
+
"4": { key: "4", code: "Digit4", keyCode: 52, text: "4" },
|
|
4155
|
+
"5": { key: "5", code: "Digit5", keyCode: 53, text: "5" },
|
|
4156
|
+
"6": { key: "6", code: "Digit6", keyCode: 54, text: "6" },
|
|
4157
|
+
"7": { key: "7", code: "Digit7", keyCode: 55, text: "7" },
|
|
4158
|
+
"8": { key: "8", code: "Digit8", keyCode: 56, text: "8" },
|
|
4159
|
+
"9": { key: "9", code: "Digit9", keyCode: 57, text: "9" },
|
|
4160
|
+
// Punctuation
|
|
4161
|
+
" ": { key: " ", code: "Space", keyCode: 32, text: " " },
|
|
4162
|
+
".": { key: ".", code: "Period", keyCode: 190, text: "." },
|
|
4163
|
+
",": { key: ",", code: "Comma", keyCode: 188, text: "," },
|
|
4164
|
+
"/": { key: "/", code: "Slash", keyCode: 191, text: "/" },
|
|
4165
|
+
";": { key: ";", code: "Semicolon", keyCode: 186, text: ";" },
|
|
4166
|
+
"'": { key: "'", code: "Quote", keyCode: 222, text: "'" },
|
|
4167
|
+
"[": { key: "[", code: "BracketLeft", keyCode: 219, text: "[" },
|
|
4168
|
+
"]": { key: "]", code: "BracketRight", keyCode: 221, text: "]" },
|
|
4169
|
+
"\\": { key: "\\", code: "Backslash", keyCode: 220, text: "\\" },
|
|
4170
|
+
"-": { key: "-", code: "Minus", keyCode: 189, text: "-" },
|
|
4171
|
+
"=": { key: "=", code: "Equal", keyCode: 187, text: "=" },
|
|
4172
|
+
"`": { key: "`", code: "Backquote", keyCode: 192, text: "`" },
|
|
4173
|
+
// Shifted punctuation
|
|
4174
|
+
"!": { key: "!", code: "Digit1", keyCode: 49, text: "!" },
|
|
4175
|
+
"@": { key: "@", code: "Digit2", keyCode: 50, text: "@" },
|
|
4176
|
+
"#": { key: "#", code: "Digit3", keyCode: 51, text: "#" },
|
|
4177
|
+
$: { key: "$", code: "Digit4", keyCode: 52, text: "$" },
|
|
4178
|
+
"%": { key: "%", code: "Digit5", keyCode: 53, text: "%" },
|
|
4179
|
+
"^": { key: "^", code: "Digit6", keyCode: 54, text: "^" },
|
|
4180
|
+
"&": { key: "&", code: "Digit7", keyCode: 55, text: "&" },
|
|
4181
|
+
"*": { key: "*", code: "Digit8", keyCode: 56, text: "*" },
|
|
4182
|
+
"(": { key: "(", code: "Digit9", keyCode: 57, text: "(" },
|
|
4183
|
+
")": { key: ")", code: "Digit0", keyCode: 48, text: ")" },
|
|
4184
|
+
_: { key: "_", code: "Minus", keyCode: 189, text: "_" },
|
|
4185
|
+
"+": { key: "+", code: "Equal", keyCode: 187, text: "+" },
|
|
4186
|
+
"{": { key: "{", code: "BracketLeft", keyCode: 219, text: "{" },
|
|
4187
|
+
"}": { key: "}", code: "BracketRight", keyCode: 221, text: "}" },
|
|
4188
|
+
"|": { key: "|", code: "Backslash", keyCode: 220, text: "|" },
|
|
4189
|
+
":": { key: ":", code: "Semicolon", keyCode: 186, text: ":" },
|
|
4190
|
+
'"': { key: '"', code: "Quote", keyCode: 222, text: '"' },
|
|
4191
|
+
"<": { key: "<", code: "Comma", keyCode: 188, text: "<" },
|
|
4192
|
+
">": { key: ">", code: "Period", keyCode: 190, text: ">" },
|
|
4193
|
+
"?": { key: "?", code: "Slash", keyCode: 191, text: "?" },
|
|
4194
|
+
"~": { key: "~", code: "Backquote", keyCode: 192, text: "~" },
|
|
4195
|
+
// Special keys (non-text: use rawKeyDown, no text field)
|
|
4196
|
+
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
|
4197
|
+
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
4198
|
+
Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
4199
|
+
Delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
4200
|
+
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
4201
|
+
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
4202
|
+
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
4203
|
+
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
4204
|
+
ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
4205
|
+
Home: { key: "Home", code: "Home", keyCode: 36 },
|
|
4206
|
+
End: { key: "End", code: "End", keyCode: 35 },
|
|
4207
|
+
PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
4208
|
+
PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
4209
|
+
};
|
|
4210
|
+
var MODIFIER_CODES = {
|
|
4211
|
+
Control: "ControlLeft",
|
|
4212
|
+
Shift: "ShiftLeft",
|
|
4213
|
+
Alt: "AltLeft",
|
|
4214
|
+
Meta: "MetaLeft"
|
|
4215
|
+
};
|
|
4216
|
+
var MODIFIER_KEY_CODES = {
|
|
4217
|
+
Control: 17,
|
|
4218
|
+
Shift: 16,
|
|
4219
|
+
Alt: 18,
|
|
4220
|
+
Meta: 91
|
|
4221
|
+
};
|
|
4222
|
+
function computeModifierBitmask(modifiers) {
|
|
4223
|
+
let mask = 0;
|
|
4224
|
+
if (modifiers.includes("Alt")) mask |= 1;
|
|
4225
|
+
if (modifiers.includes("Control")) mask |= 2;
|
|
4226
|
+
if (modifiers.includes("Meta")) mask |= 4;
|
|
4227
|
+
if (modifiers.includes("Shift")) mask |= 8;
|
|
4228
|
+
return mask;
|
|
4229
|
+
}
|
|
4230
|
+
function parseShortcut(combo) {
|
|
4231
|
+
const parts = combo.split("+");
|
|
4232
|
+
if (parts.length < 2) {
|
|
4233
|
+
throw new Error(
|
|
4234
|
+
`Invalid shortcut "${combo}": must contain at least one modifier and a key (e.g. "Control+a").`
|
|
4235
|
+
);
|
|
3311
4236
|
}
|
|
3312
|
-
const
|
|
3313
|
-
|
|
3314
|
-
|
|
4237
|
+
const key = parts[parts.length - 1];
|
|
4238
|
+
const modifiers = [];
|
|
4239
|
+
const validModifiers = new Set(Object.keys(MODIFIER_CODES));
|
|
4240
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
4241
|
+
const mod = parts[i];
|
|
4242
|
+
if (!validModifiers.has(mod)) {
|
|
4243
|
+
throw new Error(
|
|
4244
|
+
`Invalid modifier "${mod}" in shortcut "${combo}". Valid modifiers: ${[...validModifiers].join(", ")}`
|
|
4245
|
+
);
|
|
4246
|
+
}
|
|
4247
|
+
modifiers.push(mod);
|
|
3315
4248
|
}
|
|
3316
|
-
return
|
|
4249
|
+
return { modifiers, key };
|
|
3317
4250
|
}
|
|
3318
4251
|
|
|
3319
4252
|
// src/browser/page.ts
|
|
3320
4253
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
4254
|
+
var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
|
|
4255
|
+
if (globalThis.__bpEventListenerTrackerInstalled) return;
|
|
4256
|
+
Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
|
|
4257
|
+
value: true,
|
|
4258
|
+
configurable: true,
|
|
4259
|
+
});
|
|
4260
|
+
|
|
4261
|
+
const storeKey = '__bpEventListeners';
|
|
4262
|
+
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
4263
|
+
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
|
|
4264
|
+
|
|
4265
|
+
function ensureStore(target) {
|
|
4266
|
+
if (!Object.prototype.hasOwnProperty.call(target, storeKey)) {
|
|
4267
|
+
Object.defineProperty(target, storeKey, {
|
|
4268
|
+
value: Object.create(null),
|
|
4269
|
+
configurable: true,
|
|
4270
|
+
});
|
|
4271
|
+
}
|
|
4272
|
+
return target[storeKey];
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
EventTarget.prototype.addEventListener = function(type, listener, options) {
|
|
4276
|
+
try {
|
|
4277
|
+
if (listener) {
|
|
4278
|
+
const store = ensureStore(this);
|
|
4279
|
+
const bucket = store[type] || (store[type] = []);
|
|
4280
|
+
const capture =
|
|
4281
|
+
typeof options === 'boolean' ? options : !!(options && options.capture);
|
|
4282
|
+
const exists = bucket.some((entry) => entry.listener === listener && entry.capture === capture);
|
|
4283
|
+
if (!exists) {
|
|
4284
|
+
bucket.push({ listener, capture });
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
} catch {}
|
|
4288
|
+
|
|
4289
|
+
return originalAddEventListener.call(this, type, listener, options);
|
|
4290
|
+
};
|
|
4291
|
+
|
|
4292
|
+
EventTarget.prototype.removeEventListener = function(type, listener, options) {
|
|
4293
|
+
try {
|
|
4294
|
+
const store = this[storeKey];
|
|
4295
|
+
const bucket = store && store[type];
|
|
4296
|
+
const capture =
|
|
4297
|
+
typeof options === 'boolean' ? options : !!(options && options.capture);
|
|
4298
|
+
if (Array.isArray(bucket)) {
|
|
4299
|
+
store[type] = bucket.filter((entry) => {
|
|
4300
|
+
return !(entry.listener === listener && entry.capture === capture);
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
4303
|
+
} catch {}
|
|
4304
|
+
|
|
4305
|
+
return originalRemoveEventListener.call(this, type, listener, options);
|
|
4306
|
+
};
|
|
4307
|
+
})();`;
|
|
3321
4308
|
var Page = class {
|
|
3322
4309
|
cdp;
|
|
3323
4310
|
_targetId;
|
|
@@ -3339,8 +4326,12 @@ var Page = class {
|
|
|
3339
4326
|
frameExecutionContexts = /* @__PURE__ */ new Map();
|
|
3340
4327
|
/** Current frame's execution context ID (null = main frame default) */
|
|
3341
4328
|
currentFrameContextId = null;
|
|
4329
|
+
/** Frame selector if context acquisition failed (cross-origin/sandboxed) */
|
|
4330
|
+
brokenFrame = null;
|
|
3342
4331
|
/** Last matched selector from findElement (for selectorUsed tracking) */
|
|
3343
4332
|
_lastMatchedSelector;
|
|
4333
|
+
/** Last snapshot for stale ref recovery */
|
|
4334
|
+
lastSnapshot;
|
|
3344
4335
|
/** Audio input controller (lazy-initialized) */
|
|
3345
4336
|
_audioInput;
|
|
3346
4337
|
/** Audio output controller (lazy-initialized) */
|
|
@@ -3385,17 +4376,34 @@ var Page = class {
|
|
|
3385
4376
|
for (const [frameId, ctxId] of this.frameExecutionContexts.entries()) {
|
|
3386
4377
|
if (ctxId === contextId) {
|
|
3387
4378
|
this.frameExecutionContexts.delete(frameId);
|
|
4379
|
+
if (this.currentFrameContextId === contextId) {
|
|
4380
|
+
this.currentFrameContextId = null;
|
|
4381
|
+
}
|
|
3388
4382
|
break;
|
|
3389
4383
|
}
|
|
3390
4384
|
}
|
|
3391
4385
|
});
|
|
3392
|
-
this.cdp.on("Page.javascriptDialogOpening",
|
|
4386
|
+
this.cdp.on("Page.javascriptDialogOpening", (params) => {
|
|
4387
|
+
void this.handleDialogOpening(params);
|
|
4388
|
+
});
|
|
3393
4389
|
await Promise.all([
|
|
3394
4390
|
this.cdp.send("Page.enable"),
|
|
3395
4391
|
this.cdp.send("DOM.enable"),
|
|
3396
4392
|
this.cdp.send("Runtime.enable"),
|
|
3397
4393
|
this.cdp.send("Network.enable")
|
|
3398
4394
|
]);
|
|
4395
|
+
await this.installEventListenerTracker();
|
|
4396
|
+
}
|
|
4397
|
+
async installEventListenerTracker() {
|
|
4398
|
+
await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
4399
|
+
source: EVENT_LISTENER_TRACKER_SCRIPT
|
|
4400
|
+
});
|
|
4401
|
+
try {
|
|
4402
|
+
await this.cdp.send("Runtime.evaluate", {
|
|
4403
|
+
expression: EVENT_LISTENER_TRACKER_SCRIPT
|
|
4404
|
+
});
|
|
4405
|
+
} catch {
|
|
4406
|
+
}
|
|
3399
4407
|
}
|
|
3400
4408
|
// ============ Navigation ============
|
|
3401
4409
|
/**
|
|
@@ -3411,6 +4419,9 @@ var Page = class {
|
|
|
3411
4419
|
}
|
|
3412
4420
|
this.rootNodeId = null;
|
|
3413
4421
|
this.refMap.clear();
|
|
4422
|
+
this.currentFrame = null;
|
|
4423
|
+
this.currentFrameContextId = null;
|
|
4424
|
+
this.frameContexts.clear();
|
|
3414
4425
|
}
|
|
3415
4426
|
/**
|
|
3416
4427
|
* Get the current URL
|
|
@@ -3481,8 +4492,9 @@ var Page = class {
|
|
|
3481
4492
|
/**
|
|
3482
4493
|
* Click an element (supports multi-selector)
|
|
3483
4494
|
*
|
|
3484
|
-
* Uses CDP mouse events
|
|
3485
|
-
*
|
|
4495
|
+
* Uses CDP mouse events (mouseMoved + mousePressed + mouseReleased) to
|
|
4496
|
+
* simulate a real click. Real mouse events on submit buttons naturally
|
|
4497
|
+
* trigger native form submission — no JS dispatch needed.
|
|
3486
4498
|
*/
|
|
3487
4499
|
async click(selector, options = {}) {
|
|
3488
4500
|
return this.withStaleNodeRetry(async () => {
|
|
@@ -3494,27 +4506,55 @@ var Page = class {
|
|
|
3494
4506
|
throw new ElementNotFoundError(selector, hints);
|
|
3495
4507
|
}
|
|
3496
4508
|
await this.scrollIntoView(element.nodeId);
|
|
3497
|
-
const
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
4509
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4510
|
+
try {
|
|
4511
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled", "stable"], {
|
|
4512
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4513
|
+
});
|
|
4514
|
+
} catch (e) {
|
|
4515
|
+
if (options.optional) return false;
|
|
4516
|
+
throw e;
|
|
4517
|
+
}
|
|
4518
|
+
let clickX;
|
|
4519
|
+
let clickY;
|
|
4520
|
+
try {
|
|
4521
|
+
const { quads } = await this.cdp.send("DOM.getContentQuads", {
|
|
4522
|
+
objectId
|
|
4523
|
+
});
|
|
4524
|
+
if (quads?.length > 0) {
|
|
4525
|
+
const quad = quads[0];
|
|
4526
|
+
clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
4527
|
+
clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
4528
|
+
} else {
|
|
4529
|
+
throw new Error("No quads");
|
|
4530
|
+
}
|
|
4531
|
+
} catch {
|
|
4532
|
+
const box = await this.getBoxModel(element.nodeId);
|
|
4533
|
+
if (!box) throw new Error("Could not get element position");
|
|
4534
|
+
clickX = box.content[0] + box.width / 2;
|
|
4535
|
+
clickY = box.content[1] + box.height / 2;
|
|
4536
|
+
}
|
|
4537
|
+
const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
|
|
4538
|
+
const HIT_TARGET_RETRIES = 3;
|
|
4539
|
+
const HIT_TARGET_DELAY = 100;
|
|
4540
|
+
for (let attempt = 0; attempt < HIT_TARGET_RETRIES; attempt++) {
|
|
4541
|
+
try {
|
|
4542
|
+
await ensureActionable(this.cdp, objectId, ["hitTarget"], {
|
|
4543
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2,
|
|
4544
|
+
coordinates: hitTargetCoordinates
|
|
4545
|
+
});
|
|
4546
|
+
break;
|
|
4547
|
+
} catch (e) {
|
|
4548
|
+
if (options.optional) return false;
|
|
4549
|
+
if (e instanceof ActionabilityError && e.failureType === "hitTarget" && attempt < HIT_TARGET_RETRIES - 1) {
|
|
4550
|
+
await sleep5(HIT_TARGET_DELAY);
|
|
4551
|
+
await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId: element.nodeId });
|
|
4552
|
+
continue;
|
|
3510
4553
|
}
|
|
3511
|
-
|
|
3512
|
-
}
|
|
3513
|
-
);
|
|
3514
|
-
const isSubmit = submitResult.result.value?.isSubmit;
|
|
3515
|
-
if (!isSubmit) {
|
|
3516
|
-
await this.clickElement(element.nodeId);
|
|
4554
|
+
throw e;
|
|
4555
|
+
}
|
|
3517
4556
|
}
|
|
4557
|
+
await this.clickElement(element.nodeId);
|
|
3518
4558
|
return true;
|
|
3519
4559
|
});
|
|
3520
4560
|
}
|
|
@@ -3522,7 +4562,7 @@ var Page = class {
|
|
|
3522
4562
|
* Fill an input field (clears first by default)
|
|
3523
4563
|
*/
|
|
3524
4564
|
async fill(selector, value, options = {}) {
|
|
3525
|
-
const {
|
|
4565
|
+
const { blur = false } = options;
|
|
3526
4566
|
return this.withStaleNodeRetry(async () => {
|
|
3527
4567
|
const element = await this.findElement(selector, options);
|
|
3528
4568
|
if (!element) {
|
|
@@ -3531,71 +4571,158 @@ var Page = class {
|
|
|
3531
4571
|
const hints = await generateHints(this, selectorList, "fill");
|
|
3532
4572
|
throw new ElementNotFoundError(selector, hints);
|
|
3533
4573
|
}
|
|
3534
|
-
await this.cdp.send("DOM.
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
}));
|
|
3546
|
-
}
|
|
3547
|
-
})()`
|
|
3548
|
-
);
|
|
4574
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
4575
|
+
nodeId: element.nodeId
|
|
4576
|
+
});
|
|
4577
|
+
const objectId = object.objectId;
|
|
4578
|
+
try {
|
|
4579
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled", "editable"], {
|
|
4580
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4581
|
+
});
|
|
4582
|
+
} catch (e) {
|
|
4583
|
+
if (options.optional) return false;
|
|
4584
|
+
throw e;
|
|
3549
4585
|
}
|
|
3550
|
-
await this.cdp.send("
|
|
3551
|
-
|
|
3552
|
-
`(
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
4586
|
+
const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4587
|
+
objectId,
|
|
4588
|
+
functionDeclaration: `function() {
|
|
4589
|
+
return { tagName: this.tagName?.toLowerCase() || '', inputType: (this.type || '').toLowerCase() };
|
|
4590
|
+
}`,
|
|
4591
|
+
returnByValue: true
|
|
4592
|
+
});
|
|
4593
|
+
const { tagName, inputType } = tagInfo.result.value;
|
|
4594
|
+
const specialInputTypes = /* @__PURE__ */ new Set([
|
|
4595
|
+
"date",
|
|
4596
|
+
"datetime-local",
|
|
4597
|
+
"month",
|
|
4598
|
+
"week",
|
|
4599
|
+
"time",
|
|
4600
|
+
"color",
|
|
4601
|
+
"range",
|
|
4602
|
+
"file"
|
|
4603
|
+
]);
|
|
4604
|
+
const isSpecialInput = tagName === "input" && specialInputTypes.has(inputType);
|
|
4605
|
+
if (isSpecialInput) {
|
|
4606
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4607
|
+
objectId,
|
|
4608
|
+
functionDeclaration: `function(val) {
|
|
4609
|
+
this.value = val;
|
|
4610
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
4611
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
4612
|
+
}`,
|
|
4613
|
+
arguments: [{ value }],
|
|
4614
|
+
returnByValue: true
|
|
4615
|
+
});
|
|
4616
|
+
} else {
|
|
4617
|
+
await this.selectEditableContent(objectId);
|
|
4618
|
+
if (value === "") {
|
|
4619
|
+
await this.dispatchKey("Delete");
|
|
4620
|
+
} else {
|
|
4621
|
+
await this.cdp.send("Input.insertText", { text: value });
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
if (options.verify !== false) {
|
|
4625
|
+
let actualValue = await this.readEditableValue(objectId);
|
|
4626
|
+
if (actualValue !== value && !isSpecialInput) {
|
|
4627
|
+
if (value === "") {
|
|
4628
|
+
await this.clearEditableSelection(objectId, "Backspace");
|
|
4629
|
+
} else {
|
|
4630
|
+
await this.typeEditableFallback(element.nodeId, objectId, value);
|
|
3562
4631
|
}
|
|
3563
|
-
|
|
3564
|
-
|
|
4632
|
+
actualValue = await this.readEditableValue(objectId);
|
|
4633
|
+
}
|
|
4634
|
+
if (actualValue !== value) {
|
|
4635
|
+
if (options.optional) return false;
|
|
4636
|
+
throw new Error(
|
|
4637
|
+
`Fill value did not stick. Expected ${JSON.stringify(value)} but got ${JSON.stringify(actualValue)}.`
|
|
4638
|
+
);
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
3565
4641
|
if (blur) {
|
|
3566
|
-
await this.
|
|
3567
|
-
|
|
3568
|
-
|
|
4642
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4643
|
+
objectId,
|
|
4644
|
+
functionDeclaration: "function() { this.blur(); }"
|
|
4645
|
+
});
|
|
3569
4646
|
}
|
|
3570
4647
|
return true;
|
|
3571
4648
|
});
|
|
3572
4649
|
}
|
|
3573
4650
|
/**
|
|
3574
4651
|
* Type text character by character (for autocomplete fields, etc.)
|
|
4652
|
+
*
|
|
4653
|
+
* Uses proper keyDown/rawKeyDown distinction with US keyboard layout.
|
|
4654
|
+
* Printable chars use 'keyDown' with text, non-text keys use 'rawKeyDown',
|
|
4655
|
+
* and non-layout chars (emoji, CJK) fall back to Input.insertText.
|
|
3575
4656
|
*/
|
|
3576
4657
|
async type(selector, text, options = {}) {
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
if (
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
3584
|
-
for (const char of text) {
|
|
3585
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3586
|
-
type: "keyDown",
|
|
3587
|
-
key: char,
|
|
3588
|
-
text: char
|
|
3589
|
-
});
|
|
3590
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3591
|
-
type: "keyUp",
|
|
3592
|
-
key: char
|
|
3593
|
-
});
|
|
3594
|
-
if (delay > 0) {
|
|
3595
|
-
await sleep3(delay);
|
|
4658
|
+
return this.withStaleNodeRetry(async () => {
|
|
4659
|
+
const { delay = 50 } = options;
|
|
4660
|
+
const element = await this.findElement(selector, options);
|
|
4661
|
+
if (!element) {
|
|
4662
|
+
if (options.optional) return false;
|
|
4663
|
+
throw new ElementNotFoundError(selector);
|
|
3596
4664
|
}
|
|
3597
|
-
|
|
3598
|
-
|
|
4665
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4666
|
+
try {
|
|
4667
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled"], {
|
|
4668
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4669
|
+
});
|
|
4670
|
+
} catch (e) {
|
|
4671
|
+
if (options.optional) return false;
|
|
4672
|
+
throw e;
|
|
4673
|
+
}
|
|
4674
|
+
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
4675
|
+
for (const char of text) {
|
|
4676
|
+
const def = US_KEYBOARD[char];
|
|
4677
|
+
if (def) {
|
|
4678
|
+
if (def.text !== void 0) {
|
|
4679
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
4680
|
+
type: "keyDown",
|
|
4681
|
+
key: def.key,
|
|
4682
|
+
code: def.code,
|
|
4683
|
+
text: def.text,
|
|
4684
|
+
unmodifiedText: def.text,
|
|
4685
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
4686
|
+
modifiers: 0,
|
|
4687
|
+
autoRepeat: false,
|
|
4688
|
+
location: def.location ?? 0,
|
|
4689
|
+
isKeypad: false
|
|
4690
|
+
});
|
|
4691
|
+
} else {
|
|
4692
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
4693
|
+
type: "rawKeyDown",
|
|
4694
|
+
key: def.key,
|
|
4695
|
+
code: def.code,
|
|
4696
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
4697
|
+
modifiers: 0,
|
|
4698
|
+
autoRepeat: false,
|
|
4699
|
+
location: def.location ?? 0,
|
|
4700
|
+
isKeypad: false
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
4703
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
4704
|
+
type: "keyUp",
|
|
4705
|
+
key: def.key,
|
|
4706
|
+
code: def.code,
|
|
4707
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
4708
|
+
modifiers: 0,
|
|
4709
|
+
location: def.location ?? 0
|
|
4710
|
+
});
|
|
4711
|
+
} else {
|
|
4712
|
+
await this.cdp.send("Input.insertText", { text: char });
|
|
4713
|
+
}
|
|
4714
|
+
if (delay > 0) {
|
|
4715
|
+
await sleep5(delay);
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4718
|
+
if (options.blur) {
|
|
4719
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4720
|
+
objectId,
|
|
4721
|
+
functionDeclaration: "function() { this.blur(); }"
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4724
|
+
return true;
|
|
4725
|
+
});
|
|
3599
4726
|
}
|
|
3600
4727
|
async select(selectorOrConfig, valueOrOptions, maybeOptions) {
|
|
3601
4728
|
if (typeof selectorOrConfig === "object" && !Array.isArray(selectorOrConfig) && "trigger" in selectorOrConfig) {
|
|
@@ -3604,108 +4731,231 @@ var Page = class {
|
|
|
3604
4731
|
const selector = selectorOrConfig;
|
|
3605
4732
|
const value = valueOrOptions;
|
|
3606
4733
|
const options = maybeOptions ?? {};
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
if (
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
4734
|
+
return this.withStaleNodeRetry(async () => {
|
|
4735
|
+
const element = await this.findElement(selector, options);
|
|
4736
|
+
if (!element) {
|
|
4737
|
+
if (options.optional) return false;
|
|
4738
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
4739
|
+
const hints = await generateHints(this, selectorList, "select");
|
|
4740
|
+
throw new ElementNotFoundError(selector, hints);
|
|
4741
|
+
}
|
|
4742
|
+
const values = Array.isArray(value) ? value : [value];
|
|
4743
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4744
|
+
try {
|
|
4745
|
+
await this.scrollIntoView(element.nodeId);
|
|
4746
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled"], {
|
|
4747
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4748
|
+
});
|
|
4749
|
+
} catch (e) {
|
|
4750
|
+
if (options.optional) return false;
|
|
4751
|
+
throw e;
|
|
4752
|
+
}
|
|
4753
|
+
const metadata = await this.getNativeSelectMetadata(objectId, values);
|
|
4754
|
+
if (!metadata.isSelect) {
|
|
4755
|
+
throw new Error("select() target must be a native <select> element");
|
|
4756
|
+
}
|
|
4757
|
+
if (metadata.missing.length > 0) {
|
|
4758
|
+
throw new Error(`No option found for: ${metadata.missing.join(", ")}`);
|
|
4759
|
+
}
|
|
4760
|
+
if (metadata.disabled.length > 0) {
|
|
4761
|
+
throw new Error(`Cannot select disabled option(s): ${metadata.disabled.join(", ")}`);
|
|
4762
|
+
}
|
|
4763
|
+
if (!metadata.multiple && metadata.targetIndexes.length > 1) {
|
|
4764
|
+
throw new Error("Cannot select multiple values on a single-select element");
|
|
4765
|
+
}
|
|
4766
|
+
const expectedValues = metadata.targetIndexes.map((idx) => metadata.options[idx].value);
|
|
4767
|
+
if (this.selectValuesMatch(metadata.selectedValues, expectedValues, metadata.multiple)) {
|
|
3624
4768
|
return true;
|
|
3625
|
-
}
|
|
3626
|
-
|
|
4769
|
+
}
|
|
4770
|
+
if (!metadata.multiple && metadata.targetIndexes.length === 1) {
|
|
4771
|
+
await this.applyNativeSelectByKeyboard(
|
|
4772
|
+
element.nodeId,
|
|
4773
|
+
objectId,
|
|
4774
|
+
metadata.currentIndex,
|
|
4775
|
+
metadata.targetIndexes[0]
|
|
4776
|
+
);
|
|
4777
|
+
}
|
|
4778
|
+
let selectedValues = await this.readNativeSelectValues(objectId);
|
|
4779
|
+
if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
|
|
4780
|
+
await this.applyNativeSelectFallback(objectId, metadata.targetIndexes);
|
|
4781
|
+
selectedValues = await this.readNativeSelectValues(objectId);
|
|
4782
|
+
}
|
|
4783
|
+
if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
|
|
4784
|
+
await this.applyRecordedSelectFallback(objectId, metadata.targetIndexes);
|
|
4785
|
+
selectedValues = await this.readNativeSelectValues(objectId);
|
|
4786
|
+
}
|
|
4787
|
+
if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
|
|
4788
|
+
if (options.optional) return false;
|
|
4789
|
+
throw new Error(
|
|
4790
|
+
`Select value did not stick. Expected ${expectedValues.join(", ") || "(empty)"} but got ${selectedValues.join(", ") || "(empty)"}.`
|
|
4791
|
+
);
|
|
4792
|
+
}
|
|
4793
|
+
return true;
|
|
3627
4794
|
});
|
|
3628
|
-
return true;
|
|
3629
4795
|
}
|
|
3630
4796
|
/**
|
|
3631
4797
|
* Handle custom (non-native) select/dropdown components
|
|
3632
4798
|
*/
|
|
3633
4799
|
async selectCustom(config, options = {}) {
|
|
3634
4800
|
const { trigger, option, value, match = "text" } = config;
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
4801
|
+
return this.withStaleNodeRetry(async () => {
|
|
4802
|
+
await this.click(trigger, options);
|
|
4803
|
+
const optionSelectors = Array.isArray(option) ? option : [option];
|
|
4804
|
+
await waitForAnyElement(this.cdp, optionSelectors, {
|
|
4805
|
+
state: "visible",
|
|
4806
|
+
timeout: 500,
|
|
4807
|
+
contextId: this.currentFrameContextId ?? void 0
|
|
4808
|
+
}).catch(() => sleep5(100));
|
|
4809
|
+
const optionHandle = await this.evaluateInFrame(
|
|
4810
|
+
`(() => {
|
|
4811
|
+
const selectors = ${JSON.stringify(optionSelectors)};
|
|
4812
|
+
const wanted = ${JSON.stringify(value)};
|
|
4813
|
+
const mode = ${JSON.stringify(match)};
|
|
4814
|
+
|
|
4815
|
+
for (const selector of selectors) {
|
|
4816
|
+
const candidates = document.querySelectorAll(selector);
|
|
4817
|
+
for (const candidate of candidates) {
|
|
4818
|
+
const text = candidate.textContent?.trim() || '';
|
|
4819
|
+
const candidateValue =
|
|
4820
|
+
candidate.getAttribute?.('data-value') ??
|
|
4821
|
+
candidate.getAttribute?.('value') ??
|
|
4822
|
+
candidate.value ??
|
|
4823
|
+
'';
|
|
4824
|
+
const matches =
|
|
4825
|
+
mode === 'value'
|
|
4826
|
+
? candidateValue === wanted
|
|
4827
|
+
: mode === 'contains'
|
|
4828
|
+
? text.includes(wanted)
|
|
4829
|
+
: text === wanted;
|
|
4830
|
+
|
|
4831
|
+
if (matches) {
|
|
4832
|
+
return candidate;
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
3654
4835
|
}
|
|
4836
|
+
|
|
4837
|
+
return null;
|
|
4838
|
+
})()`,
|
|
4839
|
+
{ returnByValue: false }
|
|
4840
|
+
);
|
|
4841
|
+
if (!optionHandle.result.objectId) {
|
|
4842
|
+
if (options.optional) return false;
|
|
4843
|
+
throw new ElementNotFoundError(`Option with ${match} "${value}"`);
|
|
4844
|
+
}
|
|
4845
|
+
const nodeResult = await this.cdp.send("DOM.requestNode", {
|
|
4846
|
+
objectId: optionHandle.result.objectId
|
|
4847
|
+
});
|
|
4848
|
+
if (!nodeResult.nodeId) {
|
|
4849
|
+
if (options.optional) return false;
|
|
4850
|
+
throw new ElementNotFoundError(`Option with ${match} "${value}"`);
|
|
4851
|
+
}
|
|
4852
|
+
await this.scrollIntoView(nodeResult.nodeId);
|
|
4853
|
+
await ensureActionable(
|
|
4854
|
+
this.cdp,
|
|
4855
|
+
optionHandle.result.objectId,
|
|
4856
|
+
["visible", "enabled", "stable"],
|
|
4857
|
+
{
|
|
4858
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
3655
4859
|
}
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
});
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
}
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
const
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
return
|
|
3683
|
-
|
|
3684
|
-
|
|
4860
|
+
);
|
|
4861
|
+
await this.clickElement(nodeResult.nodeId);
|
|
4862
|
+
return true;
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4865
|
+
/**
|
|
4866
|
+
* Check a checkbox or radio button using real mouse click.
|
|
4867
|
+
* No-op if already checked. Verifies state changed after click.
|
|
4868
|
+
*/
|
|
4869
|
+
async check(selector, options = {}) {
|
|
4870
|
+
return this.withStaleNodeRetry(async () => {
|
|
4871
|
+
const element = await this.findElement(selector, options);
|
|
4872
|
+
if (!element) {
|
|
4873
|
+
if (options.optional) return false;
|
|
4874
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
4875
|
+
const hints = await generateHints(this, selectorList, "check");
|
|
4876
|
+
throw new ElementNotFoundError(selector, hints);
|
|
4877
|
+
}
|
|
4878
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
4879
|
+
nodeId: element.nodeId
|
|
4880
|
+
});
|
|
4881
|
+
try {
|
|
4882
|
+
await ensureActionable(this.cdp, object.objectId, ["visible", "enabled"], {
|
|
4883
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4884
|
+
});
|
|
4885
|
+
} catch (e) {
|
|
4886
|
+
if (options.optional) return false;
|
|
4887
|
+
throw e;
|
|
4888
|
+
}
|
|
4889
|
+
const before = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4890
|
+
objectId: object.objectId,
|
|
4891
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
4892
|
+
returnByValue: true
|
|
4893
|
+
});
|
|
4894
|
+
if (before.result.value) return true;
|
|
4895
|
+
await this.scrollIntoView(element.nodeId);
|
|
4896
|
+
await this.clickElement(element.nodeId);
|
|
4897
|
+
const after = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4898
|
+
objectId: object.objectId,
|
|
4899
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
4900
|
+
returnByValue: true
|
|
4901
|
+
});
|
|
4902
|
+
if (!after.result.value) {
|
|
4903
|
+
throw new Error("Clicking the checkbox did not change its state");
|
|
4904
|
+
}
|
|
4905
|
+
return true;
|
|
3685
4906
|
});
|
|
3686
|
-
return result.result.value;
|
|
3687
4907
|
}
|
|
3688
4908
|
/**
|
|
3689
|
-
* Uncheck a checkbox
|
|
4909
|
+
* Uncheck a checkbox using real mouse click.
|
|
4910
|
+
* No-op if already unchecked. Radio buttons can't be unchecked (returns true).
|
|
3690
4911
|
*/
|
|
3691
4912
|
async uncheck(selector, options = {}) {
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
if (
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
4913
|
+
return this.withStaleNodeRetry(async () => {
|
|
4914
|
+
const element = await this.findElement(selector, options);
|
|
4915
|
+
if (!element) {
|
|
4916
|
+
if (options.optional) return false;
|
|
4917
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
4918
|
+
const hints = await generateHints(this, selectorList, "uncheck");
|
|
4919
|
+
throw new ElementNotFoundError(selector, hints);
|
|
4920
|
+
}
|
|
4921
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
4922
|
+
nodeId: element.nodeId
|
|
4923
|
+
});
|
|
4924
|
+
try {
|
|
4925
|
+
await ensureActionable(this.cdp, object.objectId, ["visible", "enabled"], {
|
|
4926
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4927
|
+
});
|
|
4928
|
+
} catch (e) {
|
|
4929
|
+
if (options.optional) return false;
|
|
4930
|
+
throw e;
|
|
4931
|
+
}
|
|
4932
|
+
const isRadio = await this.cdp.send(
|
|
4933
|
+
"Runtime.callFunctionOn",
|
|
4934
|
+
{
|
|
4935
|
+
objectId: object.objectId,
|
|
4936
|
+
functionDeclaration: 'function() { return this.type === "radio"; }',
|
|
4937
|
+
returnByValue: true
|
|
4938
|
+
}
|
|
4939
|
+
);
|
|
4940
|
+
if (isRadio.result.value) return true;
|
|
4941
|
+
const before = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4942
|
+
objectId: object.objectId,
|
|
4943
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
4944
|
+
returnByValue: true
|
|
4945
|
+
});
|
|
4946
|
+
if (!before.result.value) return true;
|
|
4947
|
+
await this.scrollIntoView(element.nodeId);
|
|
4948
|
+
await this.clickElement(element.nodeId);
|
|
4949
|
+
const after = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4950
|
+
objectId: object.objectId,
|
|
4951
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
4952
|
+
returnByValue: true
|
|
4953
|
+
});
|
|
4954
|
+
if (after.result.value) {
|
|
4955
|
+
throw new Error("Clicking the checkbox did not change its state");
|
|
4956
|
+
}
|
|
4957
|
+
return true;
|
|
3707
4958
|
});
|
|
3708
|
-
return result.result.value;
|
|
3709
4959
|
}
|
|
3710
4960
|
/**
|
|
3711
4961
|
* Submit a form (tries Enter key first, then click)
|
|
@@ -3719,97 +4969,100 @@ var Page = class {
|
|
|
3719
4969
|
* the submit event and triggers HTML5 validation.
|
|
3720
4970
|
*/
|
|
3721
4971
|
async submit(selector, options = {}) {
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
if (
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
const isFormElement = await this.evaluateInFrame(
|
|
3731
|
-
`(() => {
|
|
3732
|
-
const el = document.querySelector(${JSON.stringify(element.selector)});
|
|
3733
|
-
return el instanceof HTMLFormElement;
|
|
3734
|
-
})()`
|
|
3735
|
-
);
|
|
3736
|
-
if (isFormElement.result.value) {
|
|
3737
|
-
await this.evaluateInFrame(
|
|
3738
|
-
`(() => {
|
|
3739
|
-
const form = document.querySelector(${JSON.stringify(element.selector)});
|
|
3740
|
-
if (form && form instanceof HTMLFormElement) {
|
|
3741
|
-
form.requestSubmit();
|
|
3742
|
-
}
|
|
3743
|
-
})()`
|
|
3744
|
-
);
|
|
3745
|
-
if (shouldWait === true) {
|
|
3746
|
-
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
3747
|
-
} else if (shouldWait === "auto") {
|
|
3748
|
-
await Promise.race([this.waitForNavigation({ timeout: 1e3, optional: true }), sleep3(500)]);
|
|
4972
|
+
return this.withStaleNodeRetry(async () => {
|
|
4973
|
+
const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
|
|
4974
|
+
const element = await this.findElement(selector, options);
|
|
4975
|
+
if (!element) {
|
|
4976
|
+
if (options.optional) return false;
|
|
4977
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
4978
|
+
const hints = await generateHints(this, selectorList, "submit");
|
|
4979
|
+
throw new ElementNotFoundError(selector, hints);
|
|
3749
4980
|
}
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
4981
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4982
|
+
const isFormElement = await this.cdp.send(
|
|
4983
|
+
"Runtime.callFunctionOn",
|
|
4984
|
+
{
|
|
4985
|
+
objectId,
|
|
4986
|
+
functionDeclaration: "function() { return this instanceof HTMLFormElement; }",
|
|
4987
|
+
returnByValue: true
|
|
4988
|
+
}
|
|
4989
|
+
);
|
|
4990
|
+
if (isFormElement.result.value) {
|
|
4991
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4992
|
+
objectId,
|
|
4993
|
+
functionDeclaration: `function() {
|
|
4994
|
+
if (typeof this.requestSubmit === 'function') {
|
|
4995
|
+
this.requestSubmit();
|
|
4996
|
+
} else {
|
|
4997
|
+
this.submit();
|
|
4998
|
+
}
|
|
4999
|
+
}`
|
|
5000
|
+
});
|
|
5001
|
+
if (shouldWait === true) {
|
|
3757
5002
|
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
3758
|
-
|
|
3759
|
-
|
|
5003
|
+
} else if (shouldWait === "auto") {
|
|
5004
|
+
await Promise.race([
|
|
5005
|
+
this.waitForNavigation({ timeout: 2e3, optional: true }).then(
|
|
5006
|
+
() => "navigation"
|
|
5007
|
+
),
|
|
5008
|
+
this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
|
|
5009
|
+
sleep5(1500).then(() => "timeout")
|
|
5010
|
+
]);
|
|
3760
5011
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
5012
|
+
return true;
|
|
5013
|
+
}
|
|
5014
|
+
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
5015
|
+
if (method.includes("enter")) {
|
|
5016
|
+
await this.press("Enter");
|
|
5017
|
+
if (shouldWait === true) {
|
|
5018
|
+
try {
|
|
5019
|
+
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
5020
|
+
return true;
|
|
5021
|
+
} catch {
|
|
5022
|
+
}
|
|
5023
|
+
} else if (shouldWait === "auto") {
|
|
5024
|
+
const navigationDetected = await Promise.race([
|
|
5025
|
+
this.waitForNavigation({ timeout: 2e3, optional: true }).then(
|
|
5026
|
+
(success) => success ? "nav" : null
|
|
5027
|
+
),
|
|
5028
|
+
this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
|
|
5029
|
+
sleep5(1500).then(() => "timeout")
|
|
5030
|
+
]);
|
|
5031
|
+
if (navigationDetected === "nav") {
|
|
5032
|
+
return true;
|
|
5033
|
+
}
|
|
5034
|
+
} else if (method === "enter") {
|
|
3769
5035
|
return true;
|
|
3770
5036
|
}
|
|
3771
|
-
} else {
|
|
3772
|
-
if (method === "enter") return true;
|
|
3773
5037
|
}
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
5038
|
+
if (method.includes("click")) {
|
|
5039
|
+
await this.click(element.selector, { ...options, optional: false });
|
|
5040
|
+
if (shouldWait === true) {
|
|
5041
|
+
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
5042
|
+
} else if (shouldWait === "auto") {
|
|
5043
|
+
await sleep5(100);
|
|
5044
|
+
}
|
|
3781
5045
|
}
|
|
5046
|
+
return true;
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
/**
|
|
5050
|
+
* Press a key, optionally with modifier keys held down
|
|
5051
|
+
*/
|
|
5052
|
+
async press(key, options) {
|
|
5053
|
+
const modifiers = options?.modifiers;
|
|
5054
|
+
if (modifiers && modifiers.length > 0) {
|
|
5055
|
+
await this.dispatchKeyWithModifiers(key, modifiers);
|
|
5056
|
+
} else {
|
|
5057
|
+
await this.dispatchKey(key);
|
|
3782
5058
|
}
|
|
3783
|
-
return true;
|
|
3784
5059
|
}
|
|
3785
5060
|
/**
|
|
3786
|
-
*
|
|
5061
|
+
* Execute a keyboard shortcut (e.g. "Control+a", "Meta+Shift+z")
|
|
3787
5062
|
*/
|
|
3788
|
-
async
|
|
3789
|
-
const
|
|
3790
|
-
|
|
3791
|
-
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
3792
|
-
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
3793
|
-
Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
3794
|
-
Delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
3795
|
-
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
3796
|
-
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
3797
|
-
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
3798
|
-
ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
|
|
3799
|
-
};
|
|
3800
|
-
const keyInfo = keyMap[key] ?? { key, code: key, keyCode: 0 };
|
|
3801
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3802
|
-
type: "keyDown",
|
|
3803
|
-
key: keyInfo.key,
|
|
3804
|
-
code: keyInfo.code,
|
|
3805
|
-
windowsVirtualKeyCode: keyInfo.keyCode
|
|
3806
|
-
});
|
|
3807
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3808
|
-
type: "keyUp",
|
|
3809
|
-
key: keyInfo.key,
|
|
3810
|
-
code: keyInfo.code,
|
|
3811
|
-
windowsVirtualKeyCode: keyInfo.keyCode
|
|
3812
|
-
});
|
|
5063
|
+
async shortcut(combo) {
|
|
5064
|
+
const { modifiers, key } = parseShortcut(combo);
|
|
5065
|
+
await this.dispatchKeyWithModifiers(key, modifiers);
|
|
3813
5066
|
}
|
|
3814
5067
|
/**
|
|
3815
5068
|
* Focus an element
|
|
@@ -3838,13 +5091,37 @@ var Page = class {
|
|
|
3838
5091
|
throw new ElementNotFoundError(selector, hints);
|
|
3839
5092
|
}
|
|
3840
5093
|
await this.scrollIntoView(element.nodeId);
|
|
3841
|
-
const
|
|
3842
|
-
|
|
5094
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
5095
|
+
try {
|
|
5096
|
+
await ensureActionable(this.cdp, objectId, ["visible", "stable"], {
|
|
5097
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
5098
|
+
});
|
|
5099
|
+
} catch (e) {
|
|
3843
5100
|
if (options.optional) return false;
|
|
3844
|
-
throw
|
|
5101
|
+
throw e;
|
|
5102
|
+
}
|
|
5103
|
+
let x;
|
|
5104
|
+
let y;
|
|
5105
|
+
try {
|
|
5106
|
+
const { quads } = await this.cdp.send("DOM.getContentQuads", {
|
|
5107
|
+
objectId
|
|
5108
|
+
});
|
|
5109
|
+
if (quads?.length > 0) {
|
|
5110
|
+
const quad = quads[0];
|
|
5111
|
+
x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
5112
|
+
y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
5113
|
+
} else {
|
|
5114
|
+
throw new Error("No quads");
|
|
5115
|
+
}
|
|
5116
|
+
} catch {
|
|
5117
|
+
const box = await this.getBoxModel(element.nodeId);
|
|
5118
|
+
if (!box) {
|
|
5119
|
+
if (options.optional) return false;
|
|
5120
|
+
throw new Error("Could not get element position");
|
|
5121
|
+
}
|
|
5122
|
+
x = box.content[0] + box.width / 2;
|
|
5123
|
+
y = box.content[1] + box.height / 2;
|
|
3845
5124
|
}
|
|
3846
|
-
const x = box.content[0] + box.width / 2;
|
|
3847
|
-
const y = box.content[1] + box.height / 2;
|
|
3848
5125
|
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
3849
5126
|
type: "mouseMoved",
|
|
3850
5127
|
x,
|
|
@@ -3902,15 +5179,19 @@ var Page = class {
|
|
|
3902
5179
|
if (descResult.node.frameId) {
|
|
3903
5180
|
const frameId = descResult.node.frameId;
|
|
3904
5181
|
const { timeout = DEFAULT_TIMEOUT2 } = options;
|
|
3905
|
-
const pollInterval = 50;
|
|
3906
|
-
const deadline = Date.now() + timeout;
|
|
3907
5182
|
let contextId = this.frameExecutionContexts.get(frameId);
|
|
3908
|
-
|
|
3909
|
-
await
|
|
3910
|
-
contextId = this.frameExecutionContexts.get(frameId);
|
|
5183
|
+
if (!contextId) {
|
|
5184
|
+
contextId = await this.waitForFrameContext(frameId, Math.min(timeout, 2e3));
|
|
3911
5185
|
}
|
|
3912
5186
|
if (contextId) {
|
|
3913
5187
|
this.currentFrameContextId = contextId;
|
|
5188
|
+
this.brokenFrame = null;
|
|
5189
|
+
} else {
|
|
5190
|
+
const frameKey2 = Array.isArray(selector) ? selector[0] : selector;
|
|
5191
|
+
this.brokenFrame = frameKey2;
|
|
5192
|
+
console.warn(
|
|
5193
|
+
`[browser-pilot] Frame "${frameKey2}" execution context unavailable. JS evaluation will fail in this frame. DOM operations may still work.`
|
|
5194
|
+
);
|
|
3914
5195
|
}
|
|
3915
5196
|
}
|
|
3916
5197
|
this.refMap.clear();
|
|
@@ -3923,6 +5204,7 @@ var Page = class {
|
|
|
3923
5204
|
this.currentFrame = null;
|
|
3924
5205
|
this.rootNodeId = null;
|
|
3925
5206
|
this.currentFrameContextId = null;
|
|
5207
|
+
this.brokenFrame = null;
|
|
3926
5208
|
this.refMap.clear();
|
|
3927
5209
|
}
|
|
3928
5210
|
/**
|
|
@@ -3972,109 +5254,491 @@ var Page = class {
|
|
|
3972
5254
|
}
|
|
3973
5255
|
return result.success;
|
|
3974
5256
|
}
|
|
3975
|
-
// ============ JavaScript Execution ============
|
|
3976
|
-
/**
|
|
3977
|
-
* Evaluate JavaScript in the page context (or current frame context if in iframe)
|
|
3978
|
-
*/
|
|
3979
|
-
async evaluate(expression, ...args) {
|
|
3980
|
-
let script;
|
|
3981
|
-
if (typeof expression === "function") {
|
|
3982
|
-
const argString = args.map((a) => JSON.stringify(a)).join(", ");
|
|
3983
|
-
script = `(${expression.toString()})(${argString})`;
|
|
3984
|
-
} else {
|
|
3985
|
-
script = expression;
|
|
3986
|
-
}
|
|
3987
|
-
const params = {
|
|
3988
|
-
expression: script,
|
|
3989
|
-
returnByValue: true,
|
|
3990
|
-
awaitPromise: true
|
|
3991
|
-
};
|
|
3992
|
-
if (this.currentFrameContextId !== null) {
|
|
3993
|
-
params["contextId"] = this.currentFrameContextId;
|
|
5257
|
+
// ============ JavaScript Execution ============
|
|
5258
|
+
/**
|
|
5259
|
+
* Evaluate JavaScript in the page context (or current frame context if in iframe)
|
|
5260
|
+
*/
|
|
5261
|
+
async evaluate(expression, ...args) {
|
|
5262
|
+
let script;
|
|
5263
|
+
if (typeof expression === "function") {
|
|
5264
|
+
const argString = args.map((a) => JSON.stringify(a)).join(", ");
|
|
5265
|
+
script = `(${expression.toString()})(${argString})`;
|
|
5266
|
+
} else {
|
|
5267
|
+
script = expression;
|
|
5268
|
+
}
|
|
5269
|
+
const params = {
|
|
5270
|
+
expression: script,
|
|
5271
|
+
returnByValue: true,
|
|
5272
|
+
awaitPromise: true
|
|
5273
|
+
};
|
|
5274
|
+
if (this.currentFrameContextId !== null) {
|
|
5275
|
+
params["contextId"] = this.currentFrameContextId;
|
|
5276
|
+
}
|
|
5277
|
+
const result = await this.cdp.send("Runtime.evaluate", params);
|
|
5278
|
+
if (result.exceptionDetails) {
|
|
5279
|
+
throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
|
|
5280
|
+
}
|
|
5281
|
+
return result.result.value;
|
|
5282
|
+
}
|
|
5283
|
+
// ============ Screenshots ============
|
|
5284
|
+
/**
|
|
5285
|
+
* Take a screenshot
|
|
5286
|
+
*/
|
|
5287
|
+
async screenshot(options = {}) {
|
|
5288
|
+
const { format = "png", quality, fullPage = false } = options;
|
|
5289
|
+
let clip;
|
|
5290
|
+
if (fullPage) {
|
|
5291
|
+
const metrics = await this.cdp.send("Page.getLayoutMetrics");
|
|
5292
|
+
clip = {
|
|
5293
|
+
x: 0,
|
|
5294
|
+
y: 0,
|
|
5295
|
+
width: metrics.contentSize.width,
|
|
5296
|
+
height: metrics.contentSize.height,
|
|
5297
|
+
scale: 1
|
|
5298
|
+
};
|
|
5299
|
+
}
|
|
5300
|
+
const result = await this.cdp.send("Page.captureScreenshot", {
|
|
5301
|
+
format,
|
|
5302
|
+
quality: format === "png" ? void 0 : quality,
|
|
5303
|
+
clip,
|
|
5304
|
+
captureBeyondViewport: fullPage
|
|
5305
|
+
});
|
|
5306
|
+
return result.data;
|
|
5307
|
+
}
|
|
5308
|
+
// ============ Text Extraction ============
|
|
5309
|
+
/**
|
|
5310
|
+
* Get text content from the page or a specific element
|
|
5311
|
+
*/
|
|
5312
|
+
async text(selector) {
|
|
5313
|
+
if (!selector) {
|
|
5314
|
+
const result = await this.evaluateInFrame(
|
|
5315
|
+
"document.body.innerText"
|
|
5316
|
+
);
|
|
5317
|
+
return result.result.value ?? "";
|
|
5318
|
+
}
|
|
5319
|
+
return this.withStaleNodeRetry(async () => {
|
|
5320
|
+
const element = await this.findElement(selector, { timeout: DEFAULT_TIMEOUT2 });
|
|
5321
|
+
if (!element) return "";
|
|
5322
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
5323
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5324
|
+
objectId,
|
|
5325
|
+
functionDeclaration: 'function() { return this.innerText || this.textContent || ""; }',
|
|
5326
|
+
returnByValue: true
|
|
5327
|
+
});
|
|
5328
|
+
return result.result.value ?? "";
|
|
5329
|
+
});
|
|
5330
|
+
}
|
|
5331
|
+
// ============ File Handling ============
|
|
5332
|
+
/**
|
|
5333
|
+
* Set files on a file input
|
|
5334
|
+
*/
|
|
5335
|
+
async setInputFiles(selector, files, options = {}) {
|
|
5336
|
+
return this.withStaleNodeRetry(async () => {
|
|
5337
|
+
const element = await this.findElement(selector, options);
|
|
5338
|
+
if (!element) {
|
|
5339
|
+
if (options.optional) return false;
|
|
5340
|
+
throw new ElementNotFoundError(selector);
|
|
5341
|
+
}
|
|
5342
|
+
const fileData = await Promise.all(
|
|
5343
|
+
files.map(async (f) => {
|
|
5344
|
+
let base64;
|
|
5345
|
+
if (typeof f.buffer === "string") {
|
|
5346
|
+
base64 = f.buffer;
|
|
5347
|
+
} else {
|
|
5348
|
+
const bytes = new Uint8Array(f.buffer);
|
|
5349
|
+
base64 = btoa(String.fromCharCode(...bytes));
|
|
5350
|
+
}
|
|
5351
|
+
return { name: f.name, mimeType: f.mimeType, data: base64 };
|
|
5352
|
+
})
|
|
5353
|
+
);
|
|
5354
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
5355
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5356
|
+
objectId,
|
|
5357
|
+
functionDeclaration: `function(files) {
|
|
5358
|
+
if (!(this instanceof HTMLInputElement) || this.type !== 'file') {
|
|
5359
|
+
return { ok: false, fileCount: 0 };
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5362
|
+
const dt = new DataTransfer();
|
|
5363
|
+
for (const f of files) {
|
|
5364
|
+
const bytes = Uint8Array.from(atob(f.data), function(c) { return c.charCodeAt(0); });
|
|
5365
|
+
const file = new File([bytes], f.name, { type: f.mimeType });
|
|
5366
|
+
dt.items.add(file);
|
|
5367
|
+
}
|
|
5368
|
+
|
|
5369
|
+
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'files');
|
|
5370
|
+
if (descriptor && descriptor.set) {
|
|
5371
|
+
descriptor.set.call(this, dt.files);
|
|
5372
|
+
} else {
|
|
5373
|
+
this.files = dt.files;
|
|
5374
|
+
}
|
|
5375
|
+
|
|
5376
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
5377
|
+
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
5378
|
+
return {
|
|
5379
|
+
ok: (this.files && this.files.length === files.length) || files.length === 0,
|
|
5380
|
+
fileCount: this.files ? this.files.length : 0
|
|
5381
|
+
};
|
|
5382
|
+
}`,
|
|
5383
|
+
arguments: [{ value: fileData }],
|
|
5384
|
+
returnByValue: true
|
|
5385
|
+
});
|
|
5386
|
+
if (!result.result.value.ok) {
|
|
5387
|
+
if (options.optional) return false;
|
|
5388
|
+
throw new Error("Failed to set files on input");
|
|
5389
|
+
}
|
|
5390
|
+
return true;
|
|
5391
|
+
});
|
|
5392
|
+
}
|
|
5393
|
+
async getNativeSelectMetadata(objectId, targets) {
|
|
5394
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5395
|
+
objectId,
|
|
5396
|
+
functionDeclaration: `function(targetValues) {
|
|
5397
|
+
if (!(this instanceof HTMLSelectElement)) {
|
|
5398
|
+
return {
|
|
5399
|
+
currentIndex: -1,
|
|
5400
|
+
currentValue: '',
|
|
5401
|
+
disabled: [],
|
|
5402
|
+
isSelect: false,
|
|
5403
|
+
missing: Array.isArray(targetValues) ? targetValues.map(String) : [],
|
|
5404
|
+
multiple: false,
|
|
5405
|
+
options: [],
|
|
5406
|
+
selectedValues: [],
|
|
5407
|
+
targetIndexes: []
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5411
|
+
var allOptions = Array.from(this.options).map(function(opt, index) {
|
|
5412
|
+
return { index: index, label: opt.label || opt.text || '', value: opt.value || '' };
|
|
5413
|
+
});
|
|
5414
|
+
var targetIndexes = [];
|
|
5415
|
+
var missing = [];
|
|
5416
|
+
var disabled = [];
|
|
5417
|
+
|
|
5418
|
+
for (var i = 0; i < targetValues.length; i++) {
|
|
5419
|
+
var target = String(targetValues[i]);
|
|
5420
|
+
var idx = -1;
|
|
5421
|
+
|
|
5422
|
+
for (var j = 0; j < this.options.length; j++) {
|
|
5423
|
+
var opt = this.options[j];
|
|
5424
|
+
if (opt.value === target || opt.text === target || opt.label === target) {
|
|
5425
|
+
idx = j;
|
|
5426
|
+
break;
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
if (idx === -1 && /^\\d+$/.test(target)) {
|
|
5431
|
+
var numericIndex = parseInt(target, 10);
|
|
5432
|
+
if (numericIndex >= 0 && numericIndex < this.options.length) {
|
|
5433
|
+
idx = numericIndex;
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5437
|
+
if (idx === -1) {
|
|
5438
|
+
missing.push(target);
|
|
5439
|
+
continue;
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
if (this.options[idx] && this.options[idx].disabled) {
|
|
5443
|
+
disabled.push(target);
|
|
5444
|
+
continue;
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
if (targetIndexes.indexOf(idx) === -1) {
|
|
5448
|
+
targetIndexes.push(idx);
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
|
|
5452
|
+
return {
|
|
5453
|
+
currentIndex: this.selectedIndex,
|
|
5454
|
+
currentValue: this.value || '',
|
|
5455
|
+
disabled: disabled,
|
|
5456
|
+
isSelect: true,
|
|
5457
|
+
missing: missing,
|
|
5458
|
+
multiple: !!this.multiple,
|
|
5459
|
+
options: allOptions,
|
|
5460
|
+
selectedValues: Array.from(this.selectedOptions).map(function(opt) { return opt.value || ''; }),
|
|
5461
|
+
targetIndexes: targetIndexes
|
|
5462
|
+
};
|
|
5463
|
+
}`,
|
|
5464
|
+
arguments: [{ value: targets }],
|
|
5465
|
+
returnByValue: true
|
|
5466
|
+
});
|
|
5467
|
+
return result.result.value;
|
|
5468
|
+
}
|
|
5469
|
+
async readNativeSelectValues(objectId) {
|
|
5470
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5471
|
+
objectId,
|
|
5472
|
+
functionDeclaration: 'function() { return this instanceof HTMLSelectElement ? Array.from(this.selectedOptions).map(function(opt) { return opt.value || ""; }) : []; }',
|
|
5473
|
+
returnByValue: true
|
|
5474
|
+
});
|
|
5475
|
+
return result.result.value ?? [];
|
|
5476
|
+
}
|
|
5477
|
+
selectValuesMatch(actual, expected, multiple) {
|
|
5478
|
+
if (!multiple) {
|
|
5479
|
+
return (actual[0] ?? "") === (expected[0] ?? "");
|
|
3994
5480
|
}
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
|
|
5481
|
+
if (actual.length !== expected.length) {
|
|
5482
|
+
return false;
|
|
3998
5483
|
}
|
|
3999
|
-
|
|
5484
|
+
const actualSorted = [...actual].sort();
|
|
5485
|
+
const expectedSorted = [...expected].sort();
|
|
5486
|
+
return actualSorted.every((value, index) => value === expectedSorted[index]);
|
|
4000
5487
|
}
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
const
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
height: metrics.contentSize.height,
|
|
4015
|
-
scale: 1
|
|
4016
|
-
};
|
|
5488
|
+
async applyNativeSelectByKeyboard(nodeId, objectId, currentIndex, targetIndex) {
|
|
5489
|
+
await this.cdp.send("DOM.focus", { nodeId });
|
|
5490
|
+
if (targetIndex !== currentIndex) {
|
|
5491
|
+
let effectiveIndex = currentIndex;
|
|
5492
|
+
if (effectiveIndex < 0 || targetIndex < effectiveIndex) {
|
|
5493
|
+
await this.dispatchKey("Home");
|
|
5494
|
+
effectiveIndex = 0;
|
|
5495
|
+
}
|
|
5496
|
+
const steps = targetIndex - effectiveIndex;
|
|
5497
|
+
const direction = steps >= 0 ? "ArrowDown" : "ArrowUp";
|
|
5498
|
+
for (let i = 0; i < Math.abs(steps); i++) {
|
|
5499
|
+
await this.dispatchKey(direction);
|
|
5500
|
+
}
|
|
4017
5501
|
}
|
|
4018
|
-
const
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
5502
|
+
const selectedValues = await this.readNativeSelectValues(objectId);
|
|
5503
|
+
return selectedValues[0] !== void 0;
|
|
5504
|
+
}
|
|
5505
|
+
async applyNativeSelectFallback(objectId, targetIndexes) {
|
|
5506
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
5507
|
+
objectId,
|
|
5508
|
+
functionDeclaration: `function(indexes) {
|
|
5509
|
+
if (!(this instanceof HTMLSelectElement)) return false;
|
|
5510
|
+
|
|
5511
|
+
var wanted = new Set(indexes.map(function(index) { return Number(index); }));
|
|
5512
|
+
for (var i = 0; i < this.options.length; i++) {
|
|
5513
|
+
this.options[i].selected = wanted.has(i);
|
|
5514
|
+
}
|
|
5515
|
+
if (!this.multiple && indexes.length === 1) {
|
|
5516
|
+
this.selectedIndex = indexes[0];
|
|
5517
|
+
}
|
|
5518
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
5519
|
+
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
5520
|
+
return true;
|
|
5521
|
+
}`,
|
|
5522
|
+
arguments: [{ value: targetIndexes }],
|
|
5523
|
+
returnByValue: true
|
|
4023
5524
|
});
|
|
4024
|
-
return result.data;
|
|
4025
5525
|
}
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
5526
|
+
async selectEditableContent(objectId) {
|
|
5527
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
5528
|
+
objectId,
|
|
5529
|
+
functionDeclaration: `function() {
|
|
5530
|
+
if (this.isContentEditable) {
|
|
5531
|
+
this.focus();
|
|
5532
|
+
const range = document.createRange();
|
|
5533
|
+
range.selectNodeContents(this);
|
|
5534
|
+
const selection = window.getSelection();
|
|
5535
|
+
if (selection) {
|
|
5536
|
+
selection.removeAllRanges();
|
|
5537
|
+
selection.addRange(range);
|
|
5538
|
+
}
|
|
5539
|
+
return;
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5542
|
+
if (this.tagName === 'TEXTAREA') {
|
|
5543
|
+
this.selectionStart = 0;
|
|
5544
|
+
this.selectionEnd = this.value.length;
|
|
5545
|
+
this.focus();
|
|
5546
|
+
return;
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
if (typeof this.select === 'function') {
|
|
5550
|
+
this.select();
|
|
5551
|
+
}
|
|
5552
|
+
this.focus();
|
|
5553
|
+
}`
|
|
5554
|
+
});
|
|
5555
|
+
}
|
|
5556
|
+
async clearEditableSelection(objectId, key) {
|
|
5557
|
+
await this.selectEditableContent(objectId);
|
|
5558
|
+
await this.dispatchKey(key);
|
|
5559
|
+
}
|
|
5560
|
+
async readEditableValue(objectId) {
|
|
5561
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5562
|
+
objectId,
|
|
5563
|
+
functionDeclaration: `function() {
|
|
5564
|
+
if (this.isContentEditable) {
|
|
5565
|
+
return this.textContent || '';
|
|
5566
|
+
}
|
|
5567
|
+
return this.value || '';
|
|
5568
|
+
}`,
|
|
5569
|
+
returnByValue: true
|
|
5570
|
+
});
|
|
4033
5571
|
return result.result.value ?? "";
|
|
4034
5572
|
}
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
const element = await this.findElement(selector, options);
|
|
4041
|
-
if (!element) {
|
|
4042
|
-
if (options.optional) return false;
|
|
4043
|
-
throw new ElementNotFoundError(selector);
|
|
5573
|
+
async typeEditableFallback(nodeId, objectId, value) {
|
|
5574
|
+
await this.selectEditableContent(objectId);
|
|
5575
|
+
await this.cdp.send("DOM.focus", { nodeId });
|
|
5576
|
+
for (const char of value) {
|
|
5577
|
+
await this.dispatchKey(char);
|
|
4044
5578
|
}
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
5579
|
+
}
|
|
5580
|
+
async applyRecordedSelectFallback(objectId, targetIndexes) {
|
|
5581
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
5582
|
+
objectId,
|
|
5583
|
+
functionDeclaration: `function(indexes) {
|
|
5584
|
+
if (!(this instanceof HTMLSelectElement)) return false;
|
|
5585
|
+
|
|
5586
|
+
var wanted = new Set(indexes.map(function(index) { return Number(index); }));
|
|
5587
|
+
for (var i = 0; i < this.options.length; i++) {
|
|
5588
|
+
this.options[i].selected = wanted.has(i);
|
|
4053
5589
|
}
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
5590
|
+
if (!this.multiple && indexes.length === 1) {
|
|
5591
|
+
this.selectedIndex = indexes[0];
|
|
5592
|
+
}
|
|
5593
|
+
return true;
|
|
5594
|
+
}`,
|
|
5595
|
+
arguments: [{ value: targetIndexes }],
|
|
5596
|
+
returnByValue: true
|
|
5597
|
+
});
|
|
5598
|
+
return this.invokeRecordedEventListeners(objectId, ["input", "change"]);
|
|
5599
|
+
}
|
|
5600
|
+
async invokeRecordedEventListeners(objectId, eventTypes) {
|
|
5601
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5602
|
+
objectId,
|
|
5603
|
+
functionDeclaration: `function(types) {
|
|
5604
|
+
function buildPath(target) {
|
|
5605
|
+
var path = [];
|
|
5606
|
+
var node = target;
|
|
5607
|
+
|
|
5608
|
+
while (node) {
|
|
5609
|
+
path.push(node);
|
|
5610
|
+
|
|
5611
|
+
if (node.parentElement) {
|
|
5612
|
+
node = node.parentElement;
|
|
5613
|
+
continue;
|
|
5614
|
+
}
|
|
5615
|
+
|
|
5616
|
+
if (node === document) {
|
|
5617
|
+
node = window;
|
|
5618
|
+
continue;
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
if (node.defaultView && node !== node.defaultView) {
|
|
5622
|
+
node = node.defaultView;
|
|
5623
|
+
continue;
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5626
|
+
if (node.ownerDocument && node !== node.ownerDocument) {
|
|
5627
|
+
node = node.ownerDocument;
|
|
5628
|
+
continue;
|
|
5629
|
+
}
|
|
5630
|
+
|
|
5631
|
+
var root = node.getRootNode && node.getRootNode();
|
|
5632
|
+
if (root && root !== node && root.host) {
|
|
5633
|
+
node = root.host;
|
|
5634
|
+
continue;
|
|
5635
|
+
}
|
|
4061
5636
|
|
|
4062
|
-
|
|
4063
|
-
|
|
5637
|
+
node = null;
|
|
5638
|
+
}
|
|
4064
5639
|
|
|
4065
|
-
|
|
4066
|
-
const bytes = Uint8Array.from(atob(f.data), c => c.charCodeAt(0));
|
|
4067
|
-
const file = new File([bytes], f.name, { type: f.mimeType });
|
|
4068
|
-
dt.items.add(file);
|
|
5640
|
+
return path;
|
|
4069
5641
|
}
|
|
4070
5642
|
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
5643
|
+
function createEvent(type, target, currentTarget, path, phase) {
|
|
5644
|
+
return {
|
|
5645
|
+
type: type,
|
|
5646
|
+
target: target,
|
|
5647
|
+
currentTarget: currentTarget,
|
|
5648
|
+
srcElement: target,
|
|
5649
|
+
isTrusted: true,
|
|
5650
|
+
bubbles: true,
|
|
5651
|
+
cancelable: true,
|
|
5652
|
+
composed: true,
|
|
5653
|
+
defaultPrevented: false,
|
|
5654
|
+
eventPhase: phase,
|
|
5655
|
+
timeStamp: Date.now(),
|
|
5656
|
+
preventDefault: function() {
|
|
5657
|
+
this.defaultPrevented = true;
|
|
5658
|
+
},
|
|
5659
|
+
stopPropagation: function() {
|
|
5660
|
+
this.__stopped = true;
|
|
5661
|
+
},
|
|
5662
|
+
stopImmediatePropagation: function() {
|
|
5663
|
+
this.__stopped = true;
|
|
5664
|
+
this.__immediateStopped = true;
|
|
5665
|
+
},
|
|
5666
|
+
composedPath: function() {
|
|
5667
|
+
return path.slice();
|
|
5668
|
+
}
|
|
5669
|
+
};
|
|
5670
|
+
}
|
|
5671
|
+
|
|
5672
|
+
function invokePhase(type, nodes, capture, target, path) {
|
|
5673
|
+
var invoked = false;
|
|
5674
|
+
|
|
5675
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
5676
|
+
var currentTarget = nodes[i];
|
|
5677
|
+
|
|
5678
|
+
var phase = currentTarget === target ? 2 : capture ? 1 : 3;
|
|
5679
|
+
|
|
5680
|
+
// Invoke inline handler if present (e.g. onclick, oninput)
|
|
5681
|
+
var inlineHandler = currentTarget['on' + type];
|
|
5682
|
+
if (typeof inlineHandler === 'function') {
|
|
5683
|
+
var inlineEvent = createEvent(type, target, currentTarget, path, phase);
|
|
5684
|
+
inlineHandler.call(currentTarget, inlineEvent);
|
|
5685
|
+
invoked = true;
|
|
5686
|
+
if (inlineEvent.__stopped) break;
|
|
5687
|
+
}
|
|
5688
|
+
|
|
5689
|
+
var store = currentTarget && currentTarget.__bpEventListeners;
|
|
5690
|
+
var entries = store && store[type];
|
|
5691
|
+
if (!Array.isArray(entries) || entries.length === 0) continue;
|
|
5692
|
+
|
|
5693
|
+
var event = createEvent(type, target, currentTarget, path, phase);
|
|
5694
|
+
|
|
5695
|
+
for (var j = 0; j < entries.length; j++) {
|
|
5696
|
+
var entry = entries[j];
|
|
5697
|
+
if (!!entry.capture !== capture) continue;
|
|
5698
|
+
|
|
5699
|
+
var listener = entry.listener;
|
|
5700
|
+
if (typeof listener === 'function') {
|
|
5701
|
+
listener.call(currentTarget, event);
|
|
5702
|
+
invoked = true;
|
|
5703
|
+
} else if (listener && typeof listener.handleEvent === 'function') {
|
|
5704
|
+
listener.handleEvent(event);
|
|
5705
|
+
invoked = true;
|
|
5706
|
+
}
|
|
5707
|
+
|
|
5708
|
+
if (event.__immediateStopped) {
|
|
5709
|
+
break;
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
if (event.__stopped) {
|
|
5714
|
+
break;
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
|
|
5718
|
+
return invoked;
|
|
5719
|
+
}
|
|
5720
|
+
|
|
5721
|
+
var path = buildPath(this);
|
|
5722
|
+
var capturePath = path.slice().reverse();
|
|
5723
|
+
var bubblePath = path.slice();
|
|
5724
|
+
var invokedAny = false;
|
|
5725
|
+
|
|
5726
|
+
for (var i = 0; i < types.length; i++) {
|
|
5727
|
+
var type = String(types[i]);
|
|
5728
|
+
if (invokePhase(type, capturePath, true, this, path)) {
|
|
5729
|
+
invokedAny = true;
|
|
5730
|
+
}
|
|
5731
|
+
if (invokePhase(type, bubblePath, false, this, path)) {
|
|
5732
|
+
invokedAny = true;
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
|
|
5736
|
+
return invokedAny;
|
|
5737
|
+
}`,
|
|
5738
|
+
arguments: [{ value: eventTypes }],
|
|
4075
5739
|
returnByValue: true
|
|
4076
5740
|
});
|
|
4077
|
-
return
|
|
5741
|
+
return result.result.value ?? false;
|
|
4078
5742
|
}
|
|
4079
5743
|
/**
|
|
4080
5744
|
* Wait for a download to complete, triggered by an action
|
|
@@ -4234,7 +5898,7 @@ var Page = class {
|
|
|
4234
5898
|
return lines.join("\n");
|
|
4235
5899
|
};
|
|
4236
5900
|
const text = formatTree(accessibilityTree);
|
|
4237
|
-
|
|
5901
|
+
const result = {
|
|
4238
5902
|
url,
|
|
4239
5903
|
title,
|
|
4240
5904
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4242,6 +5906,8 @@ var Page = class {
|
|
|
4242
5906
|
interactiveElements,
|
|
4243
5907
|
text
|
|
4244
5908
|
};
|
|
5909
|
+
this.lastSnapshot = result;
|
|
5910
|
+
return result;
|
|
4245
5911
|
}
|
|
4246
5912
|
/**
|
|
4247
5913
|
* Export the current ref map for cross-exec reuse (CLI).
|
|
@@ -4655,8 +6321,15 @@ var Page = class {
|
|
|
4655
6321
|
}
|
|
4656
6322
|
};
|
|
4657
6323
|
if (this.dialogHandler) {
|
|
6324
|
+
const DIALOG_TIMEOUT = 5e3;
|
|
4658
6325
|
try {
|
|
4659
|
-
await
|
|
6326
|
+
await Promise.race([
|
|
6327
|
+
this.dialogHandler(dialog),
|
|
6328
|
+
sleep5(DIALOG_TIMEOUT).then(() => {
|
|
6329
|
+
console.warn("[browser-pilot] Dialog handler timed out after 5s, auto-dismissing");
|
|
6330
|
+
return dialog.dismiss();
|
|
6331
|
+
})
|
|
6332
|
+
]);
|
|
4660
6333
|
} catch (e) {
|
|
4661
6334
|
console.error("[Dialog handler error]", e);
|
|
4662
6335
|
await dialog.dismiss();
|
|
@@ -4736,6 +6409,7 @@ var Page = class {
|
|
|
4736
6409
|
this.refMap.clear();
|
|
4737
6410
|
this.currentFrame = null;
|
|
4738
6411
|
this.currentFrameContextId = null;
|
|
6412
|
+
this.brokenFrame = null;
|
|
4739
6413
|
this.frameContexts.clear();
|
|
4740
6414
|
this.dialogHandler = null;
|
|
4741
6415
|
try {
|
|
@@ -4770,11 +6444,13 @@ var Page = class {
|
|
|
4770
6444
|
try {
|
|
4771
6445
|
return await fn();
|
|
4772
6446
|
} catch (e) {
|
|
4773
|
-
|
|
6447
|
+
const message = e instanceof Error ? e.message : "";
|
|
6448
|
+
if (e instanceof Error && (message.includes("Could not find node with given id") || message.includes("Node with given id does not belong to the document") || message.includes("No node with given id found") || message.includes("Could not find object with given id") || message.includes("Cannot find context with specified id") || message.includes("Cannot find context with given id") || message.includes("Execution context was destroyed") || message.includes("No execution context with given id") || message.includes("Argument should belong to the same JavaScript world"))) {
|
|
4774
6449
|
lastError = e;
|
|
4775
6450
|
if (attempt < retries) {
|
|
4776
6451
|
this.rootNodeId = null;
|
|
4777
|
-
|
|
6452
|
+
this.currentFrameContextId = null;
|
|
6453
|
+
await sleep5(delay);
|
|
4778
6454
|
continue;
|
|
4779
6455
|
}
|
|
4780
6456
|
}
|
|
@@ -4819,6 +6495,39 @@ var Page = class {
|
|
|
4819
6495
|
}
|
|
4820
6496
|
}
|
|
4821
6497
|
}
|
|
6498
|
+
if (selectorList.every((s) => s.startsWith("ref:")) && this.lastSnapshot) {
|
|
6499
|
+
for (const selector of selectorList) {
|
|
6500
|
+
const ref = selector.slice(4);
|
|
6501
|
+
const originalElement = this.lastSnapshot.interactiveElements.find((e) => e.ref === ref);
|
|
6502
|
+
if (!originalElement) continue;
|
|
6503
|
+
const freshSnapshot = await this.snapshot();
|
|
6504
|
+
const match = freshSnapshot.interactiveElements.find(
|
|
6505
|
+
(e) => e.role === originalElement.role && e.name === originalElement.name
|
|
6506
|
+
);
|
|
6507
|
+
if (match) {
|
|
6508
|
+
const newBackendNodeId = this.refMap.get(match.ref);
|
|
6509
|
+
if (newBackendNodeId) {
|
|
6510
|
+
try {
|
|
6511
|
+
await this.ensureRootNode();
|
|
6512
|
+
const pushResult = await this.cdp.send(
|
|
6513
|
+
"DOM.pushNodesByBackendIdsToFrontend",
|
|
6514
|
+
{ backendNodeIds: [newBackendNodeId] }
|
|
6515
|
+
);
|
|
6516
|
+
if (pushResult.nodeIds?.[0]) {
|
|
6517
|
+
this._lastMatchedSelector = `ref:${match.ref}`;
|
|
6518
|
+
return {
|
|
6519
|
+
nodeId: pushResult.nodeIds[0],
|
|
6520
|
+
backendNodeId: newBackendNodeId,
|
|
6521
|
+
selector: `ref:${match.ref}`,
|
|
6522
|
+
waitedMs: 0
|
|
6523
|
+
};
|
|
6524
|
+
}
|
|
6525
|
+
} catch {
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
4822
6531
|
const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
|
|
4823
6532
|
if (cssSelectors.length === 0) {
|
|
4824
6533
|
return null;
|
|
@@ -4882,6 +6591,34 @@ var Page = class {
|
|
|
4882
6591
|
*/
|
|
4883
6592
|
async ensureRootNode() {
|
|
4884
6593
|
if (this.rootNodeId) return;
|
|
6594
|
+
if (this.currentFrame) {
|
|
6595
|
+
const mainDocument = await this.cdp.send("DOM.getDocument", {
|
|
6596
|
+
depth: 0
|
|
6597
|
+
});
|
|
6598
|
+
const iframeNode = await this.cdp.send("DOM.querySelector", {
|
|
6599
|
+
nodeId: mainDocument.root.nodeId,
|
|
6600
|
+
selector: this.currentFrame
|
|
6601
|
+
});
|
|
6602
|
+
if (iframeNode.nodeId) {
|
|
6603
|
+
const frameResult = await this.cdp.send("DOM.describeNode", {
|
|
6604
|
+
nodeId: iframeNode.nodeId,
|
|
6605
|
+
depth: 1
|
|
6606
|
+
});
|
|
6607
|
+
if (frameResult.node.contentDocument?.nodeId) {
|
|
6608
|
+
this.rootNodeId = frameResult.node.contentDocument.nodeId;
|
|
6609
|
+
if (frameResult.node.frameId) {
|
|
6610
|
+
let contextId = this.frameExecutionContexts.get(frameResult.node.frameId);
|
|
6611
|
+
if (!contextId) {
|
|
6612
|
+
contextId = await this.waitForFrameContext(frameResult.node.frameId, 1e3);
|
|
6613
|
+
}
|
|
6614
|
+
this.currentFrameContextId = contextId ?? null;
|
|
6615
|
+
}
|
|
6616
|
+
return;
|
|
6617
|
+
}
|
|
6618
|
+
}
|
|
6619
|
+
this.currentFrame = null;
|
|
6620
|
+
this.currentFrameContextId = null;
|
|
6621
|
+
}
|
|
4885
6622
|
const doc = await this.cdp.send("DOM.getDocument", {
|
|
4886
6623
|
depth: 0
|
|
4887
6624
|
});
|
|
@@ -4892,6 +6629,11 @@ var Page = class {
|
|
|
4892
6629
|
* Automatically injects contextId when in an iframe
|
|
4893
6630
|
*/
|
|
4894
6631
|
async evaluateInFrame(expression, options = {}) {
|
|
6632
|
+
if (this.brokenFrame && this.currentFrame) {
|
|
6633
|
+
throw new Error(
|
|
6634
|
+
`Cannot evaluate JavaScript in frame "${this.brokenFrame}": execution context is unavailable (cross-origin or sandboxed iframe). DOM operations (click, fill, etc.) may still work via CDP.`
|
|
6635
|
+
);
|
|
6636
|
+
}
|
|
4895
6637
|
const params = {
|
|
4896
6638
|
expression,
|
|
4897
6639
|
returnByValue: options.returnByValue ?? true,
|
|
@@ -4903,10 +6645,43 @@ var Page = class {
|
|
|
4903
6645
|
return this.cdp.send("Runtime.evaluate", params);
|
|
4904
6646
|
}
|
|
4905
6647
|
/**
|
|
4906
|
-
* Scroll an element into view
|
|
6648
|
+
* Scroll an element into view, with fallback to center-scroll if clipped by fixed headers
|
|
4907
6649
|
*/
|
|
4908
6650
|
async scrollIntoView(nodeId) {
|
|
4909
6651
|
await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId });
|
|
6652
|
+
if (!await this.isInViewport(nodeId)) {
|
|
6653
|
+
const objectId = await this.resolveObjectId(nodeId);
|
|
6654
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
6655
|
+
objectId,
|
|
6656
|
+
functionDeclaration: `function() { this.scrollIntoView({ block: 'center', inline: 'center' }); }`
|
|
6657
|
+
});
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
6660
|
+
/**
|
|
6661
|
+
* Check if element is within the visible viewport
|
|
6662
|
+
*/
|
|
6663
|
+
async isInViewport(nodeId) {
|
|
6664
|
+
try {
|
|
6665
|
+
const objectId = await this.resolveObjectId(nodeId);
|
|
6666
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
6667
|
+
objectId,
|
|
6668
|
+
functionDeclaration: `function() {
|
|
6669
|
+
var rect = this.getBoundingClientRect();
|
|
6670
|
+
return (
|
|
6671
|
+
rect.top >= 0 &&
|
|
6672
|
+
rect.left >= 0 &&
|
|
6673
|
+
rect.bottom <= window.innerHeight &&
|
|
6674
|
+
rect.right <= window.innerWidth &&
|
|
6675
|
+
rect.width > 0 &&
|
|
6676
|
+
rect.height > 0
|
|
6677
|
+
);
|
|
6678
|
+
}`,
|
|
6679
|
+
returnByValue: true
|
|
6680
|
+
});
|
|
6681
|
+
return result?.result?.value === true;
|
|
6682
|
+
} catch {
|
|
6683
|
+
return true;
|
|
6684
|
+
}
|
|
4910
6685
|
}
|
|
4911
6686
|
/**
|
|
4912
6687
|
* Get element box model (position and dimensions)
|
|
@@ -4922,30 +6697,147 @@ var Page = class {
|
|
|
4922
6697
|
}
|
|
4923
6698
|
}
|
|
4924
6699
|
/**
|
|
4925
|
-
* Click an element by node ID
|
|
6700
|
+
* Click an element by node ID using Playwright's 3-event sequence:
|
|
6701
|
+
* mouseMoved → mousePressed → mouseReleased (sequential).
|
|
6702
|
+
* Uses DOM.getContentQuads for accurate coordinates (handles CSS transforms).
|
|
6703
|
+
* Falls back to JS this.click() if CDP mouse dispatch fails.
|
|
4926
6704
|
*/
|
|
4927
6705
|
async clickElement(nodeId) {
|
|
4928
|
-
const
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
6706
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
6707
|
+
nodeId
|
|
6708
|
+
});
|
|
6709
|
+
let x;
|
|
6710
|
+
let y;
|
|
6711
|
+
try {
|
|
6712
|
+
const { quads } = await this.cdp.send("DOM.getContentQuads", {
|
|
6713
|
+
objectId: object.objectId
|
|
6714
|
+
});
|
|
6715
|
+
if (quads && quads.length > 0) {
|
|
6716
|
+
const quad = quads[0];
|
|
6717
|
+
x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
6718
|
+
y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
6719
|
+
} else {
|
|
6720
|
+
throw new Error("No quads");
|
|
6721
|
+
}
|
|
6722
|
+
} catch {
|
|
6723
|
+
const box = await this.getBoxModel(nodeId);
|
|
6724
|
+
if (!box) throw new Error("Could not get element position for click");
|
|
6725
|
+
x = box.content[0] + box.width / 2;
|
|
6726
|
+
y = box.content[1] + box.height / 2;
|
|
6727
|
+
}
|
|
6728
|
+
try {
|
|
6729
|
+
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
6730
|
+
type: "mouseMoved",
|
|
6731
|
+
x,
|
|
6732
|
+
y,
|
|
6733
|
+
button: "none",
|
|
6734
|
+
buttons: 0,
|
|
6735
|
+
modifiers: 0
|
|
6736
|
+
});
|
|
6737
|
+
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
6738
|
+
type: "mousePressed",
|
|
6739
|
+
x,
|
|
6740
|
+
y,
|
|
6741
|
+
button: "left",
|
|
6742
|
+
buttons: 1,
|
|
6743
|
+
clickCount: 1,
|
|
6744
|
+
modifiers: 0
|
|
6745
|
+
});
|
|
6746
|
+
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
6747
|
+
type: "mouseReleased",
|
|
6748
|
+
x,
|
|
6749
|
+
y,
|
|
6750
|
+
button: "left",
|
|
6751
|
+
buttons: 0,
|
|
6752
|
+
clickCount: 1,
|
|
6753
|
+
modifiers: 0
|
|
6754
|
+
});
|
|
6755
|
+
} catch {
|
|
6756
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
6757
|
+
objectId: object.objectId,
|
|
6758
|
+
functionDeclaration: "function() { this.click(); }"
|
|
6759
|
+
});
|
|
6760
|
+
}
|
|
6761
|
+
await this.cdp.send("Runtime.evaluate", { expression: "0" });
|
|
6762
|
+
}
|
|
6763
|
+
/**
|
|
6764
|
+
* Resolve a nodeId to a Remote Object ID for use with Runtime.callFunctionOn
|
|
6765
|
+
*/
|
|
6766
|
+
async resolveObjectId(nodeId) {
|
|
6767
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
6768
|
+
nodeId
|
|
4940
6769
|
});
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
6770
|
+
return object.objectId;
|
|
6771
|
+
}
|
|
6772
|
+
async dispatchKeyDefinition(def, modifierBitmask = 0) {
|
|
6773
|
+
const downParams = {
|
|
6774
|
+
type: def.text !== void 0 ? "keyDown" : "rawKeyDown",
|
|
6775
|
+
key: def.key,
|
|
6776
|
+
code: def.code,
|
|
6777
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
6778
|
+
modifiers: modifierBitmask,
|
|
6779
|
+
autoRepeat: false,
|
|
6780
|
+
location: def.location ?? 0,
|
|
6781
|
+
isKeypad: false
|
|
6782
|
+
};
|
|
6783
|
+
if (def.text !== void 0) {
|
|
6784
|
+
downParams["text"] = def.text;
|
|
6785
|
+
downParams["unmodifiedText"] = def.text;
|
|
6786
|
+
}
|
|
6787
|
+
await this.cdp.send("Input.dispatchKeyEvent", downParams);
|
|
6788
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
6789
|
+
type: "keyUp",
|
|
6790
|
+
key: def.key,
|
|
6791
|
+
code: def.code,
|
|
6792
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
6793
|
+
modifiers: modifierBitmask,
|
|
6794
|
+
location: def.location ?? 0
|
|
4947
6795
|
});
|
|
4948
6796
|
}
|
|
6797
|
+
async dispatchKey(key) {
|
|
6798
|
+
const def = US_KEYBOARD[key];
|
|
6799
|
+
if (def) {
|
|
6800
|
+
await this.dispatchKeyDefinition(def);
|
|
6801
|
+
return;
|
|
6802
|
+
}
|
|
6803
|
+
if (key.length === 1) {
|
|
6804
|
+
await this.cdp.send("Input.insertText", { text: key });
|
|
6805
|
+
return;
|
|
6806
|
+
}
|
|
6807
|
+
await this.dispatchKeyDefinition({ key, code: key, keyCode: 0 });
|
|
6808
|
+
}
|
|
6809
|
+
async dispatchKeyWithModifiers(key, modifiers) {
|
|
6810
|
+
const mask = computeModifierBitmask(modifiers);
|
|
6811
|
+
for (const mod of modifiers) {
|
|
6812
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
6813
|
+
type: "rawKeyDown",
|
|
6814
|
+
key: mod,
|
|
6815
|
+
code: MODIFIER_CODES[mod],
|
|
6816
|
+
windowsVirtualKeyCode: MODIFIER_KEY_CODES[mod],
|
|
6817
|
+
modifiers: mask,
|
|
6818
|
+
location: 1
|
|
6819
|
+
});
|
|
6820
|
+
}
|
|
6821
|
+
const def = US_KEYBOARD[key];
|
|
6822
|
+
if (def) {
|
|
6823
|
+
await this.dispatchKeyDefinition(def, mask);
|
|
6824
|
+
} else if (key.length === 1) {
|
|
6825
|
+
await this.dispatchKeyDefinition({ key, code: key, keyCode: 0, text: key }, mask);
|
|
6826
|
+
} else {
|
|
6827
|
+
await this.dispatchKeyDefinition({ key, code: key, keyCode: 0 }, mask);
|
|
6828
|
+
}
|
|
6829
|
+
for (let i = modifiers.length - 1; i >= 0; i--) {
|
|
6830
|
+
const mod = modifiers[i];
|
|
6831
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
6832
|
+
type: "keyUp",
|
|
6833
|
+
key: mod,
|
|
6834
|
+
code: MODIFIER_CODES[mod],
|
|
6835
|
+
windowsVirtualKeyCode: MODIFIER_KEY_CODES[mod],
|
|
6836
|
+
modifiers: 0,
|
|
6837
|
+
location: 1
|
|
6838
|
+
});
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
4949
6841
|
// ============ Audio I/O ============
|
|
4950
6842
|
/**
|
|
4951
6843
|
* Audio input controller (fake microphone).
|
|
@@ -5018,7 +6910,7 @@ var Page = class {
|
|
|
5018
6910
|
const start = Date.now();
|
|
5019
6911
|
await this.audioOutput.start();
|
|
5020
6912
|
if (options.preDelay && options.preDelay > 0) {
|
|
5021
|
-
await
|
|
6913
|
+
await sleep5(options.preDelay);
|
|
5022
6914
|
}
|
|
5023
6915
|
const inputDone = this.audioInput.play(options.input, {
|
|
5024
6916
|
waitForEnd: !!options.sendSelector
|
|
@@ -5045,12 +6937,68 @@ var Page = class {
|
|
|
5045
6937
|
totalMs: Date.now() - start
|
|
5046
6938
|
};
|
|
5047
6939
|
}
|
|
6940
|
+
/**
|
|
6941
|
+
* Wait for a DOM mutation in the current frame (used for detecting client-side form handling)
|
|
6942
|
+
*/
|
|
6943
|
+
async waitForDOMMutation(options) {
|
|
6944
|
+
await this.evaluateInFrame(
|
|
6945
|
+
`new Promise((resolve) => {
|
|
6946
|
+
var observer = new MutationObserver(function() {
|
|
6947
|
+
observer.disconnect();
|
|
6948
|
+
resolve();
|
|
6949
|
+
});
|
|
6950
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
6951
|
+
setTimeout(function() { observer.disconnect(); resolve(); }, ${options.timeout});
|
|
6952
|
+
})`
|
|
6953
|
+
);
|
|
6954
|
+
}
|
|
6955
|
+
/**
|
|
6956
|
+
* Wait for a frame execution context via Runtime.executionContextCreated event
|
|
6957
|
+
*/
|
|
6958
|
+
async waitForFrameContext(frameId, timeout) {
|
|
6959
|
+
const existing = this.frameExecutionContexts.get(frameId);
|
|
6960
|
+
if (existing) return existing;
|
|
6961
|
+
return new Promise((resolve) => {
|
|
6962
|
+
const timer = setTimeout(() => {
|
|
6963
|
+
cleanup();
|
|
6964
|
+
resolve(void 0);
|
|
6965
|
+
}, timeout);
|
|
6966
|
+
const handler = (params) => {
|
|
6967
|
+
const context = params["context"];
|
|
6968
|
+
if (context.auxData?.frameId === frameId && context.auxData?.isDefault !== false) {
|
|
6969
|
+
cleanup();
|
|
6970
|
+
resolve(context.id);
|
|
6971
|
+
}
|
|
6972
|
+
};
|
|
6973
|
+
const cleanup = () => {
|
|
6974
|
+
clearTimeout(timer);
|
|
6975
|
+
this.cdp.off("Runtime.executionContextCreated", handler);
|
|
6976
|
+
};
|
|
6977
|
+
this.cdp.on("Runtime.executionContextCreated", handler);
|
|
6978
|
+
});
|
|
6979
|
+
}
|
|
5048
6980
|
};
|
|
5049
|
-
function
|
|
6981
|
+
function sleep5(ms) {
|
|
5050
6982
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5051
6983
|
}
|
|
5052
6984
|
|
|
5053
6985
|
// src/browser/browser.ts
|
|
6986
|
+
function scoreTarget(t) {
|
|
6987
|
+
let score = 0;
|
|
6988
|
+
if (t.url.startsWith("http://") || t.url.startsWith("https://")) score += 10;
|
|
6989
|
+
if (t.url.startsWith("chrome://")) score -= 20;
|
|
6990
|
+
if (t.url.startsWith("chrome-extension://")) score -= 15;
|
|
6991
|
+
if (t.url.startsWith("devtools://")) score -= 25;
|
|
6992
|
+
if (t.url === "about:blank") score -= 5;
|
|
6993
|
+
if (!t.attached) score += 3;
|
|
6994
|
+
if (t.title && t.title.length > 0) score += 2;
|
|
6995
|
+
return score;
|
|
6996
|
+
}
|
|
6997
|
+
function pickBestTarget(targets) {
|
|
6998
|
+
if (targets.length === 0) return void 0;
|
|
6999
|
+
const sorted = [...targets].sort((a, b) => scoreTarget(b) - scoreTarget(a));
|
|
7000
|
+
return sorted[0].targetId;
|
|
7001
|
+
}
|
|
5054
7002
|
var Browser = class _Browser {
|
|
5055
7003
|
cdp;
|
|
5056
7004
|
providerSession;
|
|
@@ -5072,28 +7020,46 @@ var Browser = class _Browser {
|
|
|
5072
7020
|
return new _Browser(cdp, provider, session, options);
|
|
5073
7021
|
}
|
|
5074
7022
|
/**
|
|
5075
|
-
* Get or create a page by name
|
|
5076
|
-
* If no name is provided, returns the first available page or creates a new one
|
|
7023
|
+
* Get or create a page by name.
|
|
7024
|
+
* If no name is provided, returns the first available page or creates a new one.
|
|
7025
|
+
*
|
|
7026
|
+
* Target selection heuristics (when no targetId is specified):
|
|
7027
|
+
* - Prefer http/https URLs over chrome://, devtools://, about:blank
|
|
7028
|
+
* - Prefer unattached targets (not already controlled by another client)
|
|
7029
|
+
* - Filter by targetUrl if provided
|
|
5077
7030
|
*/
|
|
5078
7031
|
async page(name, options) {
|
|
5079
7032
|
const pageName = name ?? "default";
|
|
5080
7033
|
const cached = this.pages.get(pageName);
|
|
5081
7034
|
if (cached) return cached;
|
|
5082
7035
|
const targets = await this.cdp.send("Target.getTargets");
|
|
5083
|
-
|
|
7036
|
+
let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
7037
|
+
if (options?.targetUrl) {
|
|
7038
|
+
const urlFilter = options.targetUrl;
|
|
7039
|
+
const filtered = pageTargets.filter((t) => t.url.includes(urlFilter));
|
|
7040
|
+
if (filtered.length > 0) {
|
|
7041
|
+
pageTargets = filtered;
|
|
7042
|
+
} else {
|
|
7043
|
+
console.warn(
|
|
7044
|
+
`[browser-pilot] No targets match URL filter "${urlFilter}", falling back to all page targets`
|
|
7045
|
+
);
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
5084
7048
|
let targetId;
|
|
5085
7049
|
if (options?.targetId) {
|
|
5086
|
-
const targetExists =
|
|
7050
|
+
const targetExists = targets.targetInfos.some(
|
|
7051
|
+
(t) => t.type === "page" && t.targetId === options.targetId
|
|
7052
|
+
);
|
|
5087
7053
|
if (targetExists) {
|
|
5088
7054
|
targetId = options.targetId;
|
|
5089
7055
|
} else {
|
|
5090
7056
|
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
5091
|
-
targetId = pageTargets
|
|
7057
|
+
targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send("Target.createTarget", {
|
|
5092
7058
|
url: "about:blank"
|
|
5093
7059
|
})).targetId;
|
|
5094
7060
|
}
|
|
5095
7061
|
} else if (pageTargets.length > 0) {
|
|
5096
|
-
targetId = pageTargets
|
|
7062
|
+
targetId = pickBestTarget(pageTargets);
|
|
5097
7063
|
} else {
|
|
5098
7064
|
const result = await this.cdp.send("Target.createTarget", {
|
|
5099
7065
|
url: "about:blank"
|
|
@@ -5103,6 +7069,21 @@ var Browser = class _Browser {
|
|
|
5103
7069
|
await this.cdp.attachToTarget(targetId);
|
|
5104
7070
|
const page = new Page(this.cdp, targetId);
|
|
5105
7071
|
await page.init();
|
|
7072
|
+
const minViewport = options?.minViewport !== void 0 ? options.minViewport : { width: 200, height: 200 };
|
|
7073
|
+
if (minViewport !== false) {
|
|
7074
|
+
try {
|
|
7075
|
+
const viewport = await page.evaluate(
|
|
7076
|
+
"({ w: window.innerWidth, h: window.innerHeight })"
|
|
7077
|
+
);
|
|
7078
|
+
if (viewport.w < minViewport.width || viewport.h < minViewport.height) {
|
|
7079
|
+
console.warn(
|
|
7080
|
+
`[browser-pilot] Attached target has small viewport (${viewport.w}x${viewport.h}). Applying default viewport override (1280x720). Use { minViewport: false } to disable this check.`
|
|
7081
|
+
);
|
|
7082
|
+
await page.setViewport({ width: 1280, height: 720 });
|
|
7083
|
+
}
|
|
7084
|
+
} catch {
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
5106
7087
|
this.pages.set(pageName, page);
|
|
5107
7088
|
return page;
|
|
5108
7089
|
}
|