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
|
@@ -9,25 +9,97 @@
|
|
|
9
9
|
* {@link BOUND_TARGET_SELECTOR}.
|
|
10
10
|
*/
|
|
11
11
|
import { CommandError } from '../../errors/index.js';
|
|
12
|
-
import { staleNodeError } from '../../errors/messages.js';
|
|
12
|
+
import { emptySelectorError, indexOutOfRangeError, noNodesFoundError, staleNodeError, } from '../../errors/messages.js';
|
|
13
|
+
import { frameScopedConnection } from './frameScopedConnection.js';
|
|
14
|
+
import { createLogger } from '../../ui/logging/index.js';
|
|
15
|
+
import { getErrorMessage } from '../../utils/errors.js';
|
|
13
16
|
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
14
|
-
import { parseSelectorFilters } from '../../utils/selectorFilters.js';
|
|
17
|
+
import { parseSelectorFilters, } from '../../utils/selectorFilters.js';
|
|
15
18
|
/** Selector placeholder that makes page scripts use the bound node. */
|
|
16
19
|
export const BOUND_TARGET_SELECTOR = '__bdg_bound_target__';
|
|
20
|
+
/** Removes the node bound for index-based commands from the window it was stored on */
|
|
21
|
+
export const UNBIND_TARGET_SCRIPT = 'delete window.__bdgTarget';
|
|
22
|
+
const log = createLogger('dom');
|
|
17
23
|
/**
|
|
18
|
-
* Page-side
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* Page-side matching of filters ({@link SelectorFilter}) and scoped steps
|
|
25
|
+
* ({@link ScopedStep}): called with the open shadow roots of the page, returns
|
|
26
|
+
* `{ passesAll, descend }`.
|
|
27
|
+
*
|
|
28
|
+
* Visible means rendered (`checkVisibility()`: not inside a closed
|
|
29
|
+
* `<details>` or under `content-visibility: hidden`), a non-empty box and
|
|
30
|
+
* `visibility: visible`, as in Playwright (`opacity: 0` still counts as
|
|
31
|
+
* visible). Text is the rendered text (`innerText`) of visible elements and
|
|
32
|
+
* the text nodes of hidden ones (display or visibility; not those of
|
|
33
|
+
* `<script>`, `<style>` or `<noscript>`), so text filters match hidden
|
|
34
|
+
* elements like Playwright's and `:visible` decides visibility; button inputs
|
|
35
|
+
* use their value. Whitespace is collapsed; filter texts arrive normalized
|
|
36
|
+
* (`has-text` lowercased). `:visible` is checked before the text filters,
|
|
37
|
+
* which read the text.
|
|
38
|
+
*
|
|
39
|
+
* Steps and `:has()` match CSS relative to an element (`:scope > css`). A
|
|
40
|
+
* descendant step also searches the open shadow roots under the element (the
|
|
41
|
+
* whole step CSS inside one shadow tree); a child step stays in the
|
|
42
|
+
* element's own tree.
|
|
23
43
|
*/
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
export const FILTER_MATCHING_JS = `(shadowRoots) => {
|
|
45
|
+
const skipped = /^(script|style|noscript|template)$/;
|
|
46
|
+
const hiddenText = (el) => {
|
|
47
|
+
const walker = el.ownerDocument.createTreeWalker(el, 5, {
|
|
48
|
+
acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)
|
|
49
|
+
});
|
|
50
|
+
let text = '';
|
|
51
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
52
|
+
if (node.nodeType === 3) text += node.data;
|
|
53
|
+
}
|
|
54
|
+
return text;
|
|
55
|
+
};
|
|
56
|
+
const textOf = (el) => {
|
|
57
|
+
if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;
|
|
58
|
+
const rendered = typeof el.innerText === 'string' &&
|
|
59
|
+
(typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));
|
|
60
|
+
return (rendered ? el.innerText : hiddenText(el)).replace(/\\s+/g, ' ').trim();
|
|
61
|
+
};
|
|
62
|
+
const passes = (el, filter) => {
|
|
63
|
+
if (filter.kind === 'visible') {
|
|
64
|
+
if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;
|
|
65
|
+
const rect = el.getBoundingClientRect();
|
|
66
|
+
return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';
|
|
67
|
+
}
|
|
68
|
+
if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);
|
|
69
|
+
const text = textOf(el);
|
|
70
|
+
return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);
|
|
71
|
+
};
|
|
72
|
+
const passesAll = (el, filters) =>
|
|
73
|
+
filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&
|
|
74
|
+
filters.every((filter) => filter.kind === 'visible' || passes(el, filter));
|
|
75
|
+
const isUnder = (scope, node) => {
|
|
76
|
+
for (let current = node; current; current = current.getRootNode().host) {
|
|
77
|
+
if (scope === current || scope.contains(current)) return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
const stepMatches = (scope, step) => {
|
|
82
|
+
const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];
|
|
83
|
+
if (step.combinator !== ' ') return matches;
|
|
84
|
+
for (const root of shadowRoots) {
|
|
85
|
+
if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));
|
|
86
|
+
}
|
|
87
|
+
return matches;
|
|
88
|
+
};
|
|
89
|
+
const descend = (scope, steps) => {
|
|
90
|
+
let current = [scope];
|
|
91
|
+
for (const step of steps) {
|
|
92
|
+
const next = new Set();
|
|
93
|
+
for (const el of current) {
|
|
94
|
+
for (const match of stepMatches(el, step)) {
|
|
95
|
+
if (passesAll(match, step.filters)) next.add(match);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
current = [...next];
|
|
99
|
+
}
|
|
100
|
+
return current;
|
|
101
|
+
};
|
|
102
|
+
return { passesAll: passesAll, descend: descend };
|
|
31
103
|
}`;
|
|
32
104
|
/**
|
|
33
105
|
* Page-side selector search that also reaches into open shadow roots and
|
|
@@ -35,15 +107,19 @@ const FILTER_MATCHES_JS = `(el, filter) => {
|
|
|
35
107
|
*
|
|
36
108
|
* Takes the selector and, when it has text or visibility filters, its parts
|
|
37
109
|
* from {@link parseSelectorFilters}: their CSS runs as one selector list and
|
|
38
|
-
* a match is kept when it passes the filters of a part it matches
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
110
|
+
* a match is kept when it passes the filters of a part it matches; a part
|
|
111
|
+
* with scoped steps contributes the steps' matches under it instead (each
|
|
112
|
+
* element once). Matches come in document order: those of the document
|
|
113
|
+
* first, then those of each shadow root and frame in the order they are
|
|
114
|
+
* reached. Cross-origin iframes are separate processes and cannot be
|
|
115
|
+
* searched. Throws a `SyntaxError` for an invalid selector.
|
|
42
116
|
*/
|
|
43
117
|
export const DEEP_QUERY_JS = `function (selector, parts) {
|
|
44
118
|
const css = parts ? parts.map((part) => part.css).join(', ') : selector;
|
|
45
119
|
const found = [];
|
|
120
|
+
const roots = [];
|
|
46
121
|
const visit = (root) => {
|
|
122
|
+
roots.push(root);
|
|
47
123
|
for (const match of root.querySelectorAll(css)) found.push(match);
|
|
48
124
|
for (const el of root.querySelectorAll('*')) {
|
|
49
125
|
if (el.shadowRoot) visit(el.shadowRoot);
|
|
@@ -56,8 +132,20 @@ export const DEEP_QUERY_JS = `function (selector, parts) {
|
|
|
56
132
|
};
|
|
57
133
|
visit(document);
|
|
58
134
|
if (!parts) return found;
|
|
59
|
-
const
|
|
60
|
-
|
|
135
|
+
const shadowRoots = roots.filter((root) => root.host);
|
|
136
|
+
const { passesAll, descend } = (${FILTER_MATCHING_JS})(shadowRoots);
|
|
137
|
+
const result = new Set();
|
|
138
|
+
for (const el of found) {
|
|
139
|
+
for (const part of parts) {
|
|
140
|
+
if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;
|
|
141
|
+
for (const match of descend(el, part.steps || [])) result.add(match);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const rootOrder = new Map(roots.map((root, i) => [root, i]));
|
|
145
|
+
const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;
|
|
146
|
+
return [...result].sort((a, b) =>
|
|
147
|
+
rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)
|
|
148
|
+
);
|
|
61
149
|
}`;
|
|
62
150
|
/**
|
|
63
151
|
* Page-side element lookup shared by the interaction scripts.
|
|
@@ -72,6 +160,45 @@ export const FIND_ELEMENTS_JS = `function (selector, parts) {
|
|
|
72
160
|
}
|
|
73
161
|
return (${DEEP_QUERY_JS})(selector, parts);
|
|
74
162
|
}`;
|
|
163
|
+
/**
|
|
164
|
+
* Page-side check of what selectors cannot search in the top document:
|
|
165
|
+
* iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
|
|
166
|
+
* elements. Cheap: no frame is entered.
|
|
167
|
+
*/
|
|
168
|
+
export const UNSEARCHED_CONTENT_JS = `(() => {
|
|
169
|
+
const frames = Array.from(document.querySelectorAll('iframe, frame'));
|
|
170
|
+
const unreadable = (frame) => {
|
|
171
|
+
try { return !frame.contentDocument; } catch (e) { return true; }
|
|
172
|
+
};
|
|
173
|
+
return { crossOriginFrames: frames.some(unreadable), embeds: !!document.querySelector('object, embed') };
|
|
174
|
+
})()`;
|
|
175
|
+
/** Elements whose ids or classes are read for "did you mean" (bounds the work on huge pages) */
|
|
176
|
+
const NAME_SCAN_LIMIT = 5000;
|
|
177
|
+
/**
|
|
178
|
+
* Page-side ids (`kind` "id") or classes (`kind` "class") in the top
|
|
179
|
+
* document, from the first {@link NAME_SCAN_LIMIT} elements that have one.
|
|
180
|
+
*
|
|
181
|
+
* @param kind - Which names to read
|
|
182
|
+
* @returns Expression evaluating to the distinct names
|
|
183
|
+
*/
|
|
184
|
+
export function pageNamesJS(kind) {
|
|
185
|
+
return `(() => {
|
|
186
|
+
const names = new Set();
|
|
187
|
+
const elements = document.querySelectorAll('[${kind}]');
|
|
188
|
+
for (let i = 0; i < elements.length && i < ${NAME_SCAN_LIMIT}; i++) {
|
|
189
|
+
const el = elements[i];
|
|
190
|
+
${kind === 'id' ? 'if (el.id) names.add(el.id);' : 'for (const name of el.classList) names.add(name);'}
|
|
191
|
+
}
|
|
192
|
+
return Array.from(names);
|
|
193
|
+
})()`;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Page-side: the form control a `<label>` stands for (`label.control`: its
|
|
197
|
+
* `for` target or the control inside it), or null for other elements and
|
|
198
|
+
* labels without one. Fill, click and key presses act on that control, like
|
|
199
|
+
* Playwright.
|
|
200
|
+
*/
|
|
201
|
+
export const LABEL_CONTROL_JS = `(el) => (el && el.localName === 'label' ? el.control : null)`;
|
|
75
202
|
/**
|
|
76
203
|
* Arguments for {@link DEEP_QUERY_JS} / {@link FIND_ELEMENTS_JS} and the page
|
|
77
204
|
* scripts built on them: the selector as a JS string literal and its parts
|
|
@@ -79,29 +206,36 @@ export const FIND_ELEMENTS_JS = `function (selector, parts) {
|
|
|
79
206
|
*
|
|
80
207
|
* @param selector - Selector as the user gave it (or the bound-node placeholder)
|
|
81
208
|
* @returns JS source of the two arguments, e.g. `"li:visible", [{...}]`
|
|
82
|
-
* @throws CommandError (81) for a misplaced or malformed filter
|
|
209
|
+
* @throws CommandError (81) for an empty selector or a misplaced or malformed filter
|
|
83
210
|
*/
|
|
84
211
|
export function selectorArgsJS(selector) {
|
|
212
|
+
if (selector.trim() === '') {
|
|
213
|
+
const err = emptySelectorError();
|
|
214
|
+
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
215
|
+
}
|
|
85
216
|
return `${JSON.stringify(selector)}, ${JSON.stringify(parseSelectorFilters(selector))}`;
|
|
86
217
|
}
|
|
87
218
|
/**
|
|
88
219
|
* Store the node for the page scripts. It runs in the node's own frame, while
|
|
89
|
-
* the scripts run in the top page, so the node goes on the top window
|
|
90
|
-
*
|
|
220
|
+
* the scripts run in the top page, so the node goes on the top window, which
|
|
221
|
+
* same-origin frames can reach. A cross-origin frame cannot write to the top
|
|
222
|
+
* window: the node then goes on its own window (`"frame"`) and the scripts
|
|
223
|
+
* run in that frame ({@link frameScopedConnection}).
|
|
91
224
|
*/
|
|
92
225
|
const BIND_FUNCTION = `function () {
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
return this.isConnected;
|
|
226
|
+
if (!this.isConnected) return false;
|
|
227
|
+
try { window.top.__bdgTarget = this; return 'top'; } catch (e) { window.__bdgTarget = this; return 'frame'; }
|
|
96
228
|
}`;
|
|
97
229
|
/**
|
|
98
230
|
* Make the node with `backendNodeId` the target of the next page script.
|
|
99
231
|
*
|
|
100
232
|
* @param cdp - CDP connection
|
|
101
233
|
* @param backendNodeId - Backend node id from the query cache
|
|
234
|
+
* @returns Connection for the page scripts: the session's, or one running them
|
|
235
|
+
* in the node's frame when that frame is cross-origin
|
|
102
236
|
* @throws CommandError (exit 87) when the node no longer exists in the page
|
|
103
237
|
*/
|
|
104
|
-
|
|
238
|
+
async function bindTargetNode(cdp, backendNodeId) {
|
|
105
239
|
const err = staleNodeError();
|
|
106
240
|
const stale = new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
|
|
107
241
|
let objectId;
|
|
@@ -119,25 +253,88 @@ export async function bindTargetNode(cdp, backendNodeId) {
|
|
|
119
253
|
functionDeclaration: BIND_FUNCTION,
|
|
120
254
|
returnByValue: true,
|
|
121
255
|
}));
|
|
122
|
-
|
|
256
|
+
const scope = bound.result?.value;
|
|
257
|
+
if (scope === 'frame')
|
|
258
|
+
return frameScopedConnection(cdp, objectId);
|
|
259
|
+
if (scope !== 'top')
|
|
123
260
|
throw stale;
|
|
261
|
+
return cdp;
|
|
124
262
|
}
|
|
125
263
|
/**
|
|
126
264
|
* Resolve what a page script should target for an interaction request.
|
|
127
265
|
*
|
|
128
266
|
* @param cdp - CDP connection
|
|
129
267
|
* @param params - Request with a selector (and optional index) or a backend node id
|
|
130
|
-
* @returns Selector/index for the page script
|
|
268
|
+
* @returns Selector/index for the page script and the connection to run it on
|
|
131
269
|
*/
|
|
132
|
-
|
|
270
|
+
async function resolveScriptTarget(cdp, params) {
|
|
133
271
|
if (params.backendNodeId === undefined) {
|
|
134
272
|
return {
|
|
135
273
|
selector: params.selector ?? '',
|
|
136
274
|
...(params.index !== undefined && { index: params.index }),
|
|
275
|
+
cdp,
|
|
137
276
|
};
|
|
138
277
|
}
|
|
139
|
-
await bindTargetNode(cdp, params.backendNodeId);
|
|
140
|
-
|
|
278
|
+
return { selector: BOUND_TARGET_SELECTOR, cdp: await bindTargetNode(cdp, params.backendNodeId) };
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Whether an error is about the bound node: the placeholder only appears in
|
|
282
|
+
* "not found" messages, which for a bound node mean it left the page.
|
|
283
|
+
*
|
|
284
|
+
* @param text - Error message
|
|
285
|
+
* @returns True when the bound node was not found
|
|
286
|
+
*/
|
|
287
|
+
function boundNodeMissing(text) {
|
|
288
|
+
return text?.includes(BOUND_TARGET_SELECTOR) === true;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Run an interaction on the element a request targets. Results and errors
|
|
292
|
+
* never show the internal placeholder: a bound node the page scripts could
|
|
293
|
+
* not find is reported as stale (87), and results carry the user's selector.
|
|
294
|
+
*
|
|
295
|
+
* @param cdp - CDP connection
|
|
296
|
+
* @param params - Request with a selector (and optional index) or a backend node id
|
|
297
|
+
* @param work - The interaction, given the script target
|
|
298
|
+
* @returns The interaction's result
|
|
299
|
+
* @throws CommandError (87) when the bound node left the page during the action
|
|
300
|
+
*/
|
|
301
|
+
export async function onScriptTarget(cdp, params, work) {
|
|
302
|
+
const err = staleNodeError();
|
|
303
|
+
let target;
|
|
304
|
+
try {
|
|
305
|
+
target = await resolveScriptTarget(cdp, params);
|
|
306
|
+
const result = withUserSelector(await work(target), params.selector);
|
|
307
|
+
if (!boundNodeMissing(result.error))
|
|
308
|
+
return result;
|
|
309
|
+
return {
|
|
310
|
+
...result,
|
|
311
|
+
error: err.message,
|
|
312
|
+
suggestion: err.suggestion,
|
|
313
|
+
exitCode: EXIT_CODES.STALE_CACHE,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
if (!(error instanceof CommandError) || !boundNodeMissing(error.message))
|
|
318
|
+
throw error;
|
|
319
|
+
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
if (target && target.cdp !== cdp)
|
|
323
|
+
unbindInFrame(target.cdp);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Remove a node bound in a cross-origin frame from that frame's window
|
|
328
|
+
* (through the scoped connection, so the script runs where the bind ran; the
|
|
329
|
+
* session removes the one bound on the top window after every interaction).
|
|
330
|
+
* Not waited for.
|
|
331
|
+
*
|
|
332
|
+
* @param frameConnection - Connection scoped to the node's frame
|
|
333
|
+
*/
|
|
334
|
+
function unbindInFrame(frameConnection) {
|
|
335
|
+
void frameConnection
|
|
336
|
+
.send('Runtime.evaluate', { expression: UNBIND_TARGET_SCRIPT })
|
|
337
|
+
.catch((error) => log.debug(`Frame target not unbound: ${getErrorMessage(error)}`));
|
|
141
338
|
}
|
|
142
339
|
/**
|
|
143
340
|
* Report the user's selector instead of the internal placeholder.
|
|
@@ -146,9 +343,28 @@ export async function resolveScriptTarget(cdp, params) {
|
|
|
146
343
|
* @param selector - Selector the user gave (or the cached query's selector)
|
|
147
344
|
* @returns Result with the placeholder replaced
|
|
148
345
|
*/
|
|
149
|
-
|
|
346
|
+
function withUserSelector(result, selector) {
|
|
150
347
|
if (result.selector !== BOUND_TARGET_SELECTOR)
|
|
151
348
|
return result;
|
|
152
349
|
return { ...result, ...(selector !== undefined && { selector }) };
|
|
153
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Error for an element a command could not find.
|
|
353
|
+
*
|
|
354
|
+
* @param target - Selector (and index) or backend node id the command was given
|
|
355
|
+
* @param matchCount - Elements the selector matched
|
|
356
|
+
* @returns Stale (87), out of range (81) or not found (83) error
|
|
357
|
+
*/
|
|
358
|
+
export function missingElementError(target, matchCount) {
|
|
359
|
+
if (target.backendNodeId !== undefined) {
|
|
360
|
+
const err = staleNodeError();
|
|
361
|
+
return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.STALE_CACHE);
|
|
362
|
+
}
|
|
363
|
+
if (matchCount > 0) {
|
|
364
|
+
const err = indexOutOfRangeError(target.index ?? 0, matchCount - 1);
|
|
365
|
+
return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
366
|
+
}
|
|
367
|
+
const err = noNodesFoundError(target.selector);
|
|
368
|
+
return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
|
|
369
|
+
}
|
|
154
370
|
//# sourceMappingURL=targetNode.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bdg dom wait`: wait in the page until elements appear, become visible,
|
|
3
|
+
* contain a text or are gone, and/or the page has loaded.
|
|
4
|
+
*
|
|
5
|
+
* The page watches itself (a MutationObserver plus a short poll for style
|
|
6
|
+
* and frame changes the observer cannot see) and answers as soon as what it
|
|
7
|
+
* shows changes; the condition is decided here ({@link isWaitConditionMet}).
|
|
8
|
+
* Each page call lasts at most {@link WAIT_SLICE_MS}, so a navigation during
|
|
9
|
+
* the wait (the page's context is destroyed) only costs a retry on the new
|
|
10
|
+
* document. `--gone` is confirmed by a second snapshot of the same settled
|
|
11
|
+
* document, so the empty document right after a navigation does not count.
|
|
12
|
+
*/
|
|
13
|
+
import type { DomWaitCommand, DomWaitData } from '../../ipc/protocol/commands.js';
|
|
14
|
+
import type { CDPSender } from '../../telemetry/objectExpander.js';
|
|
15
|
+
/**
|
|
16
|
+
* Wait until the page meets the condition.
|
|
17
|
+
*
|
|
18
|
+
* @param cdp - CDP connection
|
|
19
|
+
* @param params - Condition and timeout
|
|
20
|
+
* @returns What the page showed when it was met, and how long it took
|
|
21
|
+
* @throws CommandError (81) for an invalid selector, (102) when the timeout passed;
|
|
22
|
+
* CDP errors other than a lost page context (a closed connection) as they are
|
|
23
|
+
*/
|
|
24
|
+
export declare function waitForCondition(cdp: CDPSender, params: DomWaitCommand): Promise<DomWaitData>;
|
|
25
|
+
//# sourceMappingURL=wait.d.ts.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bdg dom wait`: wait in the page until elements appear, become visible,
|
|
3
|
+
* contain a text or are gone, and/or the page has loaded.
|
|
4
|
+
*
|
|
5
|
+
* The page watches itself (a MutationObserver plus a short poll for style
|
|
6
|
+
* and frame changes the observer cannot see) and answers as soon as what it
|
|
7
|
+
* shows changes; the condition is decided here ({@link isWaitConditionMet}).
|
|
8
|
+
* Each page call lasts at most {@link WAIT_SLICE_MS}, so a navigation during
|
|
9
|
+
* the wait (the page's context is destroyed) only costs a retry on the new
|
|
10
|
+
* document. `--gone` is confirmed by a second snapshot of the same settled
|
|
11
|
+
* document, so the empty document right after a navigation does not count.
|
|
12
|
+
*/
|
|
13
|
+
import { CommandError } from '../../errors/index.js';
|
|
14
|
+
import { invalidSelectorError, waitTimeoutError } from '../../errors/messages.js';
|
|
15
|
+
import { isContextLostError } from './evalHelpers.js';
|
|
16
|
+
import { DEEP_QUERY_JS, FILTER_MATCHING_JS, selectorArgsJS } from './targetNode.js';
|
|
17
|
+
import { isWaitConditionMet, needsGoneConfirmation, normalizeWaitText, } from './waitCondition.js';
|
|
18
|
+
import { createLogger } from '../../ui/logging/index.js';
|
|
19
|
+
import { delay, raceTimeout } from '../../utils/async.js';
|
|
20
|
+
import { getErrorMessage } from '../../utils/errors.js';
|
|
21
|
+
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
22
|
+
import { filterDefined } from '../../utils/objects.js';
|
|
23
|
+
import { parseSelectorFilters } from '../../utils/selectorFilters.js';
|
|
24
|
+
const log = createLogger('dom');
|
|
25
|
+
/** Longest a single page call waits for a change */
|
|
26
|
+
const WAIT_SLICE_MS = 2000;
|
|
27
|
+
/** Pause before asking a page that could not answer (navigating) again */
|
|
28
|
+
const RETRY_DELAY_MS = 100;
|
|
29
|
+
/** Pause before the snapshot that confirms elements are gone */
|
|
30
|
+
const GONE_CONFIRM_DELAY_MS = 50;
|
|
31
|
+
/**
|
|
32
|
+
* Page-side: report what the page shows (matches, text matches, visible
|
|
33
|
+
* ones, readyState) as soon as it differs from `previous` (at once when
|
|
34
|
+
* `previous` is null), or after `sliceMs` without a change.
|
|
35
|
+
*/
|
|
36
|
+
const WAIT_SNAPSHOT_JS = `async function (selector, parts, text, previous, sliceMs) {
|
|
37
|
+
const { passesAll } = (${FILTER_MATCHING_JS})([]);
|
|
38
|
+
const textFilter = text === null ? [] : [{ kind: 'has-text', text: text }];
|
|
39
|
+
const visibleFilter = [{ kind: 'visible' }];
|
|
40
|
+
const snapshot = () => {
|
|
41
|
+
const matches = selector === null ? [] : (${DEEP_QUERY_JS})(selector, parts);
|
|
42
|
+
const withText = matches.filter((el) => passesAll(el, textFilter));
|
|
43
|
+
return {
|
|
44
|
+
count: matches.length,
|
|
45
|
+
textCount: withText.length,
|
|
46
|
+
visibleCount: withText.filter((el) => passesAll(el, visibleFilter)).length,
|
|
47
|
+
readyState: document.readyState,
|
|
48
|
+
documentId: performance.timeOrigin
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const changed = (s) => previous === null || s.count !== previous.count ||
|
|
52
|
+
s.textCount !== previous.textCount || s.visibleCount !== previous.visibleCount ||
|
|
53
|
+
s.readyState !== previous.readyState || s.documentId !== previous.documentId;
|
|
54
|
+
let current = snapshot();
|
|
55
|
+
if (changed(current)) return current;
|
|
56
|
+
return await new Promise((resolve) => {
|
|
57
|
+
let scheduled = null;
|
|
58
|
+
const observer = new MutationObserver(() => { if (scheduled === null) scheduled = setTimeout(check, 16); });
|
|
59
|
+
const poll = setInterval(check, 100);
|
|
60
|
+
const slice = setTimeout(finish, sliceMs);
|
|
61
|
+
function check() {
|
|
62
|
+
scheduled = null;
|
|
63
|
+
current = snapshot();
|
|
64
|
+
if (changed(current)) finish();
|
|
65
|
+
}
|
|
66
|
+
function finish() {
|
|
67
|
+
observer.disconnect();
|
|
68
|
+
clearInterval(poll);
|
|
69
|
+
clearTimeout(slice);
|
|
70
|
+
clearTimeout(scheduled);
|
|
71
|
+
document.removeEventListener('readystatechange', check);
|
|
72
|
+
resolve(current);
|
|
73
|
+
}
|
|
74
|
+
observer.observe(document, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
75
|
+
document.addEventListener('readystatechange', check);
|
|
76
|
+
});
|
|
77
|
+
}`;
|
|
78
|
+
/**
|
|
79
|
+
* Wait until the page meets the condition.
|
|
80
|
+
*
|
|
81
|
+
* @param cdp - CDP connection
|
|
82
|
+
* @param params - Condition and timeout
|
|
83
|
+
* @returns What the page showed when it was met, and how long it took
|
|
84
|
+
* @throws CommandError (81) for an invalid selector, (102) when the timeout passed;
|
|
85
|
+
* CDP errors other than a lost page context (a closed connection) as they are
|
|
86
|
+
*/
|
|
87
|
+
export async function waitForCondition(cdp, params) {
|
|
88
|
+
const condition = waitConditionOf(params);
|
|
89
|
+
const started = Date.now();
|
|
90
|
+
const deadline = started + params.timeout;
|
|
91
|
+
let last;
|
|
92
|
+
let confirming = false;
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
const snapshot = await nextSnapshot(cdp, condition, confirming ? null : (last ?? null), deadline);
|
|
95
|
+
if (!snapshot) {
|
|
96
|
+
confirming = false;
|
|
97
|
+
await delay(RETRY_DELAY_MS);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (isWaitConditionMet(snapshot, condition, last)) {
|
|
101
|
+
return {
|
|
102
|
+
...condition,
|
|
103
|
+
elapsedMs: Date.now() - started,
|
|
104
|
+
...snapshotData(snapshot, condition),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
last = snapshot;
|
|
108
|
+
confirming = needsGoneConfirmation(snapshot, condition);
|
|
109
|
+
if (confirming)
|
|
110
|
+
await delay(GONE_CONFIRM_DELAY_MS);
|
|
111
|
+
}
|
|
112
|
+
const err = waitTimeoutError(condition, last, params.timeout);
|
|
113
|
+
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.CDP_TIMEOUT);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The condition of a request (options that are off left out).
|
|
117
|
+
*
|
|
118
|
+
* @param params - Request
|
|
119
|
+
* @returns Condition
|
|
120
|
+
*/
|
|
121
|
+
function waitConditionOf(params) {
|
|
122
|
+
return {
|
|
123
|
+
...filterDefined({ selector: params.selector, text: params.text }),
|
|
124
|
+
...(params.gone && { gone: true }),
|
|
125
|
+
...(params.visible && { visible: true }),
|
|
126
|
+
...(params.load && { load: true }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The snapshot as reported: `textCount` only when a text was given.
|
|
131
|
+
*
|
|
132
|
+
* @param snapshot - What the page showed
|
|
133
|
+
* @param condition - What was waited for
|
|
134
|
+
* @returns Snapshot fields of the result
|
|
135
|
+
*/
|
|
136
|
+
function snapshotData(snapshot, condition) {
|
|
137
|
+
return {
|
|
138
|
+
count: snapshot.count,
|
|
139
|
+
...(condition.text !== undefined && { textCount: snapshot.textCount }),
|
|
140
|
+
visibleCount: snapshot.visibleCount,
|
|
141
|
+
readyState: snapshot.readyState,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Ask the page for its next snapshot.
|
|
146
|
+
*
|
|
147
|
+
* @param cdp - CDP connection
|
|
148
|
+
* @param condition - What is waited for
|
|
149
|
+
* @param previous - Last snapshot (null for the first call)
|
|
150
|
+
* @param deadline - When the wait ends (epoch ms)
|
|
151
|
+
* @returns The snapshot, or undefined when the page could not answer (its context was lost
|
|
152
|
+
* to a navigation, or the time ran out)
|
|
153
|
+
* @throws CommandError (81) for an invalid selector; other CDP errors (a closed connection) as they are
|
|
154
|
+
*/
|
|
155
|
+
async function nextSnapshot(cdp, condition, previous, deadline) {
|
|
156
|
+
const remaining = deadline - Date.now();
|
|
157
|
+
const selectorArgs = condition.selector === undefined ? 'null, null' : selectorArgsJS(condition.selector);
|
|
158
|
+
const text = condition.text === undefined ? null : normalizeWaitText(condition.text);
|
|
159
|
+
const args = `${selectorArgs}, ${JSON.stringify(text)}, ${JSON.stringify(previous)}, ${Math.min(remaining, WAIT_SLICE_MS)}`;
|
|
160
|
+
const evaluated = cdp.send('Runtime.evaluate', {
|
|
161
|
+
expression: `(${WAIT_SNAPSHOT_JS})(${args})`,
|
|
162
|
+
awaitPromise: true,
|
|
163
|
+
returnByValue: true,
|
|
164
|
+
});
|
|
165
|
+
evaluated.catch(() => undefined);
|
|
166
|
+
const response = await raceTimeout(evaluated, remaining).catch((error) => {
|
|
167
|
+
if (!isContextLostError(error))
|
|
168
|
+
throw error;
|
|
169
|
+
log.debug(`dom wait: page context lost (${getErrorMessage(error)}), retrying`);
|
|
170
|
+
return undefined;
|
|
171
|
+
});
|
|
172
|
+
if (!response)
|
|
173
|
+
return undefined;
|
|
174
|
+
if (response.exceptionDetails)
|
|
175
|
+
return pageException(response.exceptionDetails, condition);
|
|
176
|
+
return response.result.value;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Handle an exception of the page script: an invalid selector is the
|
|
180
|
+
* user's; anything else (a document being replaced) is retried.
|
|
181
|
+
*
|
|
182
|
+
* @param details - Exception details
|
|
183
|
+
* @param condition - What is waited for
|
|
184
|
+
* @returns undefined (retry)
|
|
185
|
+
* @throws CommandError (81) for an invalid selector
|
|
186
|
+
*/
|
|
187
|
+
function pageException(details, condition) {
|
|
188
|
+
const description = details.exception?.description ?? details.text;
|
|
189
|
+
if (condition.selector !== undefined && description.startsWith('SyntaxError')) {
|
|
190
|
+
const detail = parseSelectorFilters(condition.selector)
|
|
191
|
+
? undefined
|
|
192
|
+
: description.split('\n')[0];
|
|
193
|
+
const err = invalidSelectorError(condition.selector, detail);
|
|
194
|
+
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
195
|
+
}
|
|
196
|
+
log.debug(`dom wait: page script failed (${description.split('\n')[0]}), retrying`);
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=wait.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `bdg dom wait` waits for, and whether what the page shows meets it.
|
|
3
|
+
*
|
|
4
|
+
* The page reports a {@link WaitSnapshot} whenever it changes; the condition
|
|
5
|
+
* is decided here, in one place.
|
|
6
|
+
*/
|
|
7
|
+
/** What to wait for */
|
|
8
|
+
export interface WaitCondition {
|
|
9
|
+
/** Selector (filters like :has-text and :visible allowed); absent when only waiting for the load */
|
|
10
|
+
selector?: string;
|
|
11
|
+
/** Text one of the matches must contain (case-insensitive, whitespace collapsed) */
|
|
12
|
+
text?: string;
|
|
13
|
+
/** Wait for the (visible, with `visible`) matches to be gone */
|
|
14
|
+
gone?: boolean;
|
|
15
|
+
/** Only count visible matches */
|
|
16
|
+
visible?: boolean;
|
|
17
|
+
/** Also wait for `document.readyState` to be `complete` */
|
|
18
|
+
load?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** What the page shows at one moment */
|
|
21
|
+
export interface WaitSnapshot {
|
|
22
|
+
/** Elements matching the selector */
|
|
23
|
+
count: number;
|
|
24
|
+
/** Of those, the ones containing the text (all of them without a text) */
|
|
25
|
+
textCount: number;
|
|
26
|
+
/** Of the text matches, the visible ones */
|
|
27
|
+
visibleCount: number;
|
|
28
|
+
/** `document.readyState` */
|
|
29
|
+
readyState: string;
|
|
30
|
+
/** The document's identity (`performance.timeOrigin`): a new document after a navigation has another */
|
|
31
|
+
documentId: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The text as the page compares it: whitespace collapsed, lower case (like `:has-text`).
|
|
35
|
+
*
|
|
36
|
+
* @param text - Text as given
|
|
37
|
+
* @returns Normalized text
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeWaitText(text: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* The matches the condition counts: visible ones with `visible`, else those
|
|
42
|
+
* containing the text (all matches without a text).
|
|
43
|
+
*
|
|
44
|
+
* @param snapshot - What the page shows
|
|
45
|
+
* @param condition - What is waited for
|
|
46
|
+
* @returns Number of counted matches
|
|
47
|
+
*/
|
|
48
|
+
export declare function countedMatches(snapshot: WaitSnapshot, condition: WaitCondition): number;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the page shows what is waited for.
|
|
51
|
+
*
|
|
52
|
+
* Elements are gone only when two consecutive snapshots of the same
|
|
53
|
+
* document, neither of it still `loading`, show none: right after a
|
|
54
|
+
* navigation commits, the new document is empty for a moment.
|
|
55
|
+
*
|
|
56
|
+
* @param snapshot - What the page shows
|
|
57
|
+
* @param condition - What is waited for
|
|
58
|
+
* @param previous - The snapshot before it, if any
|
|
59
|
+
* @returns True when the elements are there (or gone with `gone`) and, with `load`, the page loaded
|
|
60
|
+
*/
|
|
61
|
+
export declare function isWaitConditionMet(snapshot: WaitSnapshot, condition: WaitCondition, previous?: WaitSnapshot): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Whether a `--gone` wait should confirm this snapshot with another one: it
|
|
64
|
+
* shows no (counted) matches in a document past `loading`.
|
|
65
|
+
*
|
|
66
|
+
* @param snapshot - What the page shows
|
|
67
|
+
* @param condition - What is waited for
|
|
68
|
+
* @returns True when one more snapshot of the same document may meet the condition
|
|
69
|
+
*/
|
|
70
|
+
export declare function needsGoneConfirmation(snapshot: WaitSnapshot, condition: WaitCondition): boolean;
|
|
71
|
+
//# sourceMappingURL=waitCondition.d.ts.map
|