browser-debugger-cli 0.8.0 → 0.9.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/README.md +4 -1
- package/dist/cdp/schema.d.ts +4 -1
- package/dist/cdp/schema.js +48 -7
- package/dist/commands/cdp.js +3 -2
- package/dist/commands/cleanup.d.ts +11 -0
- package/dist/commands/cleanup.js +161 -57
- package/dist/commands/console.d.ts +20 -1
- package/dist/commands/console.js +57 -17
- package/dist/commands/details.js +3 -2
- package/dist/commands/dom/DomElementResolver.d.ts +10 -3
- package/dist/commands/dom/DomElementResolver.js +35 -17
- package/dist/commands/dom/a11y.d.ts +10 -0
- package/dist/commands/dom/a11y.js +27 -5
- package/dist/commands/dom/eval.d.ts +3 -1
- package/dist/commands/dom/eval.js +29 -4
- package/dist/commands/dom/form.js +16 -62
- package/dist/commands/dom/formInteraction.js +152 -113
- package/dist/commands/dom/formSummary.d.ts +49 -0
- package/dist/commands/dom/formSummary.js +180 -0
- package/dist/commands/dom/frames.d.ts +2 -1
- package/dist/commands/dom/frames.js +17 -2
- package/dist/commands/dom/get.d.ts +6 -5
- package/dist/commands/dom/get.js +92 -82
- 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 +44 -17
- package/dist/commands/dom/helpers/query.js +244 -97
- package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
- package/dist/commands/dom/helpers/runElementCommand.js +97 -30
- package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
- package/dist/commands/dom/helpers/screenshot.js +164 -49
- package/dist/commands/dom/index.d.ts +3 -1
- package/dist/commands/dom/index.js +16 -6
- package/dist/commands/dom/layout.d.ts +14 -0
- package/dist/commands/dom/layout.js +54 -0
- package/dist/commands/dom/listeners.d.ts +5 -1
- package/dist/commands/dom/listeners.js +13 -3
- package/dist/commands/dom/query.js +2 -3
- package/dist/commands/dom/screenshot.d.ts +12 -2
- package/dist/commands/dom/screenshot.js +27 -3
- package/dist/commands/dom/semanticUtils.d.ts +6 -13
- package/dist/commands/dom/semanticUtils.js +15 -19
- package/dist/commands/dom/wait.d.ts +13 -0
- package/dist/commands/dom/wait.js +83 -0
- package/dist/commands/helpJson.js +2 -2
- package/dist/commands/network/list.js +4 -11
- package/dist/commands/optionBehaviors.js +112 -21
- package/dist/commands/page.d.ts +2 -1
- package/dist/commands/page.js +41 -5
- package/dist/commands/peek.js +4 -11
- package/dist/commands/sessions.d.ts +8 -0
- package/dist/commands/sessions.js +19 -0
- package/dist/commands/shared/CommandRunner.js +4 -4
- package/dist/commands/shared/dataFetcher.js +2 -2
- package/dist/commands/shared/followMode.d.ts +21 -1
- package/dist/commands/shared/followMode.js +29 -2
- package/dist/commands/shared/handleValidationError.d.ts +2 -2
- package/dist/commands/shared/handleValidationError.js +12 -3
- package/dist/commands/shared/optionTypes.d.ts +40 -5
- package/dist/commands/shared/startHelpers.js +12 -3
- package/dist/commands/shared/validation.d.ts +3 -2
- package/dist/commands/shared/validation.js +4 -3
- package/dist/commands/start.d.ts +63 -0
- package/dist/commands/start.js +115 -15
- package/dist/commands/status.js +29 -7
- package/dist/commands/stop.js +7 -6
- package/dist/commands/tail.js +4 -11
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands.js +2 -0
- package/dist/connection/chromeIdentity.d.ts +65 -0
- package/dist/connection/chromeIdentity.js +143 -0
- package/dist/connection/launcher/profilePreferences.d.ts +47 -0
- package/dist/connection/launcher/profilePreferences.js +151 -0
- package/dist/connection/launcher.d.ts +21 -2
- package/dist/connection/launcher.js +42 -16
- package/dist/connection/portReservation.d.ts +14 -4
- package/dist/connection/portReservation.js +21 -6
- package/dist/connection/startupExit.d.ts +8 -0
- package/dist/connection/startupExit.js +15 -6
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +9 -2
- package/dist/daemon/SessionController.js +23 -7
- package/dist/daemon/errors.d.ts +1 -1
- package/dist/daemon/errors.js +1 -1
- package/dist/daemon/launcher.d.ts +2 -1
- package/dist/daemon/launcher.js +5 -6
- package/dist/daemon/server/SocketServer.js +1 -2
- package/dist/daemon/session/Session.d.ts +13 -0
- package/dist/daemon/session/Session.js +57 -8
- package/dist/daemon/session/chromeConnection.d.ts +9 -0
- package/dist/daemon/session/chromeConnection.js +45 -8
- package/dist/daemon/session/commandRegistry.js +52 -62
- package/dist/daemon/session/interactions.d.ts +35 -9
- package/dist/daemon/session/interactions.js +36 -9
- package/dist/daemon/session/triggeredRequests.d.ts +67 -0
- package/dist/daemon/session/triggeredRequests.js +157 -0
- package/dist/daemon/session/types.d.ts +5 -1
- package/dist/daemon.js +5393 -1600
- package/dist/errors/messages.d.ts +387 -24
- package/dist/errors/messages.js +761 -67
- package/dist/index.js +3976 -1558
- package/dist/ipc/client.d.ts +12 -1
- package/dist/ipc/client.js +22 -3
- package/dist/ipc/protocol/commands.d.ts +89 -4
- package/dist/ipc/protocol/commands.js +2 -0
- package/dist/ipc/protocol/domTypes.d.ts +258 -7
- package/dist/ipc/session/lifecycle.d.ts +8 -1
- package/dist/ipc/session/queries.d.ts +5 -1
- package/dist/ipc/session/types.d.ts +5 -0
- package/dist/ipc/transport/index.d.ts +2 -1
- package/dist/ipc/transport/index.js +2 -2
- package/dist/runtime/dom/actionEffects.d.ts +106 -0
- package/dist/runtime/dom/actionEffects.js +256 -0
- package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -0
- package/dist/runtime/dom/actionEffectsScripts.js +234 -0
- package/dist/runtime/dom/elementGeometry.d.ts +170 -0
- package/dist/runtime/dom/elementGeometry.js +553 -0
- package/dist/runtime/dom/elementInfo.d.ts +77 -0
- package/dist/runtime/dom/elementInfo.js +191 -0
- package/dist/runtime/dom/evalHelpers.d.ts +51 -6
- package/dist/runtime/dom/evalHelpers.js +136 -26
- package/dist/runtime/dom/eventListeners.d.ts +2 -1
- package/dist/runtime/dom/eventListeners.js +174 -47
- package/dist/runtime/dom/formDiscovery.d.ts +1 -1
- package/dist/runtime/dom/formDiscovery.js +116 -16
- package/dist/runtime/dom/formFillHelpers/fill.d.ts +10 -0
- package/dist/runtime/dom/formFillHelpers/fill.js +125 -10
- package/dist/runtime/dom/formFillHelpers/index.d.ts +2 -2
- package/dist/runtime/dom/formFillHelpers/index.js +2 -2
- package/dist/runtime/dom/formFillHelpers/pressKey.js +14 -3
- package/dist/runtime/dom/formFillHelpers/scroll.d.ts +3 -0
- package/dist/runtime/dom/formFillHelpers/scroll.js +60 -18
- package/dist/runtime/dom/formFillHelpers/shared.d.ts +17 -0
- package/dist/runtime/dom/formFillHelpers/shared.js +25 -1
- package/dist/runtime/dom/formFillHelpers/stability.d.ts +20 -6
- package/dist/runtime/dom/formFillHelpers/stability.js +50 -19
- package/dist/runtime/dom/formSubmitHelpers.d.ts +3 -0
- package/dist/runtime/dom/formSubmitHelpers.js +89 -15
- package/dist/runtime/dom/frameLayout.d.ts +60 -0
- package/dist/runtime/dom/frameLayout.js +140 -0
- package/dist/runtime/dom/frameOrigin.d.ts +50 -0
- package/dist/runtime/dom/frameOrigin.js +62 -0
- package/dist/runtime/dom/frameScopedConnection.d.ts +92 -0
- package/dist/runtime/dom/frameScopedConnection.js +252 -0
- package/dist/runtime/dom/frameSelection.d.ts +1 -1
- package/dist/runtime/dom/frameSelection.js +2 -2
- package/dist/runtime/dom/frames.d.ts +25 -2
- package/dist/runtime/dom/frames.js +202 -63
- package/dist/runtime/dom/layout.d.ts +67 -0
- package/dist/runtime/dom/layout.js +333 -0
- package/dist/runtime/dom/listenerPageScripts.d.ts +66 -0
- package/dist/runtime/dom/listenerPageScripts.js +279 -0
- package/dist/runtime/dom/listenerSummary.d.ts +132 -11
- package/dist/runtime/dom/listenerSummary.js +344 -22
- package/dist/runtime/dom/pageActivity.d.ts +41 -0
- package/dist/runtime/dom/pageActivity.js +123 -0
- package/dist/runtime/dom/reactEventHelpers.d.ts +58 -2
- package/dist/runtime/dom/reactEventHelpers.js +212 -41
- package/dist/runtime/dom/targetNode.d.ts +80 -27
- package/dist/runtime/dom/targetNode.js +249 -33
- package/dist/runtime/dom/wait.d.ts +25 -0
- package/dist/runtime/dom/wait.js +199 -0
- package/dist/runtime/dom/waitCondition.d.ts +71 -0
- package/dist/runtime/dom/waitCondition.js +75 -0
- package/dist/runtime/page/emulation.d.ts +51 -0
- package/dist/runtime/page/emulation.js +80 -0
- package/dist/runtime/page/loadingState.d.ts +36 -0
- package/dist/runtime/page/loadingState.js +86 -0
- package/dist/runtime/page/navigation.d.ts +46 -2
- package/dist/runtime/page/navigation.js +69 -33
- package/dist/session/QueryCacheManager.d.ts +11 -1
- package/dist/session/QueryCacheManager.js +25 -3
- package/dist/session/chromeOwners.d.ts +34 -0
- package/dist/session/chromeOwners.js +51 -0
- package/dist/session/cleanup/staleSession.d.ts +11 -1
- package/dist/session/cleanup/staleSession.js +17 -6
- package/dist/session/cleanup/userCommands.js +2 -4
- package/dist/session/metadata.d.ts +5 -1
- package/dist/session/metadata.js +2 -1
- package/dist/session/paths.d.ts +77 -3
- package/dist/session/paths.js +111 -5
- package/dist/session/port.d.ts +31 -7
- package/dist/session/port.js +50 -43
- package/dist/session/portClaims.d.ts +66 -0
- package/dist/session/portClaims.js +284 -0
- package/dist/session/sessionList.d.ts +58 -0
- package/dist/session/sessionList.js +199 -0
- package/dist/session/sessionName.d.ts +46 -0
- package/dist/session/sessionName.js +97 -0
- package/dist/telemetry/a11y.d.ts +8 -3
- package/dist/telemetry/a11y.js +92 -28
- package/dist/telemetry/requestKinds.d.ts +32 -0
- package/dist/telemetry/requestKinds.js +61 -0
- package/dist/telemetry/requestState.d.ts +31 -0
- package/dist/telemetry/requestState.js +38 -0
- package/dist/types.d.ts +80 -3
- package/dist/ui/formatters/a11y.js +3 -0
- package/dist/ui/formatters/console/chronological.d.ts +8 -0
- package/dist/ui/formatters/console/chronological.js +17 -4
- package/dist/ui/formatters/console/json.js +3 -4
- package/dist/ui/formatters/console/shared.d.ts +12 -0
- package/dist/ui/formatters/console.d.ts +2 -2
- package/dist/ui/formatters/console.js +1 -1
- package/dist/ui/formatters/details.js +2 -1
- package/dist/ui/formatters/dom.d.ts +26 -14
- package/dist/ui/formatters/dom.js +65 -52
- package/dist/ui/formatters/form.js +29 -18
- package/dist/ui/formatters/layout.d.ts +31 -0
- package/dist/ui/formatters/layout.js +53 -0
- package/dist/ui/formatters/listeners.d.ts +3 -2
- package/dist/ui/formatters/listeners.js +73 -9
- package/dist/ui/formatters/networkHeaders.js +13 -0
- package/dist/ui/formatters/preview.js +2 -1
- package/dist/ui/formatters/requestStatus.d.ts +1 -17
- package/dist/ui/formatters/requestStatus.js +2 -30
- package/dist/ui/formatters/sessions.d.ts +12 -0
- package/dist/ui/formatters/sessions.js +40 -0
- package/dist/ui/formatters/status.d.ts +21 -2
- package/dist/ui/formatters/status.js +47 -10
- package/dist/ui/formatters/triggeredRequests.d.ts +36 -0
- package/dist/ui/formatters/triggeredRequests.js +65 -0
- package/dist/ui/formatting.d.ts +10 -0
- package/dist/ui/formatting.js +28 -36
- package/dist/ui/messages/chrome.d.ts +9 -0
- package/dist/ui/messages/chrome.js +17 -5
- package/dist/ui/messages/commands.d.ts +388 -14
- package/dist/ui/messages/commands.js +664 -21
- package/dist/ui/messages/consoleMessages.d.ts +10 -0
- package/dist/ui/messages/consoleMessages.js +17 -0
- package/dist/ui/messages/hints.js +2 -1
- package/dist/ui/messages/preview.js +5 -4
- package/dist/ui/messages/session.d.ts +16 -21
- package/dist/ui/messages/session.js +28 -26
- package/dist/ui/messages/sessionCommand.d.ts +43 -0
- package/dist/ui/messages/sessionCommand.js +52 -0
- package/dist/utils/async.d.ts +8 -0
- package/dist/utils/async.js +19 -0
- package/dist/utils/http.d.ts +22 -1
- package/dist/utils/http.js +28 -9
- package/dist/utils/selectorFilters.d.ts +36 -8
- package/dist/utils/selectorFilters.js +267 -53
- package/dist/utils/shellDetection.d.ts +8 -2
- package/dist/utils/shellDetection.js +120 -33
- package/dist/utils/suggestions.d.ts +26 -0
- package/dist/utils/suggestions.js +73 -0
- package/dist/utils/taskMappings.js +10 -0
- package/dist/utils/url.d.ts +12 -2
- package/dist/utils/url.js +69 -7
- package/package.json +1 -1
- package/dist/ui/formatters/sessionFormatters.d.ts +0 -58
- package/dist/ui/formatters/sessionFormatters.js +0 -121
|
@@ -5,7 +5,96 @@
|
|
|
5
5
|
* This module provides JavaScript snippets that can be injected via Runtime.evaluate
|
|
6
6
|
* to properly trigger React's event system.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { FILL_REFUSALS, LABEL_WITHOUT_CONTROL, NAME_QUERY_PLACEHOLDER, VIA_LABEL_SUFFIX, } from '../../errors/messages.js';
|
|
9
|
+
import { ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS } from './elementInfo.js';
|
|
10
|
+
import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
|
|
11
|
+
/**
|
|
12
|
+
* Page-side read-back of a filled field: a mismatch when its value is not
|
|
13
|
+
* what was asked for (the page rejected, reformatted or moved the input),
|
|
14
|
+
* undefined when it is. Values are compared as the browser normalises them:
|
|
15
|
+
* colors case-insensitively, numbers and ranges as numbers, email trimmed,
|
|
16
|
+
* textarea line endings as `\n`, times and local date-times without zero
|
|
17
|
+
* seconds and with `T` (`2024-01-05 10:00:00` is `2024-01-05T10:00`).
|
|
18
|
+
* Checkboxes and radios compare as `checked`/`unchecked`, a multiple select
|
|
19
|
+
* as its selected values joined by ", ", contenteditable text with
|
|
20
|
+
* whitespace collapsed. A value cut to the field's maxlength sets
|
|
21
|
+
* `truncatedTo`; a password mismatch gives masked values and both lengths.
|
|
22
|
+
*/
|
|
23
|
+
export const FILL_VALUE_MISMATCH_JS = `(field, expected) => {
|
|
24
|
+
const type = (field.type || '').toLowerCase();
|
|
25
|
+
const actual = field.isContentEditable
|
|
26
|
+
? (field.textContent || '')
|
|
27
|
+
: type === 'checkbox' || type === 'radio'
|
|
28
|
+
? (field.checked ? 'checked' : 'unchecked')
|
|
29
|
+
: field.localName === 'select' && field.multiple
|
|
30
|
+
? Array.from(field.selectedOptions).map((o) => o.value).join(', ')
|
|
31
|
+
: String(field.value);
|
|
32
|
+
const time = (text) => text.trim().replace(' ', 'T').replace(/(\\d\\d:\\d\\d):00(\\.0+)?$/, '$1');
|
|
33
|
+
const normalize = (text) => {
|
|
34
|
+
text = String(text);
|
|
35
|
+
if (field.isContentEditable) return text.replace(/\\s+/g, ' ').trim();
|
|
36
|
+
if (field.localName === 'textarea') return text.replace(/\\r\\n?/g, '\\n');
|
|
37
|
+
if (type === 'color') return text.trim().toLowerCase();
|
|
38
|
+
if (type === 'email') return text.trim();
|
|
39
|
+
if (type === 'time' || type === 'datetime-local') return time(text);
|
|
40
|
+
return text;
|
|
41
|
+
};
|
|
42
|
+
const numeric = (type === 'number' || type === 'range') && actual.trim() !== '' && String(expected).trim() !== '';
|
|
43
|
+
const same = numeric ? Number(actual) === Number(expected) : normalize(actual) === normalize(expected);
|
|
44
|
+
if (same) return undefined;
|
|
45
|
+
if (type === 'password') {
|
|
46
|
+
const mask = (text) => (text === '' ? '' : '********');
|
|
47
|
+
return { expected: mask(expected), actual: mask(actual), expectedLength: expected.length, actualLength: actual.length };
|
|
48
|
+
}
|
|
49
|
+
const cut = field.maxLength > 0 && actual.length === field.maxLength && expected.length > actual.length &&
|
|
50
|
+
expected.startsWith(actual);
|
|
51
|
+
return cut ? { expected: expected, actual: actual, truncatedTo: actual.length } : { expected: expected, actual: actual };
|
|
52
|
+
}`;
|
|
53
|
+
/**
|
|
54
|
+
* Page-side list of the other text-like fields of a field's form (of the
|
|
55
|
+
* document when it has none), with their values: taken before a fill, so
|
|
56
|
+
* {@link MOVED_VALUE_JS} can tell which one the page changed.
|
|
57
|
+
*/
|
|
58
|
+
export const FIELD_VALUES_JS = `(field) => {
|
|
59
|
+
const fields = field.form ? Array.from(field.form.elements) : Array.from(field.ownerDocument.querySelectorAll('input, textarea'));
|
|
60
|
+
return new Map(fields
|
|
61
|
+
.filter((f) => f !== field && /^(input|textarea)$/.test(f.localName) &&
|
|
62
|
+
!/^(checkbox|radio|password|hidden|submit|button|reset|file|image)$/i.test(f.type || ''))
|
|
63
|
+
.map((f) => [f, String(f.value)]));
|
|
64
|
+
}`;
|
|
65
|
+
/**
|
|
66
|
+
* Page-side reason `dom fill` cannot fill an element, or null when it can:
|
|
67
|
+
* a disabled or read-only form control (naming the attribute or the disabled
|
|
68
|
+
* `<fieldset>`), or for any other element the switch that keeps it from being
|
|
69
|
+
* editable: `contenteditable="false"` (on it or the editor around it), an
|
|
70
|
+
* `inert` ancestor, `aria-readonly` or `aria-disabled`; otherwise it is not a
|
|
71
|
+
* fillable kind of element. Messages are {@link FILL_REFUSALS}.
|
|
72
|
+
*/
|
|
73
|
+
export const FILL_REFUSAL_JS = `(el) => {
|
|
74
|
+
const describe = ${ELEMENT_DESCRIPTION_JS};
|
|
75
|
+
const refusals = ${JSON.stringify(FILL_REFUSALS)};
|
|
76
|
+
const refuse = (kind, cause) => ({
|
|
77
|
+
kind: kind,
|
|
78
|
+
error: refusals[kind].message + (cause ? ' (' + cause + ')' : ''),
|
|
79
|
+
suggestion: refusals[kind].suggestion
|
|
80
|
+
});
|
|
81
|
+
const tag = el.localName;
|
|
82
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
83
|
+
if (el.disabled || el.matches(':disabled')) {
|
|
84
|
+
return refuse('disabled', el.hasAttribute('disabled') ? 'disabled attribute' : el.closest('fieldset[disabled]') ? 'inside a disabled <fieldset>' : '');
|
|
85
|
+
}
|
|
86
|
+
return el.readOnly ? refuse('readOnly', 'readonly attribute') : null;
|
|
87
|
+
}
|
|
88
|
+
if (el.isContentEditable) return null;
|
|
89
|
+
const host = el.closest('[contenteditable]');
|
|
90
|
+
if (host && String(host.getAttribute('contenteditable')).trim().toLowerCase() === 'false') {
|
|
91
|
+
return refuse('readOnly', 'contenteditable="false"' + (host === el ? '' : ' on ' + describe(host)));
|
|
92
|
+
}
|
|
93
|
+
if (el.closest('[inert]')) return refuse('inert', 'inside an inert element');
|
|
94
|
+
if (el.getAttribute('aria-readonly') === 'true') return refuse('readOnly', 'aria-readonly="true"');
|
|
95
|
+
if (el.getAttribute('aria-disabled') === 'true') return refuse('disabled', 'aria-disabled="true"');
|
|
96
|
+
return refuse('notFillable', '<' + tag + '> is not an input, textarea, select or contenteditable element');
|
|
97
|
+
}`;
|
|
9
98
|
/**
|
|
10
99
|
* JavaScript function to fill an input element in a React-compatible way.
|
|
11
100
|
*
|
|
@@ -14,6 +103,13 @@ import { FIND_ELEMENTS_JS } from './targetNode.js';
|
|
|
14
103
|
* 2. Dispatches input/change events that React listens for
|
|
15
104
|
* 3. Properly handles focus/blur for form validation
|
|
16
105
|
*
|
|
106
|
+
* A `<label>` is filled through its control ({@link LABEL_CONTROL_JS}),
|
|
107
|
+
* reported as e.g. `input (via label)`.
|
|
108
|
+
*
|
|
109
|
+
* The result is returned right away (a change handler may navigate). The
|
|
110
|
+
* field and the value to expect are left in `window.__bdgFillCheck` for
|
|
111
|
+
* {@link FILL_READ_BACK_SCRIPT}, which reads the value back a moment later.
|
|
112
|
+
*
|
|
17
113
|
* @remarks
|
|
18
114
|
* Works with React, Vue, Angular, and vanilla JS applications.
|
|
19
115
|
*/
|
|
@@ -21,11 +117,14 @@ export const REACT_FILL_SCRIPT = `
|
|
|
21
117
|
(function(selector, parts, value, options) {
|
|
22
118
|
const allMatches = (${FIND_ELEMENTS_JS})(selector, parts);
|
|
23
119
|
const warnings = [];
|
|
120
|
+
let expected = value;
|
|
24
121
|
// Why a user could not reach the field (the value is still set, so scripted
|
|
25
122
|
// flows keep working, but the result may not be what a user would see)
|
|
26
123
|
const unreachableReason = (field) => {
|
|
27
124
|
if (field.closest('[inert]')) return 'inert';
|
|
28
|
-
const
|
|
125
|
+
const shown = (node) => !node.checkVisibility || node.checkVisibility();
|
|
126
|
+
const modal = field.ownerDocument.querySelector('dialog:modal') ||
|
|
127
|
+
Array.from(field.ownerDocument.querySelectorAll('[aria-modal="true"]')).find(shown);
|
|
29
128
|
if (modal && !modal.contains(field)) return 'behind an open modal dialog';
|
|
30
129
|
if (field.checkVisibility && !field.checkVisibility({ visibilityProperty: true, opacityProperty: true })) return 'hidden';
|
|
31
130
|
return null;
|
|
@@ -84,40 +183,33 @@ export const REACT_FILL_SCRIPT = `
|
|
|
84
183
|
} else {
|
|
85
184
|
el = allMatches[0];
|
|
86
185
|
}
|
|
87
|
-
|
|
88
|
-
const tagName = el.tagName.toLowerCase();
|
|
89
|
-
const inputType = el.type?.toLowerCase();
|
|
90
|
-
|
|
91
|
-
const isFillable = (
|
|
92
|
-
tagName === 'input' ||
|
|
93
|
-
tagName === 'textarea' ||
|
|
94
|
-
tagName === 'select' ||
|
|
95
|
-
el.isContentEditable
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
if (!isFillable) {
|
|
99
|
-
return {
|
|
100
|
-
success: false,
|
|
101
|
-
error: 'Element is not fillable',
|
|
102
|
-
elementType: tagName,
|
|
103
|
-
suggestion: 'Only input, textarea, select, and contenteditable elements can be filled'
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
186
|
|
|
107
|
-
|
|
187
|
+
const labelControl = (${LABEL_CONTROL_JS})(el);
|
|
188
|
+
if (el.localName === 'label' && !labelControl) {
|
|
189
|
+
const quote = (text) => "'" + String(text).split("'").join("'\\\\''") + "'";
|
|
190
|
+
const labelText = (el.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 60);
|
|
108
191
|
return {
|
|
109
192
|
success: false,
|
|
110
|
-
error:
|
|
111
|
-
elementType:
|
|
112
|
-
|
|
193
|
+
error: ${JSON.stringify(LABEL_WITHOUT_CONTROL.message)},
|
|
194
|
+
elementType: 'label',
|
|
195
|
+
unsuitableElement: true,
|
|
196
|
+
suggestion: ${JSON.stringify(LABEL_WITHOUT_CONTROL.suggestion)}.split(${JSON.stringify(NAME_QUERY_PLACEHOLDER)}).join(quote('name=' + labelText))
|
|
113
197
|
};
|
|
114
198
|
}
|
|
115
|
-
|
|
199
|
+
const viaLabel = labelControl ? ${JSON.stringify(VIA_LABEL_SUFFIX)} : '';
|
|
200
|
+
if (labelControl) el = labelControl;
|
|
201
|
+
|
|
202
|
+
const tagName = el.tagName.toLowerCase();
|
|
203
|
+
const inputType = el.type?.toLowerCase();
|
|
204
|
+
|
|
205
|
+
const refusal = (${FILL_REFUSAL_JS})(el);
|
|
206
|
+
if (refusal) {
|
|
116
207
|
return {
|
|
117
208
|
success: false,
|
|
118
|
-
error:
|
|
119
|
-
elementType: tagName,
|
|
120
|
-
suggestion:
|
|
209
|
+
error: refusal.error,
|
|
210
|
+
elementType: tagName + viaLabel,
|
|
211
|
+
suggestion: refusal.suggestion,
|
|
212
|
+
unsuitableElement: refusal.kind === 'notFillable'
|
|
121
213
|
};
|
|
122
214
|
}
|
|
123
215
|
|
|
@@ -126,6 +218,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
126
218
|
warnings.push('The field is ' + unreachable + '; a user could not fill it (the value was set anyway)');
|
|
127
219
|
}
|
|
128
220
|
|
|
221
|
+
const fieldsBefore = (${FIELD_VALUES_JS})(el);
|
|
129
222
|
el.focus();
|
|
130
223
|
|
|
131
224
|
if (tagName === 'select' && el.multiple) {
|
|
@@ -143,11 +236,12 @@ export const REACT_FILL_SCRIPT = `
|
|
|
143
236
|
success: false,
|
|
144
237
|
error: 'Option not found: ' + missing,
|
|
145
238
|
exitCode: 81,
|
|
146
|
-
elementType: tagName,
|
|
239
|
+
elementType: tagName + viaLabel,
|
|
147
240
|
suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')
|
|
148
241
|
};
|
|
149
242
|
}
|
|
150
243
|
options.forEach((o) => { o.selected = chosen.includes(o); });
|
|
244
|
+
expected = chosen.map((o) => o.value).join(', ');
|
|
151
245
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
152
246
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
153
247
|
} else if (tagName === 'select') {
|
|
@@ -161,11 +255,12 @@ export const REACT_FILL_SCRIPT = `
|
|
|
161
255
|
success: false,
|
|
162
256
|
error: 'Option not found: ' + value,
|
|
163
257
|
exitCode: 81,
|
|
164
|
-
elementType: tagName,
|
|
258
|
+
elementType: tagName + viaLabel,
|
|
165
259
|
suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')
|
|
166
260
|
};
|
|
167
261
|
}
|
|
168
262
|
el.value = option.value;
|
|
263
|
+
expected = option.value;
|
|
169
264
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
170
265
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
171
266
|
} else if (inputType === 'checkbox' || inputType === 'radio') {
|
|
@@ -178,7 +273,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
178
273
|
return {
|
|
179
274
|
success: false,
|
|
180
275
|
error: 'Expected true or false for a ' + inputType + ', got "' + value + '"',
|
|
181
|
-
elementType: tagName,
|
|
276
|
+
elementType: tagName + viaLabel,
|
|
182
277
|
inputType: inputType,
|
|
183
278
|
suggestion: 'Use true/false (also yes/no, on/off, 1/0)'
|
|
184
279
|
};
|
|
@@ -188,11 +283,12 @@ export const REACT_FILL_SCRIPT = `
|
|
|
188
283
|
return {
|
|
189
284
|
success: false,
|
|
190
285
|
error: 'A radio button cannot be unchecked',
|
|
191
|
-
elementType: tagName,
|
|
286
|
+
elementType: tagName + viaLabel,
|
|
192
287
|
inputType: inputType,
|
|
193
288
|
suggestion: 'Select another option in the same group instead'
|
|
194
289
|
};
|
|
195
290
|
}
|
|
291
|
+
expected = shouldCheck ? 'checked' : 'unchecked';
|
|
196
292
|
if (el.checked !== shouldCheck) {
|
|
197
293
|
el.click();
|
|
198
294
|
}
|
|
@@ -200,7 +296,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
200
296
|
return {
|
|
201
297
|
success: false,
|
|
202
298
|
fileInput: true,
|
|
203
|
-
elementType: tagName,
|
|
299
|
+
elementType: tagName + viaLabel,
|
|
204
300
|
inputType: inputType,
|
|
205
301
|
error: 'File input'
|
|
206
302
|
};
|
|
@@ -226,7 +322,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
226
322
|
return {
|
|
227
323
|
success: false,
|
|
228
324
|
error: 'Value is ' + value.length + ' characters; the field accepts at most ' + el.maxLength,
|
|
229
|
-
elementType: tagName,
|
|
325
|
+
elementType: tagName + viaLabel,
|
|
230
326
|
inputType: inputType || null,
|
|
231
327
|
suggestion: 'Shorten the value (a user could not type more than maxlength characters)'
|
|
232
328
|
};
|
|
@@ -242,7 +338,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
242
338
|
return {
|
|
243
339
|
success: false,
|
|
244
340
|
error: rejection.error,
|
|
245
|
-
elementType: tagName,
|
|
341
|
+
elementType: tagName + viaLabel,
|
|
246
342
|
inputType: inputType,
|
|
247
343
|
suggestion: rejection.suggestion
|
|
248
344
|
};
|
|
@@ -258,7 +354,8 @@ export const REACT_FILL_SCRIPT = `
|
|
|
258
354
|
if (options.blur !== false) {
|
|
259
355
|
el.blur();
|
|
260
356
|
}
|
|
261
|
-
|
|
357
|
+
window.__bdgFillCheck = { el: el, expected: expected, before: fieldsBefore };
|
|
358
|
+
|
|
262
359
|
return {
|
|
263
360
|
success: true,
|
|
264
361
|
selector: selector,
|
|
@@ -269,7 +366,8 @@ export const REACT_FILL_SCRIPT = `
|
|
|
269
366
|
: tagName === 'select' && el.multiple
|
|
270
367
|
? Array.from(el.selectedOptions).map((o) => o.value).join(', ')
|
|
271
368
|
: el.value,
|
|
272
|
-
|
|
369
|
+
element: (${ELEMENT_IDENTITY_JS})(el),
|
|
370
|
+
elementType: tagName + viaLabel,
|
|
273
371
|
inputType: inputType || null,
|
|
274
372
|
checked: inputType === 'checkbox' || inputType === 'radio' ? el.checked : undefined,
|
|
275
373
|
matchCount: allMatches.length,
|
|
@@ -279,6 +377,52 @@ export const REACT_FILL_SCRIPT = `
|
|
|
279
377
|
};
|
|
280
378
|
})
|
|
281
379
|
`;
|
|
380
|
+
/**
|
|
381
|
+
* Page-side search for the field a moved value went to: one of the fields
|
|
382
|
+
* recorded before the fill ({@link FIELD_VALUES_JS}) whose value changed to
|
|
383
|
+
* the one given (trimmed, at least 2 characters). Evaluates to its id or
|
|
384
|
+
* name (`input#first-name`, `input[name="first"]`) or else its
|
|
385
|
+
* description, or undefined. Passwords are never searched for.
|
|
386
|
+
*/
|
|
387
|
+
export const MOVED_VALUE_JS = `(field, expected, before) => {
|
|
388
|
+
const wanted = String(expected).trim();
|
|
389
|
+
if (!before || wanted.length < 2 || (field.type || '').toLowerCase() === 'password') return undefined;
|
|
390
|
+
const other = Array.from(before.keys()).find((f) =>
|
|
391
|
+
String(f.value).trim() === wanted && String(before.get(f)).trim() !== wanted);
|
|
392
|
+
if (!other) return undefined;
|
|
393
|
+
if (other.id) return other.localName + '#' + other.id;
|
|
394
|
+
return other.name ? other.localName + '[name="' + other.name + '"]' : (${ELEMENT_DESCRIPTION_JS})(other);
|
|
395
|
+
}`;
|
|
396
|
+
/**
|
|
397
|
+
* Page script reading back the field the last fill left in
|
|
398
|
+
* `window.__bdgFillCheck`, after one macrotask (so frameworks that render
|
|
399
|
+
* asynchronously have updated it). The macrotask comes from a
|
|
400
|
+
* `MessageChannel`, which fake timers and page code rarely replace. Evaluates
|
|
401
|
+
* to the mismatch ({@link FILL_VALUE_MISMATCH_JS}), with `movedTo` when the
|
|
402
|
+
* value turned up in another field ({@link MOVED_VALUE_JS}), or null when the
|
|
403
|
+
* value matches, nothing was left (the page navigated) or the field left the
|
|
404
|
+
* page.
|
|
405
|
+
*/
|
|
406
|
+
export const FILL_READ_BACK_SCRIPT = `(() => {
|
|
407
|
+
const check = window.__bdgFillCheck;
|
|
408
|
+
delete window.__bdgFillCheck;
|
|
409
|
+
if (!check) return null;
|
|
410
|
+
const readBack = () => {
|
|
411
|
+
if (!check.el.isConnected) return null;
|
|
412
|
+
const mismatch = (${FILL_VALUE_MISMATCH_JS})(check.el, check.expected);
|
|
413
|
+
if (!mismatch) return null;
|
|
414
|
+
const movedTo = (${MOVED_VALUE_JS})(check.el, check.expected, check.before);
|
|
415
|
+
return movedTo ? Object.assign(mismatch, { movedTo: movedTo }) : mismatch;
|
|
416
|
+
};
|
|
417
|
+
return new Promise((resolve) => {
|
|
418
|
+
const channel = new MessageChannel();
|
|
419
|
+
channel.port1.onmessage = () => {
|
|
420
|
+
channel.port1.close();
|
|
421
|
+
resolve(readBack());
|
|
422
|
+
};
|
|
423
|
+
channel.port2.postMessage(null);
|
|
424
|
+
});
|
|
425
|
+
})()`;
|
|
282
426
|
/**
|
|
283
427
|
* JavaScript function to locate an element for clicking.
|
|
284
428
|
*
|
|
@@ -291,9 +435,15 @@ export const REACT_FILL_SCRIPT = `
|
|
|
291
435
|
* Handles both direct selector matching and indexed selection.
|
|
292
436
|
* When index is provided, selects the nth matching element (0-based).
|
|
293
437
|
* When selector matches multiple elements without index, prioritizes visible ones.
|
|
438
|
+
* A `<label>` is clicked (and double-clicked) through its control when that
|
|
439
|
+
* is visible ({@link LABEL_CONTROL_JS}), reported as e.g. `input (via label)`.
|
|
294
440
|
*/
|
|
295
441
|
export const CLICK_ELEMENT_SCRIPT = `
|
|
296
442
|
(function(selector, parts, index, action) {
|
|
443
|
+
if (window.__bdgPressProbe) {
|
|
444
|
+
window.__bdgPressProbe.stop();
|
|
445
|
+
delete window.__bdgPressProbe;
|
|
446
|
+
}
|
|
297
447
|
const allMatches = (${FIND_ELEMENTS_JS})(selector, parts);
|
|
298
448
|
|
|
299
449
|
if (allMatches.length === 0) {
|
|
@@ -343,6 +493,14 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
343
493
|
}
|
|
344
494
|
}
|
|
345
495
|
|
|
496
|
+
const labelControl = action === 'click' || action === 'double' ? (${LABEL_CONTROL_JS})(el) : null;
|
|
497
|
+
const viaLabel = labelControl &&
|
|
498
|
+
(typeof labelControl.checkVisibility !== 'function' || labelControl.checkVisibility({ visibilityProperty: true, opacityProperty: true })) &&
|
|
499
|
+
labelControl.getClientRects().length > 0
|
|
500
|
+
? ${JSON.stringify(VIA_LABEL_SUFFIX)}
|
|
501
|
+
: '';
|
|
502
|
+
if (viaLabel) el = labelControl;
|
|
503
|
+
|
|
346
504
|
const tagName = el.tagName.toLowerCase();
|
|
347
505
|
if (tagName === 'option') {
|
|
348
506
|
const quote = (text) => "'" + String(text).split("'").join("'\\\\''") + "'";
|
|
@@ -352,7 +510,7 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
352
510
|
success: false,
|
|
353
511
|
error: 'An <option> is chosen through its <select>, not clicked',
|
|
354
512
|
selector: selector,
|
|
355
|
-
elementType: tagName,
|
|
513
|
+
elementType: tagName + viaLabel,
|
|
356
514
|
exitCode: 81,
|
|
357
515
|
suggestion: 'bdg dom fill ' + quote(target) + ' ' + quote(el.value || el.text.trim())
|
|
358
516
|
};
|
|
@@ -363,7 +521,7 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
363
521
|
success: false,
|
|
364
522
|
error: 'Element is disabled',
|
|
365
523
|
selector: selector,
|
|
366
|
-
elementType: tagName,
|
|
524
|
+
elementType: tagName + viaLabel,
|
|
367
525
|
suggestion: 'A user cannot click a disabled element; enable it first (it may depend on other fields)'
|
|
368
526
|
};
|
|
369
527
|
}
|
|
@@ -430,10 +588,23 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
430
588
|
else if (!hasSize) obstruction = 'zero-size';
|
|
431
589
|
else if (!hittable) obstruction = 'covered by another element' + coveredBy();
|
|
432
590
|
|
|
591
|
+
// Records whether the coming mouse press reaches the element at all; a
|
|
592
|
+
// browser dialog or bubble can swallow input while the page looks normal.
|
|
593
|
+
if (hittable && action !== 'hover') {
|
|
594
|
+
const probe = { reached: false };
|
|
595
|
+
const markReached = (event) => {
|
|
596
|
+
if (event.composedPath().includes(el)) probe.reached = true;
|
|
597
|
+
};
|
|
598
|
+
['pointerdown', 'mousedown'].forEach((type) => view.addEventListener(type, markReached, true));
|
|
599
|
+
probe.stop = () => ['pointerdown', 'mousedown'].forEach((type) => view.removeEventListener(type, markReached, true));
|
|
600
|
+
window.__bdgPressProbe = probe;
|
|
601
|
+
}
|
|
602
|
+
|
|
433
603
|
return {
|
|
434
604
|
success: true,
|
|
435
605
|
selector: selector,
|
|
436
|
-
|
|
606
|
+
element: (${ELEMENT_IDENTITY_JS})(el),
|
|
607
|
+
elementType: tagName + viaLabel,
|
|
437
608
|
matchCount: allMatches.length,
|
|
438
609
|
selectedIndex: typeof index === 'number' ? index : undefined,
|
|
439
610
|
x: x,
|
|
@@ -9,27 +9,75 @@
|
|
|
9
9
|
* {@link BOUND_TARGET_SELECTOR}.
|
|
10
10
|
*/
|
|
11
11
|
import type { CDPConnection } from '../../connection/cdp.js';
|
|
12
|
+
import { CommandError } from '../../errors/index.js';
|
|
12
13
|
/** Selector placeholder that makes page scripts use the bound node. */
|
|
13
14
|
export declare const BOUND_TARGET_SELECTOR = "__bdg_bound_target__";
|
|
15
|
+
/** Removes the node bound for index-based commands from the window it was stored on */
|
|
16
|
+
export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget";
|
|
17
|
+
/**
|
|
18
|
+
* Page-side matching of filters ({@link SelectorFilter}) and scoped steps
|
|
19
|
+
* ({@link ScopedStep}): called with the open shadow roots of the page, returns
|
|
20
|
+
* `{ passesAll, descend }`.
|
|
21
|
+
*
|
|
22
|
+
* Visible means rendered (`checkVisibility()`: not inside a closed
|
|
23
|
+
* `<details>` or under `content-visibility: hidden`), a non-empty box and
|
|
24
|
+
* `visibility: visible`, as in Playwright (`opacity: 0` still counts as
|
|
25
|
+
* visible). Text is the rendered text (`innerText`) of visible elements and
|
|
26
|
+
* the text nodes of hidden ones (display or visibility; not those of
|
|
27
|
+
* `<script>`, `<style>` or `<noscript>`), so text filters match hidden
|
|
28
|
+
* elements like Playwright's and `:visible` decides visibility; button inputs
|
|
29
|
+
* use their value. Whitespace is collapsed; filter texts arrive normalized
|
|
30
|
+
* (`has-text` lowercased). `:visible` is checked before the text filters,
|
|
31
|
+
* which read the text.
|
|
32
|
+
*
|
|
33
|
+
* Steps and `:has()` match CSS relative to an element (`:scope > css`). A
|
|
34
|
+
* descendant step also searches the open shadow roots under the element (the
|
|
35
|
+
* whole step CSS inside one shadow tree); a child step stays in the
|
|
36
|
+
* element's own tree.
|
|
37
|
+
*/
|
|
38
|
+
export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const walker = el.ownerDocument.createTreeWalker(el, 5, {\n acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)\n });\n let text = '';\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) text += node.data;\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 return (rendered ? el.innerText : hiddenText(el)).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}";
|
|
14
39
|
/**
|
|
15
40
|
* Page-side selector search that also reaches into open shadow roots and
|
|
16
41
|
* same-origin iframes (recursively), like a user sees the page.
|
|
17
42
|
*
|
|
18
43
|
* Takes the selector and, when it has text or visibility filters, its parts
|
|
19
44
|
* from {@link parseSelectorFilters}: their CSS runs as one selector list and
|
|
20
|
-
* a match is kept when it passes the filters of a part it matches
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
45
|
+
* a match is kept when it passes the filters of a part it matches; a part
|
|
46
|
+
* with scoped steps contributes the steps' matches under it instead (each
|
|
47
|
+
* element once). Matches come in document order: those of the document
|
|
48
|
+
* first, then those of each shadow root and frame in the order they are
|
|
49
|
+
* reached. Cross-origin iframes are separate processes and cannot be
|
|
50
|
+
* searched. Throws a `SyntaxError` for an invalid selector.
|
|
24
51
|
*/
|
|
25
|
-
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 visit = (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 passes = (el, filter) => {\n
|
|
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 skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const walker = el.ownerDocument.createTreeWalker(el, 5, {\n acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)\n });\n let text = '';\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) text += node.data;\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 return (rendered ? el.innerText : hiddenText(el)).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}";
|
|
26
53
|
/**
|
|
27
54
|
* Page-side element lookup shared by the interaction scripts.
|
|
28
55
|
*
|
|
29
56
|
* Returns the bound node (if still in the page) for the placeholder selector,
|
|
30
57
|
* otherwise all matches of the selector ({@link DEEP_QUERY_JS}).
|
|
31
58
|
*/
|
|
32
|
-
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 visit = (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 passes = (el, filter) => {\n
|
|
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 skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const walker = el.ownerDocument.createTreeWalker(el, 5, {\n acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)\n });\n let text = '';\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.nodeType === 3) text += node.data;\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 return (rendered ? el.innerText : hiddenText(el)).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
|
+
/**
|
|
61
|
+
* Page-side check of what selectors cannot search in the top document:
|
|
62
|
+
* iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
|
|
63
|
+
* elements. Cheap: no frame is entered.
|
|
64
|
+
*/
|
|
65
|
+
export declare const UNSEARCHED_CONTENT_JS = "(() => {\n const frames = Array.from(document.querySelectorAll('iframe, frame'));\n const unreadable = (frame) => {\n try { return !frame.contentDocument; } catch (e) { return true; }\n };\n return { crossOriginFrames: frames.some(unreadable), embeds: !!document.querySelector('object, embed') };\n})()";
|
|
66
|
+
/**
|
|
67
|
+
* Page-side ids (`kind` "id") or classes (`kind` "class") in the top
|
|
68
|
+
* document, from the first {@link NAME_SCAN_LIMIT} elements that have one.
|
|
69
|
+
*
|
|
70
|
+
* @param kind - Which names to read
|
|
71
|
+
* @returns Expression evaluating to the distinct names
|
|
72
|
+
*/
|
|
73
|
+
export declare function pageNamesJS(kind: 'id' | 'class'): string;
|
|
74
|
+
/**
|
|
75
|
+
* Page-side: the form control a `<label>` stands for (`label.control`: its
|
|
76
|
+
* `for` target or the control inside it), or null for other elements and
|
|
77
|
+
* labels without one. Fill, click and key presses act on that control, like
|
|
78
|
+
* Playwright.
|
|
79
|
+
*/
|
|
80
|
+
export declare const LABEL_CONTROL_JS = "(el) => (el && el.localName === 'label' ? el.control : null)";
|
|
33
81
|
/**
|
|
34
82
|
* Arguments for {@link DEEP_QUERY_JS} / {@link FIND_ELEMENTS_JS} and the page
|
|
35
83
|
* scripts built on them: the selector as a JS string literal and its parts
|
|
@@ -37,42 +85,47 @@ export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (sel
|
|
|
37
85
|
*
|
|
38
86
|
* @param selector - Selector as the user gave it (or the bound-node placeholder)
|
|
39
87
|
* @returns JS source of the two arguments, e.g. `"li:visible", [{...}]`
|
|
40
|
-
* @throws CommandError (81) for a misplaced or malformed filter
|
|
88
|
+
* @throws CommandError (81) for an empty selector or a misplaced or malformed filter
|
|
41
89
|
*/
|
|
42
90
|
export declare function selectorArgsJS(selector: string): string;
|
|
43
|
-
/** Selector and index the page scripts should use. */
|
|
91
|
+
/** Selector and index the page scripts should use, and the connection to run them on. */
|
|
44
92
|
export interface ScriptTarget {
|
|
45
93
|
selector: string;
|
|
46
94
|
index?: number;
|
|
95
|
+
/** Connection for the page scripts (scoped to the element's frame when the top page cannot reach it) */
|
|
96
|
+
cdp: CDPConnection;
|
|
47
97
|
}
|
|
48
98
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* @param backendNodeId - Backend node id from the query cache
|
|
53
|
-
* @throws CommandError (exit 87) when the node no longer exists in the page
|
|
54
|
-
*/
|
|
55
|
-
export declare function bindTargetNode(cdp: CDPConnection, backendNodeId: number): Promise<void>;
|
|
56
|
-
/**
|
|
57
|
-
* Resolve what a page script should target for an interaction request.
|
|
99
|
+
* Run an interaction on the element a request targets. Results and errors
|
|
100
|
+
* never show the internal placeholder: a bound node the page scripts could
|
|
101
|
+
* not find is reported as stale (87), and results carry the user's selector.
|
|
58
102
|
*
|
|
59
103
|
* @param cdp - CDP connection
|
|
60
104
|
* @param params - Request with a selector (and optional index) or a backend node id
|
|
61
|
-
* @
|
|
105
|
+
* @param work - The interaction, given the script target
|
|
106
|
+
* @returns The interaction's result
|
|
107
|
+
* @throws CommandError (87) when the bound node left the page during the action
|
|
62
108
|
*/
|
|
63
|
-
export declare function
|
|
109
|
+
export declare function onScriptTarget<T extends {
|
|
110
|
+
selector?: string;
|
|
111
|
+
error?: string;
|
|
112
|
+
suggestion?: string;
|
|
113
|
+
exitCode?: number;
|
|
114
|
+
}>(cdp: CDPConnection, params: {
|
|
64
115
|
selector?: string;
|
|
65
116
|
index?: number;
|
|
66
117
|
backendNodeId?: number;
|
|
67
|
-
}): Promise<
|
|
118
|
+
}, work: (target: ScriptTarget) => Promise<T>): Promise<T>;
|
|
68
119
|
/**
|
|
69
|
-
*
|
|
120
|
+
* Error for an element a command could not find.
|
|
70
121
|
*
|
|
71
|
-
* @param
|
|
72
|
-
* @param
|
|
73
|
-
* @returns
|
|
122
|
+
* @param target - Selector (and index) or backend node id the command was given
|
|
123
|
+
* @param matchCount - Elements the selector matched
|
|
124
|
+
* @returns Stale (87), out of range (81) or not found (83) error
|
|
74
125
|
*/
|
|
75
|
-
export declare function
|
|
76
|
-
selector
|
|
77
|
-
|
|
126
|
+
export declare function missingElementError(target: {
|
|
127
|
+
selector: string;
|
|
128
|
+
index?: number;
|
|
129
|
+
backendNodeId?: number;
|
|
130
|
+
}, matchCount: number): CommandError;
|
|
78
131
|
//# sourceMappingURL=targetNode.d.ts.map
|