browser-debugger-cli 0.8.0 → 0.10.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 +7 -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 +29 -6
- 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 +189 -119
- 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/keyAttributes.d.ts +20 -0
- package/dist/commands/dom/helpers/keyAttributes.js +54 -0
- package/dist/commands/dom/helpers/query.d.ts +44 -17
- package/dist/commands/dom/helpers/query.js +300 -106
- package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
- package/dist/commands/dom/helpers/runElementCommand.js +98 -30
- package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
- package/dist/commands/dom/helpers/screenshot.js +239 -51
- package/dist/commands/dom/index.d.ts +4 -1
- package/dist/commands/dom/index.js +22 -7
- package/dist/commands/dom/inspect.d.ts +15 -0
- package/dist/commands/dom/inspect.js +82 -0
- 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 +15 -5
- 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 +16 -10
- package/dist/commands/dom/semanticUtils.js +53 -16
- 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 +17 -13
- package/dist/commands/optionBehaviors.js +154 -21
- package/dist/commands/page.d.ts +3 -2
- package/dist/commands/page.js +100 -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/commonOptions.d.ts +4 -0
- package/dist/commands/shared/commonOptions.js +9 -0
- 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 +61 -5
- package/dist/commands/shared/startHelpers.d.ts +66 -0
- package/dist/commands/shared/startHelpers.js +103 -13
- package/dist/commands/shared/validation.d.ts +14 -2
- package/dist/commands/shared/validation.js +20 -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 +10 -2
- package/dist/daemon/launcher.js +8 -7
- package/dist/daemon/server/SocketServer.js +1 -2
- package/dist/daemon/session/Session.d.ts +20 -0
- package/dist/daemon/session/Session.js +80 -9
- package/dist/daemon/session/chromeConnection.d.ts +9 -0
- package/dist/daemon/session/chromeConnection.js +45 -8
- package/dist/daemon/session/commandRegistry.d.ts +14 -1
- package/dist/daemon/session/commandRegistry.js +113 -67
- package/dist/daemon/session/interactions.d.ts +48 -9
- package/dist/daemon/session/interactions.js +46 -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 +10630 -3601
- package/dist/errors/messages.d.ts +456 -24
- package/dist/errors/messages.js +862 -67
- package/dist/index.js +6915 -3401
- package/dist/ipc/client.d.ts +21 -1
- package/dist/ipc/client.js +35 -3
- package/dist/ipc/protocol/commands.d.ts +145 -5
- package/dist/ipc/protocol/commands.js +4 -0
- package/dist/ipc/protocol/domTypes.d.ts +291 -7
- package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
- package/dist/ipc/protocol/inspectTypes.js +10 -0
- 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 +185 -0
- package/dist/runtime/dom/actionEffects.js +402 -0
- package/dist/runtime/dom/actionEffectsScripts.d.ts +90 -0
- package/dist/runtime/dom/actionEffectsScripts.js +426 -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 +103 -0
- package/dist/runtime/dom/elementInfo.js +256 -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 +184 -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 +9 -0
- package/dist/runtime/dom/formFillHelpers/fill.js +178 -14
- 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 +12 -1
- package/dist/runtime/dom/frameSelection.js +22 -3
- package/dist/runtime/dom/frames.d.ts +61 -5
- package/dist/runtime/dom/frames.js +329 -75
- package/dist/runtime/dom/inspect.d.ts +28 -0
- package/dist/runtime/dom/inspect.js +557 -0
- package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
- package/dist/runtime/dom/inspectAllStyles.js +385 -0
- package/dist/runtime/dom/inspectCascade.d.ts +94 -0
- package/dist/runtime/dom/inspectCascade.js +371 -0
- package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
- package/dist/runtime/dom/inspectCascadeModel.js +232 -0
- package/dist/runtime/dom/inspectHints.d.ts +62 -0
- package/dist/runtime/dom/inspectHints.js +305 -0
- package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
- package/dist/runtime/dom/inspectLayoutModel.js +346 -0
- package/dist/runtime/dom/inspectModel.d.ts +74 -0
- package/dist/runtime/dom/inspectModel.js +184 -0
- package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
- package/dist/runtime/dom/inspectPaintModel.js +461 -0
- package/dist/runtime/dom/inspectRules.d.ts +37 -0
- package/dist/runtime/dom/inspectRules.js +101 -0
- package/dist/runtime/dom/inspectScripts.d.ts +132 -0
- package/dist/runtime/dom/inspectScripts.js +263 -0
- package/dist/runtime/dom/inspectTree.d.ts +40 -0
- package/dist/runtime/dom/inspectTree.js +134 -0
- package/dist/runtime/dom/inspectVariables.d.ts +33 -0
- package/dist/runtime/dom/inspectVariables.js +94 -0
- package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
- package/dist/runtime/dom/inspectWhyModel.js +134 -0
- package/dist/runtime/dom/layout.d.ts +71 -0
- package/dist/runtime/dom/layout.js +340 -0
- package/dist/runtime/dom/listenerPageScripts.d.ts +72 -0
- package/dist/runtime/dom/listenerPageScripts.js +365 -0
- package/dist/runtime/dom/listenerSummary.d.ts +136 -11
- package/dist/runtime/dom/listenerSummary.js +361 -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 +63 -2
- package/dist/runtime/dom/reactEventHelpers.js +220 -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 +71 -0
- package/dist/runtime/page/emulation.js +117 -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 +18 -3
- package/dist/telemetry/a11y.js +170 -29
- package/dist/telemetry/console.d.ts +1 -0
- package/dist/telemetry/console.js +100 -5
- package/dist/telemetry/network.js +3 -1
- 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 +112 -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.d.ts +8 -0
- package/dist/ui/formatters/details.js +61 -4
- package/dist/ui/formatters/dom.d.ts +27 -14
- package/dist/ui/formatters/dom.js +88 -59
- package/dist/ui/formatters/form.js +29 -18
- package/dist/ui/formatters/inspect.d.ts +39 -0
- package/dist/ui/formatters/inspect.js +596 -0
- package/dist/ui/formatters/keyAttributes.d.ts +19 -0
- package/dist/ui/formatters/keyAttributes.js +84 -0
- 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.d.ts +13 -0
- package/dist/ui/formatters/networkHeaders.js +36 -3
- package/dist/ui/formatters/networkList.d.ts +29 -1
- package/dist/ui/formatters/networkList.js +86 -20
- 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 +19 -0
- package/dist/ui/formatting.js +31 -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 +504 -14
- package/dist/ui/messages/commands.js +835 -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/networkMessages.d.ts +14 -0
- package/dist/ui/messages/networkMessages.js +18 -0
- package/dist/ui/messages/preview.js +5 -4
- package/dist/ui/messages/session.d.ts +30 -21
- package/dist/ui/messages/session.js +48 -26
- package/dist/ui/messages/sessionCommand.d.ts +43 -0
- package/dist/ui/messages/sessionCommand.js +52 -0
- package/dist/utils/async.d.ts +17 -0
- package/dist/utils/async.js +36 -0
- package/dist/utils/color.d.ts +84 -0
- package/dist/utils/color.js +376 -0
- package/dist/utils/cssValues.d.ts +109 -0
- package/dist/utils/cssValues.js +236 -0
- package/dist/utils/http.d.ts +22 -1
- package/dist/utils/http.js +28 -9
- package/dist/utils/selectorFilters.d.ts +48 -8
- package/dist/utils/selectorFilters.js +296 -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
|
@@ -6,6 +6,34 @@
|
|
|
6
6
|
* to properly trigger React's event system.
|
|
7
7
|
*/
|
|
8
8
|
import type { FillResult, ClickResult } from '../../ipc/protocol/domTypes.js';
|
|
9
|
+
/**
|
|
10
|
+
* Page-side read-back of a filled field: a mismatch when its value is not
|
|
11
|
+
* what was asked for (the page rejected, reformatted or moved the input),
|
|
12
|
+
* undefined when it is. Values are compared as the browser normalises them:
|
|
13
|
+
* colors case-insensitively, numbers and ranges as numbers, email trimmed,
|
|
14
|
+
* textarea line endings as `\n`, times and local date-times without zero
|
|
15
|
+
* seconds and with `T` (`2024-01-05 10:00:00` is `2024-01-05T10:00`).
|
|
16
|
+
* Checkboxes and radios compare as `checked`/`unchecked`, a multiple select
|
|
17
|
+
* as its selected values joined by ", ", contenteditable text with
|
|
18
|
+
* whitespace collapsed. A value cut to the field's maxlength sets
|
|
19
|
+
* `truncatedTo`; a password mismatch gives masked values and both lengths.
|
|
20
|
+
*/
|
|
21
|
+
export declare const FILL_VALUE_MISMATCH_JS = "(field, expected) => {\n const type = (field.type || '').toLowerCase();\n const actual = field.isContentEditable\n ? (field.textContent || '')\n : type === 'checkbox' || type === 'radio'\n ? (field.checked ? 'checked' : 'unchecked')\n : field.localName === 'select' && field.multiple\n ? Array.from(field.selectedOptions).map((o) => o.value).join(', ')\n : String(field.value);\n const time = (text) => text.trim().replace(' ', 'T').replace(/(\\d\\d:\\d\\d):00(\\.0+)?$/, '$1');\n const normalize = (text) => {\n text = String(text);\n if (field.isContentEditable) return text.replace(/\\s+/g, ' ').trim();\n if (field.localName === 'textarea') return text.replace(/\\r\\n?/g, '\\n');\n if (type === 'color') return text.trim().toLowerCase();\n if (type === 'email') return text.trim();\n if (type === 'time' || type === 'datetime-local') return time(text);\n return text;\n };\n const numeric = (type === 'number' || type === 'range') && actual.trim() !== '' && String(expected).trim() !== '';\n const same = numeric ? Number(actual) === Number(expected) : normalize(actual) === normalize(expected);\n if (same) return undefined;\n if (type === 'password') {\n const mask = (text) => (text === '' ? '' : '********');\n return { expected: mask(expected), actual: mask(actual), expectedLength: expected.length, actualLength: actual.length };\n }\n const cut = field.maxLength > 0 && actual.length === field.maxLength && expected.length > actual.length &&\n expected.startsWith(actual);\n return cut ? { expected: expected, actual: actual, truncatedTo: actual.length } : { expected: expected, actual: actual };\n}";
|
|
22
|
+
/**
|
|
23
|
+
* Page-side list of the other text-like fields of a field's form (of the
|
|
24
|
+
* document when it has none), with their values: taken before a fill, so
|
|
25
|
+
* {@link MOVED_VALUE_JS} can tell which one the page changed.
|
|
26
|
+
*/
|
|
27
|
+
export declare const FIELD_VALUES_JS = "(field) => {\n const fields = field.form ? Array.from(field.form.elements) : Array.from(field.ownerDocument.querySelectorAll('input, textarea'));\n return new Map(fields\n .filter((f) => f !== field && /^(input|textarea)$/.test(f.localName) &&\n !/^(checkbox|radio|password|hidden|submit|button|reset|file|image)$/i.test(f.type || ''))\n .map((f) => [f, String(f.value)]));\n}";
|
|
28
|
+
/**
|
|
29
|
+
* Page-side reason `dom fill` cannot fill an element, or null when it can:
|
|
30
|
+
* a disabled or read-only form control (naming the attribute or the disabled
|
|
31
|
+
* `<fieldset>`), or for any other element the switch that keeps it from being
|
|
32
|
+
* editable: `contenteditable="false"` (on it or the editor around it), an
|
|
33
|
+
* `inert` ancestor, `aria-readonly` or `aria-disabled`; otherwise it is not a
|
|
34
|
+
* fillable kind of element. Messages are {@link FILL_REFUSALS}.
|
|
35
|
+
*/
|
|
36
|
+
export declare const FILL_REFUSAL_JS: string;
|
|
9
37
|
/**
|
|
10
38
|
* JavaScript function to fill an input element in a React-compatible way.
|
|
11
39
|
*
|
|
@@ -14,10 +42,36 @@ import type { FillResult, ClickResult } from '../../ipc/protocol/domTypes.js';
|
|
|
14
42
|
* 2. Dispatches input/change events that React listens for
|
|
15
43
|
* 3. Properly handles focus/blur for form validation
|
|
16
44
|
*
|
|
45
|
+
* A `<label>` is filled through its control ({@link LABEL_CONTROL_JS}),
|
|
46
|
+
* reported as e.g. `input (via label)`.
|
|
47
|
+
*
|
|
48
|
+
* The result is returned right away (a change handler may navigate). The
|
|
49
|
+
* field and the value to expect are left in `window.__bdgFillCheck` for
|
|
50
|
+
* {@link FILL_READ_BACK_SCRIPT}, which reads the value back a moment later.
|
|
51
|
+
*
|
|
17
52
|
* @remarks
|
|
18
53
|
* Works with React, Vue, Angular, and vanilla JS applications.
|
|
19
54
|
*/
|
|
20
|
-
export declare const REACT_FILL_SCRIPT = "\n(function(selector, parts, value, options) {\n const allMatches = (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 if (filter.kind === 'visible') {\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n const text = (typeof el.innerText === 'string' ? el.innerText : el.textContent || '').replace(/\\s+/g, ' ').trim();\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n};\n return found.filter((el) => parts.some((part) => el.matches(part.css) && part.filters.every((filter) => passes(el, filter))));\n})(selector, parts);\n})(selector, parts);\n const warnings = [];\n // Why a user could not reach the field (the value is still set, so scripted\n // flows keep working, but the result may not be what a user would see)\n const unreachableReason = (field) => {\n if (field.closest('[inert]')) return 'inert';\n const modal = field.ownerDocument.querySelector('dialog:modal');\n if (modal && !modal.contains(field)) return 'behind an open modal dialog';\n if (field.checkVisibility && !field.checkVisibility({ visibilityProperty: true, opacityProperty: true })) return 'hidden';\n return null;\n };\n // Why the browser did not take the value as given (it sanitizes instead of\n // throwing: bad dates become \"\", colors #000000, ranges are clamped)\n const rejectedValue = (field, type, text) => {\n const formats = {\n number: 'a number',\n date: 'YYYY-MM-DD',\n time: 'HH:MM',\n 'datetime-local': 'YYYY-MM-DDTHH:MM',\n month: 'YYYY-MM',\n week: 'YYYY-Www'\n };\n const rejected = 'The browser rejected \"' + text + '\" for a ' + type + ' field (it keeps its previous value)';\n if (formats[type] && text.trim() !== '' && field.value === '') {\n return { error: rejected, suggestion: 'Expected ' + formats[type] };\n }\n if (type === 'color' && field.value.toLowerCase() !== text.trim().toLowerCase()) {\n return { error: rejected, suggestion: 'Expected a hex color like #1a2b3c' };\n }\n if (type === 'range' && Number(field.value) !== Number(text)) {\n return {\n error: 'The browser would set ' + field.value + ' instead of \"' + text + '\" (range ' + (field.min || 0) + ' to ' + (field.max || 100) + ', step ' + (field.step || 1) + ')',\n suggestion: 'Use a value within the range that matches the step'\n };\n }\n return null;\n };\n \n if (allMatches.length === 0) {\n return { \n success: false, \n error: 'Element not found: ' + selector,\n selector: selector\n };\n }\n \n let el;\n const index = options.index;\n \n // If index is provided, use it directly (0-based)\n if (typeof index === 'number' && index >= 0) {\n if (index >= allMatches.length) {\n return {\n success: false,\n error: 'Index out of range',\n selector: selector,\n matchCount: allMatches.length,\n requestedIndex: index,\n suggestion: 'Use --index between 0 and ' + (allMatches.length - 1)\n };\n }\n el = allMatches[index];\n } else {\n el = allMatches[0];\n }\n \n const tagName = el.tagName.toLowerCase();\n const inputType = el.type?.toLowerCase();\n \n const isFillable = (\n tagName === 'input' || \n tagName === 'textarea' || \n tagName === 'select' ||\n el.isContentEditable\n );\n \n if (!isFillable) {\n return {\n success: false,\n error: 'Element is not fillable',\n elementType: tagName,\n suggestion: 'Only input, textarea, select, and contenteditable elements can be filled'\n };\n }\n\n if (el.disabled || el.matches(':disabled')) {\n return {\n success: false,\n error: 'Element is disabled',\n elementType: tagName,\n suggestion: 'Enable the field first (it may depend on another input)'\n };\n }\n if (el.readOnly) {\n return {\n success: false,\n error: 'Element is read-only',\n elementType: tagName,\n suggestion: 'Read-only fields cannot be filled'\n };\n }\n\n const unreachable = unreachableReason(el);\n if (unreachable) {\n warnings.push('The field is ' + unreachable + '; a user could not fill it (the value was set anyway)');\n }\n\n el.focus();\n\n if (tagName === 'select' && el.multiple) {\n // Several options, separated by commas (an option whose value contains a\n // comma matches as a whole); \"\" selects none\n const options = Array.from(el.options);\n const whole = options.some((o) => o.value === value || o.text.trim() === value);\n const wanted = whole ? [value] : value.split(',').map((part) => part.trim()).filter(Boolean);\n const chosen = wanted.map((part) =>\n options.find((o) => o.value === part) || options.find((o) => o.text.trim() === part)\n );\n const missing = wanted.find((part, i) => !chosen[i]);\n if (missing !== undefined) {\n return {\n success: false,\n error: 'Option not found: ' + missing,\n exitCode: 81,\n elementType: tagName,\n suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')\n };\n }\n options.forEach((o) => { o.selected = chosen.includes(o); });\n el.dispatchEvent(new Event('input', { bubbles: true }));\n el.dispatchEvent(new Event('change', { bubbles: true }));\n } else if (tagName === 'select') {\n // Match by option value first, then by visible label\n const options = Array.from(el.options);\n const option =\n options.find((o) => o.value === value) ||\n options.find((o) => o.text.trim() === value);\n if (!option) {\n return {\n success: false,\n error: 'Option not found: ' + value,\n exitCode: 81,\n elementType: tagName,\n suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')\n };\n }\n el.value = option.value;\n el.dispatchEvent(new Event('input', { bubbles: true }));\n el.dispatchEvent(new Event('change', { bubbles: true }));\n } else if (inputType === 'checkbox' || inputType === 'radio') {\n // Toggle through a click, like a user: frameworks (React) track checkable\n // state via click events and revert a programmatic `checked` assignment\n const normalized = String(value).trim().toLowerCase();\n const truthy = ['true', '1', 'yes', 'on', 'checked'];\n const falsy = ['false', '0', 'no', 'off', 'unchecked'];\n if (!truthy.includes(normalized) && !falsy.includes(normalized)) {\n return {\n success: false,\n error: 'Expected true or false for a ' + inputType + ', got \"' + value + '\"',\n elementType: tagName,\n inputType: inputType,\n suggestion: 'Use true/false (also yes/no, on/off, 1/0)'\n };\n }\n const shouldCheck = truthy.includes(normalized);\n if (inputType === 'radio' && !shouldCheck) {\n return {\n success: false,\n error: 'A radio button cannot be unchecked',\n elementType: tagName,\n inputType: inputType,\n suggestion: 'Select another option in the same group instead'\n };\n }\n if (el.checked !== shouldCheck) {\n el.click();\n }\n } else if (inputType === 'file') {\n return {\n success: false,\n fileInput: true,\n elementType: tagName,\n inputType: inputType,\n error: 'File input'\n };\n } else if (el.isContentEditable) {\n el.textContent = value;\n el.dispatchEvent(new Event('input', { bubbles: true }));\n } else {\n const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n el.ownerDocument.defaultView.HTMLInputElement.prototype,\n 'value'\n )?.set;\n \n const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(\n el.ownerDocument.defaultView.HTMLTextAreaElement.prototype,\n 'value'\n )?.set;\n \n const setter = tagName === 'textarea' \n ? nativeTextAreaValueSetter \n : nativeInputValueSetter;\n\n if (el.maxLength > 0 && value.length > el.maxLength) {\n return {\n success: false,\n error: 'Value is ' + value.length + ' characters; the field accepts at most ' + el.maxLength,\n elementType: tagName,\n inputType: inputType || null,\n suggestion: 'Shorten the value (a user could not type more than maxlength characters)'\n };\n }\n \n const setValue = (text) => (setter ? setter.call(el, text) : (el.value = text));\n const previous = el.value;\n setValue(value);\n const rejection = rejectedValue(el, inputType, value);\n if (rejection) {\n setValue(previous);\n if (options.blur !== false) el.blur();\n return {\n success: false,\n error: rejection.error,\n elementType: tagName,\n inputType: inputType,\n suggestion: rejection.suggestion\n };\n }\n\n el.dispatchEvent(new Event('input', { bubbles: true }));\n el.dispatchEvent(new Event('change', { bubbles: true }));\n if (el.validity && (el.validity.rangeOverflow || el.validity.rangeUnderflow)) {\n 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');\n }\n }\n \n if (options.blur !== false) {\n el.blur();\n }\n \n return {\n success: true,\n selector: selector,\n value: el.isContentEditable\n ? el.textContent\n : inputType === 'password'\n ? '********'\n : tagName === 'select' && el.multiple\n ? Array.from(el.selectedOptions).map((o) => o.value).join(', ')\n : el.value,\n elementType: tagName,\n inputType: inputType || null,\n checked: inputType === 'checkbox' || inputType === 'radio' ? el.checked : undefined,\n matchCount: allMatches.length,\n warning: warnings.concat(allMatches.length > 1 && typeof index !== 'number'\n ? [allMatches.length + ' elements match; filled the first (use --index or a more specific selector)']\n : []).join('; ') || undefined\n };\n})\n";
|
|
55
|
+
export declare const REACT_FILL_SCRIPT: string;
|
|
56
|
+
/**
|
|
57
|
+
* Page-side search for the field a moved value went to: one of the fields
|
|
58
|
+
* recorded before the fill ({@link FIELD_VALUES_JS}) whose value changed to
|
|
59
|
+
* the one given (trimmed, at least 2 characters). Evaluates to its id or
|
|
60
|
+
* name (`input#first-name`, `input[name="first"]`) or else its
|
|
61
|
+
* description, or undefined. Passwords are never searched for.
|
|
62
|
+
*/
|
|
63
|
+
export declare const MOVED_VALUE_JS = "(field, expected, before) => {\n const wanted = String(expected).trim();\n if (!before || wanted.length < 2 || (field.type || '').toLowerCase() === 'password') return undefined;\n const other = Array.from(before.keys()).find((f) =>\n String(f.value).trim() === wanted && String(before.get(f)).trim() !== wanted);\n if (!other) return undefined;\n if (other.id) return other.localName + '#' + other.id;\n return other.name ? other.localName + '[name=\"' + other.name + '\"]' : ((node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : ''))(other);\n}";
|
|
64
|
+
/**
|
|
65
|
+
* Page script reading back the field the last fill left in
|
|
66
|
+
* `window.__bdgFillCheck`, after one macrotask (so frameworks that render
|
|
67
|
+
* asynchronously have updated it). The macrotask comes from a
|
|
68
|
+
* `MessageChannel`, which fake timers and page code rarely replace. Evaluates
|
|
69
|
+
* to the mismatch ({@link FILL_VALUE_MISMATCH_JS}), with `movedTo` when the
|
|
70
|
+
* value turned up in another field ({@link MOVED_VALUE_JS}), or null when the
|
|
71
|
+
* value matches, nothing was left (the page navigated) or the field left the
|
|
72
|
+
* page.
|
|
73
|
+
*/
|
|
74
|
+
export declare const FILL_READ_BACK_SCRIPT = "(() => {\n const check = window.__bdgFillCheck;\n delete window.__bdgFillCheck;\n if (!check) return null;\n const readBack = () => {\n if (!check.el.isConnected) return null;\n const mismatch = ((field, expected) => {\n const type = (field.type || '').toLowerCase();\n const actual = field.isContentEditable\n ? (field.textContent || '')\n : type === 'checkbox' || type === 'radio'\n ? (field.checked ? 'checked' : 'unchecked')\n : field.localName === 'select' && field.multiple\n ? Array.from(field.selectedOptions).map((o) => o.value).join(', ')\n : String(field.value);\n const time = (text) => text.trim().replace(' ', 'T').replace(/(\\d\\d:\\d\\d):00(\\.0+)?$/, '$1');\n const normalize = (text) => {\n text = String(text);\n if (field.isContentEditable) return text.replace(/\\s+/g, ' ').trim();\n if (field.localName === 'textarea') return text.replace(/\\r\\n?/g, '\\n');\n if (type === 'color') return text.trim().toLowerCase();\n if (type === 'email') return text.trim();\n if (type === 'time' || type === 'datetime-local') return time(text);\n return text;\n };\n const numeric = (type === 'number' || type === 'range') && actual.trim() !== '' && String(expected).trim() !== '';\n const same = numeric ? Number(actual) === Number(expected) : normalize(actual) === normalize(expected);\n if (same) return undefined;\n if (type === 'password') {\n const mask = (text) => (text === '' ? '' : '********');\n return { expected: mask(expected), actual: mask(actual), expectedLength: expected.length, actualLength: actual.length };\n }\n const cut = field.maxLength > 0 && actual.length === field.maxLength && expected.length > actual.length &&\n expected.startsWith(actual);\n return cut ? { expected: expected, actual: actual, truncatedTo: actual.length } : { expected: expected, actual: actual };\n})(check.el, check.expected);\n if (!mismatch) return null;\n const movedTo = ((field, expected, before) => {\n const wanted = String(expected).trim();\n if (!before || wanted.length < 2 || (field.type || '').toLowerCase() === 'password') return undefined;\n const other = Array.from(before.keys()).find((f) =>\n String(f.value).trim() === wanted && String(before.get(f)).trim() !== wanted);\n if (!other) return undefined;\n if (other.id) return other.localName + '#' + other.id;\n return other.name ? other.localName + '[name=\"' + other.name + '\"]' : ((node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : ''))(other);\n})(check.el, check.expected, check.before);\n return movedTo ? Object.assign(mismatch, { movedTo: movedTo }) : mismatch;\n };\n return new Promise((resolve) => {\n const channel = new MessageChannel();\n channel.port1.onmessage = () => {\n channel.port1.close();\n resolve(readBack());\n };\n channel.port2.postMessage(null);\n });\n})()";
|
|
21
75
|
/**
|
|
22
76
|
* JavaScript function to locate an element for clicking.
|
|
23
77
|
*
|
|
@@ -30,8 +84,15 @@ export declare const REACT_FILL_SCRIPT = "\n(function(selector, parts, value, op
|
|
|
30
84
|
* Handles both direct selector matching and indexed selection.
|
|
31
85
|
* When index is provided, selects the nth matching element (0-based).
|
|
32
86
|
* When selector matches multiple elements without index, prioritizes visible ones.
|
|
87
|
+
* A `<label>` is clicked (and double-clicked) through its control when that
|
|
88
|
+
* is visible ({@link LABEL_CONTROL_JS}), reported as e.g. `input (via label)`.
|
|
89
|
+
* A hover first notes what is hidden around the element
|
|
90
|
+
* ({@link REVEAL_SNAPSHOT_JS}), so its result can say what it revealed. For
|
|
91
|
+
* a press, a probe (`window.__bdgPressProbe`) records whether the press
|
|
92
|
+
* reaches the element, and otherwise which element it landed on: a browser
|
|
93
|
+
* dialog or bubble can swallow input while the page looks normal.
|
|
33
94
|
*/
|
|
34
|
-
export declare const CLICK_ELEMENT_SCRIPT
|
|
95
|
+
export declare const CLICK_ELEMENT_SCRIPT: string;
|
|
35
96
|
/**
|
|
36
97
|
* Options for filling an element.
|
|
37
98
|
*/
|
|
@@ -5,7 +5,97 @@
|
|
|
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 { REVEAL_SNAPSHOT_JS } from './actionEffectsScripts.js';
|
|
10
|
+
import { ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS } from './elementInfo.js';
|
|
11
|
+
import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
|
|
12
|
+
/**
|
|
13
|
+
* Page-side read-back of a filled field: a mismatch when its value is not
|
|
14
|
+
* what was asked for (the page rejected, reformatted or moved the input),
|
|
15
|
+
* undefined when it is. Values are compared as the browser normalises them:
|
|
16
|
+
* colors case-insensitively, numbers and ranges as numbers, email trimmed,
|
|
17
|
+
* textarea line endings as `\n`, times and local date-times without zero
|
|
18
|
+
* seconds and with `T` (`2024-01-05 10:00:00` is `2024-01-05T10:00`).
|
|
19
|
+
* Checkboxes and radios compare as `checked`/`unchecked`, a multiple select
|
|
20
|
+
* as its selected values joined by ", ", contenteditable text with
|
|
21
|
+
* whitespace collapsed. A value cut to the field's maxlength sets
|
|
22
|
+
* `truncatedTo`; a password mismatch gives masked values and both lengths.
|
|
23
|
+
*/
|
|
24
|
+
export const FILL_VALUE_MISMATCH_JS = `(field, expected) => {
|
|
25
|
+
const type = (field.type || '').toLowerCase();
|
|
26
|
+
const actual = field.isContentEditable
|
|
27
|
+
? (field.textContent || '')
|
|
28
|
+
: type === 'checkbox' || type === 'radio'
|
|
29
|
+
? (field.checked ? 'checked' : 'unchecked')
|
|
30
|
+
: field.localName === 'select' && field.multiple
|
|
31
|
+
? Array.from(field.selectedOptions).map((o) => o.value).join(', ')
|
|
32
|
+
: String(field.value);
|
|
33
|
+
const time = (text) => text.trim().replace(' ', 'T').replace(/(\\d\\d:\\d\\d):00(\\.0+)?$/, '$1');
|
|
34
|
+
const normalize = (text) => {
|
|
35
|
+
text = String(text);
|
|
36
|
+
if (field.isContentEditable) return text.replace(/\\s+/g, ' ').trim();
|
|
37
|
+
if (field.localName === 'textarea') return text.replace(/\\r\\n?/g, '\\n');
|
|
38
|
+
if (type === 'color') return text.trim().toLowerCase();
|
|
39
|
+
if (type === 'email') return text.trim();
|
|
40
|
+
if (type === 'time' || type === 'datetime-local') return time(text);
|
|
41
|
+
return text;
|
|
42
|
+
};
|
|
43
|
+
const numeric = (type === 'number' || type === 'range') && actual.trim() !== '' && String(expected).trim() !== '';
|
|
44
|
+
const same = numeric ? Number(actual) === Number(expected) : normalize(actual) === normalize(expected);
|
|
45
|
+
if (same) return undefined;
|
|
46
|
+
if (type === 'password') {
|
|
47
|
+
const mask = (text) => (text === '' ? '' : '********');
|
|
48
|
+
return { expected: mask(expected), actual: mask(actual), expectedLength: expected.length, actualLength: actual.length };
|
|
49
|
+
}
|
|
50
|
+
const cut = field.maxLength > 0 && actual.length === field.maxLength && expected.length > actual.length &&
|
|
51
|
+
expected.startsWith(actual);
|
|
52
|
+
return cut ? { expected: expected, actual: actual, truncatedTo: actual.length } : { expected: expected, actual: actual };
|
|
53
|
+
}`;
|
|
54
|
+
/**
|
|
55
|
+
* Page-side list of the other text-like fields of a field's form (of the
|
|
56
|
+
* document when it has none), with their values: taken before a fill, so
|
|
57
|
+
* {@link MOVED_VALUE_JS} can tell which one the page changed.
|
|
58
|
+
*/
|
|
59
|
+
export const FIELD_VALUES_JS = `(field) => {
|
|
60
|
+
const fields = field.form ? Array.from(field.form.elements) : Array.from(field.ownerDocument.querySelectorAll('input, textarea'));
|
|
61
|
+
return new Map(fields
|
|
62
|
+
.filter((f) => f !== field && /^(input|textarea)$/.test(f.localName) &&
|
|
63
|
+
!/^(checkbox|radio|password|hidden|submit|button|reset|file|image)$/i.test(f.type || ''))
|
|
64
|
+
.map((f) => [f, String(f.value)]));
|
|
65
|
+
}`;
|
|
66
|
+
/**
|
|
67
|
+
* Page-side reason `dom fill` cannot fill an element, or null when it can:
|
|
68
|
+
* a disabled or read-only form control (naming the attribute or the disabled
|
|
69
|
+
* `<fieldset>`), or for any other element the switch that keeps it from being
|
|
70
|
+
* editable: `contenteditable="false"` (on it or the editor around it), an
|
|
71
|
+
* `inert` ancestor, `aria-readonly` or `aria-disabled`; otherwise it is not a
|
|
72
|
+
* fillable kind of element. Messages are {@link FILL_REFUSALS}.
|
|
73
|
+
*/
|
|
74
|
+
export const FILL_REFUSAL_JS = `(el) => {
|
|
75
|
+
const describe = ${ELEMENT_DESCRIPTION_JS};
|
|
76
|
+
const refusals = ${JSON.stringify(FILL_REFUSALS)};
|
|
77
|
+
const refuse = (kind, cause) => ({
|
|
78
|
+
kind: kind,
|
|
79
|
+
error: refusals[kind].message + (cause ? ' (' + cause + ')' : ''),
|
|
80
|
+
suggestion: refusals[kind].suggestion
|
|
81
|
+
});
|
|
82
|
+
const tag = el.localName;
|
|
83
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
84
|
+
if (el.disabled || el.matches(':disabled')) {
|
|
85
|
+
return refuse('disabled', el.hasAttribute('disabled') ? 'disabled attribute' : el.closest('fieldset[disabled]') ? 'inside a disabled <fieldset>' : '');
|
|
86
|
+
}
|
|
87
|
+
return el.readOnly ? refuse('readOnly', 'readonly attribute') : null;
|
|
88
|
+
}
|
|
89
|
+
if (el.isContentEditable) return null;
|
|
90
|
+
const host = el.closest('[contenteditable]');
|
|
91
|
+
if (host && String(host.getAttribute('contenteditable')).trim().toLowerCase() === 'false') {
|
|
92
|
+
return refuse('readOnly', 'contenteditable="false"' + (host === el ? '' : ' on ' + describe(host)));
|
|
93
|
+
}
|
|
94
|
+
if (el.closest('[inert]')) return refuse('inert', 'inside an inert element');
|
|
95
|
+
if (el.getAttribute('aria-readonly') === 'true') return refuse('readOnly', 'aria-readonly="true"');
|
|
96
|
+
if (el.getAttribute('aria-disabled') === 'true') return refuse('disabled', 'aria-disabled="true"');
|
|
97
|
+
return refuse('notFillable', '<' + tag + '> is not an input, textarea, select or contenteditable element');
|
|
98
|
+
}`;
|
|
9
99
|
/**
|
|
10
100
|
* JavaScript function to fill an input element in a React-compatible way.
|
|
11
101
|
*
|
|
@@ -14,6 +104,13 @@ import { FIND_ELEMENTS_JS } from './targetNode.js';
|
|
|
14
104
|
* 2. Dispatches input/change events that React listens for
|
|
15
105
|
* 3. Properly handles focus/blur for form validation
|
|
16
106
|
*
|
|
107
|
+
* A `<label>` is filled through its control ({@link LABEL_CONTROL_JS}),
|
|
108
|
+
* reported as e.g. `input (via label)`.
|
|
109
|
+
*
|
|
110
|
+
* The result is returned right away (a change handler may navigate). The
|
|
111
|
+
* field and the value to expect are left in `window.__bdgFillCheck` for
|
|
112
|
+
* {@link FILL_READ_BACK_SCRIPT}, which reads the value back a moment later.
|
|
113
|
+
*
|
|
17
114
|
* @remarks
|
|
18
115
|
* Works with React, Vue, Angular, and vanilla JS applications.
|
|
19
116
|
*/
|
|
@@ -21,11 +118,14 @@ export const REACT_FILL_SCRIPT = `
|
|
|
21
118
|
(function(selector, parts, value, options) {
|
|
22
119
|
const allMatches = (${FIND_ELEMENTS_JS})(selector, parts);
|
|
23
120
|
const warnings = [];
|
|
121
|
+
let expected = value;
|
|
24
122
|
// Why a user could not reach the field (the value is still set, so scripted
|
|
25
123
|
// flows keep working, but the result may not be what a user would see)
|
|
26
124
|
const unreachableReason = (field) => {
|
|
27
125
|
if (field.closest('[inert]')) return 'inert';
|
|
28
|
-
const
|
|
126
|
+
const shown = (node) => !node.checkVisibility || node.checkVisibility();
|
|
127
|
+
const modal = field.ownerDocument.querySelector('dialog:modal') ||
|
|
128
|
+
Array.from(field.ownerDocument.querySelectorAll('[aria-modal="true"]')).find(shown);
|
|
29
129
|
if (modal && !modal.contains(field)) return 'behind an open modal dialog';
|
|
30
130
|
if (field.checkVisibility && !field.checkVisibility({ visibilityProperty: true, opacityProperty: true })) return 'hidden';
|
|
31
131
|
return null;
|
|
@@ -84,40 +184,33 @@ export const REACT_FILL_SCRIPT = `
|
|
|
84
184
|
} else {
|
|
85
185
|
el = allMatches[0];
|
|
86
186
|
}
|
|
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
187
|
|
|
107
|
-
|
|
188
|
+
const labelControl = (${LABEL_CONTROL_JS})(el);
|
|
189
|
+
if (el.localName === 'label' && !labelControl) {
|
|
190
|
+
const quote = (text) => "'" + String(text).split("'").join("'\\\\''") + "'";
|
|
191
|
+
const labelText = (el.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 60);
|
|
108
192
|
return {
|
|
109
193
|
success: false,
|
|
110
|
-
error:
|
|
111
|
-
elementType:
|
|
112
|
-
|
|
194
|
+
error: ${JSON.stringify(LABEL_WITHOUT_CONTROL.message)},
|
|
195
|
+
elementType: 'label',
|
|
196
|
+
unsuitableElement: true,
|
|
197
|
+
suggestion: ${JSON.stringify(LABEL_WITHOUT_CONTROL.suggestion)}.split(${JSON.stringify(NAME_QUERY_PLACEHOLDER)}).join(quote('name=' + labelText))
|
|
113
198
|
};
|
|
114
199
|
}
|
|
115
|
-
|
|
200
|
+
const viaLabel = labelControl ? ${JSON.stringify(VIA_LABEL_SUFFIX)} : '';
|
|
201
|
+
if (labelControl) el = labelControl;
|
|
202
|
+
|
|
203
|
+
const tagName = el.tagName.toLowerCase();
|
|
204
|
+
const inputType = el.type?.toLowerCase();
|
|
205
|
+
|
|
206
|
+
const refusal = (${FILL_REFUSAL_JS})(el);
|
|
207
|
+
if (refusal) {
|
|
116
208
|
return {
|
|
117
209
|
success: false,
|
|
118
|
-
error:
|
|
119
|
-
elementType: tagName,
|
|
120
|
-
suggestion:
|
|
210
|
+
error: refusal.error,
|
|
211
|
+
elementType: tagName + viaLabel,
|
|
212
|
+
suggestion: refusal.suggestion,
|
|
213
|
+
unsuitableElement: refusal.kind === 'notFillable'
|
|
121
214
|
};
|
|
122
215
|
}
|
|
123
216
|
|
|
@@ -126,6 +219,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
126
219
|
warnings.push('The field is ' + unreachable + '; a user could not fill it (the value was set anyway)');
|
|
127
220
|
}
|
|
128
221
|
|
|
222
|
+
const fieldsBefore = (${FIELD_VALUES_JS})(el);
|
|
129
223
|
el.focus();
|
|
130
224
|
|
|
131
225
|
if (tagName === 'select' && el.multiple) {
|
|
@@ -143,11 +237,12 @@ export const REACT_FILL_SCRIPT = `
|
|
|
143
237
|
success: false,
|
|
144
238
|
error: 'Option not found: ' + missing,
|
|
145
239
|
exitCode: 81,
|
|
146
|
-
elementType: tagName,
|
|
240
|
+
elementType: tagName + viaLabel,
|
|
147
241
|
suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')
|
|
148
242
|
};
|
|
149
243
|
}
|
|
150
244
|
options.forEach((o) => { o.selected = chosen.includes(o); });
|
|
245
|
+
expected = chosen.map((o) => o.value).join(', ');
|
|
151
246
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
152
247
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
153
248
|
} else if (tagName === 'select') {
|
|
@@ -161,11 +256,12 @@ export const REACT_FILL_SCRIPT = `
|
|
|
161
256
|
success: false,
|
|
162
257
|
error: 'Option not found: ' + value,
|
|
163
258
|
exitCode: 81,
|
|
164
|
-
elementType: tagName,
|
|
259
|
+
elementType: tagName + viaLabel,
|
|
165
260
|
suggestion: 'Available options: ' + options.slice(0, 10).map((o) => o.value || o.text.trim()).join(', ')
|
|
166
261
|
};
|
|
167
262
|
}
|
|
168
263
|
el.value = option.value;
|
|
264
|
+
expected = option.value;
|
|
169
265
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
170
266
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
171
267
|
} else if (inputType === 'checkbox' || inputType === 'radio') {
|
|
@@ -178,7 +274,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
178
274
|
return {
|
|
179
275
|
success: false,
|
|
180
276
|
error: 'Expected true or false for a ' + inputType + ', got "' + value + '"',
|
|
181
|
-
elementType: tagName,
|
|
277
|
+
elementType: tagName + viaLabel,
|
|
182
278
|
inputType: inputType,
|
|
183
279
|
suggestion: 'Use true/false (also yes/no, on/off, 1/0)'
|
|
184
280
|
};
|
|
@@ -188,11 +284,12 @@ export const REACT_FILL_SCRIPT = `
|
|
|
188
284
|
return {
|
|
189
285
|
success: false,
|
|
190
286
|
error: 'A radio button cannot be unchecked',
|
|
191
|
-
elementType: tagName,
|
|
287
|
+
elementType: tagName + viaLabel,
|
|
192
288
|
inputType: inputType,
|
|
193
289
|
suggestion: 'Select another option in the same group instead'
|
|
194
290
|
};
|
|
195
291
|
}
|
|
292
|
+
expected = shouldCheck ? 'checked' : 'unchecked';
|
|
196
293
|
if (el.checked !== shouldCheck) {
|
|
197
294
|
el.click();
|
|
198
295
|
}
|
|
@@ -200,7 +297,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
200
297
|
return {
|
|
201
298
|
success: false,
|
|
202
299
|
fileInput: true,
|
|
203
|
-
elementType: tagName,
|
|
300
|
+
elementType: tagName + viaLabel,
|
|
204
301
|
inputType: inputType,
|
|
205
302
|
error: 'File input'
|
|
206
303
|
};
|
|
@@ -226,7 +323,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
226
323
|
return {
|
|
227
324
|
success: false,
|
|
228
325
|
error: 'Value is ' + value.length + ' characters; the field accepts at most ' + el.maxLength,
|
|
229
|
-
elementType: tagName,
|
|
326
|
+
elementType: tagName + viaLabel,
|
|
230
327
|
inputType: inputType || null,
|
|
231
328
|
suggestion: 'Shorten the value (a user could not type more than maxlength characters)'
|
|
232
329
|
};
|
|
@@ -242,7 +339,7 @@ export const REACT_FILL_SCRIPT = `
|
|
|
242
339
|
return {
|
|
243
340
|
success: false,
|
|
244
341
|
error: rejection.error,
|
|
245
|
-
elementType: tagName,
|
|
342
|
+
elementType: tagName + viaLabel,
|
|
246
343
|
inputType: inputType,
|
|
247
344
|
suggestion: rejection.suggestion
|
|
248
345
|
};
|
|
@@ -258,7 +355,8 @@ export const REACT_FILL_SCRIPT = `
|
|
|
258
355
|
if (options.blur !== false) {
|
|
259
356
|
el.blur();
|
|
260
357
|
}
|
|
261
|
-
|
|
358
|
+
window.__bdgFillCheck = { el: el, expected: expected, before: fieldsBefore };
|
|
359
|
+
|
|
262
360
|
return {
|
|
263
361
|
success: true,
|
|
264
362
|
selector: selector,
|
|
@@ -269,7 +367,8 @@ export const REACT_FILL_SCRIPT = `
|
|
|
269
367
|
: tagName === 'select' && el.multiple
|
|
270
368
|
? Array.from(el.selectedOptions).map((o) => o.value).join(', ')
|
|
271
369
|
: el.value,
|
|
272
|
-
|
|
370
|
+
element: (${ELEMENT_IDENTITY_JS})(el),
|
|
371
|
+
elementType: tagName + viaLabel,
|
|
273
372
|
inputType: inputType || null,
|
|
274
373
|
checked: inputType === 'checkbox' || inputType === 'radio' ? el.checked : undefined,
|
|
275
374
|
matchCount: allMatches.length,
|
|
@@ -279,6 +378,52 @@ export const REACT_FILL_SCRIPT = `
|
|
|
279
378
|
};
|
|
280
379
|
})
|
|
281
380
|
`;
|
|
381
|
+
/**
|
|
382
|
+
* Page-side search for the field a moved value went to: one of the fields
|
|
383
|
+
* recorded before the fill ({@link FIELD_VALUES_JS}) whose value changed to
|
|
384
|
+
* the one given (trimmed, at least 2 characters). Evaluates to its id or
|
|
385
|
+
* name (`input#first-name`, `input[name="first"]`) or else its
|
|
386
|
+
* description, or undefined. Passwords are never searched for.
|
|
387
|
+
*/
|
|
388
|
+
export const MOVED_VALUE_JS = `(field, expected, before) => {
|
|
389
|
+
const wanted = String(expected).trim();
|
|
390
|
+
if (!before || wanted.length < 2 || (field.type || '').toLowerCase() === 'password') return undefined;
|
|
391
|
+
const other = Array.from(before.keys()).find((f) =>
|
|
392
|
+
String(f.value).trim() === wanted && String(before.get(f)).trim() !== wanted);
|
|
393
|
+
if (!other) return undefined;
|
|
394
|
+
if (other.id) return other.localName + '#' + other.id;
|
|
395
|
+
return other.name ? other.localName + '[name="' + other.name + '"]' : (${ELEMENT_DESCRIPTION_JS})(other);
|
|
396
|
+
}`;
|
|
397
|
+
/**
|
|
398
|
+
* Page script reading back the field the last fill left in
|
|
399
|
+
* `window.__bdgFillCheck`, after one macrotask (so frameworks that render
|
|
400
|
+
* asynchronously have updated it). The macrotask comes from a
|
|
401
|
+
* `MessageChannel`, which fake timers and page code rarely replace. Evaluates
|
|
402
|
+
* to the mismatch ({@link FILL_VALUE_MISMATCH_JS}), with `movedTo` when the
|
|
403
|
+
* value turned up in another field ({@link MOVED_VALUE_JS}), or null when the
|
|
404
|
+
* value matches, nothing was left (the page navigated) or the field left the
|
|
405
|
+
* page.
|
|
406
|
+
*/
|
|
407
|
+
export const FILL_READ_BACK_SCRIPT = `(() => {
|
|
408
|
+
const check = window.__bdgFillCheck;
|
|
409
|
+
delete window.__bdgFillCheck;
|
|
410
|
+
if (!check) return null;
|
|
411
|
+
const readBack = () => {
|
|
412
|
+
if (!check.el.isConnected) return null;
|
|
413
|
+
const mismatch = (${FILL_VALUE_MISMATCH_JS})(check.el, check.expected);
|
|
414
|
+
if (!mismatch) return null;
|
|
415
|
+
const movedTo = (${MOVED_VALUE_JS})(check.el, check.expected, check.before);
|
|
416
|
+
return movedTo ? Object.assign(mismatch, { movedTo: movedTo }) : mismatch;
|
|
417
|
+
};
|
|
418
|
+
return new Promise((resolve) => {
|
|
419
|
+
const channel = new MessageChannel();
|
|
420
|
+
channel.port1.onmessage = () => {
|
|
421
|
+
channel.port1.close();
|
|
422
|
+
resolve(readBack());
|
|
423
|
+
};
|
|
424
|
+
channel.port2.postMessage(null);
|
|
425
|
+
});
|
|
426
|
+
})()`;
|
|
282
427
|
/**
|
|
283
428
|
* JavaScript function to locate an element for clicking.
|
|
284
429
|
*
|
|
@@ -291,9 +436,20 @@ export const REACT_FILL_SCRIPT = `
|
|
|
291
436
|
* Handles both direct selector matching and indexed selection.
|
|
292
437
|
* When index is provided, selects the nth matching element (0-based).
|
|
293
438
|
* When selector matches multiple elements without index, prioritizes visible ones.
|
|
439
|
+
* A `<label>` is clicked (and double-clicked) through its control when that
|
|
440
|
+
* is visible ({@link LABEL_CONTROL_JS}), reported as e.g. `input (via label)`.
|
|
441
|
+
* A hover first notes what is hidden around the element
|
|
442
|
+
* ({@link REVEAL_SNAPSHOT_JS}), so its result can say what it revealed. For
|
|
443
|
+
* a press, a probe (`window.__bdgPressProbe`) records whether the press
|
|
444
|
+
* reaches the element, and otherwise which element it landed on: a browser
|
|
445
|
+
* dialog or bubble can swallow input while the page looks normal.
|
|
294
446
|
*/
|
|
295
447
|
export const CLICK_ELEMENT_SCRIPT = `
|
|
296
448
|
(function(selector, parts, index, action) {
|
|
449
|
+
if (window.__bdgPressProbe) {
|
|
450
|
+
window.__bdgPressProbe.stop();
|
|
451
|
+
delete window.__bdgPressProbe;
|
|
452
|
+
}
|
|
297
453
|
const allMatches = (${FIND_ELEMENTS_JS})(selector, parts);
|
|
298
454
|
|
|
299
455
|
if (allMatches.length === 0) {
|
|
@@ -343,6 +499,14 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
343
499
|
}
|
|
344
500
|
}
|
|
345
501
|
|
|
502
|
+
const labelControl = action === 'click' || action === 'double' ? (${LABEL_CONTROL_JS})(el) : null;
|
|
503
|
+
const viaLabel = labelControl &&
|
|
504
|
+
(typeof labelControl.checkVisibility !== 'function' || labelControl.checkVisibility({ visibilityProperty: true, opacityProperty: true })) &&
|
|
505
|
+
labelControl.getClientRects().length > 0
|
|
506
|
+
? ${JSON.stringify(VIA_LABEL_SUFFIX)}
|
|
507
|
+
: '';
|
|
508
|
+
if (viaLabel) el = labelControl;
|
|
509
|
+
|
|
346
510
|
const tagName = el.tagName.toLowerCase();
|
|
347
511
|
if (tagName === 'option') {
|
|
348
512
|
const quote = (text) => "'" + String(text).split("'").join("'\\\\''") + "'";
|
|
@@ -352,7 +516,7 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
352
516
|
success: false,
|
|
353
517
|
error: 'An <option> is chosen through its <select>, not clicked',
|
|
354
518
|
selector: selector,
|
|
355
|
-
elementType: tagName,
|
|
519
|
+
elementType: tagName + viaLabel,
|
|
356
520
|
exitCode: 81,
|
|
357
521
|
suggestion: 'bdg dom fill ' + quote(target) + ' ' + quote(el.value || el.text.trim())
|
|
358
522
|
};
|
|
@@ -363,7 +527,7 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
363
527
|
success: false,
|
|
364
528
|
error: 'Element is disabled',
|
|
365
529
|
selector: selector,
|
|
366
|
-
elementType: tagName,
|
|
530
|
+
elementType: tagName + viaLabel,
|
|
367
531
|
suggestion: 'A user cannot click a disabled element; enable it first (it may depend on other fields)'
|
|
368
532
|
};
|
|
369
533
|
}
|
|
@@ -430,10 +594,25 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
430
594
|
else if (!hasSize) obstruction = 'zero-size';
|
|
431
595
|
else if (!hittable) obstruction = 'covered by another element' + coveredBy();
|
|
432
596
|
|
|
597
|
+
if (action === 'hover') (${REVEAL_SNAPSHOT_JS})(el);
|
|
598
|
+
|
|
599
|
+
if (hittable && action !== 'hover') {
|
|
600
|
+
const probe = { reached: false, landedOn: null };
|
|
601
|
+
const markReached = (event) => {
|
|
602
|
+
const path = event.composedPath();
|
|
603
|
+
if (path.includes(el)) probe.reached = true;
|
|
604
|
+
else if (!probe.landedOn && path[0] && path[0].nodeType === 1) probe.landedOn = describe(path[0]);
|
|
605
|
+
};
|
|
606
|
+
['pointerdown', 'mousedown'].forEach((type) => view.addEventListener(type, markReached, true));
|
|
607
|
+
probe.stop = () => ['pointerdown', 'mousedown'].forEach((type) => view.removeEventListener(type, markReached, true));
|
|
608
|
+
window.__bdgPressProbe = probe;
|
|
609
|
+
}
|
|
610
|
+
|
|
433
611
|
return {
|
|
434
612
|
success: true,
|
|
435
613
|
selector: selector,
|
|
436
|
-
|
|
614
|
+
element: (${ELEMENT_IDENTITY_JS})(el),
|
|
615
|
+
elementType: tagName + viaLabel,
|
|
437
616
|
matchCount: allMatches.length,
|
|
438
617
|
selectedIndex: typeof index === 'number' ? index : undefined,
|
|
439
618
|
x: x,
|