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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP events a DOM action causes, for its "what changed" report: main-frame
|
|
3
|
+
* navigations (with document statuses), requests and new windows.
|
|
4
|
+
*/
|
|
5
|
+
import { createLogger } from '../../ui/logging/index.js';
|
|
6
|
+
import { getErrorMessage } from '../../utils/errors.js';
|
|
7
|
+
const log = createLogger('dom');
|
|
8
|
+
/**
|
|
9
|
+
* Listen for the main frame's navigations, document responses, requests
|
|
10
|
+
* and new windows. Events of attached child targets (out-of-process frames,
|
|
11
|
+
* workers) are not main-frame navigations, but their requests count.
|
|
12
|
+
* `Network.enable` is sent without waiting (see `withActionStability`): the
|
|
13
|
+
* session's network collector usually has the domain enabled already.
|
|
14
|
+
*
|
|
15
|
+
* @param cdp - CDP connection
|
|
16
|
+
* @returns Live listener
|
|
17
|
+
*/
|
|
18
|
+
export function listenForActivity(cdp) {
|
|
19
|
+
const state = {
|
|
20
|
+
events: { statusByLoader: new Map() },
|
|
21
|
+
mainFrame: new Set(),
|
|
22
|
+
requests: 0,
|
|
23
|
+
opened: false,
|
|
24
|
+
loading: false,
|
|
25
|
+
};
|
|
26
|
+
const cleanups = [
|
|
27
|
+
...listenForNavigation(cdp, state),
|
|
28
|
+
...listenForRequests(cdp, state),
|
|
29
|
+
...listenForWindows(cdp, state),
|
|
30
|
+
];
|
|
31
|
+
findMainFrame(cdp, state);
|
|
32
|
+
return {
|
|
33
|
+
events: state.events,
|
|
34
|
+
activity: () => ({ requests: state.requests, opened: state.opened }),
|
|
35
|
+
navigationPending: () => state.loading,
|
|
36
|
+
dispose: () => cleanups.forEach((cleanup) => cleanup()),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Main-frame navigations: new documents, same-document URL changes, and
|
|
41
|
+
* whether a load is pending (started, not committed or stopped yet).
|
|
42
|
+
*
|
|
43
|
+
* @param cdp - CDP connection
|
|
44
|
+
* @param state - State to record into
|
|
45
|
+
* @returns Listener cleanups
|
|
46
|
+
*/
|
|
47
|
+
function listenForNavigation(cdp, state) {
|
|
48
|
+
const isMain = (frameId, sessionId) => sessionId === undefined && state.mainFrame.has(frameId);
|
|
49
|
+
return [
|
|
50
|
+
cdp.on('Page.frameNavigated', ({ frame }, sessionId) => {
|
|
51
|
+
if (sessionId !== undefined || frame.parentId !== undefined)
|
|
52
|
+
return;
|
|
53
|
+
state.mainFrame.add(frame.id);
|
|
54
|
+
state.loading = false;
|
|
55
|
+
const url = frame.url + (frame.urlFragment ?? '');
|
|
56
|
+
state.events.document = { url, loaderId: frame.loaderId };
|
|
57
|
+
}),
|
|
58
|
+
cdp.on('Page.navigatedWithinDocument', (params, session) => {
|
|
59
|
+
if (isMain(params.frameId, session))
|
|
60
|
+
state.events.withinDocumentUrl = params.url;
|
|
61
|
+
}),
|
|
62
|
+
cdp.on('Page.frameStartedLoading', ({ frameId }, sessionId) => {
|
|
63
|
+
if (isMain(frameId, sessionId))
|
|
64
|
+
state.loading = true;
|
|
65
|
+
}),
|
|
66
|
+
cdp.on('Page.frameStoppedLoading', ({ frameId }, sessionId) => {
|
|
67
|
+
if (isMain(frameId, sessionId))
|
|
68
|
+
state.loading = false;
|
|
69
|
+
}),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Requests (any target) and the statuses of documents.
|
|
74
|
+
*
|
|
75
|
+
* @param cdp - CDP connection
|
|
76
|
+
* @param state - State to record into
|
|
77
|
+
* @returns Listener cleanups
|
|
78
|
+
*/
|
|
79
|
+
function listenForRequests(cdp, state) {
|
|
80
|
+
void cdp
|
|
81
|
+
.send('Network.enable')
|
|
82
|
+
.catch((error) => log.debug(`Network.enable failed: ${getErrorMessage(error)}`));
|
|
83
|
+
return [
|
|
84
|
+
cdp.on('Network.responseReceived', ({ type, loaderId, response }) => {
|
|
85
|
+
if (type === 'Document' && loaderId) {
|
|
86
|
+
state.events.statusByLoader.set(loaderId, response.status);
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
cdp.on('Network.requestWillBeSent', () => {
|
|
90
|
+
state.requests++;
|
|
91
|
+
}),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* New windows, tabs and downloads.
|
|
96
|
+
*
|
|
97
|
+
* @param cdp - CDP connection
|
|
98
|
+
* @param state - State to record into
|
|
99
|
+
* @returns Listener cleanups
|
|
100
|
+
*/
|
|
101
|
+
function listenForWindows(cdp, state) {
|
|
102
|
+
const opened = () => {
|
|
103
|
+
state.opened = true;
|
|
104
|
+
};
|
|
105
|
+
return [cdp.on('Page.windowOpen', opened), cdp.on('Page.downloadWillBegin', opened)];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Learn the main frame's id (until a navigation reports it), without waiting.
|
|
109
|
+
*
|
|
110
|
+
* @param cdp - CDP connection
|
|
111
|
+
* @param state - State to record into
|
|
112
|
+
*/
|
|
113
|
+
function findMainFrame(cdp, state) {
|
|
114
|
+
void cdp
|
|
115
|
+
.send('Page.getFrameTree')
|
|
116
|
+
.then((tree) => {
|
|
117
|
+
const id = tree.frameTree?.frame?.id;
|
|
118
|
+
if (id)
|
|
119
|
+
state.mainFrame.add(id);
|
|
120
|
+
})
|
|
121
|
+
.catch((error) => log.debug(`No frame tree: ${getErrorMessage(error)}`));
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=pageActivity.js.map
|
|
@@ -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,10 @@ 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)`.
|
|
33
89
|
*/
|
|
34
|
-
export declare const CLICK_ELEMENT_SCRIPT
|
|
90
|
+
export declare const CLICK_ELEMENT_SCRIPT: string;
|
|
35
91
|
/**
|
|
36
92
|
* Options for filling an element.
|
|
37
93
|
*/
|