browser-pilot 0.0.10 → 0.0.11
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/dist/actions.cjs +16 -7
- package/dist/actions.d.cts +2 -2
- package/dist/actions.d.ts +2 -2
- package/dist/actions.mjs +1 -1
- package/dist/browser.cjs +1619 -303
- package/dist/browser.d.cts +22 -5
- package/dist/browser.d.ts +22 -5
- package/dist/browser.mjs +3 -3
- package/dist/{chunk-R3PS4PCM.mjs → chunk-BRAFQUMG.mjs} +34 -12
- package/dist/{chunk-KKW2SZLV.mjs → chunk-FAUNIZR7.mjs} +18 -8
- package/dist/{chunk-7OSR2CAE.mjs → chunk-JHAF52FA.mjs} +1611 -301
- package/dist/cli.mjs +1988 -345
- package/dist/index.cjs +1669 -327
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/providers.cjs +34 -12
- package/dist/providers.mjs +1 -1
- package/dist/{types-DOGsEYQa.d.ts → types-DtGF3yGl.d.ts} +39 -11
- package/dist/{types-CYw-7vx1.d.cts → types-GWuQJs_e.d.cts} +39 -11
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -25,8 +25,7 @@ INTERACTION
|
|
|
25
25
|
Click element. Multi-selector tries each until success.
|
|
26
26
|
|
|
27
27
|
{"action": "fill", "selector": "#input", "value": "text"}
|
|
28
|
-
|
|
29
|
-
Fill input field. Clears first by default.
|
|
28
|
+
Fill input field. Always selects all text before inserting.
|
|
30
29
|
|
|
31
30
|
{"action": "type", "selector": "#input", "value": "text", "delay": 50}
|
|
32
31
|
Type character-by-character (for autocomplete).
|
|
@@ -360,7 +359,12 @@ var ElementNotFoundError = class extends Error {
|
|
|
360
359
|
hints;
|
|
361
360
|
constructor(selectors, hints) {
|
|
362
361
|
const selectorList = Array.isArray(selectors) ? selectors : [selectors];
|
|
363
|
-
|
|
362
|
+
let msg = `Element not found: ${selectorList.join(", ")}`;
|
|
363
|
+
if (hints?.length) {
|
|
364
|
+
msg += `. Did you mean: ${hints.slice(0, 3).map((h) => `${h.element.ref} (${h.element.role} "${h.element.name}")`).join(", ")}`;
|
|
365
|
+
}
|
|
366
|
+
msg += `. Run 'bp snapshot' to see available elements.`;
|
|
367
|
+
super(msg);
|
|
364
368
|
this.name = "ElementNotFoundError";
|
|
365
369
|
this.selectors = selectorList;
|
|
366
370
|
this.hints = hints;
|
|
@@ -368,7 +372,8 @@ var ElementNotFoundError = class extends Error {
|
|
|
368
372
|
};
|
|
369
373
|
var TimeoutError = class extends Error {
|
|
370
374
|
constructor(message = "Operation timed out") {
|
|
371
|
-
|
|
375
|
+
const msg = message.includes("bp snapshot") ? message : `${message}. Run 'bp snapshot' to check current page state.`;
|
|
376
|
+
super(msg);
|
|
372
377
|
this.name = "TimeoutError";
|
|
373
378
|
}
|
|
374
379
|
};
|
|
@@ -445,7 +450,7 @@ var BatchExecutor = class {
|
|
|
445
450
|
}
|
|
446
451
|
case "click": {
|
|
447
452
|
if (!step.selector) throw new Error("click requires selector");
|
|
448
|
-
if (step.waitForNavigation) {
|
|
453
|
+
if (step.waitForNavigation === true) {
|
|
449
454
|
const navPromise = this.page.waitForNavigation({ timeout, optional });
|
|
450
455
|
await this.page.click(step.selector, { timeout, optional });
|
|
451
456
|
await navPromise;
|
|
@@ -460,7 +465,6 @@ var BatchExecutor = class {
|
|
|
460
465
|
await this.page.fill(step.selector, step.value, {
|
|
461
466
|
timeout,
|
|
462
467
|
optional,
|
|
463
|
-
clear: step.clear ?? true,
|
|
464
468
|
blur: step.blur
|
|
465
469
|
});
|
|
466
470
|
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
@@ -508,7 +512,8 @@ var BatchExecutor = class {
|
|
|
508
512
|
await this.page.submit(step.selector, {
|
|
509
513
|
timeout,
|
|
510
514
|
optional,
|
|
511
|
-
method: step.method ?? "enter+click"
|
|
515
|
+
method: step.method ?? "enter+click",
|
|
516
|
+
waitForNavigation: step.waitForNavigation
|
|
512
517
|
});
|
|
513
518
|
return { selectorUsed: this.getUsedSelector(step.selector) };
|
|
514
519
|
}
|
|
@@ -730,7 +735,6 @@ var ACTION_RULES = {
|
|
|
730
735
|
fill: {
|
|
731
736
|
required: { selector: { type: "string|string[]" }, value: { type: "string" } },
|
|
732
737
|
optional: {
|
|
733
|
-
clear: { type: "boolean" },
|
|
734
738
|
blur: { type: "boolean" }
|
|
735
739
|
}
|
|
736
740
|
},
|
|
@@ -761,7 +765,8 @@ var ACTION_RULES = {
|
|
|
761
765
|
submit: {
|
|
762
766
|
required: { selector: { type: "string|string[]" } },
|
|
763
767
|
optional: {
|
|
764
|
-
method: { type: "string", enum: ["enter", "click", "enter+click"] }
|
|
768
|
+
method: { type: "string", enum: ["enter", "click", "enter+click"] },
|
|
769
|
+
waitForNavigation: { type: "boolean|auto" }
|
|
765
770
|
}
|
|
766
771
|
},
|
|
767
772
|
press: {
|
|
@@ -839,7 +844,6 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
|
|
|
839
844
|
"timeout",
|
|
840
845
|
"optional",
|
|
841
846
|
"method",
|
|
842
|
-
"clear",
|
|
843
847
|
"blur",
|
|
844
848
|
"delay",
|
|
845
849
|
"waitForNavigation",
|
|
@@ -920,6 +924,11 @@ function checkFieldType(value, rule) {
|
|
|
920
924
|
case "boolean":
|
|
921
925
|
if (typeof value !== "boolean") return `expected boolean, got ${typeof value}`;
|
|
922
926
|
return null;
|
|
927
|
+
case "boolean|auto":
|
|
928
|
+
if (typeof value !== "boolean" && value !== "auto") {
|
|
929
|
+
return `expected boolean or "auto", got ${typeof value}`;
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
923
932
|
}
|
|
924
933
|
}
|
|
925
934
|
function validateSteps(steps) {
|
|
@@ -2643,6 +2652,34 @@ var BrowserlessProvider = class {
|
|
|
2643
2652
|
};
|
|
2644
2653
|
|
|
2645
2654
|
// src/providers/generic.ts
|
|
2655
|
+
function sleep2(ms) {
|
|
2656
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2657
|
+
}
|
|
2658
|
+
async function fetchDevToolsJson(host, path, errorPrefix, options = {}) {
|
|
2659
|
+
const protocol = host.includes("://") ? "" : "http://";
|
|
2660
|
+
const attempts = options.attempts ?? 1;
|
|
2661
|
+
let delayMs = options.initialDelayMs ?? 50;
|
|
2662
|
+
const maxDelayMs = options.maxDelayMs ?? 250;
|
|
2663
|
+
let lastError;
|
|
2664
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
2665
|
+
try {
|
|
2666
|
+
const response = await fetch(`${protocol}${host}${path}`);
|
|
2667
|
+
if (response.ok) {
|
|
2668
|
+
return await response.json();
|
|
2669
|
+
}
|
|
2670
|
+
lastError = new Error(`${errorPrefix}: ${response.status}`);
|
|
2671
|
+
} catch (error) {
|
|
2672
|
+
lastError = new Error(
|
|
2673
|
+
`${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
if (attempt < attempts) {
|
|
2677
|
+
await sleep2(delayMs);
|
|
2678
|
+
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
throw lastError ?? new Error(errorPrefix);
|
|
2682
|
+
}
|
|
2646
2683
|
var GenericProvider = class {
|
|
2647
2684
|
name = "generic";
|
|
2648
2685
|
wsUrl;
|
|
@@ -2661,12 +2698,11 @@ var GenericProvider = class {
|
|
|
2661
2698
|
}
|
|
2662
2699
|
};
|
|
2663
2700
|
async function getBrowserWebSocketUrl(host = "localhost:9222") {
|
|
2664
|
-
const
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
}
|
|
2669
|
-
const info = await response.json();
|
|
2701
|
+
const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
|
|
2702
|
+
attempts: 10,
|
|
2703
|
+
initialDelayMs: 50,
|
|
2704
|
+
maxDelayMs: 250
|
|
2705
|
+
});
|
|
2670
2706
|
return info.webSocketDebuggerUrl;
|
|
2671
2707
|
}
|
|
2672
2708
|
|
|
@@ -2953,7 +2989,7 @@ async function isElementAttached(cdp, selector, contextId) {
|
|
|
2953
2989
|
const result = await cdp.send("Runtime.evaluate", params);
|
|
2954
2990
|
return result.result.value === true;
|
|
2955
2991
|
}
|
|
2956
|
-
function
|
|
2992
|
+
function sleep3(ms) {
|
|
2957
2993
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2958
2994
|
}
|
|
2959
2995
|
async function waitForAnyElement(cdp, selectors, options = {}) {
|
|
@@ -2981,7 +3017,7 @@ async function waitForAnyElement(cdp, selectors, options = {}) {
|
|
|
2981
3017
|
return { success: true, selector, waitedMs: Date.now() - startTime };
|
|
2982
3018
|
}
|
|
2983
3019
|
}
|
|
2984
|
-
await
|
|
3020
|
+
await sleep3(pollInterval);
|
|
2985
3021
|
}
|
|
2986
3022
|
return { success: false, waitedMs: Date.now() - startTime };
|
|
2987
3023
|
}
|
|
@@ -3030,7 +3066,7 @@ async function waitForNavigation(cdp, options = {}) {
|
|
|
3030
3066
|
}
|
|
3031
3067
|
const pollUrl = async () => {
|
|
3032
3068
|
while (!resolved && Date.now() < startTime + timeout) {
|
|
3033
|
-
await
|
|
3069
|
+
await sleep3(100);
|
|
3034
3070
|
if (resolved) return;
|
|
3035
3071
|
try {
|
|
3036
3072
|
const currentUrl = await getCurrentUrl(cdp);
|
|
@@ -3090,6 +3126,335 @@ async function waitForNetworkIdle(cdp, options = {}) {
|
|
|
3090
3126
|
});
|
|
3091
3127
|
}
|
|
3092
3128
|
|
|
3129
|
+
// src/browser/actionability.ts
|
|
3130
|
+
var ActionabilityError = class extends Error {
|
|
3131
|
+
failureType;
|
|
3132
|
+
coveringElement;
|
|
3133
|
+
constructor(message, failureType, coveringElement) {
|
|
3134
|
+
super(message);
|
|
3135
|
+
this.name = "ActionabilityError";
|
|
3136
|
+
this.failureType = failureType;
|
|
3137
|
+
this.coveringElement = coveringElement;
|
|
3138
|
+
}
|
|
3139
|
+
};
|
|
3140
|
+
var CHECK_VISIBLE = `function() {
|
|
3141
|
+
// checkVisibility handles display:none, visibility:hidden, content-visibility up the tree
|
|
3142
|
+
if (typeof this.checkVisibility === 'function' && !this.checkVisibility()) {
|
|
3143
|
+
return { actionable: false, reason: 'Element is not visible (checkVisibility failed). Try scrolling or check if a prior action is needed to reveal it.' };
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
var style = getComputedStyle(this);
|
|
3147
|
+
|
|
3148
|
+
if (style.visibility !== 'visible') {
|
|
3149
|
+
return { actionable: false, reason: 'Element has visibility: ' + style.visibility + '. Try scrolling or check if a prior action is needed to reveal it.' };
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// display:contents elements have no box themselves \u2014 check children
|
|
3153
|
+
if (style.display === 'contents') {
|
|
3154
|
+
var children = this.children;
|
|
3155
|
+
if (children.length === 0) {
|
|
3156
|
+
return { actionable: false, reason: 'Element has display:contents with no children. Try scrolling or check if a prior action is needed to reveal it.' };
|
|
3157
|
+
}
|
|
3158
|
+
for (var i = 0; i < children.length; i++) {
|
|
3159
|
+
var childRect = children[i].getBoundingClientRect();
|
|
3160
|
+
if (childRect.width > 0 && childRect.height > 0) {
|
|
3161
|
+
return { actionable: true };
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
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.' };
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
var rect = this.getBoundingClientRect();
|
|
3168
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
3169
|
+
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.' };
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
return { actionable: true };
|
|
3173
|
+
}`;
|
|
3174
|
+
var CHECK_ENABLED = `function() {
|
|
3175
|
+
// Native disabled property
|
|
3176
|
+
var disableable = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'];
|
|
3177
|
+
if (disableable.indexOf(this.tagName) !== -1 && this.disabled) {
|
|
3178
|
+
return { actionable: false, reason: 'Element is disabled. Check if a prerequisite field needs to be filled first.' };
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
// Check ancestor FIELDSET[disabled]
|
|
3182
|
+
var parent = this.parentElement;
|
|
3183
|
+
while (parent) {
|
|
3184
|
+
if (parent.tagName === 'FIELDSET' && parent.disabled) {
|
|
3185
|
+
// Exception: elements inside the first <legend> of a disabled fieldset are NOT disabled
|
|
3186
|
+
var legend = parent.querySelector(':scope > legend');
|
|
3187
|
+
if (!legend || !legend.contains(this)) {
|
|
3188
|
+
return { actionable: false, reason: 'Element is inside a disabled fieldset. Check if a prerequisite field needs to be filled first.' };
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
parent = parent.parentElement;
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
// aria-disabled="true" walking up ancestor chain (crosses shadow DOM)
|
|
3195
|
+
var node = this;
|
|
3196
|
+
while (node) {
|
|
3197
|
+
if (node.nodeType === 1 && node.getAttribute && node.getAttribute('aria-disabled') === 'true') {
|
|
3198
|
+
return { actionable: false, reason: 'Element or ancestor has aria-disabled="true". Check if a prerequisite field needs to be filled first.' };
|
|
3199
|
+
}
|
|
3200
|
+
if (node.parentElement) {
|
|
3201
|
+
node = node.parentElement;
|
|
3202
|
+
} else if (node.getRootNode && node.getRootNode() !== node) {
|
|
3203
|
+
// Cross shadow DOM boundary
|
|
3204
|
+
var root = node.getRootNode();
|
|
3205
|
+
node = root.host || null;
|
|
3206
|
+
} else {
|
|
3207
|
+
break;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
return { actionable: true };
|
|
3212
|
+
}`;
|
|
3213
|
+
var CHECK_STABLE = `function() {
|
|
3214
|
+
var self = this;
|
|
3215
|
+
return new Promise(function(resolve) {
|
|
3216
|
+
var maxFrames = 30;
|
|
3217
|
+
var prev = null;
|
|
3218
|
+
var frame = 0;
|
|
3219
|
+
var resolved = false;
|
|
3220
|
+
|
|
3221
|
+
var fallbackTimer = setTimeout(function() {
|
|
3222
|
+
if (!resolved) {
|
|
3223
|
+
resolved = true;
|
|
3224
|
+
resolve({ actionable: false, reason: 'Element stability check timed out (tab may be backgrounded)' });
|
|
3225
|
+
}
|
|
3226
|
+
}, 2000);
|
|
3227
|
+
|
|
3228
|
+
function check() {
|
|
3229
|
+
if (resolved) return;
|
|
3230
|
+
frame++;
|
|
3231
|
+
if (frame > maxFrames) {
|
|
3232
|
+
resolved = true;
|
|
3233
|
+
clearTimeout(fallbackTimer);
|
|
3234
|
+
resolve({ actionable: false, reason: 'Element position not stable after ' + maxFrames + ' frames' });
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
var rect = self.getBoundingClientRect();
|
|
3239
|
+
var cur = { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
|
|
3240
|
+
|
|
3241
|
+
if (prev !== null &&
|
|
3242
|
+
prev.x === cur.x && prev.y === cur.y &&
|
|
3243
|
+
prev.w === cur.w && prev.h === cur.h) {
|
|
3244
|
+
resolved = true;
|
|
3245
|
+
clearTimeout(fallbackTimer);
|
|
3246
|
+
resolve({ actionable: true });
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
prev = cur;
|
|
3251
|
+
requestAnimationFrame(check);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
requestAnimationFrame(check);
|
|
3255
|
+
});
|
|
3256
|
+
}`;
|
|
3257
|
+
var CHECK_HIT_TARGET = `function(x, y) {
|
|
3258
|
+
// Compute click center if coordinates not provided
|
|
3259
|
+
if (x === undefined || y === undefined) {
|
|
3260
|
+
var rect = this.getBoundingClientRect();
|
|
3261
|
+
x = rect.x + rect.width / 2;
|
|
3262
|
+
y = rect.y + rect.height / 2;
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
function checkPoint(root, px, py) {
|
|
3266
|
+
var method = root.elementsFromPoint || root.msElementsFromPoint;
|
|
3267
|
+
if (!method) return [];
|
|
3268
|
+
return method.call(root, px, py) || [];
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
// Follow only the top-most hit through nested shadow roots.
|
|
3272
|
+
// Accepting any hit in the stack creates false positives for covered elements.
|
|
3273
|
+
var root = document;
|
|
3274
|
+
var topHits = [];
|
|
3275
|
+
var seenRoots = [];
|
|
3276
|
+
while (root && seenRoots.indexOf(root) === -1) {
|
|
3277
|
+
seenRoots.push(root);
|
|
3278
|
+
var hits = checkPoint(root, x, y);
|
|
3279
|
+
if (!hits.length) break;
|
|
3280
|
+
var top = hits[0];
|
|
3281
|
+
topHits.push(top);
|
|
3282
|
+
if (top && top.shadowRoot) {
|
|
3283
|
+
root = top.shadowRoot;
|
|
3284
|
+
continue;
|
|
3285
|
+
}
|
|
3286
|
+
break;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
// Target must be the top-most hit element or an ancestor/descendant
|
|
3290
|
+
for (var j = 0; j < topHits.length; j++) {
|
|
3291
|
+
var hit = topHits[j];
|
|
3292
|
+
if (hit === this || this.contains(hit) || hit.contains(this)) {
|
|
3293
|
+
return { actionable: true };
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// Report the covering element
|
|
3298
|
+
var top = topHits.length > 0 ? topHits[topHits.length - 1] : null;
|
|
3299
|
+
if (top) {
|
|
3300
|
+
return {
|
|
3301
|
+
actionable: false,
|
|
3302
|
+
reason: 'Element is covered by <' + top.tagName.toLowerCase() + '>' +
|
|
3303
|
+
(top.id ? '#' + top.id : '') +
|
|
3304
|
+
(top.className && typeof top.className === 'string' ? '.' + top.className.split(' ').join('.') : '') +
|
|
3305
|
+
'. Try dismissing overlays first.',
|
|
3306
|
+
coveringElement: {
|
|
3307
|
+
tag: top.tagName.toLowerCase(),
|
|
3308
|
+
id: top.id || undefined,
|
|
3309
|
+
className: (typeof top.className === 'string' && top.className) || undefined
|
|
3310
|
+
}
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
return { actionable: false, reason: 'No element found at click point (' + x + ', ' + y + '). Try scrolling the element into view first.' };
|
|
3315
|
+
}`;
|
|
3316
|
+
var CHECK_EDITABLE = `function() {
|
|
3317
|
+
// Must be an editable element type
|
|
3318
|
+
var tag = this.tagName;
|
|
3319
|
+
var isEditable = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' ||
|
|
3320
|
+
this.isContentEditable;
|
|
3321
|
+
if (!isEditable) {
|
|
3322
|
+
return { actionable: false, reason: 'Element is not an editable type (<' + tag.toLowerCase() + '>). Target an <input>, <textarea>, <select>, or [contenteditable] element instead.' };
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
// Check disabled
|
|
3326
|
+
var disableable = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'];
|
|
3327
|
+
if (disableable.indexOf(tag) !== -1 && this.disabled) {
|
|
3328
|
+
return { actionable: false, reason: 'Element is disabled. Check if a prerequisite field needs to be filled first.' };
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
// Check ancestor FIELDSET[disabled]
|
|
3332
|
+
var parent = this.parentElement;
|
|
3333
|
+
while (parent) {
|
|
3334
|
+
if (parent.tagName === 'FIELDSET' && parent.disabled) {
|
|
3335
|
+
var legend = parent.querySelector(':scope > legend');
|
|
3336
|
+
if (!legend || !legend.contains(this)) {
|
|
3337
|
+
return { actionable: false, reason: 'Element is inside a disabled fieldset. Check if a prerequisite field needs to be filled first.' };
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
parent = parent.parentElement;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
// aria-disabled walking up (crosses shadow DOM)
|
|
3344
|
+
var node = this;
|
|
3345
|
+
while (node) {
|
|
3346
|
+
if (node.nodeType === 1 && node.getAttribute && node.getAttribute('aria-disabled') === 'true') {
|
|
3347
|
+
return { actionable: false, reason: 'Element or ancestor has aria-disabled="true". Check if a prerequisite field needs to be filled first.' };
|
|
3348
|
+
}
|
|
3349
|
+
if (node.parentElement) {
|
|
3350
|
+
node = node.parentElement;
|
|
3351
|
+
} else if (node.getRootNode && node.getRootNode() !== node) {
|
|
3352
|
+
var root = node.getRootNode();
|
|
3353
|
+
node = root.host || null;
|
|
3354
|
+
} else {
|
|
3355
|
+
break;
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
// Check readonly
|
|
3360
|
+
if (this.hasAttribute && this.hasAttribute('readonly')) {
|
|
3361
|
+
return { actionable: false, reason: 'Cannot fill a readonly input. Remove the readonly attribute or target a different element.' };
|
|
3362
|
+
}
|
|
3363
|
+
if (this.getAttribute && this.getAttribute('aria-readonly') === 'true') {
|
|
3364
|
+
return { actionable: false, reason: 'Cannot fill a readonly input (aria-readonly="true"). Remove the attribute or target a different element.' };
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
return { actionable: true };
|
|
3368
|
+
}`;
|
|
3369
|
+
function sleep4(ms) {
|
|
3370
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3371
|
+
}
|
|
3372
|
+
var BACKOFF = [0, 20, 100, 100];
|
|
3373
|
+
async function runCheck(cdp, objectId, check, options) {
|
|
3374
|
+
let script;
|
|
3375
|
+
let awaitPromise = false;
|
|
3376
|
+
const args = [];
|
|
3377
|
+
switch (check) {
|
|
3378
|
+
case "visible":
|
|
3379
|
+
script = CHECK_VISIBLE;
|
|
3380
|
+
break;
|
|
3381
|
+
case "enabled":
|
|
3382
|
+
script = CHECK_ENABLED;
|
|
3383
|
+
break;
|
|
3384
|
+
case "stable":
|
|
3385
|
+
script = CHECK_STABLE;
|
|
3386
|
+
awaitPromise = true;
|
|
3387
|
+
break;
|
|
3388
|
+
case "hitTarget":
|
|
3389
|
+
script = CHECK_HIT_TARGET;
|
|
3390
|
+
if (options?.coordinates) {
|
|
3391
|
+
args.push({ value: options.coordinates.x });
|
|
3392
|
+
args.push({ value: options.coordinates.y });
|
|
3393
|
+
} else {
|
|
3394
|
+
args.push({ value: void 0 });
|
|
3395
|
+
args.push({ value: void 0 });
|
|
3396
|
+
}
|
|
3397
|
+
break;
|
|
3398
|
+
case "editable":
|
|
3399
|
+
script = CHECK_EDITABLE;
|
|
3400
|
+
break;
|
|
3401
|
+
default: {
|
|
3402
|
+
const _exhaustive = check;
|
|
3403
|
+
throw new Error(`Unknown actionability check: ${_exhaustive}`);
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
const params = {
|
|
3407
|
+
functionDeclaration: script,
|
|
3408
|
+
objectId,
|
|
3409
|
+
returnByValue: true,
|
|
3410
|
+
arguments: args
|
|
3411
|
+
};
|
|
3412
|
+
if (awaitPromise) {
|
|
3413
|
+
params["awaitPromise"] = true;
|
|
3414
|
+
}
|
|
3415
|
+
const response = await cdp.send("Runtime.callFunctionOn", params);
|
|
3416
|
+
if (response.exceptionDetails) {
|
|
3417
|
+
return {
|
|
3418
|
+
actionable: false,
|
|
3419
|
+
reason: `Check "${check}" threw: ${response.exceptionDetails.text}`,
|
|
3420
|
+
failureType: check
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
const result = response.result.value;
|
|
3424
|
+
if (!result.actionable) {
|
|
3425
|
+
result.failureType = check;
|
|
3426
|
+
}
|
|
3427
|
+
return result;
|
|
3428
|
+
}
|
|
3429
|
+
async function runChecks(cdp, objectId, checks, options) {
|
|
3430
|
+
for (const check of checks) {
|
|
3431
|
+
const result = await runCheck(cdp, objectId, check, options);
|
|
3432
|
+
if (!result.actionable) {
|
|
3433
|
+
return result;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
return { actionable: true };
|
|
3437
|
+
}
|
|
3438
|
+
async function ensureActionable(cdp, objectId, checks, options) {
|
|
3439
|
+
const timeout = options?.timeout ?? 3e4;
|
|
3440
|
+
const start = Date.now();
|
|
3441
|
+
let attempt = 0;
|
|
3442
|
+
while (true) {
|
|
3443
|
+
const result = await runChecks(cdp, objectId, checks, options);
|
|
3444
|
+
if (result.actionable) return;
|
|
3445
|
+
if (Date.now() - start >= timeout) {
|
|
3446
|
+
throw new ActionabilityError(
|
|
3447
|
+
`Element not actionable: ${result.reason}`,
|
|
3448
|
+
result.failureType,
|
|
3449
|
+
result.coveringElement
|
|
3450
|
+
);
|
|
3451
|
+
}
|
|
3452
|
+
const delay = attempt < BACKOFF.length ? BACKOFF[attempt] ?? 0 : 500;
|
|
3453
|
+
if (delay > 0) await sleep4(delay);
|
|
3454
|
+
attempt++;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3093
3458
|
// src/browser/fuzzy-match.ts
|
|
3094
3459
|
function jaroWinkler(a, b) {
|
|
3095
3460
|
if (a.length === 0 && b.length === 0) return 0;
|
|
@@ -3335,8 +3700,180 @@ async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
|
|
|
3335
3700
|
return diversifyHints(matches, maxHints);
|
|
3336
3701
|
}
|
|
3337
3702
|
|
|
3703
|
+
// src/browser/keyboard.ts
|
|
3704
|
+
var US_KEYBOARD = {
|
|
3705
|
+
// Letters (lowercase)
|
|
3706
|
+
a: { key: "a", code: "KeyA", keyCode: 65, text: "a" },
|
|
3707
|
+
b: { key: "b", code: "KeyB", keyCode: 66, text: "b" },
|
|
3708
|
+
c: { key: "c", code: "KeyC", keyCode: 67, text: "c" },
|
|
3709
|
+
d: { key: "d", code: "KeyD", keyCode: 68, text: "d" },
|
|
3710
|
+
e: { key: "e", code: "KeyE", keyCode: 69, text: "e" },
|
|
3711
|
+
f: { key: "f", code: "KeyF", keyCode: 70, text: "f" },
|
|
3712
|
+
g: { key: "g", code: "KeyG", keyCode: 71, text: "g" },
|
|
3713
|
+
h: { key: "h", code: "KeyH", keyCode: 72, text: "h" },
|
|
3714
|
+
i: { key: "i", code: "KeyI", keyCode: 73, text: "i" },
|
|
3715
|
+
j: { key: "j", code: "KeyJ", keyCode: 74, text: "j" },
|
|
3716
|
+
k: { key: "k", code: "KeyK", keyCode: 75, text: "k" },
|
|
3717
|
+
l: { key: "l", code: "KeyL", keyCode: 76, text: "l" },
|
|
3718
|
+
m: { key: "m", code: "KeyM", keyCode: 77, text: "m" },
|
|
3719
|
+
n: { key: "n", code: "KeyN", keyCode: 78, text: "n" },
|
|
3720
|
+
o: { key: "o", code: "KeyO", keyCode: 79, text: "o" },
|
|
3721
|
+
p: { key: "p", code: "KeyP", keyCode: 80, text: "p" },
|
|
3722
|
+
q: { key: "q", code: "KeyQ", keyCode: 81, text: "q" },
|
|
3723
|
+
r: { key: "r", code: "KeyR", keyCode: 82, text: "r" },
|
|
3724
|
+
s: { key: "s", code: "KeyS", keyCode: 83, text: "s" },
|
|
3725
|
+
t: { key: "t", code: "KeyT", keyCode: 84, text: "t" },
|
|
3726
|
+
u: { key: "u", code: "KeyU", keyCode: 85, text: "u" },
|
|
3727
|
+
v: { key: "v", code: "KeyV", keyCode: 86, text: "v" },
|
|
3728
|
+
w: { key: "w", code: "KeyW", keyCode: 87, text: "w" },
|
|
3729
|
+
x: { key: "x", code: "KeyX", keyCode: 88, text: "x" },
|
|
3730
|
+
y: { key: "y", code: "KeyY", keyCode: 89, text: "y" },
|
|
3731
|
+
z: { key: "z", code: "KeyZ", keyCode: 90, text: "z" },
|
|
3732
|
+
// Letters (uppercase)
|
|
3733
|
+
A: { key: "A", code: "KeyA", keyCode: 65, text: "A" },
|
|
3734
|
+
B: { key: "B", code: "KeyB", keyCode: 66, text: "B" },
|
|
3735
|
+
C: { key: "C", code: "KeyC", keyCode: 67, text: "C" },
|
|
3736
|
+
D: { key: "D", code: "KeyD", keyCode: 68, text: "D" },
|
|
3737
|
+
E: { key: "E", code: "KeyE", keyCode: 69, text: "E" },
|
|
3738
|
+
F: { key: "F", code: "KeyF", keyCode: 70, text: "F" },
|
|
3739
|
+
G: { key: "G", code: "KeyG", keyCode: 71, text: "G" },
|
|
3740
|
+
H: { key: "H", code: "KeyH", keyCode: 72, text: "H" },
|
|
3741
|
+
I: { key: "I", code: "KeyI", keyCode: 73, text: "I" },
|
|
3742
|
+
J: { key: "J", code: "KeyJ", keyCode: 74, text: "J" },
|
|
3743
|
+
K: { key: "K", code: "KeyK", keyCode: 75, text: "K" },
|
|
3744
|
+
L: { key: "L", code: "KeyL", keyCode: 76, text: "L" },
|
|
3745
|
+
M: { key: "M", code: "KeyM", keyCode: 77, text: "M" },
|
|
3746
|
+
N: { key: "N", code: "KeyN", keyCode: 78, text: "N" },
|
|
3747
|
+
O: { key: "O", code: "KeyO", keyCode: 79, text: "O" },
|
|
3748
|
+
P: { key: "P", code: "KeyP", keyCode: 80, text: "P" },
|
|
3749
|
+
Q: { key: "Q", code: "KeyQ", keyCode: 81, text: "Q" },
|
|
3750
|
+
R: { key: "R", code: "KeyR", keyCode: 82, text: "R" },
|
|
3751
|
+
S: { key: "S", code: "KeyS", keyCode: 83, text: "S" },
|
|
3752
|
+
T: { key: "T", code: "KeyT", keyCode: 84, text: "T" },
|
|
3753
|
+
U: { key: "U", code: "KeyU", keyCode: 85, text: "U" },
|
|
3754
|
+
V: { key: "V", code: "KeyV", keyCode: 86, text: "V" },
|
|
3755
|
+
W: { key: "W", code: "KeyW", keyCode: 87, text: "W" },
|
|
3756
|
+
X: { key: "X", code: "KeyX", keyCode: 88, text: "X" },
|
|
3757
|
+
Y: { key: "Y", code: "KeyY", keyCode: 89, text: "Y" },
|
|
3758
|
+
Z: { key: "Z", code: "KeyZ", keyCode: 90, text: "Z" },
|
|
3759
|
+
// Numbers
|
|
3760
|
+
"0": { key: "0", code: "Digit0", keyCode: 48, text: "0" },
|
|
3761
|
+
"1": { key: "1", code: "Digit1", keyCode: 49, text: "1" },
|
|
3762
|
+
"2": { key: "2", code: "Digit2", keyCode: 50, text: "2" },
|
|
3763
|
+
"3": { key: "3", code: "Digit3", keyCode: 51, text: "3" },
|
|
3764
|
+
"4": { key: "4", code: "Digit4", keyCode: 52, text: "4" },
|
|
3765
|
+
"5": { key: "5", code: "Digit5", keyCode: 53, text: "5" },
|
|
3766
|
+
"6": { key: "6", code: "Digit6", keyCode: 54, text: "6" },
|
|
3767
|
+
"7": { key: "7", code: "Digit7", keyCode: 55, text: "7" },
|
|
3768
|
+
"8": { key: "8", code: "Digit8", keyCode: 56, text: "8" },
|
|
3769
|
+
"9": { key: "9", code: "Digit9", keyCode: 57, text: "9" },
|
|
3770
|
+
// Punctuation
|
|
3771
|
+
" ": { key: " ", code: "Space", keyCode: 32, text: " " },
|
|
3772
|
+
".": { key: ".", code: "Period", keyCode: 190, text: "." },
|
|
3773
|
+
",": { key: ",", code: "Comma", keyCode: 188, text: "," },
|
|
3774
|
+
"/": { key: "/", code: "Slash", keyCode: 191, text: "/" },
|
|
3775
|
+
";": { key: ";", code: "Semicolon", keyCode: 186, text: ";" },
|
|
3776
|
+
"'": { key: "'", code: "Quote", keyCode: 222, text: "'" },
|
|
3777
|
+
"[": { key: "[", code: "BracketLeft", keyCode: 219, text: "[" },
|
|
3778
|
+
"]": { key: "]", code: "BracketRight", keyCode: 221, text: "]" },
|
|
3779
|
+
"\\": { key: "\\", code: "Backslash", keyCode: 220, text: "\\" },
|
|
3780
|
+
"-": { key: "-", code: "Minus", keyCode: 189, text: "-" },
|
|
3781
|
+
"=": { key: "=", code: "Equal", keyCode: 187, text: "=" },
|
|
3782
|
+
"`": { key: "`", code: "Backquote", keyCode: 192, text: "`" },
|
|
3783
|
+
// Shifted punctuation
|
|
3784
|
+
"!": { key: "!", code: "Digit1", keyCode: 49, text: "!" },
|
|
3785
|
+
"@": { key: "@", code: "Digit2", keyCode: 50, text: "@" },
|
|
3786
|
+
"#": { key: "#", code: "Digit3", keyCode: 51, text: "#" },
|
|
3787
|
+
$: { key: "$", code: "Digit4", keyCode: 52, text: "$" },
|
|
3788
|
+
"%": { key: "%", code: "Digit5", keyCode: 53, text: "%" },
|
|
3789
|
+
"^": { key: "^", code: "Digit6", keyCode: 54, text: "^" },
|
|
3790
|
+
"&": { key: "&", code: "Digit7", keyCode: 55, text: "&" },
|
|
3791
|
+
"*": { key: "*", code: "Digit8", keyCode: 56, text: "*" },
|
|
3792
|
+
"(": { key: "(", code: "Digit9", keyCode: 57, text: "(" },
|
|
3793
|
+
")": { key: ")", code: "Digit0", keyCode: 48, text: ")" },
|
|
3794
|
+
_: { key: "_", code: "Minus", keyCode: 189, text: "_" },
|
|
3795
|
+
"+": { key: "+", code: "Equal", keyCode: 187, text: "+" },
|
|
3796
|
+
"{": { key: "{", code: "BracketLeft", keyCode: 219, text: "{" },
|
|
3797
|
+
"}": { key: "}", code: "BracketRight", keyCode: 221, text: "}" },
|
|
3798
|
+
"|": { key: "|", code: "Backslash", keyCode: 220, text: "|" },
|
|
3799
|
+
":": { key: ":", code: "Semicolon", keyCode: 186, text: ":" },
|
|
3800
|
+
'"': { key: '"', code: "Quote", keyCode: 222, text: '"' },
|
|
3801
|
+
"<": { key: "<", code: "Comma", keyCode: 188, text: "<" },
|
|
3802
|
+
">": { key: ">", code: "Period", keyCode: 190, text: ">" },
|
|
3803
|
+
"?": { key: "?", code: "Slash", keyCode: 191, text: "?" },
|
|
3804
|
+
"~": { key: "~", code: "Backquote", keyCode: 192, text: "~" },
|
|
3805
|
+
// Special keys (non-text: use rawKeyDown, no text field)
|
|
3806
|
+
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
|
3807
|
+
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
3808
|
+
Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
3809
|
+
Delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
3810
|
+
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
3811
|
+
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
3812
|
+
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
3813
|
+
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
3814
|
+
ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
|
|
3815
|
+
Home: { key: "Home", code: "Home", keyCode: 36 },
|
|
3816
|
+
End: { key: "End", code: "End", keyCode: 35 },
|
|
3817
|
+
PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
|
|
3818
|
+
PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
3819
|
+
};
|
|
3820
|
+
|
|
3338
3821
|
// src/browser/page.ts
|
|
3339
3822
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
3823
|
+
var EVENT_LISTENER_TRACKER_SCRIPT = `(() => {
|
|
3824
|
+
if (globalThis.__bpEventListenerTrackerInstalled) return;
|
|
3825
|
+
Object.defineProperty(globalThis, '__bpEventListenerTrackerInstalled', {
|
|
3826
|
+
value: true,
|
|
3827
|
+
configurable: true,
|
|
3828
|
+
});
|
|
3829
|
+
|
|
3830
|
+
const storeKey = '__bpEventListeners';
|
|
3831
|
+
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
3832
|
+
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
|
|
3833
|
+
|
|
3834
|
+
function ensureStore(target) {
|
|
3835
|
+
if (!Object.prototype.hasOwnProperty.call(target, storeKey)) {
|
|
3836
|
+
Object.defineProperty(target, storeKey, {
|
|
3837
|
+
value: Object.create(null),
|
|
3838
|
+
configurable: true,
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
return target[storeKey];
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
EventTarget.prototype.addEventListener = function(type, listener, options) {
|
|
3845
|
+
try {
|
|
3846
|
+
if (listener) {
|
|
3847
|
+
const store = ensureStore(this);
|
|
3848
|
+
const bucket = store[type] || (store[type] = []);
|
|
3849
|
+
const capture =
|
|
3850
|
+
typeof options === 'boolean' ? options : !!(options && options.capture);
|
|
3851
|
+
const exists = bucket.some((entry) => entry.listener === listener && entry.capture === capture);
|
|
3852
|
+
if (!exists) {
|
|
3853
|
+
bucket.push({ listener, capture });
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
} catch {}
|
|
3857
|
+
|
|
3858
|
+
return originalAddEventListener.call(this, type, listener, options);
|
|
3859
|
+
};
|
|
3860
|
+
|
|
3861
|
+
EventTarget.prototype.removeEventListener = function(type, listener, options) {
|
|
3862
|
+
try {
|
|
3863
|
+
const store = this[storeKey];
|
|
3864
|
+
const bucket = store && store[type];
|
|
3865
|
+
const capture =
|
|
3866
|
+
typeof options === 'boolean' ? options : !!(options && options.capture);
|
|
3867
|
+
if (Array.isArray(bucket)) {
|
|
3868
|
+
store[type] = bucket.filter((entry) => {
|
|
3869
|
+
return !(entry.listener === listener && entry.capture === capture);
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
} catch {}
|
|
3873
|
+
|
|
3874
|
+
return originalRemoveEventListener.call(this, type, listener, options);
|
|
3875
|
+
};
|
|
3876
|
+
})();`;
|
|
3340
3877
|
var Page = class {
|
|
3341
3878
|
cdp;
|
|
3342
3879
|
_targetId;
|
|
@@ -3360,6 +3897,8 @@ var Page = class {
|
|
|
3360
3897
|
currentFrameContextId = null;
|
|
3361
3898
|
/** Last matched selector from findElement (for selectorUsed tracking) */
|
|
3362
3899
|
_lastMatchedSelector;
|
|
3900
|
+
/** Last snapshot for stale ref recovery */
|
|
3901
|
+
lastSnapshot;
|
|
3363
3902
|
/** Audio input controller (lazy-initialized) */
|
|
3364
3903
|
_audioInput;
|
|
3365
3904
|
/** Audio output controller (lazy-initialized) */
|
|
@@ -3404,6 +3943,9 @@ var Page = class {
|
|
|
3404
3943
|
for (const [frameId, ctxId] of this.frameExecutionContexts.entries()) {
|
|
3405
3944
|
if (ctxId === contextId) {
|
|
3406
3945
|
this.frameExecutionContexts.delete(frameId);
|
|
3946
|
+
if (this.currentFrameContextId === contextId) {
|
|
3947
|
+
this.currentFrameContextId = null;
|
|
3948
|
+
}
|
|
3407
3949
|
break;
|
|
3408
3950
|
}
|
|
3409
3951
|
}
|
|
@@ -3415,6 +3957,18 @@ var Page = class {
|
|
|
3415
3957
|
this.cdp.send("Runtime.enable"),
|
|
3416
3958
|
this.cdp.send("Network.enable")
|
|
3417
3959
|
]);
|
|
3960
|
+
await this.installEventListenerTracker();
|
|
3961
|
+
}
|
|
3962
|
+
async installEventListenerTracker() {
|
|
3963
|
+
await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
3964
|
+
source: EVENT_LISTENER_TRACKER_SCRIPT
|
|
3965
|
+
});
|
|
3966
|
+
try {
|
|
3967
|
+
await this.cdp.send("Runtime.evaluate", {
|
|
3968
|
+
expression: EVENT_LISTENER_TRACKER_SCRIPT
|
|
3969
|
+
});
|
|
3970
|
+
} catch {
|
|
3971
|
+
}
|
|
3418
3972
|
}
|
|
3419
3973
|
// ============ Navigation ============
|
|
3420
3974
|
/**
|
|
@@ -3430,6 +3984,9 @@ var Page = class {
|
|
|
3430
3984
|
}
|
|
3431
3985
|
this.rootNodeId = null;
|
|
3432
3986
|
this.refMap.clear();
|
|
3987
|
+
this.currentFrame = null;
|
|
3988
|
+
this.currentFrameContextId = null;
|
|
3989
|
+
this.frameContexts.clear();
|
|
3433
3990
|
}
|
|
3434
3991
|
/**
|
|
3435
3992
|
* Get the current URL
|
|
@@ -3500,8 +4057,9 @@ var Page = class {
|
|
|
3500
4057
|
/**
|
|
3501
4058
|
* Click an element (supports multi-selector)
|
|
3502
4059
|
*
|
|
3503
|
-
* Uses CDP mouse events
|
|
3504
|
-
*
|
|
4060
|
+
* Uses CDP mouse events (mouseMoved + mousePressed + mouseReleased) to
|
|
4061
|
+
* simulate a real click. Real mouse events on submit buttons naturally
|
|
4062
|
+
* trigger native form submission — no JS dispatch needed.
|
|
3505
4063
|
*/
|
|
3506
4064
|
async click(selector, options = {}) {
|
|
3507
4065
|
return this.withStaleNodeRetry(async () => {
|
|
@@ -3513,27 +4071,45 @@ var Page = class {
|
|
|
3513
4071
|
throw new ElementNotFoundError(selector, hints);
|
|
3514
4072
|
}
|
|
3515
4073
|
await this.scrollIntoView(element.nodeId);
|
|
3516
|
-
const
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
4074
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4075
|
+
try {
|
|
4076
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled", "stable"], {
|
|
4077
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4078
|
+
});
|
|
4079
|
+
} catch (e) {
|
|
4080
|
+
if (options.optional) return false;
|
|
4081
|
+
throw e;
|
|
4082
|
+
}
|
|
4083
|
+
let clickX;
|
|
4084
|
+
let clickY;
|
|
4085
|
+
try {
|
|
4086
|
+
const { quads } = await this.cdp.send("DOM.getContentQuads", {
|
|
4087
|
+
objectId
|
|
4088
|
+
});
|
|
4089
|
+
if (quads?.length > 0) {
|
|
4090
|
+
const quad = quads[0];
|
|
4091
|
+
clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
4092
|
+
clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
4093
|
+
} else {
|
|
4094
|
+
throw new Error("No quads");
|
|
4095
|
+
}
|
|
4096
|
+
} catch {
|
|
4097
|
+
const box = await this.getBoxModel(element.nodeId);
|
|
4098
|
+
if (!box) throw new Error("Could not get element position");
|
|
4099
|
+
clickX = box.content[0] + box.width / 2;
|
|
4100
|
+
clickY = box.content[1] + box.height / 2;
|
|
4101
|
+
}
|
|
4102
|
+
const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
|
|
4103
|
+
try {
|
|
4104
|
+
await ensureActionable(this.cdp, objectId, ["hitTarget"], {
|
|
4105
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2,
|
|
4106
|
+
coordinates: hitTargetCoordinates
|
|
4107
|
+
});
|
|
4108
|
+
} catch (e) {
|
|
4109
|
+
if (options.optional) return false;
|
|
4110
|
+
throw e;
|
|
3536
4111
|
}
|
|
4112
|
+
await this.clickElement(element.nodeId);
|
|
3537
4113
|
return true;
|
|
3538
4114
|
});
|
|
3539
4115
|
}
|
|
@@ -3541,7 +4117,7 @@ var Page = class {
|
|
|
3541
4117
|
* Fill an input field (clears first by default)
|
|
3542
4118
|
*/
|
|
3543
4119
|
async fill(selector, value, options = {}) {
|
|
3544
|
-
const {
|
|
4120
|
+
const { blur = false } = options;
|
|
3545
4121
|
return this.withStaleNodeRetry(async () => {
|
|
3546
4122
|
const element = await this.findElement(selector, options);
|
|
3547
4123
|
if (!element) {
|
|
@@ -3550,71 +4126,152 @@ var Page = class {
|
|
|
3550
4126
|
const hints = await generateHints(this, selectorList, "fill");
|
|
3551
4127
|
throw new ElementNotFoundError(selector, hints);
|
|
3552
4128
|
}
|
|
3553
|
-
await this.cdp.send("DOM.
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
}));
|
|
3565
|
-
}
|
|
3566
|
-
})()`
|
|
3567
|
-
);
|
|
4129
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
4130
|
+
nodeId: element.nodeId
|
|
4131
|
+
});
|
|
4132
|
+
const objectId = object.objectId;
|
|
4133
|
+
try {
|
|
4134
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled", "editable"], {
|
|
4135
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4136
|
+
});
|
|
4137
|
+
} catch (e) {
|
|
4138
|
+
if (options.optional) return false;
|
|
4139
|
+
throw e;
|
|
3568
4140
|
}
|
|
3569
|
-
await this.cdp.send("
|
|
3570
|
-
|
|
3571
|
-
`(
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
4141
|
+
const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4142
|
+
objectId,
|
|
4143
|
+
functionDeclaration: `function() {
|
|
4144
|
+
return { tagName: this.tagName?.toLowerCase() || '', inputType: (this.type || '').toLowerCase() };
|
|
4145
|
+
}`,
|
|
4146
|
+
returnByValue: true
|
|
4147
|
+
});
|
|
4148
|
+
const { tagName, inputType } = tagInfo.result.value;
|
|
4149
|
+
const specialInputTypes = /* @__PURE__ */ new Set([
|
|
4150
|
+
"date",
|
|
4151
|
+
"datetime-local",
|
|
4152
|
+
"month",
|
|
4153
|
+
"week",
|
|
4154
|
+
"time",
|
|
4155
|
+
"color",
|
|
4156
|
+
"range",
|
|
4157
|
+
"file"
|
|
4158
|
+
]);
|
|
4159
|
+
const isSpecialInput = tagName === "input" && specialInputTypes.has(inputType);
|
|
4160
|
+
if (isSpecialInput) {
|
|
4161
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4162
|
+
objectId,
|
|
4163
|
+
functionDeclaration: `function(val) {
|
|
4164
|
+
this.value = val;
|
|
4165
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
4166
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
4167
|
+
}`,
|
|
4168
|
+
arguments: [{ value }],
|
|
4169
|
+
returnByValue: true
|
|
4170
|
+
});
|
|
4171
|
+
} else {
|
|
4172
|
+
await this.selectEditableContent(objectId);
|
|
4173
|
+
if (value === "") {
|
|
4174
|
+
await this.dispatchKey("Delete");
|
|
4175
|
+
} else {
|
|
4176
|
+
await this.cdp.send("Input.insertText", { text: value });
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
if (options.verify !== false) {
|
|
4180
|
+
let actualValue = await this.readEditableValue(objectId);
|
|
4181
|
+
if (actualValue !== value && !isSpecialInput) {
|
|
4182
|
+
if (value === "") {
|
|
4183
|
+
await this.clearEditableSelection(objectId, "Backspace");
|
|
4184
|
+
} else {
|
|
4185
|
+
await this.typeEditableFallback(element.nodeId, objectId, value);
|
|
3581
4186
|
}
|
|
3582
|
-
|
|
3583
|
-
|
|
4187
|
+
actualValue = await this.readEditableValue(objectId);
|
|
4188
|
+
}
|
|
4189
|
+
if (actualValue !== value) {
|
|
4190
|
+
if (options.optional) return false;
|
|
4191
|
+
throw new Error(
|
|
4192
|
+
`Fill value did not stick. Expected ${JSON.stringify(value)} but got ${JSON.stringify(actualValue)}.`
|
|
4193
|
+
);
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
3584
4196
|
if (blur) {
|
|
3585
|
-
await this.
|
|
3586
|
-
|
|
3587
|
-
|
|
4197
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4198
|
+
objectId,
|
|
4199
|
+
functionDeclaration: "function() { this.blur(); }"
|
|
4200
|
+
});
|
|
3588
4201
|
}
|
|
3589
4202
|
return true;
|
|
3590
4203
|
});
|
|
3591
4204
|
}
|
|
3592
4205
|
/**
|
|
3593
4206
|
* Type text character by character (for autocomplete fields, etc.)
|
|
4207
|
+
*
|
|
4208
|
+
* Uses proper keyDown/rawKeyDown distinction with US keyboard layout.
|
|
4209
|
+
* Printable chars use 'keyDown' with text, non-text keys use 'rawKeyDown',
|
|
4210
|
+
* and non-layout chars (emoji, CJK) fall back to Input.insertText.
|
|
3594
4211
|
*/
|
|
3595
4212
|
async type(selector, text, options = {}) {
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
if (
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
3603
|
-
for (const char of text) {
|
|
3604
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3605
|
-
type: "keyDown",
|
|
3606
|
-
key: char,
|
|
3607
|
-
text: char
|
|
3608
|
-
});
|
|
3609
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3610
|
-
type: "keyUp",
|
|
3611
|
-
key: char
|
|
3612
|
-
});
|
|
3613
|
-
if (delay > 0) {
|
|
3614
|
-
await sleep3(delay);
|
|
4213
|
+
return this.withStaleNodeRetry(async () => {
|
|
4214
|
+
const { delay = 50 } = options;
|
|
4215
|
+
const element = await this.findElement(selector, options);
|
|
4216
|
+
if (!element) {
|
|
4217
|
+
if (options.optional) return false;
|
|
4218
|
+
throw new ElementNotFoundError(selector);
|
|
3615
4219
|
}
|
|
3616
|
-
|
|
3617
|
-
|
|
4220
|
+
try {
|
|
4221
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4222
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled"], {
|
|
4223
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4224
|
+
});
|
|
4225
|
+
} catch (e) {
|
|
4226
|
+
if (options.optional) return false;
|
|
4227
|
+
throw e;
|
|
4228
|
+
}
|
|
4229
|
+
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
4230
|
+
for (const char of text) {
|
|
4231
|
+
const def = US_KEYBOARD[char];
|
|
4232
|
+
if (def) {
|
|
4233
|
+
if (def.text !== void 0) {
|
|
4234
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
4235
|
+
type: "keyDown",
|
|
4236
|
+
key: def.key,
|
|
4237
|
+
code: def.code,
|
|
4238
|
+
text: def.text,
|
|
4239
|
+
unmodifiedText: def.text,
|
|
4240
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
4241
|
+
modifiers: 0,
|
|
4242
|
+
autoRepeat: false,
|
|
4243
|
+
location: def.location ?? 0,
|
|
4244
|
+
isKeypad: false
|
|
4245
|
+
});
|
|
4246
|
+
} else {
|
|
4247
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
4248
|
+
type: "rawKeyDown",
|
|
4249
|
+
key: def.key,
|
|
4250
|
+
code: def.code,
|
|
4251
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
4252
|
+
modifiers: 0,
|
|
4253
|
+
autoRepeat: false,
|
|
4254
|
+
location: def.location ?? 0,
|
|
4255
|
+
isKeypad: false
|
|
4256
|
+
});
|
|
4257
|
+
}
|
|
4258
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
4259
|
+
type: "keyUp",
|
|
4260
|
+
key: def.key,
|
|
4261
|
+
code: def.code,
|
|
4262
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
4263
|
+
modifiers: 0,
|
|
4264
|
+
location: def.location ?? 0
|
|
4265
|
+
});
|
|
4266
|
+
} else {
|
|
4267
|
+
await this.cdp.send("Input.insertText", { text: char });
|
|
4268
|
+
}
|
|
4269
|
+
if (delay > 0) {
|
|
4270
|
+
await sleep5(delay);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
return true;
|
|
4274
|
+
});
|
|
3618
4275
|
}
|
|
3619
4276
|
async select(selectorOrConfig, valueOrOptions, maybeOptions) {
|
|
3620
4277
|
if (typeof selectorOrConfig === "object" && !Array.isArray(selectorOrConfig) && "trigger" in selectorOrConfig) {
|
|
@@ -3623,108 +4280,227 @@ var Page = class {
|
|
|
3623
4280
|
const selector = selectorOrConfig;
|
|
3624
4281
|
const value = valueOrOptions;
|
|
3625
4282
|
const options = maybeOptions ?? {};
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
if (
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
4283
|
+
return this.withStaleNodeRetry(async () => {
|
|
4284
|
+
const element = await this.findElement(selector, options);
|
|
4285
|
+
if (!element) {
|
|
4286
|
+
if (options.optional) return false;
|
|
4287
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
4288
|
+
const hints = await generateHints(this, selectorList, "select");
|
|
4289
|
+
throw new ElementNotFoundError(selector, hints);
|
|
4290
|
+
}
|
|
4291
|
+
const values = Array.isArray(value) ? value : [value];
|
|
4292
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4293
|
+
try {
|
|
4294
|
+
await this.scrollIntoView(element.nodeId);
|
|
4295
|
+
await ensureActionable(this.cdp, objectId, ["visible", "enabled"], {
|
|
4296
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4297
|
+
});
|
|
4298
|
+
} catch (e) {
|
|
4299
|
+
if (options.optional) return false;
|
|
4300
|
+
throw e;
|
|
4301
|
+
}
|
|
4302
|
+
const metadata = await this.getNativeSelectMetadata(objectId, values);
|
|
4303
|
+
if (!metadata.isSelect) {
|
|
4304
|
+
throw new Error("select() target must be a native <select> element");
|
|
4305
|
+
}
|
|
4306
|
+
if (metadata.missing.length > 0) {
|
|
4307
|
+
throw new Error(`No option found for: ${metadata.missing.join(", ")}`);
|
|
4308
|
+
}
|
|
4309
|
+
if (metadata.disabled.length > 0) {
|
|
4310
|
+
throw new Error(`Cannot select disabled option(s): ${metadata.disabled.join(", ")}`);
|
|
4311
|
+
}
|
|
4312
|
+
if (!metadata.multiple && metadata.targetIndexes.length > 1) {
|
|
4313
|
+
throw new Error("Cannot select multiple values on a single-select element");
|
|
4314
|
+
}
|
|
4315
|
+
const expectedValues = metadata.targetIndexes.map((idx) => metadata.options[idx].value);
|
|
4316
|
+
if (this.selectValuesMatch(metadata.selectedValues, expectedValues, metadata.multiple)) {
|
|
4317
|
+
return true;
|
|
4318
|
+
}
|
|
4319
|
+
if (!metadata.multiple && metadata.targetIndexes.length === 1) {
|
|
4320
|
+
await this.applyNativeSelectByKeyboard(
|
|
4321
|
+
element.nodeId,
|
|
4322
|
+
objectId,
|
|
4323
|
+
metadata.currentIndex,
|
|
4324
|
+
metadata.targetIndexes[0]
|
|
4325
|
+
);
|
|
4326
|
+
}
|
|
4327
|
+
let selectedValues = await this.readNativeSelectValues(objectId);
|
|
4328
|
+
if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
|
|
4329
|
+
await this.applyNativeSelectFallback(objectId, metadata.targetIndexes);
|
|
4330
|
+
selectedValues = await this.readNativeSelectValues(objectId);
|
|
4331
|
+
}
|
|
4332
|
+
if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
|
|
4333
|
+
await this.applyRecordedSelectFallback(objectId, metadata.targetIndexes);
|
|
4334
|
+
selectedValues = await this.readNativeSelectValues(objectId);
|
|
4335
|
+
}
|
|
4336
|
+
if (!this.selectValuesMatch(selectedValues, expectedValues, metadata.multiple)) {
|
|
4337
|
+
if (options.optional) return false;
|
|
4338
|
+
throw new Error(
|
|
4339
|
+
`Select value did not stick. Expected ${expectedValues.join(", ") || "(empty)"} but got ${selectedValues.join(", ") || "(empty)"}.`
|
|
4340
|
+
);
|
|
4341
|
+
}
|
|
4342
|
+
return true;
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
/**
|
|
4346
|
+
* Handle custom (non-native) select/dropdown components
|
|
4347
|
+
*/
|
|
4348
|
+
async selectCustom(config, options = {}) {
|
|
3653
4349
|
const { trigger, option, value, match = "text" } = config;
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
4350
|
+
return this.withStaleNodeRetry(async () => {
|
|
4351
|
+
await this.click(trigger, options);
|
|
4352
|
+
await sleep5(100);
|
|
4353
|
+
const optionSelectors = Array.isArray(option) ? option : [option];
|
|
4354
|
+
const optionHandle = await this.evaluateInFrame(
|
|
4355
|
+
`(() => {
|
|
4356
|
+
const selectors = ${JSON.stringify(optionSelectors)};
|
|
4357
|
+
const wanted = ${JSON.stringify(value)};
|
|
4358
|
+
const mode = ${JSON.stringify(match)};
|
|
4359
|
+
|
|
4360
|
+
for (const selector of selectors) {
|
|
4361
|
+
const candidates = document.querySelectorAll(selector);
|
|
4362
|
+
for (const candidate of candidates) {
|
|
4363
|
+
const text = candidate.textContent?.trim() || '';
|
|
4364
|
+
const candidateValue =
|
|
4365
|
+
candidate.getAttribute?.('data-value') ??
|
|
4366
|
+
candidate.getAttribute?.('value') ??
|
|
4367
|
+
candidate.value ??
|
|
4368
|
+
'';
|
|
4369
|
+
const matches =
|
|
4370
|
+
mode === 'value'
|
|
4371
|
+
? candidateValue === wanted
|
|
4372
|
+
: mode === 'contains'
|
|
4373
|
+
? text.includes(wanted)
|
|
4374
|
+
: text === wanted;
|
|
4375
|
+
|
|
4376
|
+
if (matches) {
|
|
4377
|
+
return candidate;
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
3673
4380
|
}
|
|
4381
|
+
|
|
4382
|
+
return null;
|
|
4383
|
+
})()`,
|
|
4384
|
+
{ returnByValue: false }
|
|
4385
|
+
);
|
|
4386
|
+
if (!optionHandle.result.objectId) {
|
|
4387
|
+
if (options.optional) return false;
|
|
4388
|
+
throw new ElementNotFoundError(`Option with ${match} "${value}"`);
|
|
4389
|
+
}
|
|
4390
|
+
const nodeResult = await this.cdp.send("DOM.requestNode", {
|
|
4391
|
+
objectId: optionHandle.result.objectId
|
|
4392
|
+
});
|
|
4393
|
+
if (!nodeResult.nodeId) {
|
|
4394
|
+
if (options.optional) return false;
|
|
4395
|
+
throw new ElementNotFoundError(`Option with ${match} "${value}"`);
|
|
4396
|
+
}
|
|
4397
|
+
await this.scrollIntoView(nodeResult.nodeId);
|
|
4398
|
+
await ensureActionable(
|
|
4399
|
+
this.cdp,
|
|
4400
|
+
optionHandle.result.objectId,
|
|
4401
|
+
["visible", "enabled", "stable"],
|
|
4402
|
+
{
|
|
4403
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
3674
4404
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
4405
|
+
);
|
|
4406
|
+
await this.clickElement(nodeResult.nodeId);
|
|
4407
|
+
return true;
|
|
3678
4408
|
});
|
|
3679
|
-
if (!result.result.value) {
|
|
3680
|
-
if (options.optional) return false;
|
|
3681
|
-
throw new ElementNotFoundError(`Option with ${match} "${value}"`);
|
|
3682
|
-
}
|
|
3683
|
-
return true;
|
|
3684
4409
|
}
|
|
3685
4410
|
/**
|
|
3686
|
-
* Check a checkbox or radio button
|
|
4411
|
+
* Check a checkbox or radio button using real mouse click.
|
|
4412
|
+
* No-op if already checked. Verifies state changed after click.
|
|
3687
4413
|
*/
|
|
3688
4414
|
async check(selector, options = {}) {
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
if (
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
4415
|
+
return this.withStaleNodeRetry(async () => {
|
|
4416
|
+
const element = await this.findElement(selector, options);
|
|
4417
|
+
if (!element) {
|
|
4418
|
+
if (options.optional) return false;
|
|
4419
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
4420
|
+
const hints = await generateHints(this, selectorList, "check");
|
|
4421
|
+
throw new ElementNotFoundError(selector, hints);
|
|
4422
|
+
}
|
|
4423
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
4424
|
+
nodeId: element.nodeId
|
|
4425
|
+
});
|
|
4426
|
+
try {
|
|
4427
|
+
await ensureActionable(this.cdp, object.objectId, ["visible", "enabled"], {
|
|
4428
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4429
|
+
});
|
|
4430
|
+
} catch (e) {
|
|
4431
|
+
if (options.optional) return false;
|
|
4432
|
+
throw e;
|
|
4433
|
+
}
|
|
4434
|
+
const before = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4435
|
+
objectId: object.objectId,
|
|
4436
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
4437
|
+
returnByValue: true
|
|
4438
|
+
});
|
|
4439
|
+
if (before.result.value) return true;
|
|
4440
|
+
await this.scrollIntoView(element.nodeId);
|
|
4441
|
+
await this.clickElement(element.nodeId);
|
|
4442
|
+
const after = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4443
|
+
objectId: object.objectId,
|
|
4444
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
4445
|
+
returnByValue: true
|
|
4446
|
+
});
|
|
4447
|
+
if (!after.result.value) {
|
|
4448
|
+
throw new Error("Clicking the checkbox did not change its state");
|
|
4449
|
+
}
|
|
4450
|
+
return true;
|
|
3704
4451
|
});
|
|
3705
|
-
return result.result.value;
|
|
3706
4452
|
}
|
|
3707
4453
|
/**
|
|
3708
|
-
* Uncheck a checkbox
|
|
4454
|
+
* Uncheck a checkbox using real mouse click.
|
|
4455
|
+
* No-op if already unchecked. Radio buttons can't be unchecked (returns true).
|
|
3709
4456
|
*/
|
|
3710
4457
|
async uncheck(selector, options = {}) {
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
if (
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
4458
|
+
return this.withStaleNodeRetry(async () => {
|
|
4459
|
+
const element = await this.findElement(selector, options);
|
|
4460
|
+
if (!element) {
|
|
4461
|
+
if (options.optional) return false;
|
|
4462
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
4463
|
+
const hints = await generateHints(this, selectorList, "uncheck");
|
|
4464
|
+
throw new ElementNotFoundError(selector, hints);
|
|
4465
|
+
}
|
|
4466
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
4467
|
+
nodeId: element.nodeId
|
|
4468
|
+
});
|
|
4469
|
+
try {
|
|
4470
|
+
await ensureActionable(this.cdp, object.objectId, ["visible", "enabled"], {
|
|
4471
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4472
|
+
});
|
|
4473
|
+
} catch (e) {
|
|
4474
|
+
if (options.optional) return false;
|
|
4475
|
+
throw e;
|
|
4476
|
+
}
|
|
4477
|
+
const isRadio = await this.cdp.send(
|
|
4478
|
+
"Runtime.callFunctionOn",
|
|
4479
|
+
{
|
|
4480
|
+
objectId: object.objectId,
|
|
4481
|
+
functionDeclaration: 'function() { return this.type === "radio"; }',
|
|
4482
|
+
returnByValue: true
|
|
4483
|
+
}
|
|
4484
|
+
);
|
|
4485
|
+
if (isRadio.result.value) return true;
|
|
4486
|
+
const before = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4487
|
+
objectId: object.objectId,
|
|
4488
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
4489
|
+
returnByValue: true
|
|
4490
|
+
});
|
|
4491
|
+
if (!before.result.value) return true;
|
|
4492
|
+
await this.scrollIntoView(element.nodeId);
|
|
4493
|
+
await this.clickElement(element.nodeId);
|
|
4494
|
+
const after = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4495
|
+
objectId: object.objectId,
|
|
4496
|
+
functionDeclaration: "function() { return !!this.checked; }",
|
|
4497
|
+
returnByValue: true
|
|
4498
|
+
});
|
|
4499
|
+
if (after.result.value) {
|
|
4500
|
+
throw new Error("Clicking the checkbox did not change its state");
|
|
4501
|
+
}
|
|
4502
|
+
return true;
|
|
3726
4503
|
});
|
|
3727
|
-
return result.result.value;
|
|
3728
4504
|
}
|
|
3729
4505
|
/**
|
|
3730
4506
|
* Submit a form (tries Enter key first, then click)
|
|
@@ -3738,97 +4514,84 @@ var Page = class {
|
|
|
3738
4514
|
* the submit event and triggers HTML5 validation.
|
|
3739
4515
|
*/
|
|
3740
4516
|
async submit(selector, options = {}) {
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
if (
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
const isFormElement = await this.evaluateInFrame(
|
|
3750
|
-
`(() => {
|
|
3751
|
-
const el = document.querySelector(${JSON.stringify(element.selector)});
|
|
3752
|
-
return el instanceof HTMLFormElement;
|
|
3753
|
-
})()`
|
|
3754
|
-
);
|
|
3755
|
-
if (isFormElement.result.value) {
|
|
3756
|
-
await this.evaluateInFrame(
|
|
3757
|
-
`(() => {
|
|
3758
|
-
const form = document.querySelector(${JSON.stringify(element.selector)});
|
|
3759
|
-
if (form && form instanceof HTMLFormElement) {
|
|
3760
|
-
form.requestSubmit();
|
|
3761
|
-
}
|
|
3762
|
-
})()`
|
|
3763
|
-
);
|
|
3764
|
-
if (shouldWait === true) {
|
|
3765
|
-
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
3766
|
-
} else if (shouldWait === "auto") {
|
|
3767
|
-
await Promise.race([this.waitForNavigation({ timeout: 1e3, optional: true }), sleep3(500)]);
|
|
4517
|
+
return this.withStaleNodeRetry(async () => {
|
|
4518
|
+
const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
|
|
4519
|
+
const element = await this.findElement(selector, options);
|
|
4520
|
+
if (!element) {
|
|
4521
|
+
if (options.optional) return false;
|
|
4522
|
+
const selectorList = Array.isArray(selector) ? selector : [selector];
|
|
4523
|
+
const hints = await generateHints(this, selectorList, "submit");
|
|
4524
|
+
throw new ElementNotFoundError(selector, hints);
|
|
3768
4525
|
}
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
4526
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4527
|
+
const isFormElement = await this.cdp.send(
|
|
4528
|
+
"Runtime.callFunctionOn",
|
|
4529
|
+
{
|
|
4530
|
+
objectId,
|
|
4531
|
+
functionDeclaration: "function() { return this instanceof HTMLFormElement; }",
|
|
4532
|
+
returnByValue: true
|
|
4533
|
+
}
|
|
4534
|
+
);
|
|
4535
|
+
if (isFormElement.result.value) {
|
|
4536
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
4537
|
+
objectId,
|
|
4538
|
+
functionDeclaration: `function() {
|
|
4539
|
+
if (typeof this.requestSubmit === 'function') {
|
|
4540
|
+
this.requestSubmit();
|
|
4541
|
+
} else {
|
|
4542
|
+
this.submit();
|
|
4543
|
+
}
|
|
4544
|
+
}`
|
|
4545
|
+
});
|
|
4546
|
+
if (shouldWait === true) {
|
|
3776
4547
|
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
3777
|
-
|
|
3778
|
-
|
|
4548
|
+
} else if (shouldWait === "auto") {
|
|
4549
|
+
await Promise.race([
|
|
4550
|
+
this.waitForNavigation({ timeout: 1e3, optional: true }),
|
|
4551
|
+
sleep5(500)
|
|
4552
|
+
]);
|
|
3779
4553
|
}
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
4554
|
+
return true;
|
|
4555
|
+
}
|
|
4556
|
+
await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
|
|
4557
|
+
if (method.includes("enter")) {
|
|
4558
|
+
await this.press("Enter");
|
|
4559
|
+
if (shouldWait === true) {
|
|
4560
|
+
try {
|
|
4561
|
+
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
4562
|
+
return true;
|
|
4563
|
+
} catch {
|
|
4564
|
+
}
|
|
4565
|
+
} else if (shouldWait === "auto") {
|
|
4566
|
+
const navigationDetected = await Promise.race([
|
|
4567
|
+
this.waitForNavigation({ timeout: 1e3, optional: true }).then(
|
|
4568
|
+
(success) => success ? "nav" : null
|
|
4569
|
+
),
|
|
4570
|
+
sleep5(500).then(() => "timeout")
|
|
4571
|
+
]);
|
|
4572
|
+
if (navigationDetected === "nav") {
|
|
4573
|
+
return true;
|
|
4574
|
+
}
|
|
4575
|
+
} else if (method === "enter") {
|
|
3788
4576
|
return true;
|
|
3789
4577
|
}
|
|
3790
|
-
} else {
|
|
3791
|
-
if (method === "enter") return true;
|
|
3792
4578
|
}
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
4579
|
+
if (method.includes("click")) {
|
|
4580
|
+
await this.click(element.selector, { ...options, optional: false });
|
|
4581
|
+
if (shouldWait === true) {
|
|
4582
|
+
await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
|
|
4583
|
+
} else if (shouldWait === "auto") {
|
|
4584
|
+
await sleep5(100);
|
|
4585
|
+
}
|
|
3800
4586
|
}
|
|
3801
|
-
|
|
3802
|
-
|
|
4587
|
+
return true;
|
|
4588
|
+
});
|
|
3803
4589
|
}
|
|
3804
4590
|
/**
|
|
3805
4591
|
* Press a key
|
|
3806
4592
|
*/
|
|
3807
4593
|
async press(key) {
|
|
3808
|
-
|
|
3809
|
-
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
|
|
3810
|
-
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
|
|
3811
|
-
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
|
|
3812
|
-
Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
|
|
3813
|
-
Delete: { key: "Delete", code: "Delete", keyCode: 46 },
|
|
3814
|
-
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
|
|
3815
|
-
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
|
|
3816
|
-
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
|
|
3817
|
-
ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
|
|
3818
|
-
};
|
|
3819
|
-
const keyInfo = keyMap[key] ?? { key, code: key, keyCode: 0 };
|
|
3820
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3821
|
-
type: "keyDown",
|
|
3822
|
-
key: keyInfo.key,
|
|
3823
|
-
code: keyInfo.code,
|
|
3824
|
-
windowsVirtualKeyCode: keyInfo.keyCode
|
|
3825
|
-
});
|
|
3826
|
-
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
3827
|
-
type: "keyUp",
|
|
3828
|
-
key: keyInfo.key,
|
|
3829
|
-
code: keyInfo.code,
|
|
3830
|
-
windowsVirtualKeyCode: keyInfo.keyCode
|
|
3831
|
-
});
|
|
4594
|
+
await this.dispatchKey(key);
|
|
3832
4595
|
}
|
|
3833
4596
|
/**
|
|
3834
4597
|
* Focus an element
|
|
@@ -3857,6 +4620,15 @@ var Page = class {
|
|
|
3857
4620
|
throw new ElementNotFoundError(selector, hints);
|
|
3858
4621
|
}
|
|
3859
4622
|
await this.scrollIntoView(element.nodeId);
|
|
4623
|
+
try {
|
|
4624
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4625
|
+
await ensureActionable(this.cdp, objectId, ["visible", "stable"], {
|
|
4626
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT2
|
|
4627
|
+
});
|
|
4628
|
+
} catch (e) {
|
|
4629
|
+
if (options.optional) return false;
|
|
4630
|
+
throw e;
|
|
4631
|
+
}
|
|
3860
4632
|
const box = await this.getBoxModel(element.nodeId);
|
|
3861
4633
|
if (!box) {
|
|
3862
4634
|
if (options.optional) return false;
|
|
@@ -4047,53 +4819,424 @@ var Page = class {
|
|
|
4047
4819
|
* Get text content from the page or a specific element
|
|
4048
4820
|
*/
|
|
4049
4821
|
async text(selector) {
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4822
|
+
if (!selector) {
|
|
4823
|
+
const result = await this.evaluateInFrame(
|
|
4824
|
+
"document.body.innerText"
|
|
4825
|
+
);
|
|
4826
|
+
return result.result.value ?? "";
|
|
4827
|
+
}
|
|
4828
|
+
return this.withStaleNodeRetry(async () => {
|
|
4829
|
+
const element = await this.findElement(selector, { timeout: DEFAULT_TIMEOUT2 });
|
|
4830
|
+
if (!element) return "";
|
|
4831
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4832
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4833
|
+
objectId,
|
|
4834
|
+
functionDeclaration: 'function() { return this.innerText || this.textContent || ""; }',
|
|
4835
|
+
returnByValue: true
|
|
4836
|
+
});
|
|
4837
|
+
return result.result.value ?? "";
|
|
4838
|
+
});
|
|
4053
4839
|
}
|
|
4054
4840
|
// ============ File Handling ============
|
|
4055
4841
|
/**
|
|
4056
4842
|
* Set files on a file input
|
|
4057
4843
|
*/
|
|
4058
4844
|
async setInputFiles(selector, files, options = {}) {
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
if (
|
|
4062
|
-
|
|
4845
|
+
return this.withStaleNodeRetry(async () => {
|
|
4846
|
+
const element = await this.findElement(selector, options);
|
|
4847
|
+
if (!element) {
|
|
4848
|
+
if (options.optional) return false;
|
|
4849
|
+
throw new ElementNotFoundError(selector);
|
|
4850
|
+
}
|
|
4851
|
+
const fileData = await Promise.all(
|
|
4852
|
+
files.map(async (f) => {
|
|
4853
|
+
let base64;
|
|
4854
|
+
if (typeof f.buffer === "string") {
|
|
4855
|
+
base64 = f.buffer;
|
|
4856
|
+
} else {
|
|
4857
|
+
const bytes = new Uint8Array(f.buffer);
|
|
4858
|
+
base64 = btoa(String.fromCharCode(...bytes));
|
|
4859
|
+
}
|
|
4860
|
+
return { name: f.name, mimeType: f.mimeType, data: base64 };
|
|
4861
|
+
})
|
|
4862
|
+
);
|
|
4863
|
+
const objectId = await this.resolveObjectId(element.nodeId);
|
|
4864
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4865
|
+
objectId,
|
|
4866
|
+
functionDeclaration: `function(files) {
|
|
4867
|
+
if (!(this instanceof HTMLInputElement) || this.type !== 'file') {
|
|
4868
|
+
return { ok: false, fileCount: 0 };
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4871
|
+
const dt = new DataTransfer();
|
|
4872
|
+
for (const f of files) {
|
|
4873
|
+
const bytes = Uint8Array.from(atob(f.data), function(c) { return c.charCodeAt(0); });
|
|
4874
|
+
const file = new File([bytes], f.name, { type: f.mimeType });
|
|
4875
|
+
dt.items.add(file);
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4878
|
+
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'files');
|
|
4879
|
+
if (descriptor && descriptor.set) {
|
|
4880
|
+
descriptor.set.call(this, dt.files);
|
|
4881
|
+
} else {
|
|
4882
|
+
this.files = dt.files;
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
4886
|
+
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
4887
|
+
return {
|
|
4888
|
+
ok: (this.files && this.files.length === files.length) || files.length === 0,
|
|
4889
|
+
fileCount: this.files ? this.files.length : 0
|
|
4890
|
+
};
|
|
4891
|
+
}`,
|
|
4892
|
+
arguments: [{ value: fileData }],
|
|
4893
|
+
returnByValue: true
|
|
4894
|
+
});
|
|
4895
|
+
if (!result.result.value.ok) {
|
|
4896
|
+
if (options.optional) return false;
|
|
4897
|
+
throw new Error("Failed to set files on input");
|
|
4898
|
+
}
|
|
4899
|
+
return true;
|
|
4900
|
+
});
|
|
4901
|
+
}
|
|
4902
|
+
async getNativeSelectMetadata(objectId, targets) {
|
|
4903
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4904
|
+
objectId,
|
|
4905
|
+
functionDeclaration: `function(targetValues) {
|
|
4906
|
+
if (!(this instanceof HTMLSelectElement)) {
|
|
4907
|
+
return {
|
|
4908
|
+
currentIndex: -1,
|
|
4909
|
+
currentValue: '',
|
|
4910
|
+
disabled: [],
|
|
4911
|
+
isSelect: false,
|
|
4912
|
+
missing: Array.isArray(targetValues) ? targetValues.map(String) : [],
|
|
4913
|
+
multiple: false,
|
|
4914
|
+
options: [],
|
|
4915
|
+
selectedValues: [],
|
|
4916
|
+
targetIndexes: []
|
|
4917
|
+
};
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
var allOptions = Array.from(this.options).map(function(opt, index) {
|
|
4921
|
+
return { index: index, label: opt.label || opt.text || '', value: opt.value || '' };
|
|
4922
|
+
});
|
|
4923
|
+
var targetIndexes = [];
|
|
4924
|
+
var missing = [];
|
|
4925
|
+
var disabled = [];
|
|
4926
|
+
|
|
4927
|
+
for (var i = 0; i < targetValues.length; i++) {
|
|
4928
|
+
var target = String(targetValues[i]);
|
|
4929
|
+
var idx = -1;
|
|
4930
|
+
|
|
4931
|
+
for (var j = 0; j < this.options.length; j++) {
|
|
4932
|
+
var opt = this.options[j];
|
|
4933
|
+
if (opt.value === target || opt.text === target || opt.label === target) {
|
|
4934
|
+
idx = j;
|
|
4935
|
+
break;
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4939
|
+
if (idx === -1 && /^\\d+$/.test(target)) {
|
|
4940
|
+
var numericIndex = parseInt(target, 10);
|
|
4941
|
+
if (numericIndex >= 0 && numericIndex < this.options.length) {
|
|
4942
|
+
idx = numericIndex;
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
if (idx === -1) {
|
|
4947
|
+
missing.push(target);
|
|
4948
|
+
continue;
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
if (this.options[idx] && this.options[idx].disabled) {
|
|
4952
|
+
disabled.push(target);
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
if (targetIndexes.indexOf(idx) === -1) {
|
|
4957
|
+
targetIndexes.push(idx);
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
return {
|
|
4962
|
+
currentIndex: this.selectedIndex,
|
|
4963
|
+
currentValue: this.value || '',
|
|
4964
|
+
disabled: disabled,
|
|
4965
|
+
isSelect: true,
|
|
4966
|
+
missing: missing,
|
|
4967
|
+
multiple: !!this.multiple,
|
|
4968
|
+
options: allOptions,
|
|
4969
|
+
selectedValues: Array.from(this.selectedOptions).map(function(opt) { return opt.value || ''; }),
|
|
4970
|
+
targetIndexes: targetIndexes
|
|
4971
|
+
};
|
|
4972
|
+
}`,
|
|
4973
|
+
arguments: [{ value: targets }],
|
|
4974
|
+
returnByValue: true
|
|
4975
|
+
});
|
|
4976
|
+
return result.result.value;
|
|
4977
|
+
}
|
|
4978
|
+
async readNativeSelectValues(objectId) {
|
|
4979
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
4980
|
+
objectId,
|
|
4981
|
+
functionDeclaration: 'function() { return this instanceof HTMLSelectElement ? Array.from(this.selectedOptions).map(function(opt) { return opt.value || ""; }) : []; }',
|
|
4982
|
+
returnByValue: true
|
|
4983
|
+
});
|
|
4984
|
+
return result.result.value ?? [];
|
|
4985
|
+
}
|
|
4986
|
+
selectValuesMatch(actual, expected, multiple) {
|
|
4987
|
+
if (!multiple) {
|
|
4988
|
+
return (actual[0] ?? "") === (expected[0] ?? "");
|
|
4989
|
+
}
|
|
4990
|
+
if (actual.length !== expected.length) {
|
|
4991
|
+
return false;
|
|
4992
|
+
}
|
|
4993
|
+
const actualSorted = [...actual].sort();
|
|
4994
|
+
const expectedSorted = [...expected].sort();
|
|
4995
|
+
return actualSorted.every((value, index) => value === expectedSorted[index]);
|
|
4996
|
+
}
|
|
4997
|
+
async applyNativeSelectByKeyboard(nodeId, objectId, currentIndex, targetIndex) {
|
|
4998
|
+
await this.cdp.send("DOM.focus", { nodeId });
|
|
4999
|
+
if (targetIndex !== currentIndex) {
|
|
5000
|
+
let effectiveIndex = currentIndex;
|
|
5001
|
+
if (effectiveIndex < 0 || targetIndex < effectiveIndex) {
|
|
5002
|
+
await this.dispatchKey("Home");
|
|
5003
|
+
effectiveIndex = 0;
|
|
5004
|
+
}
|
|
5005
|
+
const steps = targetIndex - effectiveIndex;
|
|
5006
|
+
const direction = steps >= 0 ? "ArrowDown" : "ArrowUp";
|
|
5007
|
+
for (let i = 0; i < Math.abs(steps); i++) {
|
|
5008
|
+
await this.dispatchKey(direction);
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
const selectedValues = await this.readNativeSelectValues(objectId);
|
|
5012
|
+
return selectedValues[0] !== void 0;
|
|
5013
|
+
}
|
|
5014
|
+
async applyNativeSelectFallback(objectId, targetIndexes) {
|
|
5015
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
5016
|
+
objectId,
|
|
5017
|
+
functionDeclaration: `function(indexes) {
|
|
5018
|
+
if (!(this instanceof HTMLSelectElement)) return false;
|
|
5019
|
+
|
|
5020
|
+
var wanted = new Set(indexes.map(function(index) { return Number(index); }));
|
|
5021
|
+
for (var i = 0; i < this.options.length; i++) {
|
|
5022
|
+
this.options[i].selected = wanted.has(i);
|
|
5023
|
+
}
|
|
5024
|
+
if (!this.multiple && indexes.length === 1) {
|
|
5025
|
+
this.selectedIndex = indexes[0];
|
|
5026
|
+
}
|
|
5027
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
5028
|
+
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
|
5029
|
+
return true;
|
|
5030
|
+
}`,
|
|
5031
|
+
arguments: [{ value: targetIndexes }],
|
|
5032
|
+
returnByValue: true
|
|
5033
|
+
});
|
|
5034
|
+
}
|
|
5035
|
+
async selectEditableContent(objectId) {
|
|
5036
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
5037
|
+
objectId,
|
|
5038
|
+
functionDeclaration: `function() {
|
|
5039
|
+
if (this.isContentEditable) {
|
|
5040
|
+
this.focus();
|
|
5041
|
+
const range = document.createRange();
|
|
5042
|
+
range.selectNodeContents(this);
|
|
5043
|
+
const selection = window.getSelection();
|
|
5044
|
+
if (selection) {
|
|
5045
|
+
selection.removeAllRanges();
|
|
5046
|
+
selection.addRange(range);
|
|
5047
|
+
}
|
|
5048
|
+
return;
|
|
5049
|
+
}
|
|
5050
|
+
|
|
5051
|
+
if (this.tagName === 'TEXTAREA') {
|
|
5052
|
+
this.selectionStart = 0;
|
|
5053
|
+
this.selectionEnd = this.value.length;
|
|
5054
|
+
this.focus();
|
|
5055
|
+
return;
|
|
5056
|
+
}
|
|
5057
|
+
|
|
5058
|
+
if (typeof this.select === 'function') {
|
|
5059
|
+
this.select();
|
|
5060
|
+
}
|
|
5061
|
+
this.focus();
|
|
5062
|
+
}`
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
5065
|
+
async clearEditableSelection(objectId, key) {
|
|
5066
|
+
await this.selectEditableContent(objectId);
|
|
5067
|
+
await this.dispatchKey(key);
|
|
5068
|
+
}
|
|
5069
|
+
async readEditableValue(objectId) {
|
|
5070
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5071
|
+
objectId,
|
|
5072
|
+
functionDeclaration: `function() {
|
|
5073
|
+
if (this.isContentEditable) {
|
|
5074
|
+
return this.textContent || '';
|
|
5075
|
+
}
|
|
5076
|
+
return this.value || '';
|
|
5077
|
+
}`,
|
|
5078
|
+
returnByValue: true
|
|
5079
|
+
});
|
|
5080
|
+
return result.result.value ?? "";
|
|
5081
|
+
}
|
|
5082
|
+
async typeEditableFallback(nodeId, objectId, value) {
|
|
5083
|
+
await this.selectEditableContent(objectId);
|
|
5084
|
+
await this.cdp.send("DOM.focus", { nodeId });
|
|
5085
|
+
for (const char of value) {
|
|
5086
|
+
await this.dispatchKey(char);
|
|
4063
5087
|
}
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
5088
|
+
}
|
|
5089
|
+
async applyRecordedSelectFallback(objectId, targetIndexes) {
|
|
5090
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
5091
|
+
objectId,
|
|
5092
|
+
functionDeclaration: `function(indexes) {
|
|
5093
|
+
if (!(this instanceof HTMLSelectElement)) return false;
|
|
5094
|
+
|
|
5095
|
+
var wanted = new Set(indexes.map(function(index) { return Number(index); }));
|
|
5096
|
+
for (var i = 0; i < this.options.length; i++) {
|
|
5097
|
+
this.options[i].selected = wanted.has(i);
|
|
4072
5098
|
}
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
5099
|
+
if (!this.multiple && indexes.length === 1) {
|
|
5100
|
+
this.selectedIndex = indexes[0];
|
|
5101
|
+
}
|
|
5102
|
+
return true;
|
|
5103
|
+
}`,
|
|
5104
|
+
arguments: [{ value: targetIndexes }],
|
|
5105
|
+
returnByValue: true
|
|
5106
|
+
});
|
|
5107
|
+
return this.invokeRecordedEventListeners(objectId, ["input", "change"]);
|
|
5108
|
+
}
|
|
5109
|
+
async invokeRecordedEventListeners(objectId, eventTypes) {
|
|
5110
|
+
const result = await this.cdp.send("Runtime.callFunctionOn", {
|
|
5111
|
+
objectId,
|
|
5112
|
+
functionDeclaration: `function(types) {
|
|
5113
|
+
function buildPath(target) {
|
|
5114
|
+
var path = [];
|
|
5115
|
+
var node = target;
|
|
4080
5116
|
|
|
4081
|
-
|
|
4082
|
-
|
|
5117
|
+
while (node) {
|
|
5118
|
+
path.push(node);
|
|
4083
5119
|
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
5120
|
+
if (node.parentElement) {
|
|
5121
|
+
node = node.parentElement;
|
|
5122
|
+
continue;
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5125
|
+
if (node === document) {
|
|
5126
|
+
node = window;
|
|
5127
|
+
continue;
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
if (node.defaultView && node !== node.defaultView) {
|
|
5131
|
+
node = node.defaultView;
|
|
5132
|
+
continue;
|
|
5133
|
+
}
|
|
5134
|
+
|
|
5135
|
+
if (node.ownerDocument && node !== node.ownerDocument) {
|
|
5136
|
+
node = node.ownerDocument;
|
|
5137
|
+
continue;
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
var root = node.getRootNode && node.getRootNode();
|
|
5141
|
+
if (root && root !== node && root.host) {
|
|
5142
|
+
node = root.host;
|
|
5143
|
+
continue;
|
|
5144
|
+
}
|
|
5145
|
+
|
|
5146
|
+
node = null;
|
|
5147
|
+
}
|
|
5148
|
+
|
|
5149
|
+
return path;
|
|
4088
5150
|
}
|
|
4089
5151
|
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
5152
|
+
function createEvent(type, target, currentTarget, path, phase) {
|
|
5153
|
+
return {
|
|
5154
|
+
type: type,
|
|
5155
|
+
target: target,
|
|
5156
|
+
currentTarget: currentTarget,
|
|
5157
|
+
srcElement: target,
|
|
5158
|
+
isTrusted: true,
|
|
5159
|
+
bubbles: true,
|
|
5160
|
+
cancelable: true,
|
|
5161
|
+
composed: true,
|
|
5162
|
+
defaultPrevented: false,
|
|
5163
|
+
eventPhase: phase,
|
|
5164
|
+
timeStamp: Date.now(),
|
|
5165
|
+
preventDefault: function() {
|
|
5166
|
+
this.defaultPrevented = true;
|
|
5167
|
+
},
|
|
5168
|
+
stopPropagation: function() {
|
|
5169
|
+
this.__stopped = true;
|
|
5170
|
+
},
|
|
5171
|
+
stopImmediatePropagation: function() {
|
|
5172
|
+
this.__stopped = true;
|
|
5173
|
+
this.__immediateStopped = true;
|
|
5174
|
+
},
|
|
5175
|
+
composedPath: function() {
|
|
5176
|
+
return path.slice();
|
|
5177
|
+
}
|
|
5178
|
+
};
|
|
5179
|
+
}
|
|
5180
|
+
|
|
5181
|
+
function invokePhase(type, nodes, capture, target, path) {
|
|
5182
|
+
var invoked = false;
|
|
5183
|
+
|
|
5184
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
5185
|
+
var currentTarget = nodes[i];
|
|
5186
|
+
var store = currentTarget && currentTarget.__bpEventListeners;
|
|
5187
|
+
var entries = store && store[type];
|
|
5188
|
+
if (!Array.isArray(entries) || entries.length === 0) continue;
|
|
5189
|
+
|
|
5190
|
+
var phase = currentTarget === target ? 2 : capture ? 1 : 3;
|
|
5191
|
+
var event = createEvent(type, target, currentTarget, path, phase);
|
|
5192
|
+
|
|
5193
|
+
for (var j = 0; j < entries.length; j++) {
|
|
5194
|
+
var entry = entries[j];
|
|
5195
|
+
if (!!entry.capture !== capture) continue;
|
|
5196
|
+
|
|
5197
|
+
var listener = entry.listener;
|
|
5198
|
+
if (typeof listener === 'function') {
|
|
5199
|
+
listener.call(currentTarget, event);
|
|
5200
|
+
invoked = true;
|
|
5201
|
+
} else if (listener && typeof listener.handleEvent === 'function') {
|
|
5202
|
+
listener.handleEvent(event);
|
|
5203
|
+
invoked = true;
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
if (event.__immediateStopped) {
|
|
5207
|
+
break;
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
if (event.__stopped) {
|
|
5212
|
+
break;
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5216
|
+
return invoked;
|
|
5217
|
+
}
|
|
5218
|
+
|
|
5219
|
+
var path = buildPath(this);
|
|
5220
|
+
var capturePath = path.slice().reverse();
|
|
5221
|
+
var bubblePath = path.slice();
|
|
5222
|
+
var invokedAny = false;
|
|
5223
|
+
|
|
5224
|
+
for (var i = 0; i < types.length; i++) {
|
|
5225
|
+
var type = String(types[i]);
|
|
5226
|
+
if (invokePhase(type, capturePath, true, this, path)) {
|
|
5227
|
+
invokedAny = true;
|
|
5228
|
+
}
|
|
5229
|
+
if (invokePhase(type, bubblePath, false, this, path)) {
|
|
5230
|
+
invokedAny = true;
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
return invokedAny;
|
|
5235
|
+
}`,
|
|
5236
|
+
arguments: [{ value: eventTypes }],
|
|
4094
5237
|
returnByValue: true
|
|
4095
5238
|
});
|
|
4096
|
-
return
|
|
5239
|
+
return result.result.value ?? false;
|
|
4097
5240
|
}
|
|
4098
5241
|
/**
|
|
4099
5242
|
* Wait for a download to complete, triggered by an action
|
|
@@ -4253,7 +5396,7 @@ var Page = class {
|
|
|
4253
5396
|
return lines.join("\n");
|
|
4254
5397
|
};
|
|
4255
5398
|
const text = formatTree(accessibilityTree);
|
|
4256
|
-
|
|
5399
|
+
const result = {
|
|
4257
5400
|
url,
|
|
4258
5401
|
title,
|
|
4259
5402
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4261,6 +5404,8 @@ var Page = class {
|
|
|
4261
5404
|
interactiveElements,
|
|
4262
5405
|
text
|
|
4263
5406
|
};
|
|
5407
|
+
this.lastSnapshot = result;
|
|
5408
|
+
return result;
|
|
4264
5409
|
}
|
|
4265
5410
|
/**
|
|
4266
5411
|
* Export the current ref map for cross-exec reuse (CLI).
|
|
@@ -4789,11 +5934,13 @@ var Page = class {
|
|
|
4789
5934
|
try {
|
|
4790
5935
|
return await fn();
|
|
4791
5936
|
} catch (e) {
|
|
4792
|
-
|
|
5937
|
+
const message = e instanceof Error ? e.message : "";
|
|
5938
|
+
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"))) {
|
|
4793
5939
|
lastError = e;
|
|
4794
5940
|
if (attempt < retries) {
|
|
4795
5941
|
this.rootNodeId = null;
|
|
4796
|
-
|
|
5942
|
+
this.currentFrameContextId = null;
|
|
5943
|
+
await sleep5(delay);
|
|
4797
5944
|
continue;
|
|
4798
5945
|
}
|
|
4799
5946
|
}
|
|
@@ -4838,6 +5985,39 @@ var Page = class {
|
|
|
4838
5985
|
}
|
|
4839
5986
|
}
|
|
4840
5987
|
}
|
|
5988
|
+
if (selectorList.every((s) => s.startsWith("ref:")) && this.lastSnapshot) {
|
|
5989
|
+
for (const selector of selectorList) {
|
|
5990
|
+
const ref = selector.slice(4);
|
|
5991
|
+
const originalElement = this.lastSnapshot.interactiveElements.find((e) => e.ref === ref);
|
|
5992
|
+
if (!originalElement) continue;
|
|
5993
|
+
const freshSnapshot = await this.snapshot();
|
|
5994
|
+
const match = freshSnapshot.interactiveElements.find(
|
|
5995
|
+
(e) => e.role === originalElement.role && e.name === originalElement.name
|
|
5996
|
+
);
|
|
5997
|
+
if (match) {
|
|
5998
|
+
const newBackendNodeId = this.refMap.get(match.ref);
|
|
5999
|
+
if (newBackendNodeId) {
|
|
6000
|
+
try {
|
|
6001
|
+
await this.ensureRootNode();
|
|
6002
|
+
const pushResult = await this.cdp.send(
|
|
6003
|
+
"DOM.pushNodesByBackendIdsToFrontend",
|
|
6004
|
+
{ backendNodeIds: [newBackendNodeId] }
|
|
6005
|
+
);
|
|
6006
|
+
if (pushResult.nodeIds?.[0]) {
|
|
6007
|
+
this._lastMatchedSelector = `ref:${match.ref}`;
|
|
6008
|
+
return {
|
|
6009
|
+
nodeId: pushResult.nodeIds[0],
|
|
6010
|
+
backendNodeId: newBackendNodeId,
|
|
6011
|
+
selector: `ref:${match.ref}`,
|
|
6012
|
+
waitedMs: 0
|
|
6013
|
+
};
|
|
6014
|
+
}
|
|
6015
|
+
} catch {
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
4841
6021
|
const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
|
|
4842
6022
|
if (cssSelectors.length === 0) {
|
|
4843
6023
|
return null;
|
|
@@ -4901,6 +6081,38 @@ var Page = class {
|
|
|
4901
6081
|
*/
|
|
4902
6082
|
async ensureRootNode() {
|
|
4903
6083
|
if (this.rootNodeId) return;
|
|
6084
|
+
if (this.currentFrame) {
|
|
6085
|
+
const mainDocument = await this.cdp.send("DOM.getDocument", {
|
|
6086
|
+
depth: 0
|
|
6087
|
+
});
|
|
6088
|
+
const iframeNode = await this.cdp.send("DOM.querySelector", {
|
|
6089
|
+
nodeId: mainDocument.root.nodeId,
|
|
6090
|
+
selector: this.currentFrame
|
|
6091
|
+
});
|
|
6092
|
+
if (iframeNode.nodeId) {
|
|
6093
|
+
const frameResult = await this.cdp.send("DOM.describeNode", {
|
|
6094
|
+
nodeId: iframeNode.nodeId,
|
|
6095
|
+
depth: 1
|
|
6096
|
+
});
|
|
6097
|
+
if (frameResult.node.contentDocument?.nodeId) {
|
|
6098
|
+
this.rootNodeId = frameResult.node.contentDocument.nodeId;
|
|
6099
|
+
if (frameResult.node.frameId) {
|
|
6100
|
+
let contextId = this.frameExecutionContexts.get(frameResult.node.frameId);
|
|
6101
|
+
if (!contextId) {
|
|
6102
|
+
for (let i = 0; i < 10; i++) {
|
|
6103
|
+
await sleep5(100);
|
|
6104
|
+
contextId = this.frameExecutionContexts.get(frameResult.node.frameId);
|
|
6105
|
+
if (contextId) break;
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
this.currentFrameContextId = contextId ?? null;
|
|
6109
|
+
}
|
|
6110
|
+
return;
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
this.currentFrame = null;
|
|
6114
|
+
this.currentFrameContextId = null;
|
|
6115
|
+
}
|
|
4904
6116
|
const doc = await this.cdp.send("DOM.getDocument", {
|
|
4905
6117
|
depth: 0
|
|
4906
6118
|
});
|
|
@@ -4941,30 +6153,115 @@ var Page = class {
|
|
|
4941
6153
|
}
|
|
4942
6154
|
}
|
|
4943
6155
|
/**
|
|
4944
|
-
* Click an element by node ID
|
|
6156
|
+
* Click an element by node ID using Playwright's 3-event sequence:
|
|
6157
|
+
* mouseMoved → mousePressed → mouseReleased (sequential).
|
|
6158
|
+
* Uses DOM.getContentQuads for accurate coordinates (handles CSS transforms).
|
|
6159
|
+
* Falls back to JS this.click() if CDP mouse dispatch fails.
|
|
4945
6160
|
*/
|
|
4946
6161
|
async clickElement(nodeId) {
|
|
4947
|
-
const
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
6162
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
6163
|
+
nodeId
|
|
6164
|
+
});
|
|
6165
|
+
let x;
|
|
6166
|
+
let y;
|
|
6167
|
+
try {
|
|
6168
|
+
const { quads } = await this.cdp.send("DOM.getContentQuads", {
|
|
6169
|
+
objectId: object.objectId
|
|
6170
|
+
});
|
|
6171
|
+
if (quads && quads.length > 0) {
|
|
6172
|
+
const quad = quads[0];
|
|
6173
|
+
x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
6174
|
+
y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
6175
|
+
} else {
|
|
6176
|
+
throw new Error("No quads");
|
|
6177
|
+
}
|
|
6178
|
+
} catch {
|
|
6179
|
+
const box = await this.getBoxModel(nodeId);
|
|
6180
|
+
if (!box) throw new Error("Could not get element position for click");
|
|
6181
|
+
x = box.content[0] + box.width / 2;
|
|
6182
|
+
y = box.content[1] + box.height / 2;
|
|
6183
|
+
}
|
|
6184
|
+
try {
|
|
6185
|
+
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
6186
|
+
type: "mouseMoved",
|
|
6187
|
+
x,
|
|
6188
|
+
y,
|
|
6189
|
+
button: "none",
|
|
6190
|
+
buttons: 0,
|
|
6191
|
+
modifiers: 0
|
|
6192
|
+
});
|
|
6193
|
+
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
6194
|
+
type: "mousePressed",
|
|
6195
|
+
x,
|
|
6196
|
+
y,
|
|
6197
|
+
button: "left",
|
|
6198
|
+
buttons: 1,
|
|
6199
|
+
clickCount: 1,
|
|
6200
|
+
modifiers: 0
|
|
6201
|
+
});
|
|
6202
|
+
await this.cdp.send("Input.dispatchMouseEvent", {
|
|
6203
|
+
type: "mouseReleased",
|
|
6204
|
+
x,
|
|
6205
|
+
y,
|
|
6206
|
+
button: "left",
|
|
6207
|
+
buttons: 0,
|
|
6208
|
+
clickCount: 1,
|
|
6209
|
+
modifiers: 0
|
|
6210
|
+
});
|
|
6211
|
+
} catch {
|
|
6212
|
+
await this.cdp.send("Runtime.callFunctionOn", {
|
|
6213
|
+
objectId: object.objectId,
|
|
6214
|
+
functionDeclaration: "function() { this.click(); }"
|
|
6215
|
+
});
|
|
6216
|
+
}
|
|
6217
|
+
await this.cdp.send("Runtime.evaluate", { expression: "0" });
|
|
6218
|
+
}
|
|
6219
|
+
/**
|
|
6220
|
+
* Resolve a nodeId to a Remote Object ID for use with Runtime.callFunctionOn
|
|
6221
|
+
*/
|
|
6222
|
+
async resolveObjectId(nodeId) {
|
|
6223
|
+
const { object } = await this.cdp.send("DOM.resolveNode", {
|
|
6224
|
+
nodeId
|
|
4959
6225
|
});
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
6226
|
+
return object.objectId;
|
|
6227
|
+
}
|
|
6228
|
+
async dispatchKeyDefinition(def) {
|
|
6229
|
+
const downParams = {
|
|
6230
|
+
type: def.text !== void 0 ? "keyDown" : "rawKeyDown",
|
|
6231
|
+
key: def.key,
|
|
6232
|
+
code: def.code,
|
|
6233
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
6234
|
+
modifiers: 0,
|
|
6235
|
+
autoRepeat: false,
|
|
6236
|
+
location: def.location ?? 0,
|
|
6237
|
+
isKeypad: false
|
|
6238
|
+
};
|
|
6239
|
+
if (def.text !== void 0) {
|
|
6240
|
+
downParams["text"] = def.text;
|
|
6241
|
+
downParams["unmodifiedText"] = def.text;
|
|
6242
|
+
}
|
|
6243
|
+
await this.cdp.send("Input.dispatchKeyEvent", downParams);
|
|
6244
|
+
await this.cdp.send("Input.dispatchKeyEvent", {
|
|
6245
|
+
type: "keyUp",
|
|
6246
|
+
key: def.key,
|
|
6247
|
+
code: def.code,
|
|
6248
|
+
windowsVirtualKeyCode: def.keyCode,
|
|
6249
|
+
modifiers: 0,
|
|
6250
|
+
location: def.location ?? 0
|
|
4966
6251
|
});
|
|
4967
6252
|
}
|
|
6253
|
+
async dispatchKey(key) {
|
|
6254
|
+
const def = US_KEYBOARD[key];
|
|
6255
|
+
if (def) {
|
|
6256
|
+
await this.dispatchKeyDefinition(def);
|
|
6257
|
+
return;
|
|
6258
|
+
}
|
|
6259
|
+
if ([...key].length === 1) {
|
|
6260
|
+
await this.cdp.send("Input.insertText", { text: key });
|
|
6261
|
+
return;
|
|
6262
|
+
}
|
|
6263
|
+
await this.dispatchKeyDefinition({ key, code: key, keyCode: 0 });
|
|
6264
|
+
}
|
|
4968
6265
|
// ============ Audio I/O ============
|
|
4969
6266
|
/**
|
|
4970
6267
|
* Audio input controller (fake microphone).
|
|
@@ -5037,7 +6334,7 @@ var Page = class {
|
|
|
5037
6334
|
const start = Date.now();
|
|
5038
6335
|
await this.audioOutput.start();
|
|
5039
6336
|
if (options.preDelay && options.preDelay > 0) {
|
|
5040
|
-
await
|
|
6337
|
+
await sleep5(options.preDelay);
|
|
5041
6338
|
}
|
|
5042
6339
|
const inputDone = this.audioInput.play(options.input, {
|
|
5043
6340
|
waitForEnd: !!options.sendSelector
|
|
@@ -5065,11 +6362,27 @@ var Page = class {
|
|
|
5065
6362
|
};
|
|
5066
6363
|
}
|
|
5067
6364
|
};
|
|
5068
|
-
function
|
|
6365
|
+
function sleep5(ms) {
|
|
5069
6366
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
5070
6367
|
}
|
|
5071
6368
|
|
|
5072
6369
|
// src/browser/browser.ts
|
|
6370
|
+
function scoreTarget(t) {
|
|
6371
|
+
let score = 0;
|
|
6372
|
+
if (t.url.startsWith("http://") || t.url.startsWith("https://")) score += 10;
|
|
6373
|
+
if (t.url.startsWith("chrome://")) score -= 20;
|
|
6374
|
+
if (t.url.startsWith("chrome-extension://")) score -= 15;
|
|
6375
|
+
if (t.url.startsWith("devtools://")) score -= 25;
|
|
6376
|
+
if (t.url === "about:blank") score -= 5;
|
|
6377
|
+
if (!t.attached) score += 3;
|
|
6378
|
+
if (t.title && t.title.length > 0) score += 2;
|
|
6379
|
+
return score;
|
|
6380
|
+
}
|
|
6381
|
+
function pickBestTarget(targets) {
|
|
6382
|
+
if (targets.length === 0) return void 0;
|
|
6383
|
+
const sorted = [...targets].sort((a, b) => scoreTarget(b) - scoreTarget(a));
|
|
6384
|
+
return sorted[0].targetId;
|
|
6385
|
+
}
|
|
5073
6386
|
var Browser = class _Browser {
|
|
5074
6387
|
cdp;
|
|
5075
6388
|
providerSession;
|
|
@@ -5091,28 +6404,46 @@ var Browser = class _Browser {
|
|
|
5091
6404
|
return new _Browser(cdp, provider, session, options);
|
|
5092
6405
|
}
|
|
5093
6406
|
/**
|
|
5094
|
-
* Get or create a page by name
|
|
5095
|
-
* If no name is provided, returns the first available page or creates a new one
|
|
6407
|
+
* Get or create a page by name.
|
|
6408
|
+
* If no name is provided, returns the first available page or creates a new one.
|
|
6409
|
+
*
|
|
6410
|
+
* Target selection heuristics (when no targetId is specified):
|
|
6411
|
+
* - Prefer http/https URLs over chrome://, devtools://, about:blank
|
|
6412
|
+
* - Prefer unattached targets (not already controlled by another client)
|
|
6413
|
+
* - Filter by targetUrl if provided
|
|
5096
6414
|
*/
|
|
5097
6415
|
async page(name, options) {
|
|
5098
6416
|
const pageName = name ?? "default";
|
|
5099
6417
|
const cached = this.pages.get(pageName);
|
|
5100
6418
|
if (cached) return cached;
|
|
5101
6419
|
const targets = await this.cdp.send("Target.getTargets");
|
|
5102
|
-
|
|
6420
|
+
let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
|
|
6421
|
+
if (options?.targetUrl) {
|
|
6422
|
+
const urlFilter = options.targetUrl;
|
|
6423
|
+
const filtered = pageTargets.filter((t) => t.url.includes(urlFilter));
|
|
6424
|
+
if (filtered.length > 0) {
|
|
6425
|
+
pageTargets = filtered;
|
|
6426
|
+
} else {
|
|
6427
|
+
console.warn(
|
|
6428
|
+
`[browser-pilot] No targets match URL filter "${urlFilter}", falling back to all page targets`
|
|
6429
|
+
);
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
5103
6432
|
let targetId;
|
|
5104
6433
|
if (options?.targetId) {
|
|
5105
|
-
const targetExists =
|
|
6434
|
+
const targetExists = targets.targetInfos.some(
|
|
6435
|
+
(t) => t.type === "page" && t.targetId === options.targetId
|
|
6436
|
+
);
|
|
5106
6437
|
if (targetExists) {
|
|
5107
6438
|
targetId = options.targetId;
|
|
5108
6439
|
} else {
|
|
5109
6440
|
console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
|
|
5110
|
-
targetId = pageTargets
|
|
6441
|
+
targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send("Target.createTarget", {
|
|
5111
6442
|
url: "about:blank"
|
|
5112
6443
|
})).targetId;
|
|
5113
6444
|
}
|
|
5114
6445
|
} else if (pageTargets.length > 0) {
|
|
5115
|
-
targetId = pageTargets
|
|
6446
|
+
targetId = pickBestTarget(pageTargets);
|
|
5116
6447
|
} else {
|
|
5117
6448
|
const result = await this.cdp.send("Target.createTarget", {
|
|
5118
6449
|
url: "about:blank"
|
|
@@ -5122,6 +6453,21 @@ var Browser = class _Browser {
|
|
|
5122
6453
|
await this.cdp.attachToTarget(targetId);
|
|
5123
6454
|
const page = new Page(this.cdp, targetId);
|
|
5124
6455
|
await page.init();
|
|
6456
|
+
const minViewport = options?.minViewport !== void 0 ? options.minViewport : { width: 200, height: 200 };
|
|
6457
|
+
if (minViewport !== false) {
|
|
6458
|
+
try {
|
|
6459
|
+
const viewport = await page.evaluate(
|
|
6460
|
+
"({ w: window.innerWidth, h: window.innerHeight })"
|
|
6461
|
+
);
|
|
6462
|
+
if (viewport.w < minViewport.width || viewport.h < minViewport.height) {
|
|
6463
|
+
console.warn(
|
|
6464
|
+
`[browser-pilot] Attached target has small viewport (${viewport.w}x${viewport.h}). Applying default viewport override (1280x720). Use { minViewport: false } to disable this check.`
|
|
6465
|
+
);
|
|
6466
|
+
await page.setViewport({ width: 1280, height: 720 });
|
|
6467
|
+
}
|
|
6468
|
+
} catch {
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
5125
6471
|
this.pages.set(pageName, page);
|
|
5126
6472
|
return page;
|
|
5127
6473
|
}
|
|
@@ -5764,7 +7110,7 @@ async function audioCommand(args, globalOptions) {
|
|
|
5764
7110
|
let capture;
|
|
5765
7111
|
if (options.duration && options.duration > 0) {
|
|
5766
7112
|
await page.audioOutput.start();
|
|
5767
|
-
await
|
|
7113
|
+
await sleep6(options.duration);
|
|
5768
7114
|
capture = await page.audioOutput.stop();
|
|
5769
7115
|
} else {
|
|
5770
7116
|
capture = await page.audioOutput.captureUntilSilence({
|
|
@@ -5914,7 +7260,7 @@ Available: setup, play, capture, roundtrip, check`
|
|
|
5914
7260
|
await browser.disconnect();
|
|
5915
7261
|
}
|
|
5916
7262
|
}
|
|
5917
|
-
function
|
|
7263
|
+
function sleep6(ms) {
|
|
5918
7264
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
5919
7265
|
}
|
|
5920
7266
|
|
|
@@ -6065,6 +7411,7 @@ Options:
|
|
|
6065
7411
|
--url <ws-url> WebSocket URL (auto-discovered for generic provider)
|
|
6066
7412
|
-n, --name <id> Custom session name (default: auto-generated)
|
|
6067
7413
|
-r, --resume <id> Resume an existing session by ID
|
|
7414
|
+
--target-url <str> Filter targets to those whose URL contains this string
|
|
6068
7415
|
--api-key <key> API key for cloud providers
|
|
6069
7416
|
--project-id <id> Project ID for BrowserBase provider
|
|
6070
7417
|
--export-log <path> Export session log to file on close
|
|
@@ -6077,6 +7424,7 @@ Examples:
|
|
|
6077
7424
|
bp connect --provider generic --name dev # Connect with custom session name
|
|
6078
7425
|
bp connect --url ws://localhost:9222/devtools # Explicit WebSocket URL
|
|
6079
7426
|
bp connect --resume dev # Resume a previous session
|
|
7427
|
+
bp connect --target-url localhost:3000 # Attach to tab matching URL
|
|
6080
7428
|
`.trimEnd();
|
|
6081
7429
|
function parseConnectArgs(args) {
|
|
6082
7430
|
const options = {};
|
|
@@ -6090,6 +7438,8 @@ function parseConnectArgs(args) {
|
|
|
6090
7438
|
options.name = args[++i];
|
|
6091
7439
|
} else if (arg === "--resume" || arg === "-r") {
|
|
6092
7440
|
options.resume = args[++i];
|
|
7441
|
+
} else if (arg === "--target-url") {
|
|
7442
|
+
options.targetUrl = args[++i];
|
|
6093
7443
|
} else if (arg === "--api-key") {
|
|
6094
7444
|
options.apiKey = args[++i];
|
|
6095
7445
|
} else if (arg === "--project-id") {
|
|
@@ -6140,7 +7490,8 @@ async function connectCommand(args, globalOptions) {
|
|
|
6140
7490
|
projectId: options.projectId
|
|
6141
7491
|
};
|
|
6142
7492
|
const browser = await connect(connectOptions);
|
|
6143
|
-
const
|
|
7493
|
+
const pageOptions = options.targetUrl ? { targetUrl: options.targetUrl } : void 0;
|
|
7494
|
+
const page = await browser.page(void 0, pageOptions);
|
|
6144
7495
|
const currentUrl = await page.url();
|
|
6145
7496
|
const sessionId = options.name ?? generateSessionId();
|
|
6146
7497
|
const session = {
|
|
@@ -7176,7 +8527,7 @@ Run 'bp actions' for complete action reference.`
|
|
|
7176
8527
|
} catch {
|
|
7177
8528
|
const snippet = actionsJson.substring(0, 80);
|
|
7178
8529
|
const looksLikeEvaluate = /evaluate/i.test(actionsJson);
|
|
7179
|
-
const evalTip = looksLikeEvaluate ? "\n\nTip:
|
|
8530
|
+
const evalTip = looksLikeEvaluate ? "\n\nTip: If you truly need raw JavaScript evaluation, use 'bp eval' instead \u2014 no JSON wrapping needed:\n bp eval 'your.expression.here'\nUse high-level actions plus refs first whenever possible." : "";
|
|
7180
8531
|
throw new Error(
|
|
7181
8532
|
`Invalid JSON: ${snippet}${actionsJson.length > 80 ? "..." : ""}
|
|
7182
8533
|
|
|
@@ -7292,7 +8643,7 @@ Session file has been cleaned up. Run "bp connect" to create a new session.`
|
|
|
7292
8643
|
if (failedEval) {
|
|
7293
8644
|
console.error(
|
|
7294
8645
|
`
|
|
7295
|
-
Tip: Use "bp eval 'expression'" for simpler JavaScript
|
|
8646
|
+
Tip: Use "bp eval 'expression'" for simpler JavaScript inspection/debugging (no JSON escaping needed). Prefer high-level actions for interactions.`
|
|
7296
8647
|
);
|
|
7297
8648
|
}
|
|
7298
8649
|
} finally {
|
|
@@ -8674,13 +10025,26 @@ var RECORDER_SCRIPT = `(function() {
|
|
|
8674
10025
|
// src/recording/recorder.ts
|
|
8675
10026
|
var Recorder = class {
|
|
8676
10027
|
cdp;
|
|
10028
|
+
options;
|
|
8677
10029
|
events = [];
|
|
8678
10030
|
recording = false;
|
|
8679
10031
|
startTime = 0;
|
|
8680
10032
|
startUrl = "";
|
|
8681
10033
|
bindingHandler = null;
|
|
8682
|
-
|
|
10034
|
+
// Network capture state
|
|
10035
|
+
listenOpts = null;
|
|
10036
|
+
networkRequests = [];
|
|
10037
|
+
networkResponses = [];
|
|
10038
|
+
wsEvents = [];
|
|
10039
|
+
wsFrames = [];
|
|
10040
|
+
networkHandlers = [];
|
|
10041
|
+
matchRegex = null;
|
|
10042
|
+
pendingBodies = [];
|
|
10043
|
+
wsUrls = /* @__PURE__ */ new Map();
|
|
10044
|
+
httpUrls = /* @__PURE__ */ new Map();
|
|
10045
|
+
constructor(cdp, options) {
|
|
8683
10046
|
this.cdp = cdp;
|
|
10047
|
+
this.options = options ?? {};
|
|
8684
10048
|
}
|
|
8685
10049
|
/**
|
|
8686
10050
|
* Check if recording is currently active.
|
|
@@ -8726,6 +10090,13 @@ var Recorder = class {
|
|
|
8726
10090
|
}
|
|
8727
10091
|
};
|
|
8728
10092
|
this.cdp.on("Runtime.bindingCalled", this.bindingHandler);
|
|
10093
|
+
if (this.options.listen) {
|
|
10094
|
+
const listenOpts = typeof this.options.listen === "boolean" ? { mode: "all" } : this.options.listen;
|
|
10095
|
+
this.listenOpts = listenOpts;
|
|
10096
|
+
this.matchRegex = listenOpts.match ? globToRegex2(listenOpts.match) : null;
|
|
10097
|
+
await this.cdp.send("Network.enable");
|
|
10098
|
+
this.setupNetworkListeners(listenOpts);
|
|
10099
|
+
}
|
|
8729
10100
|
}
|
|
8730
10101
|
/**
|
|
8731
10102
|
* Stop recording and return aggregated output.
|
|
@@ -8742,13 +10113,39 @@ var Recorder = class {
|
|
|
8742
10113
|
this.cdp.off("Runtime.bindingCalled", this.bindingHandler);
|
|
8743
10114
|
this.bindingHandler = null;
|
|
8744
10115
|
}
|
|
10116
|
+
for (const { event, handler } of this.networkHandlers) {
|
|
10117
|
+
this.cdp.off(event, handler);
|
|
10118
|
+
}
|
|
10119
|
+
this.networkHandlers = [];
|
|
10120
|
+
if (this.listenOpts) {
|
|
10121
|
+
await this.cdp.send("Network.disable");
|
|
10122
|
+
}
|
|
10123
|
+
await Promise.allSettled(this.pendingBodies);
|
|
10124
|
+
this.pendingBodies = [];
|
|
8745
10125
|
const steps = aggregateEvents(this.events, this.startUrl);
|
|
8746
|
-
|
|
10126
|
+
const result = {
|
|
8747
10127
|
recordedAt: new Date(this.startTime).toISOString(),
|
|
8748
10128
|
startUrl: this.startUrl,
|
|
8749
10129
|
duration,
|
|
8750
10130
|
steps
|
|
8751
10131
|
};
|
|
10132
|
+
if (this.listenOpts) {
|
|
10133
|
+
const mode = this.listenOpts.mode ?? "all";
|
|
10134
|
+
if (mode === "http" || mode === "all") {
|
|
10135
|
+
result.network = {
|
|
10136
|
+
requests: this.networkRequests,
|
|
10137
|
+
responses: this.networkResponses
|
|
10138
|
+
};
|
|
10139
|
+
}
|
|
10140
|
+
if (mode === "ws" || mode === "all") {
|
|
10141
|
+
result.websockets = {
|
|
10142
|
+
events: this.wsEvents,
|
|
10143
|
+
frames: this.wsFrames
|
|
10144
|
+
};
|
|
10145
|
+
}
|
|
10146
|
+
result.timeline = this.buildTimeline();
|
|
10147
|
+
}
|
|
10148
|
+
return result;
|
|
8752
10149
|
}
|
|
8753
10150
|
/**
|
|
8754
10151
|
* Get raw recorded events (for debugging).
|
|
@@ -8767,7 +10164,201 @@ var Recorder = class {
|
|
|
8767
10164
|
} catch {
|
|
8768
10165
|
}
|
|
8769
10166
|
}
|
|
10167
|
+
/** Subscribe to a CDP event, tracking for cleanup. */
|
|
10168
|
+
subscribeNetwork(event, handler) {
|
|
10169
|
+
this.cdp.on(event, handler);
|
|
10170
|
+
this.networkHandlers.push({ event, handler });
|
|
10171
|
+
}
|
|
10172
|
+
/** Check if a URL matches the configured filter. */
|
|
10173
|
+
matchesUrl(url) {
|
|
10174
|
+
if (!this.matchRegex) return true;
|
|
10175
|
+
return this.matchRegex.test(url);
|
|
10176
|
+
}
|
|
10177
|
+
/** Elapsed milliseconds since recording started. */
|
|
10178
|
+
elapsed() {
|
|
10179
|
+
return Date.now() - this.startTime;
|
|
10180
|
+
}
|
|
10181
|
+
/** Format a WebSocket payload, truncating or replacing binary data. */
|
|
10182
|
+
formatPayload(payloadData, opcode) {
|
|
10183
|
+
const data = payloadData ?? "";
|
|
10184
|
+
const maxPayload = this.listenOpts?.maxPayload ?? 256;
|
|
10185
|
+
if (opcode === 2) {
|
|
10186
|
+
const byteLength = Math.floor(data.length * 3 / 4);
|
|
10187
|
+
return { payload: `[binary: ${byteLength} bytes]`, length: data.length };
|
|
10188
|
+
}
|
|
10189
|
+
const length = data.length;
|
|
10190
|
+
if (length > maxPayload) {
|
|
10191
|
+
return {
|
|
10192
|
+
payload: `${data.slice(0, maxPayload)}... [truncated, ${length} total]`,
|
|
10193
|
+
length
|
|
10194
|
+
};
|
|
10195
|
+
}
|
|
10196
|
+
return { payload: data, length };
|
|
10197
|
+
}
|
|
10198
|
+
/** Set up CDP event listeners for network traffic capture. */
|
|
10199
|
+
setupNetworkListeners(opts) {
|
|
10200
|
+
const mode = opts.mode ?? "all";
|
|
10201
|
+
if (mode === "ws" || mode === "all") {
|
|
10202
|
+
this.subscribeNetwork("Network.webSocketCreated", (params) => {
|
|
10203
|
+
const url = params["url"];
|
|
10204
|
+
const requestId = params["requestId"];
|
|
10205
|
+
if (!this.matchesUrl(url)) return;
|
|
10206
|
+
this.wsUrls.set(requestId, url);
|
|
10207
|
+
const now = Date.now();
|
|
10208
|
+
this.wsEvents.push({
|
|
10209
|
+
requestId,
|
|
10210
|
+
timestamp: now,
|
|
10211
|
+
elapsedMs: this.elapsed(),
|
|
10212
|
+
type: "created",
|
|
10213
|
+
url
|
|
10214
|
+
});
|
|
10215
|
+
});
|
|
10216
|
+
this.subscribeNetwork("Network.webSocketFrameSent", (params) => {
|
|
10217
|
+
const requestId = params["requestId"];
|
|
10218
|
+
if (!this.wsUrls.has(requestId)) return;
|
|
10219
|
+
const response = params["response"];
|
|
10220
|
+
const opcode = response?.opcode ?? 1;
|
|
10221
|
+
const { payload, length } = this.formatPayload(response?.payloadData, opcode);
|
|
10222
|
+
const now = Date.now();
|
|
10223
|
+
this.wsFrames.push({
|
|
10224
|
+
requestId,
|
|
10225
|
+
timestamp: now,
|
|
10226
|
+
elapsedMs: this.elapsed(),
|
|
10227
|
+
direction: "sent",
|
|
10228
|
+
opcode,
|
|
10229
|
+
payload,
|
|
10230
|
+
length
|
|
10231
|
+
});
|
|
10232
|
+
});
|
|
10233
|
+
this.subscribeNetwork("Network.webSocketFrameReceived", (params) => {
|
|
10234
|
+
const requestId = params["requestId"];
|
|
10235
|
+
if (!this.wsUrls.has(requestId)) return;
|
|
10236
|
+
const response = params["response"];
|
|
10237
|
+
const opcode = response?.opcode ?? 1;
|
|
10238
|
+
const { payload, length } = this.formatPayload(response?.payloadData, opcode);
|
|
10239
|
+
const now = Date.now();
|
|
10240
|
+
this.wsFrames.push({
|
|
10241
|
+
requestId,
|
|
10242
|
+
timestamp: now,
|
|
10243
|
+
elapsedMs: this.elapsed(),
|
|
10244
|
+
direction: "received",
|
|
10245
|
+
opcode,
|
|
10246
|
+
payload,
|
|
10247
|
+
length
|
|
10248
|
+
});
|
|
10249
|
+
});
|
|
10250
|
+
this.subscribeNetwork("Network.webSocketClosed", (params) => {
|
|
10251
|
+
const requestId = params["requestId"];
|
|
10252
|
+
if (!this.wsUrls.has(requestId)) return;
|
|
10253
|
+
this.wsUrls.delete(requestId);
|
|
10254
|
+
const now = Date.now();
|
|
10255
|
+
this.wsEvents.push({
|
|
10256
|
+
requestId,
|
|
10257
|
+
timestamp: now,
|
|
10258
|
+
elapsedMs: this.elapsed(),
|
|
10259
|
+
type: "closed"
|
|
10260
|
+
});
|
|
10261
|
+
});
|
|
10262
|
+
}
|
|
10263
|
+
if (mode === "http" || mode === "all") {
|
|
10264
|
+
this.subscribeNetwork("Network.requestWillBeSent", (params) => {
|
|
10265
|
+
const request = params["request"];
|
|
10266
|
+
const url = request?.url ?? "";
|
|
10267
|
+
const requestId = params["requestId"];
|
|
10268
|
+
if (!this.matchesUrl(url)) return;
|
|
10269
|
+
this.httpUrls.set(requestId, url);
|
|
10270
|
+
const now = Date.now();
|
|
10271
|
+
this.networkRequests.push({
|
|
10272
|
+
requestId,
|
|
10273
|
+
timestamp: now,
|
|
10274
|
+
elapsedMs: this.elapsed(),
|
|
10275
|
+
method: request?.method ?? "GET",
|
|
10276
|
+
url,
|
|
10277
|
+
headers: request?.headers,
|
|
10278
|
+
body: request?.postData
|
|
10279
|
+
});
|
|
10280
|
+
});
|
|
10281
|
+
this.subscribeNetwork("Network.responseReceived", (params) => {
|
|
10282
|
+
const requestId = params["requestId"];
|
|
10283
|
+
if (!this.httpUrls.has(requestId)) return;
|
|
10284
|
+
const response = params["response"];
|
|
10285
|
+
const now = Date.now();
|
|
10286
|
+
this.networkResponses.push({
|
|
10287
|
+
requestId,
|
|
10288
|
+
timestamp: now,
|
|
10289
|
+
elapsedMs: this.elapsed(),
|
|
10290
|
+
status: response?.status ?? 0,
|
|
10291
|
+
headers: response?.headers,
|
|
10292
|
+
mimeType: response?.mimeType
|
|
10293
|
+
});
|
|
10294
|
+
if (this.listenOpts?.captureResponseBodies) {
|
|
10295
|
+
const bodyPromise = this.cdp.send("Network.getResponseBody", {
|
|
10296
|
+
requestId
|
|
10297
|
+
}).then((result) => {
|
|
10298
|
+
const resp = this.networkResponses.find((r) => r.requestId === requestId);
|
|
10299
|
+
if (resp) {
|
|
10300
|
+
resp.body = result.base64Encoded ? `[base64: ${result.body.length} chars]` : result.body;
|
|
10301
|
+
resp.bodySize = result.body.length;
|
|
10302
|
+
}
|
|
10303
|
+
}).catch(() => {
|
|
10304
|
+
});
|
|
10305
|
+
this.pendingBodies.push(bodyPromise);
|
|
10306
|
+
}
|
|
10307
|
+
});
|
|
10308
|
+
}
|
|
10309
|
+
}
|
|
10310
|
+
/** Build a merged timeline from action events and network events. */
|
|
10311
|
+
buildTimeline() {
|
|
10312
|
+
const entries = [];
|
|
10313
|
+
for (const event of this.events) {
|
|
10314
|
+
entries.push({
|
|
10315
|
+
timestamp: event.timestamp,
|
|
10316
|
+
elapsedMs: event.timestamp - this.startTime,
|
|
10317
|
+
type: "action",
|
|
10318
|
+
data: { kind: event.kind, url: event.url, selectors: event.selectors, value: event.value }
|
|
10319
|
+
});
|
|
10320
|
+
}
|
|
10321
|
+
for (const req of this.networkRequests) {
|
|
10322
|
+
entries.push({
|
|
10323
|
+
timestamp: req.timestamp,
|
|
10324
|
+
elapsedMs: req.elapsedMs,
|
|
10325
|
+
type: "network-request",
|
|
10326
|
+
data: req
|
|
10327
|
+
});
|
|
10328
|
+
}
|
|
10329
|
+
for (const resp of this.networkResponses) {
|
|
10330
|
+
entries.push({
|
|
10331
|
+
timestamp: resp.timestamp,
|
|
10332
|
+
elapsedMs: resp.elapsedMs,
|
|
10333
|
+
type: "network-response",
|
|
10334
|
+
data: resp
|
|
10335
|
+
});
|
|
10336
|
+
}
|
|
10337
|
+
for (const evt of this.wsEvents) {
|
|
10338
|
+
entries.push({
|
|
10339
|
+
timestamp: evt.timestamp,
|
|
10340
|
+
elapsedMs: evt.elapsedMs,
|
|
10341
|
+
type: "ws-event",
|
|
10342
|
+
data: evt
|
|
10343
|
+
});
|
|
10344
|
+
}
|
|
10345
|
+
for (const frame of this.wsFrames) {
|
|
10346
|
+
entries.push({
|
|
10347
|
+
timestamp: frame.timestamp,
|
|
10348
|
+
elapsedMs: frame.elapsedMs,
|
|
10349
|
+
type: "ws-frame",
|
|
10350
|
+
data: frame
|
|
10351
|
+
});
|
|
10352
|
+
}
|
|
10353
|
+
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
10354
|
+
return entries;
|
|
10355
|
+
}
|
|
8770
10356
|
};
|
|
10357
|
+
function globToRegex2(pattern) {
|
|
10358
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
10359
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
10360
|
+
return new RegExp(`^${withWildcards}$`);
|
|
10361
|
+
}
|
|
8771
10362
|
|
|
8772
10363
|
// src/cli/commands/record.ts
|
|
8773
10364
|
var RECORD_HELP = `
|
|
@@ -8783,16 +10374,25 @@ Options:
|
|
|
8783
10374
|
- -s <id>: use specific session
|
|
8784
10375
|
-f, --file <path> Output file (default: recording.json)
|
|
8785
10376
|
--timeout <ms> Auto-stop after timeout (optional)
|
|
10377
|
+
--listen [mode] Capture network traffic: ws, http, or all (default: all)
|
|
10378
|
+
--bodies Capture HTTP response bodies (requires --listen)
|
|
10379
|
+
-m, --match <glob> Filter network URLs by glob pattern (requires --listen)
|
|
10380
|
+
--max-payload <n> Max WebSocket payload preview length (default: 256)
|
|
8786
10381
|
-h, --help Show this help
|
|
8787
10382
|
|
|
8788
10383
|
Examples:
|
|
8789
|
-
bp record
|
|
8790
|
-
bp record -s
|
|
8791
|
-
bp record -s mysession
|
|
8792
|
-
bp record -f login.json
|
|
8793
|
-
bp record --timeout 60000
|
|
10384
|
+
bp record # Auto-connect to local Chrome
|
|
10385
|
+
bp record -s # Use most recent session
|
|
10386
|
+
bp record -s mysession # Use specific session
|
|
10387
|
+
bp record -f login.json # Save to specific file
|
|
10388
|
+
bp record --timeout 60000 # Auto-stop after 60s
|
|
10389
|
+
bp record --listen # Record actions + all network traffic
|
|
10390
|
+
bp record --listen ws -m "*voice*" # Record actions + matching WS traffic
|
|
10391
|
+
bp record --listen http --bodies # Record actions + HTTP with bodies
|
|
8794
10392
|
|
|
8795
10393
|
Recording captures: clicks, inputs, form submissions, navigation.
|
|
10394
|
+
When --listen is enabled, network traffic is captured alongside actions
|
|
10395
|
+
and merged into a unified timeline in the output file.
|
|
8796
10396
|
Password fields are automatically redacted as [REDACTED].
|
|
8797
10397
|
|
|
8798
10398
|
Press Ctrl+C to stop recording and save.
|
|
@@ -8812,6 +10412,20 @@ function parseRecordArgs(args) {
|
|
|
8812
10412
|
if (!nextArg || nextArg.startsWith("-")) {
|
|
8813
10413
|
options.useLatestSession = true;
|
|
8814
10414
|
}
|
|
10415
|
+
} else if (arg === "--listen") {
|
|
10416
|
+
const nextArg = args[i + 1];
|
|
10417
|
+
if (nextArg && (nextArg === "ws" || nextArg === "http" || nextArg === "all")) {
|
|
10418
|
+
options.listen = nextArg;
|
|
10419
|
+
i++;
|
|
10420
|
+
} else {
|
|
10421
|
+
options.listen = true;
|
|
10422
|
+
}
|
|
10423
|
+
} else if (arg === "--bodies") {
|
|
10424
|
+
options.bodies = true;
|
|
10425
|
+
} else if (arg === "-m" || arg === "--match") {
|
|
10426
|
+
options.match = args[++i];
|
|
10427
|
+
} else if (arg === "--max-payload") {
|
|
10428
|
+
options.maxPayload = Number.parseInt(args[++i] ?? "", 10);
|
|
8815
10429
|
}
|
|
8816
10430
|
}
|
|
8817
10431
|
return options;
|
|
@@ -8884,7 +10498,17 @@ async function recordCommand(args, globalOptions) {
|
|
|
8884
10498
|
}
|
|
8885
10499
|
const page = await browser.page();
|
|
8886
10500
|
const cdp = page.cdpClient;
|
|
8887
|
-
|
|
10501
|
+
let listenConfig;
|
|
10502
|
+
if (options.listen) {
|
|
10503
|
+
const listenOpts = {
|
|
10504
|
+
mode: typeof options.listen === "string" ? options.listen : "all",
|
|
10505
|
+
match: options.match,
|
|
10506
|
+
captureResponseBodies: options.bodies,
|
|
10507
|
+
maxPayload: options.maxPayload
|
|
10508
|
+
};
|
|
10509
|
+
listenConfig = listenOpts;
|
|
10510
|
+
}
|
|
10511
|
+
const recorder = new Recorder(cdp, listenConfig ? { listen: listenConfig } : void 0);
|
|
8888
10512
|
let stopping = false;
|
|
8889
10513
|
async function stopAndSave() {
|
|
8890
10514
|
if (stopping) return;
|
|
@@ -8896,15 +10520,23 @@ async function recordCommand(args, globalOptions) {
|
|
|
8896
10520
|
const currentUrl = await page.url();
|
|
8897
10521
|
await updateSession(session.id, { currentUrl });
|
|
8898
10522
|
await browser.disconnect();
|
|
8899
|
-
|
|
8900
|
-
|
|
10523
|
+
const networkInfo = recording.network ? `, ${recording.network.requests.length} HTTP requests` : "";
|
|
10524
|
+
const wsInfo = recording.websockets ? `, ${recording.websockets.frames.length} WS frames` : "";
|
|
10525
|
+
const timelineInfo = recording.timeline ? ` (${recording.timeline.length} timeline entries)` : "";
|
|
10526
|
+
console.log(
|
|
10527
|
+
`
|
|
10528
|
+
Saved ${recording.steps.length} steps${networkInfo}${wsInfo}${timelineInfo} to ${outputFile}`
|
|
10529
|
+
);
|
|
8901
10530
|
if (globalOptions.format === "json") {
|
|
8902
10531
|
output(
|
|
8903
10532
|
{
|
|
8904
10533
|
success: true,
|
|
8905
10534
|
file: outputFile,
|
|
8906
10535
|
steps: recording.steps.length,
|
|
8907
|
-
duration: recording.duration
|
|
10536
|
+
duration: recording.duration,
|
|
10537
|
+
networkRequests: recording.network?.requests.length ?? 0,
|
|
10538
|
+
wsFrames: recording.websockets?.frames.length ?? 0,
|
|
10539
|
+
timelineEntries: recording.timeline?.length ?? 0
|
|
8908
10540
|
},
|
|
8909
10541
|
"json"
|
|
8910
10542
|
);
|
|
@@ -8915,13 +10547,24 @@ Saved ${recording.steps.length} steps to ${outputFile}`);
|
|
|
8915
10547
|
process.exit(1);
|
|
8916
10548
|
}
|
|
8917
10549
|
}
|
|
8918
|
-
|
|
8919
|
-
|
|
10550
|
+
const handleSignal = () => {
|
|
10551
|
+
stopAndSave().catch((err) => {
|
|
10552
|
+
console.error("Error during shutdown:", err);
|
|
10553
|
+
process.exit(1);
|
|
10554
|
+
});
|
|
10555
|
+
};
|
|
10556
|
+
process.on("SIGINT", handleSignal);
|
|
10557
|
+
process.on("SIGTERM", handleSignal);
|
|
8920
10558
|
if (options.timeout && options.timeout > 0) {
|
|
8921
10559
|
setTimeout(stopAndSave, options.timeout);
|
|
8922
10560
|
}
|
|
8923
10561
|
await recorder.start();
|
|
8924
10562
|
console.log(`Recording... Press Ctrl+C to stop and save to ${outputFile}`);
|
|
10563
|
+
if (options.listen) {
|
|
10564
|
+
const listenMode = typeof options.listen === "string" ? options.listen : "all";
|
|
10565
|
+
const matchLabel = options.match ? ` matching "${options.match}"` : "";
|
|
10566
|
+
console.log(`Network capture: ${listenMode} traffic${matchLabel}`);
|
|
10567
|
+
}
|
|
8925
10568
|
console.log(`Session: ${session.id}`);
|
|
8926
10569
|
console.log(`URL: ${await page.url()}`);
|
|
8927
10570
|
}
|
|
@@ -9313,7 +10956,7 @@ function parseSnapshotArgs(args) {
|
|
|
9313
10956
|
}
|
|
9314
10957
|
return options;
|
|
9315
10958
|
}
|
|
9316
|
-
function
|
|
10959
|
+
function sleep7(ms) {
|
|
9317
10960
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
9318
10961
|
}
|
|
9319
10962
|
async function snapshotCommand(args, globalOptions) {
|
|
@@ -9372,7 +11015,7 @@ async function snapshotCommand(args, globalOptions) {
|
|
|
9372
11015
|
);
|
|
9373
11016
|
} else {
|
|
9374
11017
|
console.log("Overlay will be removed in 10 seconds...");
|
|
9375
|
-
await
|
|
11018
|
+
await sleep7(1e4);
|
|
9376
11019
|
await removeRefOverlay(page);
|
|
9377
11020
|
console.log("Overlay removed.");
|
|
9378
11021
|
}
|