browser-debugger-cli 0.12.0 → 0.14.0
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/.claude/skills/bdg/SKILL.md +100 -186
- package/README.md +5 -4
- package/dist/commands/cdp.d.ts +22 -1
- package/dist/commands/cdp.js +100 -43
- package/dist/commands/console.d.ts +12 -0
- package/dist/commands/console.js +67 -13
- package/dist/commands/dom/DomElementResolver.d.ts +3 -1
- package/dist/commands/dom/DomElementResolver.js +10 -3
- package/dist/commands/dom/a11y.d.ts +1 -1
- package/dist/commands/dom/a11y.js +23 -22
- package/dist/commands/dom/eval.d.ts +4 -2
- package/dist/commands/dom/eval.js +31 -7
- package/dist/commands/dom/form.js +10 -9
- package/dist/commands/dom/formInteraction.js +9 -8
- package/dist/commands/dom/get.js +32 -14
- package/dist/commands/dom/helpers/index.d.ts +1 -1
- package/dist/commands/dom/helpers/index.js +1 -1
- package/dist/commands/dom/helpers/query.d.ts +27 -3
- package/dist/commands/dom/helpers/query.js +152 -64
- package/dist/commands/dom/helpers/screenshot.js +13 -13
- package/dist/commands/dom/index.js +10 -3
- package/dist/commands/dom/query.d.ts +20 -2
- package/dist/commands/dom/query.js +39 -6
- package/dist/commands/dom/screenshot.js +3 -1
- package/dist/commands/dom/semanticUtils.d.ts +3 -2
- package/dist/commands/dom/semanticUtils.js +40 -9
- package/dist/commands/helpJson.d.ts +82 -19
- package/dist/commands/helpJson.js +112 -41
- package/dist/commands/helpTopic.d.ts +16 -1
- package/dist/commands/helpTopic.js +59 -1
- package/dist/commands/installSkill.d.ts +15 -5
- package/dist/commands/installSkill.js +86 -16
- package/dist/commands/network/list.js +65 -12
- package/dist/commands/optionBehaviors.d.ts +25 -2
- package/dist/commands/optionBehaviors.js +81 -46
- package/dist/commands/peek.js +3 -0
- package/dist/commands/shared/CommandRunner.js +13 -13
- package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
- package/dist/commands/shared/daemonErrorHandler.js +21 -10
- package/dist/commands/shared/dataFetcher.d.ts +14 -4
- package/dist/commands/shared/dataFetcher.js +20 -4
- package/dist/commands/shared/followMode.d.ts +9 -1
- package/dist/commands/shared/followMode.js +22 -4
- package/dist/commands/shared/handleValidationError.js +3 -3
- package/dist/commands/shared/optionTypes.d.ts +17 -3
- package/dist/commands/shared/outputFile.js +6 -1
- package/dist/commands/shared/startHelpers.js +3 -3
- package/dist/commands/start.d.ts +7 -5
- package/dist/commands/start.js +65 -21
- package/dist/commands/stop.d.ts +11 -0
- package/dist/commands/stop.js +24 -1
- package/dist/commands.js +1 -1
- package/dist/connection/cdp.d.ts +7 -0
- package/dist/connection/cdp.js +9 -0
- package/dist/connection/chromeIdentity.d.ts +8 -2
- package/dist/connection/chromeIdentity.js +85 -13
- package/dist/connection/launcher.js +3 -2
- package/dist/constants.d.ts +29 -1
- package/dist/constants.js +35 -1
- package/dist/daemon/SessionController.js +8 -1
- package/dist/daemon/launcher.d.ts +3 -2
- package/dist/daemon/launcher.js +47 -3
- package/dist/daemon/session/Session.d.ts +5 -1
- package/dist/daemon/session/Session.js +42 -3
- package/dist/daemon/session/TelemetryStore.d.ts +15 -1
- package/dist/daemon/session/TelemetryStore.js +19 -1
- package/dist/daemon/session/commandRegistry.js +52 -18
- package/dist/daemon/session/interactions.d.ts +2 -1
- package/dist/daemon/session/interactions.js +13 -1
- package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
- package/dist/daemon/session/matchedStylesReset.js +46 -0
- package/dist/daemon/session/plugins.js +17 -2
- package/dist/daemon/session/teardown.js +1 -1
- package/dist/daemon/session/triggeredRequests.d.ts +0 -5
- package/dist/daemon/session/triggeredRequests.js +13 -7
- package/dist/daemon.js +2385 -1229
- package/dist/errors/messages.d.ts +62 -11
- package/dist/errors/messages.js +119 -22
- package/dist/index.js +14995 -9866
- package/dist/ipc/client.d.ts +18 -2
- package/dist/ipc/client.js +26 -5
- package/dist/ipc/protocol/auditTypes.d.ts +8 -2
- package/dist/ipc/protocol/commands.d.ts +16 -0
- package/dist/ipc/protocol/domTypes.d.ts +12 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
- package/dist/ipc/session/types.d.ts +7 -1
- package/dist/program.d.ts +14 -0
- package/dist/program.js +53 -0
- package/dist/runtime/dom/actionEffects.d.ts +5 -1
- package/dist/runtime/dom/actionEffects.js +26 -14
- package/dist/runtime/dom/audit.js +3 -2
- package/dist/runtime/dom/auditModel.js +6 -1
- package/dist/runtime/dom/auditScripts.d.ts +9 -3
- package/dist/runtime/dom/auditScripts.js +41 -5
- package/dist/runtime/dom/elementGeometry.d.ts +33 -3
- package/dist/runtime/dom/elementGeometry.js +44 -19
- package/dist/runtime/dom/elementInfo.d.ts +76 -18
- package/dist/runtime/dom/elementInfo.js +190 -40
- package/dist/runtime/dom/evalHelpers.d.ts +12 -2
- package/dist/runtime/dom/evalHelpers.js +67 -7
- package/dist/runtime/dom/formDiscovery.d.ts +6 -2
- package/dist/runtime/dom/formDiscovery.js +20 -3
- package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
- package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
- package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
- package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
- package/dist/runtime/dom/formSubmitHelpers.js +4 -3
- package/dist/runtime/dom/frameLayout.js +1 -0
- package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
- package/dist/runtime/dom/frameScopedConnection.js +2 -2
- package/dist/runtime/dom/inspect.d.ts +17 -3
- package/dist/runtime/dom/inspect.js +45 -32
- package/dist/runtime/dom/inspectAllStyles.js +1 -0
- package/dist/runtime/dom/inspectHints.d.ts +1 -1
- package/dist/runtime/dom/inspectModel.d.ts +5 -4
- package/dist/runtime/dom/inspectModel.js +7 -3
- package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
- package/dist/runtime/dom/inspectPaintModel.js +3 -1
- package/dist/runtime/dom/inspectRules.d.ts +29 -3
- package/dist/runtime/dom/inspectRules.js +205 -11
- package/dist/runtime/dom/inspectScripts.d.ts +29 -2
- package/dist/runtime/dom/inspectScripts.js +49 -10
- package/dist/runtime/dom/layout.d.ts +0 -2
- package/dist/runtime/dom/layout.js +10 -9
- package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
- package/dist/runtime/dom/reactEventHelpers.js +71 -28
- package/dist/runtime/dom/targetNode.d.ts +27 -10
- package/dist/runtime/dom/targetNode.js +283 -16
- package/dist/runtime/dom/wait.js +2 -1
- package/dist/runtime/page/bdgWorld.d.ts +57 -0
- package/dist/runtime/page/bdgWorld.js +180 -0
- package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
- package/dist/runtime/page/replacedBuiltins.js +136 -0
- package/dist/session/QueryCacheManager.d.ts +4 -1
- package/dist/session/QueryCacheManager.js +5 -2
- package/dist/session/chrome.d.ts +4 -1
- package/dist/session/chrome.js +7 -1
- package/dist/session/cleanup/staleSession.d.ts +21 -4
- package/dist/session/cleanup/staleSession.js +79 -9
- package/dist/session/cleanup/userCommands.d.ts +4 -1
- package/dist/session/cleanup/userCommands.js +10 -5
- package/dist/session/daemonSocket.d.ts +10 -0
- package/dist/session/daemonSocket.js +22 -0
- package/dist/session/lastSession.d.ts +6 -3
- package/dist/session/lastSession.js +11 -5
- package/dist/session/paths.d.ts +3 -1
- package/dist/session/paths.js +5 -5
- package/dist/session/portClaims.js +4 -3
- package/dist/session/sessionList.d.ts +13 -5
- package/dist/session/sessionList.js +31 -7
- package/dist/telemetry/a11y.d.ts +15 -1
- package/dist/telemetry/a11y.js +85 -2
- package/dist/telemetry/console.d.ts +2 -1
- package/dist/telemetry/console.js +30 -21
- package/dist/telemetry/har/builder.js +1 -1
- package/dist/telemetry/network.d.ts +13 -16
- package/dist/telemetry/network.js +30 -52
- package/dist/telemetry/networkRetention.d.ts +83 -0
- package/dist/telemetry/networkRetention.js +117 -0
- package/dist/telemetry/pageCrash.d.ts +26 -0
- package/dist/telemetry/pageCrash.js +53 -0
- package/dist/types.d.ts +42 -0
- package/dist/ui/OutputBuilder.d.ts +10 -0
- package/dist/ui/OutputBuilder.js +12 -0
- package/dist/ui/formatters/a11y.d.ts +5 -7
- package/dist/ui/formatters/a11y.js +7 -61
- package/dist/ui/formatters/audit.js +14 -5
- package/dist/ui/formatters/cdp.d.ts +138 -0
- package/dist/ui/formatters/cdp.js +131 -0
- package/dist/ui/formatters/console/chronological.js +7 -5
- package/dist/ui/formatters/console/follow.d.ts +5 -2
- package/dist/ui/formatters/console/follow.js +7 -4
- package/dist/ui/formatters/console/json.d.ts +4 -7
- package/dist/ui/formatters/console/json.js +16 -14
- package/dist/ui/formatters/console/shared.d.ts +47 -2
- package/dist/ui/formatters/console/shared.js +33 -0
- package/dist/ui/formatters/console/summarize.d.ts +9 -2
- package/dist/ui/formatters/console/summarize.js +57 -11
- package/dist/ui/formatters/console.d.ts +3 -2
- package/dist/ui/formatters/console.js +8 -10
- package/dist/ui/formatters/details.js +4 -2
- package/dist/ui/formatters/dom.d.ts +14 -5
- package/dist/ui/formatters/dom.js +30 -13
- package/dist/ui/formatters/helpFormatters.js +1 -1
- package/dist/ui/formatters/inspect.js +9 -3
- package/dist/ui/formatters/installSkill.d.ts +9 -1
- package/dist/ui/formatters/installSkill.js +32 -6
- package/dist/ui/formatters/layout.js +4 -2
- package/dist/ui/formatters/longValues.d.ts +14 -0
- package/dist/ui/formatters/longValues.js +23 -0
- package/dist/ui/formatters/networkList.d.ts +8 -2
- package/dist/ui/formatters/networkList.js +11 -3
- package/dist/ui/formatters/preview.d.ts +6 -1
- package/dist/ui/formatters/preview.js +67 -15
- package/dist/ui/formatters/sessions.d.ts +2 -2
- package/dist/ui/formatters/sessions.js +9 -2
- package/dist/ui/formatters/status.js +7 -0
- package/dist/ui/formatters/triggeredRequests.js +2 -1
- package/dist/ui/logging/logger.d.ts +1 -1
- package/dist/ui/messages/chrome.d.ts +20 -1
- package/dist/ui/messages/chrome.js +29 -3
- package/dist/ui/messages/commands.d.ts +153 -12
- package/dist/ui/messages/commands.js +198 -15
- package/dist/ui/messages/consoleMessages.d.ts +24 -0
- package/dist/ui/messages/consoleMessages.js +32 -0
- package/dist/ui/messages/networkMessages.d.ts +24 -0
- package/dist/ui/messages/networkMessages.js +45 -0
- package/dist/ui/messages/preview.d.ts +6 -0
- package/dist/ui/messages/preview.js +9 -1
- package/dist/ui/messages/session.d.ts +13 -2
- package/dist/ui/messages/session.js +22 -3
- package/dist/utils/directories.d.ts +34 -0
- package/dist/utils/directories.js +88 -0
- package/dist/utils/display.d.ts +16 -0
- package/dist/utils/display.js +42 -0
- package/dist/utils/exitCodes.d.ts +1 -0
- package/dist/utils/exitCodes.js +6 -0
- package/dist/utils/http.d.ts +9 -2
- package/dist/utils/http.js +4 -3
- package/dist/utils/process.d.ts +12 -0
- package/dist/utils/process.js +25 -0
- package/dist/utils/strings.d.ts +19 -0
- package/dist/utils/strings.js +16 -0
- package/package.json +2 -2
|
@@ -7,8 +7,18 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { FILL_REFUSALS, LABEL_WITHOUT_CONTROL, NAME_QUERY_PLACEHOLDER, VIA_LABEL_SUFFIX, } from '../../errors/messages.js';
|
|
9
9
|
import { REVEAL_SNAPSHOT_JS } from './actionEffectsScripts.js';
|
|
10
|
-
import {
|
|
10
|
+
import { ANCESTOR_CLIP_JS } from './elementGeometry.js';
|
|
11
|
+
import { DISABLED_CAUSE_JS, ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS, } from './elementInfo.js';
|
|
11
12
|
import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
|
|
13
|
+
/**
|
|
14
|
+
* Page-side copy of a list (`select.options`, a NodeList) as an array,
|
|
15
|
+
* without `Array.from`, which pages replace.
|
|
16
|
+
*/
|
|
17
|
+
const LIST_JS = `(list) => {
|
|
18
|
+
const items = [];
|
|
19
|
+
for (let i = 0; i < list.length; i++) items[items.length] = list[i];
|
|
20
|
+
return items;
|
|
21
|
+
}`;
|
|
12
22
|
/**
|
|
13
23
|
* Page-side read-back of a filled field: a mismatch when its value is not
|
|
14
24
|
* what was asked for (the page rejected, reformatted or moved the input),
|
|
@@ -28,7 +38,7 @@ export const FILL_VALUE_MISMATCH_JS = `(field, expected) => {
|
|
|
28
38
|
: type === 'checkbox' || type === 'radio'
|
|
29
39
|
? (field.checked ? 'checked' : 'unchecked')
|
|
30
40
|
: field.localName === 'select' && field.multiple
|
|
31
|
-
?
|
|
41
|
+
? (${LIST_JS})(field.selectedOptions).map((o) => o.value).join(', ')
|
|
32
42
|
: String(field.value);
|
|
33
43
|
const time = (text) => text.trim().replace(' ', 'T').replace(/(\\d\\d:\\d\\d):00(\\.0+)?$/, '$1');
|
|
34
44
|
const normalize = (text) => {
|
|
@@ -81,9 +91,8 @@ export const FILL_REFUSAL_JS = `(el) => {
|
|
|
81
91
|
});
|
|
82
92
|
const tag = el.localName;
|
|
83
93
|
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
94
|
+
const disabled = (${DISABLED_CAUSE_JS})(el);
|
|
95
|
+
if (disabled !== null) return refuse('disabled', disabled);
|
|
87
96
|
return el.readOnly ? refuse('readOnly', 'readonly attribute') : null;
|
|
88
97
|
}
|
|
89
98
|
if (el.isContentEditable) return null;
|
|
@@ -111,6 +120,17 @@ export const SHADOW_FIELD_JS = `(el) => {
|
|
|
111
120
|
const fields = el.shadowRoot.querySelectorAll('input:not([type="hidden"]), textarea, select, [contenteditable]:not([contenteditable="false"])');
|
|
112
121
|
return fields.length === 1 ? fields[0] : null;
|
|
113
122
|
}`;
|
|
123
|
+
/**
|
|
124
|
+
* Page-side: fire a bubbling, non-cancelable event of a type on an element,
|
|
125
|
+
* like `new Event(type, { bubbles: true })`. The event comes from
|
|
126
|
+
* `document.createEvent`, which keeps working on pages that replace the
|
|
127
|
+
* `Event` constructor (MooTools 1.2 does).
|
|
128
|
+
*/
|
|
129
|
+
export const FIRE_EVENT_JS = `(target, type) => {
|
|
130
|
+
const event = target.ownerDocument.createEvent('Event');
|
|
131
|
+
event.initEvent(type, true, false);
|
|
132
|
+
return target.dispatchEvent(event);
|
|
133
|
+
}`;
|
|
114
134
|
/**
|
|
115
135
|
* JavaScript function to fill an input element in a React-compatible way.
|
|
116
136
|
*
|
|
@@ -132,6 +152,8 @@ export const SHADOW_FIELD_JS = `(el) => {
|
|
|
132
152
|
export const REACT_FILL_SCRIPT = `
|
|
133
153
|
(function(selector, parts, value, options) {
|
|
134
154
|
const allMatches = (${FIND_ELEMENTS_JS})(selector, parts);
|
|
155
|
+
const fire = ${FIRE_EVENT_JS};
|
|
156
|
+
const listOf = ${LIST_JS};
|
|
135
157
|
const warnings = [];
|
|
136
158
|
let expected = value;
|
|
137
159
|
// Why a user could not reach the field (the value is still set, so scripted
|
|
@@ -243,7 +265,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
243
265
|
if (tagName === 'select' && el.multiple) {
|
|
244
266
|
// Several options, separated by commas (an option whose value contains a
|
|
245
267
|
// comma matches as a whole); "" selects none
|
|
246
|
-
const options =
|
|
268
|
+
const options = listOf(el.options);
|
|
247
269
|
const whole = options.some((o) => o.value === value || o.text.trim() === value);
|
|
248
270
|
const wanted = whole ? [value] : value.split(',').map((part) => part.trim()).filter(Boolean);
|
|
249
271
|
const chosen = wanted.map((part) =>
|
|
@@ -261,11 +283,11 @@ export const REACT_FILL_SCRIPT = `
|
|
|
261
283
|
}
|
|
262
284
|
options.forEach((o) => { o.selected = chosen.includes(o); });
|
|
263
285
|
expected = chosen.map((o) => o.value).join(', ');
|
|
264
|
-
el
|
|
265
|
-
el
|
|
286
|
+
fire(el, 'input');
|
|
287
|
+
fire(el, 'change');
|
|
266
288
|
} else if (tagName === 'select') {
|
|
267
289
|
// Match by option value first, then by visible label
|
|
268
|
-
const options =
|
|
290
|
+
const options = listOf(el.options);
|
|
269
291
|
const option =
|
|
270
292
|
options.find((o) => o.value === value) ||
|
|
271
293
|
options.find((o) => o.text.trim() === value);
|
|
@@ -280,8 +302,8 @@ export const REACT_FILL_SCRIPT = `
|
|
|
280
302
|
}
|
|
281
303
|
el.value = option.value;
|
|
282
304
|
expected = option.value;
|
|
283
|
-
el
|
|
284
|
-
el
|
|
305
|
+
fire(el, 'input');
|
|
306
|
+
fire(el, 'change');
|
|
285
307
|
} else if (inputType === 'checkbox' || inputType === 'radio') {
|
|
286
308
|
// Toggle through a click, like a user: frameworks (React) track checkable
|
|
287
309
|
// state via click events and revert a programmatic \`checked\` assignment
|
|
@@ -321,7 +343,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
321
343
|
};
|
|
322
344
|
} else if (el.isContentEditable) {
|
|
323
345
|
el.textContent = value;
|
|
324
|
-
el
|
|
346
|
+
fire(el, 'input');
|
|
325
347
|
} else {
|
|
326
348
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
327
349
|
el.ownerDocument.defaultView.HTMLInputElement.prototype,
|
|
@@ -363,8 +385,8 @@ export const REACT_FILL_SCRIPT = `
|
|
|
363
385
|
};
|
|
364
386
|
}
|
|
365
387
|
|
|
366
|
-
el
|
|
367
|
-
el
|
|
388
|
+
fire(el, 'input');
|
|
389
|
+
fire(el, 'change');
|
|
368
390
|
if (el.validity && (el.validity.rangeOverflow || el.validity.rangeUnderflow)) {
|
|
369
391
|
warnings.push('The value is outside the allowed range (' + (el.min || 'no minimum') + ' to ' + (el.max || 'no maximum') + '); the form will not submit until it is fixed');
|
|
370
392
|
}
|
|
@@ -383,7 +405,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
383
405
|
: inputType === 'password'
|
|
384
406
|
? '********'
|
|
385
407
|
: tagName === 'select' && el.multiple
|
|
386
|
-
?
|
|
408
|
+
? listOf(el.selectedOptions).map((o) => o.value).join(', ')
|
|
387
409
|
: el.value,
|
|
388
410
|
element: (${ELEMENT_IDENTITY_JS})(el),
|
|
389
411
|
elementType: tagName + viaLabel + viaShadow,
|
|
@@ -445,7 +467,7 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
|
|
|
445
467
|
/**
|
|
446
468
|
* JavaScript function to locate an element for clicking.
|
|
447
469
|
*
|
|
448
|
-
* Scrolls the element into view and reports its center point and whether it
|
|
470
|
+
* Scrolls the element into view (reporting how far the page moved) and reports its center point and whether it
|
|
449
471
|
* is the topmost element there (`hittable`). It does not click; see
|
|
450
472
|
* `clickElement`, which dispatches real mouse events or falls back to
|
|
451
473
|
* `el.click()`.
|
|
@@ -461,6 +483,12 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
|
|
|
461
483
|
* a press, a probe (`window.__bdgPressProbe`) records whether the press
|
|
462
484
|
* reaches the element, and otherwise which element it landed on: a browser
|
|
463
485
|
* dialog or bubble can swallow input while the page looks normal.
|
|
486
|
+
* Slotted text hit-tests as its shadow host, so an element in a shadow root
|
|
487
|
+
* that shows slotted content (a button labelled through a `<slot>`) is
|
|
488
|
+
* topmost where its host is hit. An element that is not topmost because an
|
|
489
|
+
* ancestor in the flat tree clips it away (a collapsed
|
|
490
|
+
* `height: 0; overflow: hidden` accordion, {@link ANCESTOR_CLIP_JS}) is
|
|
491
|
+
* reported as hidden by it, not as covered.
|
|
464
492
|
*/
|
|
465
493
|
export const CLICK_ELEMENT_SCRIPT = `
|
|
466
494
|
(function(selector, parts, index, action) {
|
|
@@ -540,7 +568,7 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
540
568
|
};
|
|
541
569
|
}
|
|
542
570
|
// Disabled elements still get hover (tooltips often explain why)
|
|
543
|
-
if (action !== 'hover' && (el
|
|
571
|
+
if (action !== 'hover' && (${DISABLED_CAUSE_JS})(el) !== null) {
|
|
544
572
|
return {
|
|
545
573
|
success: false,
|
|
546
574
|
error: 'Element is disabled',
|
|
@@ -549,7 +577,11 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
549
577
|
suggestion: 'A user cannot click a disabled element; enable it first (it may depend on other fields)'
|
|
550
578
|
};
|
|
551
579
|
}
|
|
552
|
-
|
|
580
|
+
let topView = el.ownerDocument.defaultView;
|
|
581
|
+
while (topView.frameElement) topView = topView.parent;
|
|
582
|
+
const scrollBefore = { x: topView.scrollX, y: topView.scrollY };
|
|
583
|
+
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
584
|
+
const scrolledBy = { x: Math.round(topView.scrollX - scrollBefore.x), y: Math.round(topView.scrollY - scrollBefore.y) };
|
|
553
585
|
|
|
554
586
|
// The caller clicks with real mouse events at (x, y) when the element is the
|
|
555
587
|
// topmost thing there; otherwise it falls back to el.click() via this handle.
|
|
@@ -560,10 +592,11 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
560
592
|
window.__bdgClickTarget = el;
|
|
561
593
|
const rect = el.getBoundingClientRect();
|
|
562
594
|
const view = el.ownerDocument.defaultView;
|
|
595
|
+
const showsSlotted = (node) => Array.from(node.querySelectorAll('slot')).some((slot) => slot.assignedNodes().length > 0);
|
|
563
596
|
const hitTest = (node, px, py) => {
|
|
564
597
|
const root = node.getRootNode();
|
|
565
598
|
const hit = (typeof root.elementFromPoint === 'function' ? root : node.ownerDocument).elementFromPoint(px, py);
|
|
566
|
-
return hit !== null && (hit === node || node.contains(hit));
|
|
599
|
+
return hit !== null && (hit === node || node.contains(hit) || (hit === root.host && showsSlotted(node)));
|
|
567
600
|
};
|
|
568
601
|
const toTopPage = (px, py) => {
|
|
569
602
|
let hittable = hitTest(el, px, py);
|
|
@@ -587,16 +620,16 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
587
620
|
const fractions = [[0.5, 0.5], [0.5, 0.25], [0.5, 0.75], [0.25, 0.5], [0.75, 0.5], [0.25, 0.25], [0.75, 0.25], [0.25, 0.75], [0.75, 0.75]];
|
|
588
621
|
const hasSize = rect.width > 0 && rect.height > 0;
|
|
589
622
|
let point = null;
|
|
590
|
-
|
|
591
|
-
|
|
623
|
+
const tried = hasSize ? fractions : [[0.5, 0.5]];
|
|
624
|
+
for (let i = 0; i < tried.length; i++) {
|
|
625
|
+
const candidate = toTopPage(area.left + area.width * tried[i][0], area.top + area.height * tried[i][1]);
|
|
592
626
|
point = point || candidate;
|
|
593
627
|
if (hasSize && candidate.hittable) { point = candidate; break; }
|
|
594
628
|
}
|
|
595
629
|
const x = point.x;
|
|
596
630
|
const y = point.y;
|
|
597
631
|
const hittable = hasSize && point.hittable;
|
|
598
|
-
const describe =
|
|
599
|
-
(node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');
|
|
632
|
+
const describe = ${ELEMENT_DESCRIPTION_JS};
|
|
600
633
|
const coveredBy = () => {
|
|
601
634
|
const root = el.getRootNode();
|
|
602
635
|
const hit = (typeof root.elementFromPoint === 'function' ? root : el.ownerDocument)
|
|
@@ -610,7 +643,10 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
610
643
|
else if (el.closest('[inert]')) obstruction = 'inert (the page made it non-interactive)';
|
|
611
644
|
else if (style.pointerEvents === 'none') obstruction = 'not clickable (pointer-events: none)';
|
|
612
645
|
else if (!hasSize) obstruction = 'zero-size';
|
|
613
|
-
else if (!hittable)
|
|
646
|
+
else if (!hittable) {
|
|
647
|
+
const collapsed = (${ANCESTOR_CLIP_JS})(el, rect, describe).collapsed;
|
|
648
|
+
obstruction = collapsed ? 'hidden (' + collapsed + ')' : 'covered by another element' + coveredBy();
|
|
649
|
+
}
|
|
614
650
|
|
|
615
651
|
if (action === 'hover') (${REVEAL_SNAPSHOT_JS})(el);
|
|
616
652
|
|
|
@@ -618,11 +654,17 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
618
654
|
const probe = { reached: false, landedOn: null };
|
|
619
655
|
const markReached = (event) => {
|
|
620
656
|
const path = event.composedPath();
|
|
621
|
-
|
|
657
|
+
let reached = false;
|
|
658
|
+
for (let i = 0; i < path.length; i++) reached = reached || path[i] === el;
|
|
659
|
+
if (reached) probe.reached = true;
|
|
622
660
|
else if (!probe.landedOn && path[0] && path[0].nodeType === 1) probe.landedOn = describe(path[0]);
|
|
623
661
|
};
|
|
624
|
-
|
|
625
|
-
|
|
662
|
+
view.addEventListener('pointerdown', markReached, true);
|
|
663
|
+
view.addEventListener('mousedown', markReached, true);
|
|
664
|
+
probe.stop = () => {
|
|
665
|
+
view.removeEventListener('pointerdown', markReached, true);
|
|
666
|
+
view.removeEventListener('mousedown', markReached, true);
|
|
667
|
+
};
|
|
626
668
|
window.__bdgPressProbe = probe;
|
|
627
669
|
}
|
|
628
670
|
|
|
@@ -636,7 +678,8 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
636
678
|
x: x,
|
|
637
679
|
y: y,
|
|
638
680
|
hittable: hittable,
|
|
639
|
-
obstruction: obstruction
|
|
681
|
+
obstruction: obstruction,
|
|
682
|
+
scrolledBy: scrolledBy.x !== 0 || scrolledBy.y !== 0 ? scrolledBy : undefined
|
|
640
683
|
};
|
|
641
684
|
})
|
|
642
685
|
`;
|
|
@@ -12,8 +12,8 @@ import type { CDPConnection } from '../../connection/cdp.js';
|
|
|
12
12
|
import { CommandError } from '../../errors/index.js';
|
|
13
13
|
/** Selector placeholder that makes page scripts use the bound node. */
|
|
14
14
|
export declare const BOUND_TARGET_SELECTOR = "__bdg_bound_target__";
|
|
15
|
-
/** Removes the node bound for index-based commands from the window
|
|
16
|
-
export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget";
|
|
15
|
+
/** Removes the node bound for index-based commands, and matches bound for a selector, from the window they were stored on */
|
|
16
|
+
export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget; delete window.__bdgMatches";
|
|
17
17
|
/**
|
|
18
18
|
* Page-side matching of filters ({@link SelectorFilter}) and scoped steps
|
|
19
19
|
* ({@link ScopedStep}): called with the open shadow roots of the page, returns
|
|
@@ -26,16 +26,20 @@ export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget";
|
|
|
26
26
|
* the text nodes of hidden ones (display or visibility; not those of
|
|
27
27
|
* `<script>`, `<style>` or `<noscript>`), so text filters match hidden
|
|
28
28
|
* elements like Playwright's and `:visible` decides visibility; button inputs
|
|
29
|
-
* use their value.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* use their value. Text is read in the flat tree, as `bdg dom query` shows
|
|
30
|
+
* it: the text a web component's open shadow root renders, slotted content
|
|
31
|
+
* in place of its slots. A visible component, slot or element holding either
|
|
32
|
+
* ({@link COMPOSED_JS}) is read with {@link FLAT_TEXT_JS}, including the
|
|
33
|
+
* selects and editable regions `innerText` reads. Whitespace is collapsed;
|
|
34
|
+
* filter texts arrive normalized (`has-text` lowercased). `:visible` is
|
|
35
|
+
* checked before the text filters, which read the text.
|
|
32
36
|
*
|
|
33
37
|
* Steps and `:has()` match CSS relative to an element (`:scope > css`). A
|
|
34
38
|
* descendant step also searches the open shadow roots under the element (the
|
|
35
39
|
* whole step CSS inside one shadow tree); a child step stays in the
|
|
36
40
|
* element's own tree.
|
|
37
41
|
*/
|
|
38
|
-
export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const
|
|
42
|
+
export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n}";
|
|
39
43
|
/**
|
|
40
44
|
* Page-side selector search that also reaches into open shadow roots and
|
|
41
45
|
* same-origin iframes (recursively), like a user sees the page.
|
|
@@ -49,14 +53,16 @@ export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const skipped =
|
|
|
49
53
|
* reached. Cross-origin iframes are separate processes and cannot be
|
|
50
54
|
* searched. Throws a `SyntaxError` for an invalid selector.
|
|
51
55
|
*/
|
|
52
|
-
export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const
|
|
56
|
+
export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n}";
|
|
53
57
|
/**
|
|
54
58
|
* Page-side element lookup shared by the interaction scripts.
|
|
55
59
|
*
|
|
56
60
|
* Returns the bound node (if still in the page) for the placeholder selector,
|
|
57
|
-
*
|
|
61
|
+
* the matches bound for the selector when the page replaced the search
|
|
62
|
+
* built-ins ({@link bindMatches}), otherwise all matches of the selector
|
|
63
|
+
* ({@link DEEP_QUERY_JS}).
|
|
58
64
|
*/
|
|
59
|
-
export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const
|
|
65
|
+
export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n const bound = window.__bdgMatches;\n if (bound && bound.selector === selector) {\n const connected = [];\n for (let i = 0; i < bound.nodes.length; i++) {\n if (bound.nodes[i] && bound.nodes[i].isConnected) connected[connected.length] = bound.nodes[i];\n }\n return connected;\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n})(selector, parts);\n}";
|
|
60
66
|
/**
|
|
61
67
|
* Page-side check of what selectors cannot search in the top document:
|
|
62
68
|
* iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
|
|
@@ -94,23 +100,34 @@ export interface ScriptTarget {
|
|
|
94
100
|
index?: number;
|
|
95
101
|
/** Connection for the page scripts (scoped to the element's frame when the top page cannot reach it) */
|
|
96
102
|
cdp: CDPConnection;
|
|
103
|
+
/** Built-ins the page replaced that the page scripts use (they run in the page's world) */
|
|
104
|
+
replacedBuiltins?: string[];
|
|
105
|
+
/** The page replaced the selector search, so the matches were found in bdg's world */
|
|
106
|
+
boundInBdgWorld?: boolean;
|
|
97
107
|
}
|
|
98
108
|
/**
|
|
99
109
|
* Run an interaction on the element a request targets. Results and errors
|
|
100
110
|
* never show the internal placeholder: a bound node the page scripts could
|
|
101
111
|
* not find is reported as stale (87), and results carry the user's selector.
|
|
112
|
+
* On a page that replaced the selector search, a result warns that the
|
|
113
|
+
* element was found in bdg's world; on a page that replaced built-ins the
|
|
114
|
+
* scripts use, a failure adds that they may be the cause, and a script that
|
|
115
|
+
* threw is reported as broken by the page (90), naming them.
|
|
102
116
|
*
|
|
103
117
|
* @param cdp - CDP connection
|
|
104
118
|
* @param params - Request with a selector (and optional index) or a backend node id
|
|
105
119
|
* @param work - The interaction, given the script target
|
|
106
120
|
* @returns The interaction's result
|
|
107
|
-
* @throws CommandError (87) when the bound node left the page during the
|
|
121
|
+
* @throws CommandError (87) when the bound node left the page during the
|
|
122
|
+
* action, (90) when its script threw on a page that replaced built-ins
|
|
108
123
|
*/
|
|
109
124
|
export declare function onScriptTarget<T extends {
|
|
110
125
|
selector?: string;
|
|
111
126
|
error?: string;
|
|
112
127
|
suggestion?: string;
|
|
113
128
|
exitCode?: number;
|
|
129
|
+
warning?: string;
|
|
130
|
+
replacedBuiltins?: string[];
|
|
114
131
|
}>(cdp: CDPConnection, params: {
|
|
115
132
|
selector?: string;
|
|
116
133
|
index?: number;
|