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
package/dist/errors/messages.js
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
* Centralized location for reusable error messages with consistent formatting.
|
|
5
5
|
*/
|
|
6
6
|
import * as path from 'path';
|
|
7
|
+
import { countedMatches, } from '../runtime/dom/waitCondition.js';
|
|
8
|
+
import { getSessionBaseDir, getSessionName } from '../session/paths.js';
|
|
7
9
|
import { escapeControlChars, formatDuration, joinLines } from '../ui/formatting.js';
|
|
8
|
-
import { frameLabel } from '../ui/messages/commands.js';
|
|
10
|
+
import { documentRequestText, frameLabel, frameUrlLabel, pendingRequestsText, waitSnapshotSummary, waitTargetLabel, } from '../ui/messages/commands.js';
|
|
11
|
+
import { noActiveSessionMessage, sessionCommand, startSessionSuggestion, } from '../ui/messages/sessionCommand.js';
|
|
9
12
|
import { detectSelectorQuoteDamage, detectScriptQuoteDamage, hasAttributeSelector, } from '../utils/shellDetection.js';
|
|
10
13
|
/**
|
|
11
|
-
* Generate "session already running" error message
|
|
14
|
+
* Generate "session already running" error message (commands carry
|
|
15
|
+
* `--session` for a named session).
|
|
12
16
|
*
|
|
13
17
|
* @param pid - Process ID of running session
|
|
14
18
|
* @param duration - Session duration in milliseconds
|
|
@@ -22,10 +26,42 @@ import { detectSelectorQuoteDamage, detectScriptQuoteDamage, hasAttributeSelecto
|
|
|
22
26
|
* ```
|
|
23
27
|
*/
|
|
24
28
|
export function sessionAlreadyRunningError(pid, duration, targetUrl) {
|
|
25
|
-
return joinLines('',
|
|
29
|
+
return joinLines('', `Error: ${sessionLabel()} already running`, '', ` PID: ${pid}`, targetUrl && ` Target: ${targetUrl}`, ` Duration: ${formatDuration(duration)}`, '', 'Suggestions:', ` View session: ${sessionCommand('bdg status')}`, ` Stop and restart: ${stopAndRestartCommand()}`, '');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The daemon's one-line answer to `bdg <url>` while its session runs.
|
|
33
|
+
*
|
|
34
|
+
* @param pid - Daemon PID
|
|
35
|
+
* @returns Message
|
|
36
|
+
*/
|
|
37
|
+
export function sessionAlreadyRunningMessage(pid) {
|
|
38
|
+
return `${sessionLabel()} already running (PID ${pid}). Stop it first with: ${sessionCommand('bdg stop')}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* "Session" or `Session "<name>"` for the selected session.
|
|
42
|
+
*
|
|
43
|
+
* @returns Label
|
|
44
|
+
*/
|
|
45
|
+
function sessionLabel() {
|
|
46
|
+
const name = getSessionName();
|
|
47
|
+
return name === null ? 'Session' : `Session "${name}"`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Stop the selected session and start it again.
|
|
51
|
+
*
|
|
52
|
+
* @returns Command line
|
|
53
|
+
*/
|
|
54
|
+
function stopAndRestartCommand() {
|
|
55
|
+
return `${sessionCommand('bdg stop')} && ${sessionCommand('bdg <url>')}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What to do when `bdg <url>` finds the selected session already running.
|
|
59
|
+
*
|
|
60
|
+
* @returns Suggestion
|
|
61
|
+
*/
|
|
62
|
+
export function alreadyRunningSuggestion() {
|
|
63
|
+
return `Use the running session (${sessionCommand('bdg status')}), or stop it first: ${stopAndRestartCommand()}`;
|
|
26
64
|
}
|
|
27
|
-
/** What to do when `bdg <url>` finds a session already running */
|
|
28
|
-
export const ALREADY_RUNNING_SUGGESTION = 'Use the running session (bdg status), or stop it first: bdg stop && bdg <url>';
|
|
29
65
|
/**
|
|
30
66
|
* Human-readable description of a bdg-launched Chrome (as opposed to one
|
|
31
67
|
* reached via `--chrome-ws-url`). Used in mismatch errors where the active
|
|
@@ -45,7 +81,7 @@ export const LAUNCHED_CHROME_DESCRIPTION = 'launched Chrome (bdg-managed)';
|
|
|
45
81
|
* @returns Formatted error message
|
|
46
82
|
*/
|
|
47
83
|
export function sessionTargetMismatchError(currentTarget, requestedTarget) {
|
|
48
|
-
return joinLines('', 'Error: Active session is attached to a different Chrome target', '', ` Current: ${currentTarget ?? '(unknown)'}`, ` Requested: ${requestedTarget ?? '(unknown)'}`, '',
|
|
84
|
+
return joinLines('', 'Error: Active session is attached to a different Chrome target', '', ` Current: ${currentTarget ?? '(unknown)'}`, ` Requested: ${requestedTarget ?? '(unknown)'}`, '', `Run '${sessionCommand('bdg stop')}' before attaching to a different target.`, '');
|
|
49
85
|
}
|
|
50
86
|
/**
|
|
51
87
|
* The daemon accepted the connection but did not answer in time (frozen or
|
|
@@ -57,7 +93,7 @@ export function sessionTargetMismatchError(currentTarget, requestedTarget) {
|
|
|
57
93
|
export function sessionNotRespondingError(seconds) {
|
|
58
94
|
return {
|
|
59
95
|
message: `The session did not respond within ${seconds}s`,
|
|
60
|
-
suggestion:
|
|
96
|
+
suggestion: `Retry in a moment; if it stays unresponsive, end it with: ${sessionCommand('bdg cleanup --force')} (stops the daemon and its Chrome)`,
|
|
61
97
|
};
|
|
62
98
|
}
|
|
63
99
|
/**
|
|
@@ -69,7 +105,7 @@ export function sessionNotRespondingError(seconds) {
|
|
|
69
105
|
export function commandTimedOutError(seconds) {
|
|
70
106
|
return {
|
|
71
107
|
message: `The command did not finish within ${seconds}s (the page may be busy or frozen)`,
|
|
72
|
-
suggestion:
|
|
108
|
+
suggestion: `Check the session with: ${sessionCommand('bdg status')}; if the page stays frozen, end it with: ${sessionCommand('bdg cleanup --force')}`,
|
|
73
109
|
};
|
|
74
110
|
}
|
|
75
111
|
/**
|
|
@@ -80,8 +116,8 @@ export function commandTimedOutError(seconds) {
|
|
|
80
116
|
*/
|
|
81
117
|
export function sessionUnavailableSuggestion(exitCode) {
|
|
82
118
|
return exitCode === 85
|
|
83
|
-
?
|
|
84
|
-
:
|
|
119
|
+
? `Wait until "${sessionCommand('bdg <url>')}" returns, then retry`
|
|
120
|
+
: startSessionSuggestion();
|
|
85
121
|
}
|
|
86
122
|
/**
|
|
87
123
|
* Generate unified "daemon not running" error message with context.
|
|
@@ -107,7 +143,7 @@ export function sessionUnavailableSuggestion(exitCode) {
|
|
|
107
143
|
* ```
|
|
108
144
|
*/
|
|
109
145
|
export function daemonNotRunningError(context) {
|
|
110
|
-
return joinLines(
|
|
146
|
+
return joinLines(`Error: ${noActiveSessionMessage()}`, context?.staleCleanedUp && '(Stale daemon files were cleaned up)', context?.lastError && `Last error: ${context.lastError}`, startSessionSuggestion(), context?.suggestStatus && '', context?.suggestStatus && 'Or check daemon status:', context?.suggestStatus && ` ${sessionCommand('bdg status')}`, context?.suggestRetry && '', context?.suggestRetry && 'Or try the command again if this was transient');
|
|
111
147
|
}
|
|
112
148
|
/**
|
|
113
149
|
* Generate generic error message with optional context.
|
|
@@ -166,8 +202,8 @@ export function elementNotFoundError(selector) {
|
|
|
166
202
|
const message = `Element not found: ${selector}`;
|
|
167
203
|
const discovery = [
|
|
168
204
|
'Discovery path:',
|
|
169
|
-
` 1. Query first: bdg dom query '${selector}'`,
|
|
170
|
-
|
|
205
|
+
` 1. Query first: ${sessionCommand(`bdg dom query '${selector}'`)}`,
|
|
206
|
+
` 2. Then inspect: ${sessionCommand('bdg dom a11y describe 0')}`,
|
|
171
207
|
];
|
|
172
208
|
const quoteCheck = detectSelectorQuoteDamage(selector);
|
|
173
209
|
if (quoteCheck.damaged) {
|
|
@@ -184,7 +220,7 @@ export function elementNotFoundError(selector) {
|
|
|
184
220
|
}
|
|
185
221
|
return {
|
|
186
222
|
message,
|
|
187
|
-
suggestion: joinLines(
|
|
223
|
+
suggestion: joinLines(`Check the selector syntax, or wait for the element to load (${sessionCommand('bdg peek')} shows the page state)`, unreachableElementsNote(selector)),
|
|
188
224
|
};
|
|
189
225
|
}
|
|
190
226
|
/**
|
|
@@ -196,8 +232,24 @@ export function indexOutOfRangeError(index, max) {
|
|
|
196
232
|
suggestion: `Use an index between 0 and ${max}`,
|
|
197
233
|
};
|
|
198
234
|
}
|
|
235
|
+
/** Finding elements by accessible name, quoted for the shell (the name may contain spaces and colons). */
|
|
236
|
+
export const A11Y_NAME_QUERY_EXAMPLE = "bdg dom a11y query 'name=…'";
|
|
237
|
+
/** Where text filters can go, for selector errors. */
|
|
238
|
+
const SCOPED_FILTER_EXAMPLES = 'Put the filter on the element it tests, e.g. li:has-text("Buy milk") .toggle (the .toggle in that row) or label:has-text("Name") input, or test what an element contains with :has(), e.g. li:has(label:text-is("Buy milk"))';
|
|
199
239
|
/** Playwright selector syntax bdg does not support (`:text()`, `>>` chains, `text=` engines, layout pseudo-classes). */
|
|
200
240
|
const PLAYWRIGHT_ONLY_SYNTAX = /:(?:text|text-matches|nth-match|left-of|right-of|above|below|near)\(|>>|^\s*(?:text|css|xpath|role|id|data-testid|internal:\w+)=/i;
|
|
241
|
+
/**
|
|
242
|
+
* Empty (or blank) selector: the browser rejects it, so it is caught before
|
|
243
|
+
* any page script runs.
|
|
244
|
+
*
|
|
245
|
+
* @returns Message and suggestion
|
|
246
|
+
*/
|
|
247
|
+
export function emptySelectorError() {
|
|
248
|
+
return {
|
|
249
|
+
message: 'Invalid CSS selector: The provided selector is empty',
|
|
250
|
+
suggestion: 'Pass a selector, e.g. bdg dom query "button.primary"',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
201
253
|
/**
|
|
202
254
|
* CSS selector rejected by the browser. Playwright-only syntax (`:text()`,
|
|
203
255
|
* `>>`, `text=`) gets the filters bdg supports instead.
|
|
@@ -210,13 +262,13 @@ export function invalidSelectorError(selector, detail) {
|
|
|
210
262
|
return {
|
|
211
263
|
message: `Invalid CSS selector: ${selector}${detail ? ` (${detail})` : ''}`,
|
|
212
264
|
suggestion: PLAYWRIGHT_ONLY_SYNTAX.test(selector)
|
|
213
|
-
?
|
|
265
|
+
? `Playwright-only syntax is not CSS. bdg supports :has-text("…"), :text-is("…") and :visible, e.g. button:has-text("Save"), or scoped to a row or label: li:has-text("Buy milk") .toggle; or find elements by accessible name: ${A11Y_NAME_QUERY_EXAMPLE}`
|
|
214
266
|
: 'Check the selector syntax, e.g. bdg dom query "button.primary"',
|
|
215
267
|
};
|
|
216
268
|
}
|
|
217
269
|
/**
|
|
218
|
-
* A text or visibility filter (`:has-text()`, `:text-is()`, `:visible`)
|
|
219
|
-
*
|
|
270
|
+
* A text or visibility filter (`:has-text()`, `:text-is()`, `:visible`)
|
|
271
|
+
* inside a pseudo-class other than `:has()`, e.g. `:not(:visible)`.
|
|
220
272
|
*
|
|
221
273
|
* @param selector - Selector as given
|
|
222
274
|
* @param filter - The misplaced filter as written
|
|
@@ -224,8 +276,70 @@ export function invalidSelectorError(selector, detail) {
|
|
|
224
276
|
*/
|
|
225
277
|
export function misplacedSelectorFilterError(selector, filter) {
|
|
226
278
|
return {
|
|
227
|
-
message: `${filter}
|
|
228
|
-
suggestion:
|
|
279
|
+
message: `${filter} can only be used on an element of the selector or inside :has(), not inside other pseudo-classes: ${selector}`,
|
|
280
|
+
suggestion: `${SCOPED_FILTER_EXAMPLES}; or find elements by accessible name: ${A11Y_NAME_QUERY_EXAMPLE}`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* A sibling combinator (`+`, `~`) after a filtered compound: the rest of the
|
|
285
|
+
* selector is matched under the filtered element, so only descendant and
|
|
286
|
+
* child combinators can follow it.
|
|
287
|
+
*
|
|
288
|
+
* @param selector - Selector as given
|
|
289
|
+
* @param combinator - The combinator found
|
|
290
|
+
* @returns Message and suggestion
|
|
291
|
+
*/
|
|
292
|
+
export function siblingAfterFilterError(selector, combinator) {
|
|
293
|
+
return {
|
|
294
|
+
message: `Only a descendant (space) or child (>) combinator can follow a text or visibility filter, not "${combinator}": ${selector}`,
|
|
295
|
+
suggestion: `Put the filter on the element to match, e.g. h2 + p:has-text("x"), or scope by a common ancestor: section:has-text("x") p`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* A `:has()` with filters whose selector starts with a sibling combinator
|
|
300
|
+
* (`:has(+ a:visible)`): its matches are searched under the element, where
|
|
301
|
+
* siblings are not.
|
|
302
|
+
*
|
|
303
|
+
* @param selector - Selector as given
|
|
304
|
+
* @param combinator - `+` or `~`
|
|
305
|
+
* @returns Message and suggestion
|
|
306
|
+
*/
|
|
307
|
+
export function siblingInHasError(selector, combinator) {
|
|
308
|
+
return {
|
|
309
|
+
message: `:has() with text or visibility filters can only look inside an element, not at its siblings ("${combinator}"): ${selector}`,
|
|
310
|
+
suggestion: 'Use a descendant or child: li:has(a:visible), li:has(> a:visible); for a sibling, put the filter on it: li + a:visible',
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/** How a selector with filters is malformed (before the browser sees it). */
|
|
314
|
+
const MALFORMED_SELECTOR_DETAILS = {
|
|
315
|
+
'empty-in-list': () => 'a selector in the list is empty',
|
|
316
|
+
'leading-combinator': (combinator) => `it starts with the combinator "${combinator}"`,
|
|
317
|
+
'trailing-combinator': () => 'it ends with a combinator',
|
|
318
|
+
'empty-has': () => ':has() has an empty selector',
|
|
319
|
+
'scope-in-has': () => ':has() with filters is already relative to the element; write :has(> a:visible) instead of :has(:scope > a:visible)',
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* A selector with filters that is malformed in a way bdg detects while
|
|
323
|
+
* splitting it (so the error shows the selector as given, not rewritten CSS).
|
|
324
|
+
*
|
|
325
|
+
* @param selector - Selector as given
|
|
326
|
+
* @param problem - What is wrong
|
|
327
|
+
* @param combinator - The combinator, for `leading-combinator`
|
|
328
|
+
* @returns Message and suggestion
|
|
329
|
+
*/
|
|
330
|
+
export function malformedSelectorError(selector, problem, combinator = '') {
|
|
331
|
+
return invalidSelectorError(selector, MALFORMED_SELECTOR_DETAILS[problem](combinator));
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* `:has-text()` with empty text, which every element would match.
|
|
335
|
+
*
|
|
336
|
+
* @param selector - Selector as given
|
|
337
|
+
* @returns Message and suggestion
|
|
338
|
+
*/
|
|
339
|
+
export function emptyTextFilterError(selector) {
|
|
340
|
+
return {
|
|
341
|
+
message: `:has-text() needs the text to look for (empty text matches every element): ${selector}`,
|
|
342
|
+
suggestion: 'Give the text, e.g. button:has-text("Save"); use :text-is("") for elements without text',
|
|
229
343
|
};
|
|
230
344
|
}
|
|
231
345
|
/**
|
|
@@ -301,6 +415,31 @@ export function missingStartUrlError() {
|
|
|
301
415
|
suggestion: 'Put the URL first, e.g. bdg localhost:3000 --port 9333',
|
|
302
416
|
};
|
|
303
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* `--viewport` given something that is not a width and height.
|
|
420
|
+
*
|
|
421
|
+
* @param value - What was given
|
|
422
|
+
* @param max - Largest side accepted
|
|
423
|
+
*/
|
|
424
|
+
export function invalidViewportError(value, max) {
|
|
425
|
+
return {
|
|
426
|
+
message: `Invalid --viewport: "${value}"`,
|
|
427
|
+
suggestion: `Give width x height in CSS px (1-${max} each), e.g. --viewport 1280x800`,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* `--color-scheme` given another value than the ones it takes.
|
|
432
|
+
*
|
|
433
|
+
* @param value - What was given
|
|
434
|
+
* @param similar - Close matches
|
|
435
|
+
* @param schemes - Accepted values
|
|
436
|
+
*/
|
|
437
|
+
export function invalidColorSchemeError(value, similar, schemes) {
|
|
438
|
+
return {
|
|
439
|
+
message: `Unknown --color-scheme: "${value}"`,
|
|
440
|
+
suggestion: similar[0] ? `Did you mean: ${similar[0]}?` : `Available: ${schemes.join(', ')}`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
304
443
|
/**
|
|
305
444
|
* `-u` / `--user-data-dir` given something that is not a directory path.
|
|
306
445
|
*
|
|
@@ -313,6 +452,54 @@ export function invalidUserDataDirError(value, reason) {
|
|
|
313
452
|
suggestion: 'Give a directory for the Chrome profile, e.g. -u ./profile (it is created if missing)',
|
|
314
453
|
};
|
|
315
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* A `--session` / `BDG_SESSION` name bdg cannot use as a directory name.
|
|
457
|
+
*
|
|
458
|
+
* @param name - The rejected name
|
|
459
|
+
* @param maxLength - Longest allowed name
|
|
460
|
+
*/
|
|
461
|
+
export function invalidSessionNameError(name, maxLength) {
|
|
462
|
+
return {
|
|
463
|
+
message: `Invalid session name "${name}"`,
|
|
464
|
+
suggestion: `Use 1-${maxLength} letters, digits, "-" or "_", starting with a letter or digit, e.g. --session agent-1`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* A session name whose daemon socket path would exceed the OS limit.
|
|
469
|
+
*
|
|
470
|
+
* @param name - Session name
|
|
471
|
+
* @param socketPath - Resulting socket path
|
|
472
|
+
* @param max - Longest socket path in bytes
|
|
473
|
+
*/
|
|
474
|
+
export function sessionNameSocketTooLongError(name, socketPath, max) {
|
|
475
|
+
return {
|
|
476
|
+
message: `Session name "${name}" makes the daemon socket path too long (${Buffer.byteLength(socketPath)} bytes, at most ${max}): ${socketPath}`,
|
|
477
|
+
suggestion: 'Use a shorter session name, or a shorter BDG_SESSION_DIR (e.g. /tmp/bdg)',
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* `bdg cleanup --purge` without a named session (the default session's
|
|
482
|
+
* directory holds the named sessions).
|
|
483
|
+
*/
|
|
484
|
+
export function purgeNeedsNamedSessionError() {
|
|
485
|
+
return {
|
|
486
|
+
message: "--purge deletes a named session's directory and needs --session <name>",
|
|
487
|
+
suggestion: 'Name the session: bdg cleanup --session <name> --purge (see bdg sessions)',
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* `bdg cleanup --purge` found the session still holding its directory after
|
|
492
|
+
* cleaning up, so the directory is kept.
|
|
493
|
+
*
|
|
494
|
+
* @param dir - Session directory
|
|
495
|
+
* @param reason - What still holds it
|
|
496
|
+
*/
|
|
497
|
+
export function purgeRefusedError(dir, reason) {
|
|
498
|
+
return {
|
|
499
|
+
message: `Not deleting ${dir}: ${reason}`,
|
|
500
|
+
suggestion: `End the session first (${sessionCommand('bdg cleanup --force')}), then retry: ${sessionCommand('bdg cleanup --purge')}`,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
316
503
|
/** Fix for an unusable session directory */
|
|
317
504
|
const SESSION_DIR_SUGGESTION = 'Set BDG_SESSION_DIR to a short, writable directory, e.g. BDG_SESSION_DIR=/tmp/bdg';
|
|
318
505
|
/**
|
|
@@ -343,7 +530,7 @@ export function sessionDirNotWritableError(dir, reason) {
|
|
|
343
530
|
*/
|
|
344
531
|
export function socketPathTooLongError(socketPath, max) {
|
|
345
532
|
return {
|
|
346
|
-
message: `Session directory path is too long for the daemon socket (${socketPath
|
|
533
|
+
message: `Session directory path is too long for the daemon socket (${Buffer.byteLength(socketPath)} bytes, at most ${max}): ${socketPath}`,
|
|
347
534
|
suggestion: SESSION_DIR_SUGGESTION,
|
|
348
535
|
};
|
|
349
536
|
}
|
|
@@ -361,6 +548,36 @@ export function externalChromeUnreachableError(endpoint, secure) {
|
|
|
361
548
|
: 'Check that Chrome runs with --remote-debugging-port and is reachable from here',
|
|
362
549
|
};
|
|
363
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Something answers on the `--chrome-ws-url` endpoint, but it is not Chrome's
|
|
553
|
+
* DevTools HTTP endpoint (e.g. a web server on that port).
|
|
554
|
+
*
|
|
555
|
+
* @param endpoint - e.g. http://127.0.0.1:3000
|
|
556
|
+
*/
|
|
557
|
+
export function notDevToolsEndpointError(endpoint) {
|
|
558
|
+
return {
|
|
559
|
+
message: `${endpoint} answers, but it is not a Chrome DevTools endpoint (no /json/version)`,
|
|
560
|
+
suggestion: "Give Chrome's debugging port (the --remote-debugging-port it was started with), not the page's port",
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Another running bdg session uses the Chrome (or tab) `--chrome-ws-url`
|
|
565
|
+
* points to.
|
|
566
|
+
*
|
|
567
|
+
* @param endpoint - e.g. http://127.0.0.1:9222
|
|
568
|
+
* @param owner - The other session: name (null for a default session),
|
|
569
|
+
* directory, base directory and whether it launched that Chrome
|
|
570
|
+
*/
|
|
571
|
+
export function chromeInUseBySessionError(endpoint, owner) {
|
|
572
|
+
const label = owner.name === null ? 'the default bdg session' : `bdg session "${owner.name}"`;
|
|
573
|
+
const what = owner.launched ? 'was launched by' : 'has its tab driven by';
|
|
574
|
+
const envPrefix = owner.baseDir === getSessionBaseDir() ? '' : `BDG_SESSION_DIR=${owner.baseDir} `;
|
|
575
|
+
const ownerCommand = (command) => envPrefix + sessionCommand(command, owner.name);
|
|
576
|
+
return {
|
|
577
|
+
message: `The Chrome at ${endpoint} ${what} ${label} (${owner.dir}); attaching would take it over`,
|
|
578
|
+
suggestion: `Use that session (${ownerCommand('bdg status')}), stop it first (${ownerCommand('bdg stop')}), or attach to another Chrome or tab (a page URL from ${endpoint}/json/list)`,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
364
581
|
/**
|
|
365
582
|
* The page id of a `--chrome-ws-url` page URL does not exist.
|
|
366
583
|
*
|
|
@@ -382,7 +599,7 @@ export function externalPageNotFoundError(id, endpoint) {
|
|
|
382
599
|
export function externalBrowserIdMismatchError(endpoint, actual) {
|
|
383
600
|
return {
|
|
384
601
|
message: `The Chrome at ${endpoint} has a different browser id (it was restarted, or the URL is from another Chrome)`,
|
|
385
|
-
suggestion: `Use its current URL: bdg <url> --chrome-ws-url ${actual}`,
|
|
602
|
+
suggestion: `Use its current URL: ${sessionCommand(`bdg <url> --chrome-ws-url ${actual}`)}`,
|
|
386
603
|
};
|
|
387
604
|
}
|
|
388
605
|
/**
|
|
@@ -392,7 +609,7 @@ export function externalBrowserIdMismatchError(endpoint, actual) {
|
|
|
392
609
|
*/
|
|
393
610
|
export function chromeWsUrlConflictError(options) {
|
|
394
611
|
return {
|
|
395
|
-
message: `${options.join(' and ')} cannot be used with --chrome-ws-url (the running Chrome already has its port and
|
|
612
|
+
message: `${options.join(' and ')} cannot be used with --chrome-ws-url (the running Chrome already has its port, profile and window mode)`,
|
|
396
613
|
suggestion: 'Drop them, or let bdg launch Chrome without --chrome-ws-url',
|
|
397
614
|
};
|
|
398
615
|
}
|
|
@@ -400,11 +617,12 @@ export function chromeWsUrlConflictError(options) {
|
|
|
400
617
|
* A numeric index given together with `--index`.
|
|
401
618
|
*
|
|
402
619
|
* @param index - The index argument
|
|
620
|
+
* @param command - The `bdg dom` subcommand it was given to, e.g. "layout"
|
|
403
621
|
*/
|
|
404
|
-
export function indexWithIndexOptionError(index) {
|
|
622
|
+
export function indexWithIndexOptionError(index, command) {
|
|
405
623
|
return {
|
|
406
624
|
message: `--index applies to a selector, but "${index}" is already an index from the last query`,
|
|
407
|
-
suggestion: `Use one: bdg dom
|
|
625
|
+
suggestion: `Use one: bdg dom ${command} ${index}, or bdg dom ${command} "<selector>" --index <n>`,
|
|
408
626
|
};
|
|
409
627
|
}
|
|
410
628
|
/** Problems with `bdg dom scroll` options, and how to fix each */
|
|
@@ -450,8 +668,20 @@ export function unknownKeyError(keyName, similar) {
|
|
|
450
668
|
: 'Keys: Enter, Tab, Escape, Space, Backspace, Delete, ArrowUp/Down/Left/Right, Home, End, PageUp, PageDown, F1-F12, a-z, A-Z, 0-9, !@#$%^&*()',
|
|
451
669
|
};
|
|
452
670
|
}
|
|
453
|
-
/**
|
|
454
|
-
|
|
671
|
+
/**
|
|
672
|
+
* `dom screenshot` given an element both as an argument and as an option,
|
|
673
|
+
* and they differ.
|
|
674
|
+
*
|
|
675
|
+
* @param option - The option and its value, e.g. `--selector #a`
|
|
676
|
+
* @param positional - The element argument
|
|
677
|
+
* @returns Message and suggestion
|
|
678
|
+
*/
|
|
679
|
+
export function conflictingTargetError(option, positional) {
|
|
680
|
+
return {
|
|
681
|
+
message: `${option} and the element argument "${positional}" name different elements`,
|
|
682
|
+
suggestion: 'Name the element once: bdg dom screenshot <path> <selector|index>',
|
|
683
|
+
};
|
|
684
|
+
}
|
|
455
685
|
/**
|
|
456
686
|
* Two options given together where one would be ignored.
|
|
457
687
|
*
|
|
@@ -484,7 +714,7 @@ export function invalidChromeFlagError(flag) {
|
|
|
484
714
|
export function sessionEndedDuringCommandError() {
|
|
485
715
|
return {
|
|
486
716
|
message: 'The session ended while the command was running',
|
|
487
|
-
suggestion:
|
|
717
|
+
suggestion: `Start a new session with: ${sessionCommand('bdg <url>')}`,
|
|
488
718
|
};
|
|
489
719
|
}
|
|
490
720
|
/**
|
|
@@ -545,18 +775,79 @@ export function noHistoryEntryError(direction) {
|
|
|
545
775
|
};
|
|
546
776
|
}
|
|
547
777
|
/**
|
|
548
|
-
*
|
|
778
|
+
* What to do about a submit that timed out waiting for a navigation, by how
|
|
779
|
+
* far its page request got: without one the form probably submits via fetch.
|
|
780
|
+
*
|
|
781
|
+
* @param document - The page request, if one was sent
|
|
782
|
+
* @returns Suggestion
|
|
783
|
+
*/
|
|
784
|
+
function submitNavigationSuggestion(document) {
|
|
785
|
+
if (document === undefined) {
|
|
786
|
+
return 'The form sent no page request, so it may not navigate (e.g. it submits via fetch); retry without --wait-navigation';
|
|
787
|
+
}
|
|
788
|
+
const details = `see it with ${sessionCommand('bdg network list --last 10')}`;
|
|
789
|
+
if (document.errorText !== undefined)
|
|
790
|
+
return `The page request failed; ${details}`;
|
|
791
|
+
if (document.status !== undefined && document.status >= 400) {
|
|
792
|
+
return `The server answered with an error; ${details}`;
|
|
793
|
+
}
|
|
794
|
+
if (document.status !== undefined) {
|
|
795
|
+
return 'The server answered without a new page; retry without --wait-navigation';
|
|
796
|
+
}
|
|
797
|
+
return 'The server has not answered the page request yet; retry with a larger --timeout';
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* What a timed-out submit was waiting on, in words: its page request when a
|
|
801
|
+
* navigation was awaited or the request had not loaded a page, otherwise the
|
|
802
|
+
* requests still running.
|
|
803
|
+
*
|
|
804
|
+
* @param waitNavigation - Whether a navigation was awaited
|
|
805
|
+
* @param blockers - The page request and the requests still running
|
|
806
|
+
* @returns e.g. `POST …/authenticate pending for 10s`; undefined when nothing is known
|
|
807
|
+
*/
|
|
808
|
+
function submitWaitDetail(waitNavigation, blockers) {
|
|
809
|
+
const { document, pending = [] } = blockers;
|
|
810
|
+
const loadedPage = document?.status !== undefined && document.status < 400 && document.errorText === undefined;
|
|
811
|
+
if (document && (waitNavigation || !loadedPage))
|
|
812
|
+
return documentRequestText(document);
|
|
813
|
+
if (pending.length === 0)
|
|
814
|
+
return undefined;
|
|
815
|
+
return `waiting on ${pendingRequestsText(pending, blockers.pendingCount ?? pending.length)}`;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* The form was submitted but the wait for its result timed out. The message
|
|
819
|
+
* names what it was waiting on: the page request the submit sent (pending,
|
|
820
|
+
* answered with an error, failed; waiting for a navigation, also one that
|
|
821
|
+
* answered), or else the requests still running.
|
|
549
822
|
*
|
|
550
823
|
* @param timeout - Timeout in ms
|
|
551
824
|
* @param waitNavigation - Whether a navigation was awaited
|
|
825
|
+
* @param blockers - The page request and the requests still running
|
|
826
|
+
* @returns e.g. `Form submitted, but timed out after 10000ms waiting for navigation: POST …/authenticate pending for 10s`
|
|
552
827
|
*/
|
|
553
|
-
export function submitTimeoutError(timeout, waitNavigation) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
828
|
+
export function submitTimeoutError(timeout, waitNavigation, blockers = {}) {
|
|
829
|
+
const detail = submitWaitDetail(waitNavigation, blockers);
|
|
830
|
+
const message = `Form submitted, but timed out after ${timeout}ms waiting for ${waitNavigation ? 'navigation' : 'network idle'}${detail ? `: ${detail}` : ''}`;
|
|
831
|
+
if (!waitNavigation) {
|
|
832
|
+
return {
|
|
833
|
+
message,
|
|
834
|
+
suggestion: 'Increase --timeout, or use --wait-network 0 to return right after submitting',
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
return { message, suggestion: submitNavigationSuggestion(blockers.document) };
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Warning of a submit whose navigation happened but whose network was still
|
|
841
|
+
* busy when the wait ran out (the new page is there; a slow script or
|
|
842
|
+
* tracker kept loading).
|
|
843
|
+
*
|
|
844
|
+
* @param timeout - Timeout in ms
|
|
845
|
+
* @param pending - Requests still in flight
|
|
846
|
+
* @returns Warning text
|
|
847
|
+
*/
|
|
848
|
+
export function submitNetworkBusyWarning(timeout, pending) {
|
|
849
|
+
const requests = pending === 1 ? '1 request' : `${pending} requests`;
|
|
850
|
+
return `The new page loaded, but ${requests} still had not finished after ${timeout}ms`;
|
|
560
851
|
}
|
|
561
852
|
/**
|
|
562
853
|
* No element has the given node id.
|
|
@@ -569,12 +860,70 @@ export function nodeIdNotFoundError(nodeId) {
|
|
|
569
860
|
suggestion: 'Get current node ids with "bdg dom query <selector>" or "bdg dom get <selector> --raw"',
|
|
570
861
|
};
|
|
571
862
|
}
|
|
863
|
+
/**
|
|
864
|
+
* The list an index refers to, in words.
|
|
865
|
+
*
|
|
866
|
+
* @param source - Where the index comes from
|
|
867
|
+
* @returns e.g. `index 0 of the last dom query "h3"`, `index 2 of the last dom form`
|
|
868
|
+
*/
|
|
869
|
+
export function indexSourceText(source) {
|
|
870
|
+
return `index ${source.index} of ${cachedListText(source)}`;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* The cached list an index refers to, in words.
|
|
874
|
+
*
|
|
875
|
+
* @param source - Where the index comes from
|
|
876
|
+
* @returns e.g. `the last dom query "h3"`
|
|
877
|
+
*/
|
|
878
|
+
function cachedListText(source) {
|
|
879
|
+
const query = source.query === undefined ? '' : ` "${source.query}"`;
|
|
880
|
+
return `the last ${source.command}${query}`;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* The command that refreshes the list an index refers to.
|
|
884
|
+
*
|
|
885
|
+
* @param source - Where the index comes from
|
|
886
|
+
* @returns e.g. `bdg dom query 'h3'`
|
|
887
|
+
*/
|
|
888
|
+
function refreshCommand(source) {
|
|
889
|
+
if (source.query === undefined)
|
|
890
|
+
return sessionCommand(`bdg ${source.command}`);
|
|
891
|
+
return sessionCommand(`bdg ${source.command} ${shellQuote(source.query)}`);
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* An element of a cross-origin iframe (from an a11y query) whose iframe
|
|
895
|
+
* could not be placed in the top-level viewport, so a mouse event or layout
|
|
896
|
+
* would land in the wrong place.
|
|
897
|
+
*
|
|
898
|
+
* @param problem - The iframe or element has no box, is rotated or skewed, or could not be read
|
|
899
|
+
* @param detail - Underlying error, if any
|
|
900
|
+
* @returns Message and suggestion
|
|
901
|
+
*/
|
|
902
|
+
export function frameMappingError(problem, detail) {
|
|
903
|
+
const reasons = {
|
|
904
|
+
'no-box': 'neither the element nor its iframe document has a box (hidden or removed)',
|
|
905
|
+
rotated: 'the iframe is rotated or skewed, which bdg cannot map',
|
|
906
|
+
unreadable: `it could not be measured${detail ? ` (${detail})` : ''}`,
|
|
907
|
+
};
|
|
908
|
+
return {
|
|
909
|
+
message: `Cannot place the element's cross-origin iframe in the page: ${reasons[problem]}`,
|
|
910
|
+
suggestion: `Act inside the frame instead: ${sessionCommand('bdg dom frames')}, then ${sessionCommand('bdg dom eval --frame <n> \'document.querySelector("…").click()\'')}`,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
572
913
|
/**
|
|
573
914
|
* A cached node is gone (page navigated or the element was removed).
|
|
574
915
|
*
|
|
575
|
-
* @param index - Index the user gave
|
|
916
|
+
* @param index - Index the user gave, if any
|
|
917
|
+
* @param source - The list the index refers to, when known
|
|
918
|
+
* @returns Message and suggestion
|
|
576
919
|
*/
|
|
577
|
-
export function staleNodeError(index) {
|
|
920
|
+
export function staleNodeError(index, source) {
|
|
921
|
+
if (source) {
|
|
922
|
+
return {
|
|
923
|
+
message: `The element at ${indexSourceText(source)} is no longer in the page (it was removed or the page navigated)`,
|
|
924
|
+
suggestion: `Re-run "${refreshCommand(source)}" to get fresh indices`,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
578
927
|
const element = index === undefined ? 'The element' : `The element at index ${index}`;
|
|
579
928
|
return {
|
|
580
929
|
message: `${element} is no longer in the page (it was removed or the page navigated)`,
|
|
@@ -582,25 +931,222 @@ export function staleNodeError(index) {
|
|
|
582
931
|
};
|
|
583
932
|
}
|
|
584
933
|
/**
|
|
585
|
-
*
|
|
934
|
+
* An index beyond the cached results.
|
|
935
|
+
*
|
|
936
|
+
* @param source - The index and the list it refers to
|
|
937
|
+
* @param count - Number of cached results
|
|
938
|
+
* @returns Message and suggestion
|
|
586
939
|
*/
|
|
587
|
-
export function
|
|
940
|
+
export function cachedIndexOutOfRangeError(source, count) {
|
|
941
|
+
const results = count === 1 ? '1 result' : `${count} results`;
|
|
588
942
|
return {
|
|
589
|
-
message: `
|
|
590
|
-
suggestion:
|
|
943
|
+
message: `Index ${source.index} is out of range for ${cachedListText(source)} (${results})`,
|
|
944
|
+
suggestion: count > 0
|
|
945
|
+
? `Use an index between 0 and ${count - 1}, or re-run "${refreshCommand(source)}"`
|
|
946
|
+
: `Re-run "${refreshCommand(source)}"`,
|
|
591
947
|
};
|
|
592
948
|
}
|
|
593
|
-
/** Where selectors do not reach (open shadow roots and same-origin iframes are searched) */
|
|
594
|
-
export const CROSS_ORIGIN_FRAMES_NOTE = 'Elements inside cross-origin iframes and closed shadow roots are not searched';
|
|
595
949
|
/**
|
|
596
|
-
*
|
|
950
|
+
* Note for a form command (fill, submit) whose index refers to results of
|
|
951
|
+
* another command and hit an element it cannot act on.
|
|
952
|
+
*
|
|
953
|
+
* @param source - The index and the list it refers to
|
|
954
|
+
* @param preview - What the cached element is, e.g. `h3 "Welcome"`
|
|
955
|
+
* @returns e.g. `index 0 refers to the last dom query results ("h3": h3 "Welcome"); run bdg dom form to target form fields by index`
|
|
597
956
|
*/
|
|
598
|
-
export function
|
|
957
|
+
export function otherIndexSourceNote(source, preview) {
|
|
958
|
+
const query = source.query === undefined ? '' : `"${source.query}"`;
|
|
959
|
+
const what = [query, preview].filter(Boolean).join(': ');
|
|
960
|
+
return `index ${source.index} refers to the last ${source.command} results${what ? ` (${what})` : ''}; run ${sessionCommand('bdg dom form')} to target form fields by index`;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Where selectors do not reach (open shadow roots and same-origin iframes are
|
|
964
|
+
* searched), and how to reach an element in a cross-origin iframe instead.
|
|
965
|
+
* When the page was checked, only what it has is named (nothing when it has
|
|
966
|
+
* neither cross-origin iframes nor embeds; closed shadow roots cannot be
|
|
967
|
+
* detected).
|
|
968
|
+
*
|
|
969
|
+
* @param selector - Selector that matched nothing
|
|
970
|
+
* @param unsearched - What the page holds, when it was checked
|
|
971
|
+
* @returns Note for "not found" suggestions (empty when nothing applies)
|
|
972
|
+
*/
|
|
973
|
+
export function unreachableElementsNote(selector, unsearched) {
|
|
974
|
+
const script = `document.querySelector(${JSON.stringify(selector)})`.replaceAll("'", `'\\''`);
|
|
975
|
+
const framesHelp = `For an element in a cross-origin iframe: ${sessionCommand('bdg dom frames')}, then ${sessionCommand(`bdg dom eval --frame <n> '${script}'`)}`;
|
|
976
|
+
if (!unsearched) {
|
|
977
|
+
return joinLines('Closed shadow roots, cross-origin iframes and <object>/<embed> documents are not searched.', framesHelp);
|
|
978
|
+
}
|
|
979
|
+
const places = [
|
|
980
|
+
unsearched.crossOriginFrames && 'cross-origin iframes',
|
|
981
|
+
unsearched.embeds && '<object>/<embed> documents',
|
|
982
|
+
].filter(Boolean);
|
|
983
|
+
if (places.length === 0)
|
|
984
|
+
return '';
|
|
985
|
+
return joinLines(`The page has ${places.join(' and ')}, which are not searched.`, unsearched.crossOriginFrames ? framesHelp : undefined);
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* "Did you mean" for a selector that is a single id or class.
|
|
989
|
+
*
|
|
990
|
+
* @param kind - Id or class
|
|
991
|
+
* @param names - Similar names on the page
|
|
992
|
+
* @returns e.g. `Did you mean #remove-backpack? (similar id on the page)`; empty without names
|
|
993
|
+
*/
|
|
994
|
+
export function similarSelectorsLine(kind, names) {
|
|
995
|
+
if (names.length === 0)
|
|
996
|
+
return '';
|
|
997
|
+
const sigil = kind === 'id' ? '#' : '.';
|
|
998
|
+
const what = kind === 'id' ? 'id' : 'class';
|
|
999
|
+
const plural = names.length === 1 ? what : kind === 'id' ? 'ids' : 'classes';
|
|
1000
|
+
return `Did you mean ${names.map((name) => sigil + name).join(', ')}? (similar ${plural} on the page)`;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* No nodes found for selector.
|
|
1004
|
+
*
|
|
1005
|
+
* @param selector - Selector as given
|
|
1006
|
+
* @param context - What the page says about it (hidden matches, readyState, unsearched content, similar names)
|
|
1007
|
+
*/
|
|
1008
|
+
export function noNodesFoundError(selector, context = {}) {
|
|
1009
|
+
const hiddenMatches = context.hidden ?? 0;
|
|
1010
|
+
const hidden = hiddenMatches > 0
|
|
1011
|
+
? hiddenMatches === 1
|
|
1012
|
+
? '1 element matches without :visible but is hidden (check with bdg dom layout). '
|
|
1013
|
+
: `${hiddenMatches} elements match without :visible but are hidden (check with bdg dom layout). `
|
|
1014
|
+
: '';
|
|
1015
|
+
const note = unreachableElementsNote(selector, context.unsearched);
|
|
599
1016
|
return {
|
|
600
1017
|
message: `No nodes found matching "${selector}"`,
|
|
601
|
-
suggestion:
|
|
1018
|
+
suggestion: withLoadingHint(joinLines(context.similar, `${hidden}Verify the CSS selector is correct.`, note ? note : undefined), context.readyState, selector),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Quote a value for a POSIX shell command line.
|
|
1023
|
+
*
|
|
1024
|
+
* @param value - Value
|
|
1025
|
+
* @returns The value in single quotes
|
|
1026
|
+
*/
|
|
1027
|
+
function shellQuote(value) {
|
|
1028
|
+
return `'${value.split("'").join(`'\\''`)}'`;
|
|
1029
|
+
}
|
|
1030
|
+
/** Edge of the page in each direction */
|
|
1031
|
+
const SCROLL_EDGES = { up: 'top', down: 'bottom', left: 'left edge', right: 'right edge' };
|
|
1032
|
+
/**
|
|
1033
|
+
* Warning of `bdg dom scroll` when the page did not move, with the
|
|
1034
|
+
* still-loading hint while the document is not complete (it may grow).
|
|
1035
|
+
*
|
|
1036
|
+
* @param effect - Why nothing scrolled
|
|
1037
|
+
* @returns e.g. `Nothing to scroll: the document is no taller than the viewport (993px)`
|
|
1038
|
+
*/
|
|
1039
|
+
export function scrollNoEffectWarning(effect) {
|
|
1040
|
+
const vertical = effect.direction === 'up' || effect.direction === 'down';
|
|
1041
|
+
const reasons = {
|
|
1042
|
+
'too-small': `Nothing to scroll: the document is no ${vertical ? 'taller' : 'wider'} than the viewport (${effect.viewport}px)`,
|
|
1043
|
+
'at-edge': `Nothing scrolled: the page is already at the ${SCROLL_EDGES[effect.direction]}`,
|
|
1044
|
+
locked: 'Nothing scrolled although the page is larger than the viewport; its scrolling may be locked (e.g. overflow: hidden while a dialog is open)',
|
|
1045
|
+
};
|
|
1046
|
+
const loading = effect.readyState === 'complete' ? undefined : pageStillLoadingHint(effect.readyState);
|
|
1047
|
+
return [reasons[effect.reason], loading].filter(Boolean).join('. ');
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Hint for something not found while the page has not finished loading.
|
|
1051
|
+
*
|
|
1052
|
+
* @param readyState - The page's `document.readyState` (not `complete`)
|
|
1053
|
+
* @param selector - Selector that matched nothing, if any
|
|
1054
|
+
* @returns e.g. `The page is still loading (document.readyState: loading); wait for the element with: bdg dom wait '#login'`
|
|
1055
|
+
*/
|
|
1056
|
+
export function pageStillLoadingHint(readyState, selector) {
|
|
1057
|
+
const wait = selector !== undefined && selector.trim() !== ''
|
|
1058
|
+
? `wait for the element with: ${sessionCommand(`bdg dom wait ${shellQuote(selector)}`)}`
|
|
1059
|
+
: `wait for it with: ${sessionCommand('bdg dom wait --load')}`;
|
|
1060
|
+
return `The page is still loading (document.readyState: ${readyState}); ${wait}`;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Put the still-loading hint before a "not found" suggestion when the page
|
|
1064
|
+
* has not finished loading.
|
|
1065
|
+
*
|
|
1066
|
+
* @param suggestion - Suggestion of the error
|
|
1067
|
+
* @param readyState - The page's `document.readyState`, if known
|
|
1068
|
+
* @param selector - Selector that matched nothing, if any
|
|
1069
|
+
* @returns The suggestion, with the hint first while the page loads
|
|
1070
|
+
*/
|
|
1071
|
+
export function withLoadingHint(suggestion, readyState, selector) {
|
|
1072
|
+
if (readyState === undefined || readyState === 'complete')
|
|
1073
|
+
return suggestion;
|
|
1074
|
+
return joinLines(pageStillLoadingHint(readyState, selector), suggestion || undefined);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* `bdg dom wait` without a selector or --load, or with --text/--gone but no selector.
|
|
1078
|
+
*
|
|
1079
|
+
* @returns Message and suggestion
|
|
1080
|
+
*/
|
|
1081
|
+
export function waitTargetRequiredError() {
|
|
1082
|
+
return {
|
|
1083
|
+
message: 'dom wait needs a selector (--text and --gone apply to its matches), or --load',
|
|
1084
|
+
suggestion: `e.g. ${sessionCommand("bdg dom wait '#result' --visible")}, ${sessionCommand("bdg dom wait body --text 'Welcome'")} or ${sessionCommand('bdg dom wait --load')}`,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* `bdg dom wait` that ran out of time: what it waited for and what the page
|
|
1089
|
+
* showed last, with a next step that fits.
|
|
1090
|
+
*
|
|
1091
|
+
* @param condition - What was waited for
|
|
1092
|
+
* @param snapshot - Last thing the page reported (none when it never answered)
|
|
1093
|
+
* @param timeoutMs - The --timeout
|
|
1094
|
+
* @returns Message and suggestion
|
|
1095
|
+
*/
|
|
1096
|
+
export function waitTimeoutError(condition, snapshot, timeoutMs) {
|
|
1097
|
+
const seen = snapshot
|
|
1098
|
+
? `last seen: ${waitSnapshotSummary(snapshot, condition)}`
|
|
1099
|
+
: 'the page did not answer';
|
|
1100
|
+
return {
|
|
1101
|
+
message: `Timed out after ${formatDuration(timeoutMs)} waiting for ${waitGoal(condition)} (${seen})`,
|
|
1102
|
+
suggestion: waitTimeoutSuggestion(condition, snapshot),
|
|
602
1103
|
};
|
|
603
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* What `bdg dom wait` waits for, as a phrase.
|
|
1107
|
+
*
|
|
1108
|
+
* @param condition - What is waited for
|
|
1109
|
+
* @returns e.g. `#finish to be visible` or `the page to load`
|
|
1110
|
+
*/
|
|
1111
|
+
function waitGoal(condition) {
|
|
1112
|
+
if (condition.selector === undefined)
|
|
1113
|
+
return 'the page to load';
|
|
1114
|
+
const state = condition.gone
|
|
1115
|
+
? condition.visible
|
|
1116
|
+
? 'to be hidden'
|
|
1117
|
+
: 'to be gone'
|
|
1118
|
+
: condition.visible
|
|
1119
|
+
? 'to be visible'
|
|
1120
|
+
: 'to appear';
|
|
1121
|
+
return `${waitTargetLabel(condition)} ${state}${condition.load ? ' and the page to load' : ''}`;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Next step after a `bdg dom wait` timeout.
|
|
1125
|
+
*
|
|
1126
|
+
* @param condition - What was waited for
|
|
1127
|
+
* @param snapshot - Last thing the page reported
|
|
1128
|
+
* @returns Suggestion
|
|
1129
|
+
*/
|
|
1130
|
+
function waitTimeoutSuggestion(condition, snapshot) {
|
|
1131
|
+
const more = 'allow more time with --timeout <ms>';
|
|
1132
|
+
const selector = condition.selector;
|
|
1133
|
+
if (snapshot && snapshot.readyState !== 'complete') {
|
|
1134
|
+
return `The page is still loading; see the requests it waits on with ${sessionCommand('bdg peek')}, or ${more}`;
|
|
1135
|
+
}
|
|
1136
|
+
if (!snapshot ||
|
|
1137
|
+
selector === undefined ||
|
|
1138
|
+
condition.gone ||
|
|
1139
|
+
countedMatches(snapshot, condition) > 0) {
|
|
1140
|
+
return `Check the page with ${sessionCommand('bdg peek')}, or ${more}`;
|
|
1141
|
+
}
|
|
1142
|
+
if (snapshot.count === 0) {
|
|
1143
|
+
return `Check the selector with ${sessionCommand(`bdg dom query ${shellQuote(selector)}`)}, or ${more}`;
|
|
1144
|
+
}
|
|
1145
|
+
if (snapshot.textCount === 0) {
|
|
1146
|
+
return `The matches do not contain the text; see what they say with ${sessionCommand(`bdg dom query ${shellQuote(selector)}`)}`;
|
|
1147
|
+
}
|
|
1148
|
+
return `The matches are hidden; see why with ${sessionCommand(`bdg dom layout ${shellQuote(selector)}`)}, or ${more}`;
|
|
1149
|
+
}
|
|
604
1150
|
/**
|
|
605
1151
|
* Element not visible/rendered.
|
|
606
1152
|
*/
|
|
@@ -643,7 +1189,7 @@ export function eitherArgumentRequiredError(arg1, arg2, example) {
|
|
|
643
1189
|
export function invalidQueryPatternError(pattern) {
|
|
644
1190
|
return {
|
|
645
1191
|
message: 'Query pattern must specify at least one field',
|
|
646
|
-
suggestion: `Received: "${pattern}". Try: bdg dom a11y query
|
|
1192
|
+
suggestion: `Received: "${pattern}". Try: bdg dom a11y query role=button, or ${A11Y_NAME_QUERY_EXAMPLE}`,
|
|
647
1193
|
};
|
|
648
1194
|
}
|
|
649
1195
|
/**
|
|
@@ -683,11 +1229,17 @@ export function singleFileInputError(count) {
|
|
|
683
1229
|
* Unknown field in an a11y query pattern.
|
|
684
1230
|
*
|
|
685
1231
|
* @param field - The unrecognized key
|
|
686
|
-
|
|
687
|
-
|
|
1232
|
+
* @param similar - A known field it looks like a typo of
|
|
1233
|
+
* @param value - The `name=…`/`description=…` field that absorbed it
|
|
1234
|
+
*/
|
|
1235
|
+
export function unknownQueryFieldError(field, similar, value) {
|
|
1236
|
+
const usage = "Use role, name or description, e.g. bdg dom a11y query 'role=button name=Sign in'. Quote the whole pattern for the shell; a name with spaces or colons goes last ('name=E-mail address:') or in inner quotes ('name=\"Role: admin\" role=textbox')";
|
|
1237
|
+
if (!similar)
|
|
1238
|
+
return { message: `Unknown query field: "${field}"`, suggestion: usage };
|
|
1239
|
+
const [key = '', ...text] = (value ?? '').split('=');
|
|
688
1240
|
return {
|
|
689
|
-
message: `Unknown query field: "${field}"`,
|
|
690
|
-
suggestion:
|
|
1241
|
+
message: `Unknown query field: "${field}" (did you mean "${similar}"?)`,
|
|
1242
|
+
suggestion: `Fix the field name, e.g. ${similar}=…; if the ${key} really contains "${field}:", put it in inner quotes: '${key}="${text.join('=')}"'`,
|
|
691
1243
|
};
|
|
692
1244
|
}
|
|
693
1245
|
/**
|
|
@@ -720,6 +1272,42 @@ export function fillableElementNotFoundError(selector) {
|
|
|
720
1272
|
suggestion: 'Verify the selector matches a fillable element (input, textarea, select)',
|
|
721
1273
|
};
|
|
722
1274
|
}
|
|
1275
|
+
/** Appended to the element type when an action went to a label's control. */
|
|
1276
|
+
export const VIA_LABEL_SUFFIX = ' (via label)';
|
|
1277
|
+
/** Placeholder for the shell-quoted `name=…` field in {@link LABEL_WITHOUT_CONTROL}. */
|
|
1278
|
+
export const NAME_QUERY_PLACEHOLDER = '{nameQuery}';
|
|
1279
|
+
/**
|
|
1280
|
+
* Filling a `<label>` that has no form control (used by the page script,
|
|
1281
|
+
* which puts the label's quoted `name=<text>` in place of
|
|
1282
|
+
* {@link NAME_QUERY_PLACEHOLDER}).
|
|
1283
|
+
*/
|
|
1284
|
+
export const LABEL_WITHOUT_CONTROL = {
|
|
1285
|
+
message: 'Element is not fillable (a <label> not associated with a form control)',
|
|
1286
|
+
suggestion: `Find the field by its accessible name: bdg dom a11y query ${NAME_QUERY_PLACEHOLDER}, or list the form fields: bdg dom form`,
|
|
1287
|
+
};
|
|
1288
|
+
/**
|
|
1289
|
+
* Why `dom fill` refused an element (used by the page script, which adds the
|
|
1290
|
+
* cause in parentheses, e.g. `The element is read-only (contenteditable="false")`).
|
|
1291
|
+
* All are invalid targets (exit 81), like any element that is not fillable.
|
|
1292
|
+
*/
|
|
1293
|
+
export const FILL_REFUSALS = {
|
|
1294
|
+
disabled: {
|
|
1295
|
+
message: 'The element is disabled',
|
|
1296
|
+
suggestion: 'Enable the field first (it may depend on another input)',
|
|
1297
|
+
},
|
|
1298
|
+
readOnly: {
|
|
1299
|
+
message: 'The element is read-only',
|
|
1300
|
+
suggestion: 'A user cannot change it either; the page has to make it editable first',
|
|
1301
|
+
},
|
|
1302
|
+
inert: {
|
|
1303
|
+
message: 'The element is inert',
|
|
1304
|
+
suggestion: 'The page made it non-interactive (often behind a dialog); close what covers it first',
|
|
1305
|
+
},
|
|
1306
|
+
notFillable: {
|
|
1307
|
+
message: 'Element is not fillable',
|
|
1308
|
+
suggestion: 'Only input, textarea, select, and contenteditable elements can be filled',
|
|
1309
|
+
},
|
|
1310
|
+
};
|
|
723
1311
|
/**
|
|
724
1312
|
* Clickable element not found.
|
|
725
1313
|
*/
|
|
@@ -760,15 +1348,23 @@ export function scriptTimeoutError(timeoutMs) {
|
|
|
760
1348
|
};
|
|
761
1349
|
}
|
|
762
1350
|
/**
|
|
763
|
-
* The page was kept busy by a script (e.g. a loop started
|
|
764
|
-
* bdg terminated it.
|
|
1351
|
+
* The page (or an iframe) was kept busy by a script (e.g. a loop started
|
|
1352
|
+
* from a timer) and bdg terminated it.
|
|
765
1353
|
*
|
|
766
1354
|
* @param timeoutMs - Time waited
|
|
1355
|
+
* @param scope - What was busy: the page, or the iframe a command ran in
|
|
767
1356
|
*/
|
|
768
|
-
export function pageBusyError(timeoutMs) {
|
|
1357
|
+
export function pageBusyError(timeoutMs, scope = 'page', recovered = true) {
|
|
1358
|
+
const busy = `The ${scope} was busy for ${Math.round(timeoutMs / 1000)}s (a script kept it running)`;
|
|
1359
|
+
if (!recovered) {
|
|
1360
|
+
return {
|
|
1361
|
+
message: `${busy} and its scripts could not be stopped`,
|
|
1362
|
+
suggestion: 'Retry in a moment; if it stays busy, reload the page: bdg page reload',
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
769
1365
|
return {
|
|
770
|
-
message:
|
|
771
|
-
suggestion:
|
|
1366
|
+
message: `${busy}, so its scripts were terminated`,
|
|
1367
|
+
suggestion: `The ${scope} is usable again; re-run the command`,
|
|
772
1368
|
};
|
|
773
1369
|
}
|
|
774
1370
|
/**
|
|
@@ -799,8 +1395,8 @@ export function emptyScriptError() {
|
|
|
799
1395
|
*/
|
|
800
1396
|
export function promiseTimeoutError(timeoutMs) {
|
|
801
1397
|
return {
|
|
802
|
-
message: `The
|
|
803
|
-
suggestion: '
|
|
1398
|
+
message: `The awaited promise did not settle within ${Math.round(timeoutMs / 1000)}s`,
|
|
1399
|
+
suggestion: 'Nothing was busy; check that the promise resolves or rejects, or race it with a timeout in the script',
|
|
804
1400
|
};
|
|
805
1401
|
}
|
|
806
1402
|
/** How to see the frames `--frame` accepts */
|
|
@@ -811,7 +1407,7 @@ const LIST_FRAMES_HINT = 'List frames: bdg dom frames';
|
|
|
811
1407
|
export function emptyFrameError() {
|
|
812
1408
|
return {
|
|
813
1409
|
message: 'The frame is empty',
|
|
814
|
-
suggestion: `Pass an index, a name/id attribute, or part of the URL. ${LIST_FRAMES_HINT}`,
|
|
1410
|
+
suggestion: `Pass an index, a name/id attribute, or part of the name, id or URL. ${LIST_FRAMES_HINT}`,
|
|
815
1411
|
};
|
|
816
1412
|
}
|
|
817
1413
|
/**
|
|
@@ -838,20 +1434,90 @@ export function frameNotFoundError(query, frames) {
|
|
|
838
1434
|
export function ambiguousFrameError(query, candidates) {
|
|
839
1435
|
return {
|
|
840
1436
|
message: `Frame "${query}" matches ${candidates.length} frames`,
|
|
841
|
-
suggestion: joinLines('Pick one by index or a longer part of the URL:', ...candidates.map((frame) => ` ${frameLabel(frame)}`)),
|
|
1437
|
+
suggestion: joinLines('Pick one by index, or a longer part of the name, id or URL:', ...candidates.map((frame) => ` ${frameLabel(frame)}`)),
|
|
842
1438
|
};
|
|
843
1439
|
}
|
|
844
1440
|
/**
|
|
845
|
-
* An iframe without a JavaScript context (still loading, or
|
|
1441
|
+
* An iframe without a JavaScript context (still loading, or gone).
|
|
846
1442
|
*
|
|
847
1443
|
* @param url - Frame URL
|
|
848
1444
|
*/
|
|
849
1445
|
export function frameNotReadyError(url) {
|
|
850
1446
|
return {
|
|
851
|
-
message: `The frame has no JavaScript context: ${url}`,
|
|
852
|
-
suggestion: `Wait for it to load and retry
|
|
1447
|
+
message: `The frame has no JavaScript context: ${frameUrlLabel(url)}`,
|
|
1448
|
+
suggestion: `Wait for it to load and retry, or re-run bdg dom frames: the frame may no longer exist`,
|
|
853
1449
|
};
|
|
854
1450
|
}
|
|
1451
|
+
/**
|
|
1452
|
+
* The iframe a `dom eval --frame` script ran in navigated before it finished.
|
|
1453
|
+
*
|
|
1454
|
+
* @param url - Frame URL when the script started
|
|
1455
|
+
*/
|
|
1456
|
+
export function frameNavigatedDuringEvalError(url) {
|
|
1457
|
+
return {
|
|
1458
|
+
message: `The frame navigated while the script ran: ${frameUrlLabel(url)}`,
|
|
1459
|
+
suggestion: 'The result was lost with the old document; re-run the script once the frame has loaded. List frames: bdg dom frames',
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* The iframe a `dom eval --frame` script ran in was removed (or vanished
|
|
1464
|
+
* before it started).
|
|
1465
|
+
*
|
|
1466
|
+
* @param url - Frame URL when the script started
|
|
1467
|
+
*/
|
|
1468
|
+
export function frameRemovedDuringEvalError(url) {
|
|
1469
|
+
return {
|
|
1470
|
+
message: `The frame was removed before the script finished: ${frameUrlLabel(url)}`,
|
|
1471
|
+
suggestion: 'The frame no longer exists; re-run bdg dom frames to see the current ones',
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* The page (its tab) was closed while a `dom eval` script ran.
|
|
1476
|
+
*/
|
|
1477
|
+
export function pageClosedDuringEvalError() {
|
|
1478
|
+
return {
|
|
1479
|
+
message: 'The page was closed while the script ran',
|
|
1480
|
+
suggestion: 'Its tab is gone; start a new session with: bdg <url>',
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* The iframe a `dom eval --frame` script ran in went away, and bdg could not
|
|
1485
|
+
* tell whether it navigated or was removed.
|
|
1486
|
+
*
|
|
1487
|
+
* @param url - Frame URL when the script started
|
|
1488
|
+
*/
|
|
1489
|
+
export function frameLostDuringEvalError(url) {
|
|
1490
|
+
return {
|
|
1491
|
+
message: `The frame navigated or was removed while the script ran: ${frameUrlLabel(url)}`,
|
|
1492
|
+
suggestion: 'Re-run bdg dom frames to see the current frames, then the script',
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* The page navigated while a `dom eval` script ran (e.g. it set
|
|
1497
|
+
* `location.href` and then awaited).
|
|
1498
|
+
*/
|
|
1499
|
+
export function pageNavigatedDuringEvalError() {
|
|
1500
|
+
return {
|
|
1501
|
+
message: 'The page navigated while the script ran',
|
|
1502
|
+
suggestion: 'The result was lost with the old document; re-run the script on the new page (navigate with: bdg page navigate <url>)',
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
/** `Identifier 'x' has already been declared` */
|
|
1506
|
+
const REDECLARED_PATTERN = /^SyntaxError: Identifier '([\w$]+)' has already been declared/;
|
|
1507
|
+
/**
|
|
1508
|
+
* Tip for a top-level `const`/`let` whose name the page or the script itself
|
|
1509
|
+
* already declares (`dom eval` runs like the console: names persist between
|
|
1510
|
+
* calls, and a page's own top-level `let`/`const`/`class` cannot be declared again).
|
|
1511
|
+
*
|
|
1512
|
+
* @param name - The redeclared identifier
|
|
1513
|
+
* @returns Tip
|
|
1514
|
+
*/
|
|
1515
|
+
function redeclarationTip(name) {
|
|
1516
|
+
return [
|
|
1517
|
+
`'${name}' is already declared at the top level, by the page or earlier in this script.`,
|
|
1518
|
+
`Rename it, or wrap the script in a block so its names stay local: ${sessionCommand(`bdg dom eval '{ const ${name} = ...; ${name} }'`)}`,
|
|
1519
|
+
].join('\n');
|
|
1520
|
+
}
|
|
855
1521
|
/**
|
|
856
1522
|
* Script execution error with shell quote detection.
|
|
857
1523
|
*
|
|
@@ -869,7 +1535,8 @@ export function scriptExecutionError(errorMessage, receivedScript) {
|
|
|
869
1535
|
suggestion: 'Check JavaScript syntax and ensure the expression is valid',
|
|
870
1536
|
};
|
|
871
1537
|
}
|
|
872
|
-
const quoteCheck = detectScriptQuoteDamage(receivedScript);
|
|
1538
|
+
const quoteCheck = detectScriptQuoteDamage(receivedScript, errorMessage);
|
|
1539
|
+
const redeclared = REDECLARED_PATTERN.exec(errorMessage)?.[1];
|
|
873
1540
|
const truncatedScript = receivedScript.length > 100 ? receivedScript.slice(0, 100) + '...' : receivedScript;
|
|
874
1541
|
const lines = [];
|
|
875
1542
|
lines.push(`Script received: ${truncatedScript}`);
|
|
@@ -884,6 +1551,10 @@ export function scriptExecutionError(errorMessage, receivedScript) {
|
|
|
884
1551
|
lines.push(quoteCheck.suggestion);
|
|
885
1552
|
}
|
|
886
1553
|
}
|
|
1554
|
+
else if (redeclared) {
|
|
1555
|
+
lines.push('');
|
|
1556
|
+
lines.push(redeclarationTip(redeclared));
|
|
1557
|
+
}
|
|
887
1558
|
else if (/^SyntaxError\b/.test(errorMessage)) {
|
|
888
1559
|
lines.push('');
|
|
889
1560
|
lines.push('Tips:');
|
|
@@ -939,10 +1610,33 @@ export function internalError(context) {
|
|
|
939
1610
|
/**
|
|
940
1611
|
* No forms found on page.
|
|
941
1612
|
*/
|
|
942
|
-
export function noFormsFoundError() {
|
|
1613
|
+
export function noFormsFoundError(readyState) {
|
|
1614
|
+
const check = 'Check if forms exist with: bdg dom query "form, input, [role=textbox]" or inspect the page manually';
|
|
1615
|
+
if (readyState === undefined || readyState === 'complete') {
|
|
1616
|
+
return { message: 'No forms discovered on the page', suggestion: check };
|
|
1617
|
+
}
|
|
1618
|
+
return {
|
|
1619
|
+
message: 'No forms discovered on the page yet; it is still loading',
|
|
1620
|
+
suggestion: joinLines(pageStillLoadingHint(readyState), check),
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* The form discovery script threw.
|
|
1625
|
+
*
|
|
1626
|
+
* @param detail - What it threw, e.g. `TypeError: Cannot read properties of null`
|
|
1627
|
+
* @param readyState - The page's `document.readyState`, when known
|
|
1628
|
+
* @returns Message and suggestion (the still-loading hint while the page loads)
|
|
1629
|
+
*/
|
|
1630
|
+
export function formDiscoveryFailedError(detail, readyState) {
|
|
1631
|
+
if (readyState !== undefined && readyState !== 'complete') {
|
|
1632
|
+
return {
|
|
1633
|
+
message: `Form discovery failed while the page is still loading (${detail})`,
|
|
1634
|
+
suggestion: pageStillLoadingHint(readyState),
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
943
1637
|
return {
|
|
944
|
-
message:
|
|
945
|
-
suggestion:
|
|
1638
|
+
message: `Form discovery failed: ${detail}`,
|
|
1639
|
+
suggestion: `The page changed or threw while it was read; retry, or look at the fields with ${sessionCommand('bdg dom query "form, input, select, textarea"')}`,
|
|
946
1640
|
};
|
|
947
1641
|
}
|
|
948
1642
|
/**
|