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.
Files changed (251) hide show
  1. package/README.md +4 -1
  2. package/dist/cdp/schema.d.ts +4 -1
  3. package/dist/cdp/schema.js +48 -7
  4. package/dist/commands/cdp.js +3 -2
  5. package/dist/commands/cleanup.d.ts +11 -0
  6. package/dist/commands/cleanup.js +161 -57
  7. package/dist/commands/console.d.ts +20 -1
  8. package/dist/commands/console.js +57 -17
  9. package/dist/commands/details.js +3 -2
  10. package/dist/commands/dom/DomElementResolver.d.ts +10 -3
  11. package/dist/commands/dom/DomElementResolver.js +35 -17
  12. package/dist/commands/dom/a11y.d.ts +10 -0
  13. package/dist/commands/dom/a11y.js +27 -5
  14. package/dist/commands/dom/eval.d.ts +3 -1
  15. package/dist/commands/dom/eval.js +29 -4
  16. package/dist/commands/dom/form.js +16 -62
  17. package/dist/commands/dom/formInteraction.js +152 -113
  18. package/dist/commands/dom/formSummary.d.ts +49 -0
  19. package/dist/commands/dom/formSummary.js +180 -0
  20. package/dist/commands/dom/frames.d.ts +2 -1
  21. package/dist/commands/dom/frames.js +17 -2
  22. package/dist/commands/dom/get.d.ts +6 -5
  23. package/dist/commands/dom/get.js +92 -82
  24. package/dist/commands/dom/helpers/index.d.ts +1 -1
  25. package/dist/commands/dom/helpers/index.js +1 -1
  26. package/dist/commands/dom/helpers/query.d.ts +44 -17
  27. package/dist/commands/dom/helpers/query.js +244 -97
  28. package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
  29. package/dist/commands/dom/helpers/runElementCommand.js +97 -30
  30. package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
  31. package/dist/commands/dom/helpers/screenshot.js +164 -49
  32. package/dist/commands/dom/index.d.ts +3 -1
  33. package/dist/commands/dom/index.js +16 -6
  34. package/dist/commands/dom/layout.d.ts +14 -0
  35. package/dist/commands/dom/layout.js +54 -0
  36. package/dist/commands/dom/listeners.d.ts +5 -1
  37. package/dist/commands/dom/listeners.js +13 -3
  38. package/dist/commands/dom/query.js +2 -3
  39. package/dist/commands/dom/screenshot.d.ts +12 -2
  40. package/dist/commands/dom/screenshot.js +27 -3
  41. package/dist/commands/dom/semanticUtils.d.ts +6 -13
  42. package/dist/commands/dom/semanticUtils.js +15 -19
  43. package/dist/commands/dom/wait.d.ts +13 -0
  44. package/dist/commands/dom/wait.js +83 -0
  45. package/dist/commands/helpJson.js +2 -2
  46. package/dist/commands/network/list.js +4 -11
  47. package/dist/commands/optionBehaviors.js +112 -21
  48. package/dist/commands/page.d.ts +2 -1
  49. package/dist/commands/page.js +41 -5
  50. package/dist/commands/peek.js +4 -11
  51. package/dist/commands/sessions.d.ts +8 -0
  52. package/dist/commands/sessions.js +19 -0
  53. package/dist/commands/shared/CommandRunner.js +4 -4
  54. package/dist/commands/shared/dataFetcher.js +2 -2
  55. package/dist/commands/shared/followMode.d.ts +21 -1
  56. package/dist/commands/shared/followMode.js +29 -2
  57. package/dist/commands/shared/handleValidationError.d.ts +2 -2
  58. package/dist/commands/shared/handleValidationError.js +12 -3
  59. package/dist/commands/shared/optionTypes.d.ts +40 -5
  60. package/dist/commands/shared/startHelpers.js +12 -3
  61. package/dist/commands/shared/validation.d.ts +3 -2
  62. package/dist/commands/shared/validation.js +4 -3
  63. package/dist/commands/start.d.ts +63 -0
  64. package/dist/commands/start.js +115 -15
  65. package/dist/commands/status.js +29 -7
  66. package/dist/commands/stop.js +7 -6
  67. package/dist/commands/tail.js +4 -11
  68. package/dist/commands/types.d.ts +2 -0
  69. package/dist/commands.js +2 -0
  70. package/dist/connection/chromeIdentity.d.ts +65 -0
  71. package/dist/connection/chromeIdentity.js +143 -0
  72. package/dist/connection/launcher/profilePreferences.d.ts +47 -0
  73. package/dist/connection/launcher/profilePreferences.js +151 -0
  74. package/dist/connection/launcher.d.ts +21 -2
  75. package/dist/connection/launcher.js +42 -16
  76. package/dist/connection/portReservation.d.ts +14 -4
  77. package/dist/connection/portReservation.js +21 -6
  78. package/dist/connection/startupExit.d.ts +8 -0
  79. package/dist/connection/startupExit.js +15 -6
  80. package/dist/constants.d.ts +6 -2
  81. package/dist/constants.js +9 -2
  82. package/dist/daemon/SessionController.js +23 -7
  83. package/dist/daemon/errors.d.ts +1 -1
  84. package/dist/daemon/errors.js +1 -1
  85. package/dist/daemon/launcher.d.ts +2 -1
  86. package/dist/daemon/launcher.js +5 -6
  87. package/dist/daemon/server/SocketServer.js +1 -2
  88. package/dist/daemon/session/Session.d.ts +13 -0
  89. package/dist/daemon/session/Session.js +57 -8
  90. package/dist/daemon/session/chromeConnection.d.ts +9 -0
  91. package/dist/daemon/session/chromeConnection.js +45 -8
  92. package/dist/daemon/session/commandRegistry.js +52 -62
  93. package/dist/daemon/session/interactions.d.ts +35 -9
  94. package/dist/daemon/session/interactions.js +36 -9
  95. package/dist/daemon/session/triggeredRequests.d.ts +67 -0
  96. package/dist/daemon/session/triggeredRequests.js +157 -0
  97. package/dist/daemon/session/types.d.ts +5 -1
  98. package/dist/daemon.js +5393 -1600
  99. package/dist/errors/messages.d.ts +387 -24
  100. package/dist/errors/messages.js +761 -67
  101. package/dist/index.js +3976 -1558
  102. package/dist/ipc/client.d.ts +12 -1
  103. package/dist/ipc/client.js +22 -3
  104. package/dist/ipc/protocol/commands.d.ts +89 -4
  105. package/dist/ipc/protocol/commands.js +2 -0
  106. package/dist/ipc/protocol/domTypes.d.ts +258 -7
  107. package/dist/ipc/session/lifecycle.d.ts +8 -1
  108. package/dist/ipc/session/queries.d.ts +5 -1
  109. package/dist/ipc/session/types.d.ts +5 -0
  110. package/dist/ipc/transport/index.d.ts +2 -1
  111. package/dist/ipc/transport/index.js +2 -2
  112. package/dist/runtime/dom/actionEffects.d.ts +106 -0
  113. package/dist/runtime/dom/actionEffects.js +256 -0
  114. package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -0
  115. package/dist/runtime/dom/actionEffectsScripts.js +234 -0
  116. package/dist/runtime/dom/elementGeometry.d.ts +170 -0
  117. package/dist/runtime/dom/elementGeometry.js +553 -0
  118. package/dist/runtime/dom/elementInfo.d.ts +77 -0
  119. package/dist/runtime/dom/elementInfo.js +191 -0
  120. package/dist/runtime/dom/evalHelpers.d.ts +51 -6
  121. package/dist/runtime/dom/evalHelpers.js +136 -26
  122. package/dist/runtime/dom/eventListeners.d.ts +2 -1
  123. package/dist/runtime/dom/eventListeners.js +174 -47
  124. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  125. package/dist/runtime/dom/formDiscovery.js +116 -16
  126. package/dist/runtime/dom/formFillHelpers/fill.d.ts +10 -0
  127. package/dist/runtime/dom/formFillHelpers/fill.js +125 -10
  128. package/dist/runtime/dom/formFillHelpers/index.d.ts +2 -2
  129. package/dist/runtime/dom/formFillHelpers/index.js +2 -2
  130. package/dist/runtime/dom/formFillHelpers/pressKey.js +14 -3
  131. package/dist/runtime/dom/formFillHelpers/scroll.d.ts +3 -0
  132. package/dist/runtime/dom/formFillHelpers/scroll.js +60 -18
  133. package/dist/runtime/dom/formFillHelpers/shared.d.ts +17 -0
  134. package/dist/runtime/dom/formFillHelpers/shared.js +25 -1
  135. package/dist/runtime/dom/formFillHelpers/stability.d.ts +20 -6
  136. package/dist/runtime/dom/formFillHelpers/stability.js +50 -19
  137. package/dist/runtime/dom/formSubmitHelpers.d.ts +3 -0
  138. package/dist/runtime/dom/formSubmitHelpers.js +89 -15
  139. package/dist/runtime/dom/frameLayout.d.ts +60 -0
  140. package/dist/runtime/dom/frameLayout.js +140 -0
  141. package/dist/runtime/dom/frameOrigin.d.ts +50 -0
  142. package/dist/runtime/dom/frameOrigin.js +62 -0
  143. package/dist/runtime/dom/frameScopedConnection.d.ts +92 -0
  144. package/dist/runtime/dom/frameScopedConnection.js +252 -0
  145. package/dist/runtime/dom/frameSelection.d.ts +1 -1
  146. package/dist/runtime/dom/frameSelection.js +2 -2
  147. package/dist/runtime/dom/frames.d.ts +25 -2
  148. package/dist/runtime/dom/frames.js +202 -63
  149. package/dist/runtime/dom/layout.d.ts +67 -0
  150. package/dist/runtime/dom/layout.js +333 -0
  151. package/dist/runtime/dom/listenerPageScripts.d.ts +66 -0
  152. package/dist/runtime/dom/listenerPageScripts.js +279 -0
  153. package/dist/runtime/dom/listenerSummary.d.ts +132 -11
  154. package/dist/runtime/dom/listenerSummary.js +344 -22
  155. package/dist/runtime/dom/pageActivity.d.ts +41 -0
  156. package/dist/runtime/dom/pageActivity.js +123 -0
  157. package/dist/runtime/dom/reactEventHelpers.d.ts +58 -2
  158. package/dist/runtime/dom/reactEventHelpers.js +212 -41
  159. package/dist/runtime/dom/targetNode.d.ts +80 -27
  160. package/dist/runtime/dom/targetNode.js +249 -33
  161. package/dist/runtime/dom/wait.d.ts +25 -0
  162. package/dist/runtime/dom/wait.js +199 -0
  163. package/dist/runtime/dom/waitCondition.d.ts +71 -0
  164. package/dist/runtime/dom/waitCondition.js +75 -0
  165. package/dist/runtime/page/emulation.d.ts +51 -0
  166. package/dist/runtime/page/emulation.js +80 -0
  167. package/dist/runtime/page/loadingState.d.ts +36 -0
  168. package/dist/runtime/page/loadingState.js +86 -0
  169. package/dist/runtime/page/navigation.d.ts +46 -2
  170. package/dist/runtime/page/navigation.js +69 -33
  171. package/dist/session/QueryCacheManager.d.ts +11 -1
  172. package/dist/session/QueryCacheManager.js +25 -3
  173. package/dist/session/chromeOwners.d.ts +34 -0
  174. package/dist/session/chromeOwners.js +51 -0
  175. package/dist/session/cleanup/staleSession.d.ts +11 -1
  176. package/dist/session/cleanup/staleSession.js +17 -6
  177. package/dist/session/cleanup/userCommands.js +2 -4
  178. package/dist/session/metadata.d.ts +5 -1
  179. package/dist/session/metadata.js +2 -1
  180. package/dist/session/paths.d.ts +77 -3
  181. package/dist/session/paths.js +111 -5
  182. package/dist/session/port.d.ts +31 -7
  183. package/dist/session/port.js +50 -43
  184. package/dist/session/portClaims.d.ts +66 -0
  185. package/dist/session/portClaims.js +284 -0
  186. package/dist/session/sessionList.d.ts +58 -0
  187. package/dist/session/sessionList.js +199 -0
  188. package/dist/session/sessionName.d.ts +46 -0
  189. package/dist/session/sessionName.js +97 -0
  190. package/dist/telemetry/a11y.d.ts +8 -3
  191. package/dist/telemetry/a11y.js +92 -28
  192. package/dist/telemetry/requestKinds.d.ts +32 -0
  193. package/dist/telemetry/requestKinds.js +61 -0
  194. package/dist/telemetry/requestState.d.ts +31 -0
  195. package/dist/telemetry/requestState.js +38 -0
  196. package/dist/types.d.ts +80 -3
  197. package/dist/ui/formatters/a11y.js +3 -0
  198. package/dist/ui/formatters/console/chronological.d.ts +8 -0
  199. package/dist/ui/formatters/console/chronological.js +17 -4
  200. package/dist/ui/formatters/console/json.js +3 -4
  201. package/dist/ui/formatters/console/shared.d.ts +12 -0
  202. package/dist/ui/formatters/console.d.ts +2 -2
  203. package/dist/ui/formatters/console.js +1 -1
  204. package/dist/ui/formatters/details.js +2 -1
  205. package/dist/ui/formatters/dom.d.ts +26 -14
  206. package/dist/ui/formatters/dom.js +65 -52
  207. package/dist/ui/formatters/form.js +29 -18
  208. package/dist/ui/formatters/layout.d.ts +31 -0
  209. package/dist/ui/formatters/layout.js +53 -0
  210. package/dist/ui/formatters/listeners.d.ts +3 -2
  211. package/dist/ui/formatters/listeners.js +73 -9
  212. package/dist/ui/formatters/networkHeaders.js +13 -0
  213. package/dist/ui/formatters/preview.js +2 -1
  214. package/dist/ui/formatters/requestStatus.d.ts +1 -17
  215. package/dist/ui/formatters/requestStatus.js +2 -30
  216. package/dist/ui/formatters/sessions.d.ts +12 -0
  217. package/dist/ui/formatters/sessions.js +40 -0
  218. package/dist/ui/formatters/status.d.ts +21 -2
  219. package/dist/ui/formatters/status.js +47 -10
  220. package/dist/ui/formatters/triggeredRequests.d.ts +36 -0
  221. package/dist/ui/formatters/triggeredRequests.js +65 -0
  222. package/dist/ui/formatting.d.ts +10 -0
  223. package/dist/ui/formatting.js +28 -36
  224. package/dist/ui/messages/chrome.d.ts +9 -0
  225. package/dist/ui/messages/chrome.js +17 -5
  226. package/dist/ui/messages/commands.d.ts +388 -14
  227. package/dist/ui/messages/commands.js +664 -21
  228. package/dist/ui/messages/consoleMessages.d.ts +10 -0
  229. package/dist/ui/messages/consoleMessages.js +17 -0
  230. package/dist/ui/messages/hints.js +2 -1
  231. package/dist/ui/messages/preview.js +5 -4
  232. package/dist/ui/messages/session.d.ts +16 -21
  233. package/dist/ui/messages/session.js +28 -26
  234. package/dist/ui/messages/sessionCommand.d.ts +43 -0
  235. package/dist/ui/messages/sessionCommand.js +52 -0
  236. package/dist/utils/async.d.ts +8 -0
  237. package/dist/utils/async.js +19 -0
  238. package/dist/utils/http.d.ts +22 -1
  239. package/dist/utils/http.js +28 -9
  240. package/dist/utils/selectorFilters.d.ts +36 -8
  241. package/dist/utils/selectorFilters.js +267 -53
  242. package/dist/utils/shellDetection.d.ts +8 -2
  243. package/dist/utils/shellDetection.js +120 -33
  244. package/dist/utils/suggestions.d.ts +26 -0
  245. package/dist/utils/suggestions.js +73 -0
  246. package/dist/utils/taskMappings.js +10 -0
  247. package/dist/utils/url.d.ts +12 -2
  248. package/dist/utils/url.js +69 -7
  249. package/package.json +1 -1
  250. package/dist/ui/formatters/sessionFormatters.d.ts +0 -58
  251. package/dist/ui/formatters/sessionFormatters.js +0 -121
@@ -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('', 'Error: Session already running', '', ` PID: ${pid}`, targetUrl && ` Target: ${targetUrl}`, ` Duration: ${formatDuration(duration)}`, '', 'Suggestions:', ' View session: bdg status', ' Stop and restart: bdg stop && bdg <url>', '');
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)'}`, '', "Run 'bdg stop' before attaching to a different target.", '');
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: 'Retry in a moment; if it stays unresponsive, end it with: bdg cleanup --force (stops the daemon and its Chrome)',
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: 'Check the session with: bdg status; if the page stays frozen, end it with: bdg cleanup --force',
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
- ? 'Wait until "bdg <url>" returns, then retry'
84
- : 'Start a session with: bdg <url>';
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('Error: No active session', context?.staleCleanedUp && '(Stale daemon files were cleaned up)', context?.lastError && `Last error: ${context.lastError}`, 'Start a session with: bdg <url>', context?.suggestStatus && '', context?.suggestStatus && 'Or check daemon status:', context?.suggestStatus && ' bdg status', context?.suggestRetry && '', context?.suggestRetry && 'Or try the command again if this was transient');
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
- ' 2. Then inspect: bdg dom a11y describe 0',
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('Check the selector syntax, or wait for the element to load (bdg peek shows the page state)', CROSS_ORIGIN_FRAMES_NOTE),
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
- ? 'Playwright-only syntax is not CSS. bdg supports :has-text("…"), :text-is("…") and :visible at the end of a selector, e.g. button:has-text("Save"), or find elements by accessible name: bdg dom a11y query name="…"'
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`) that
219
- * is not at the end of a selector.
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} must come last in a selector (after the CSS of the element to match): ${selector}`,
228
- suggestion: `Move it to the end, e.g. form button:has-text("Save"); to match an element by what it contains use CSS :has(), e.g. div:has(> button), or find elements by accessible name: bdg dom a11y query name="…"`,
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.length} characters, at most ${max})`,
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 profile)`,
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 click ${index}, or bdg dom click "<selector>" --index <n>`,
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
- /** Where selectors cannot look, for "not found" errors */
454
- export const UNREACHABLE_ELEMENTS_HINT = 'elements in closed shadow roots and cross-origin iframes cannot be reached';
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: 'Start a new session with: bdg <url>',
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
- * The form was submitted but the wait for its result timed out.
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
- return {
555
- message: `Form submitted, but timed out after ${timeout}ms waiting for ${waitNavigation ? 'navigation' : 'network idle'}`,
556
- suggestion: waitNavigation
557
- ? 'The form may not navigate (e.g. it submits via fetch); retry without --wait-navigation or with a larger --timeout'
558
- : 'Increase --timeout, or use --wait-network 0 to return right after submitting',
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 (query or form results), if any
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
- * Element at index not found (stale cache).
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 elementAtIndexNotFoundError(index, selector) {
940
+ export function cachedIndexOutOfRangeError(source, count) {
941
+ const results = count === 1 ? '1 result' : `${count} results`;
588
942
  return {
589
- message: `Element at index ${index} not found`,
590
- suggestion: `Re-run "bdg dom query ${selector}" to refresh the cache`,
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
- * No nodes found for selector.
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 noNodesFoundError(selector) {
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: `Verify the CSS selector is correct. ${CROSS_ORIGIN_FRAMES_NOTE}`,
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 "role:button" or "name:Submit"`,
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
- export function unknownQueryFieldError(field) {
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: 'Use role, name or description, e.g.: bdg dom a11y query "role:button name:Submit"',
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 from a timer) and
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: `The page was busy for ${Math.round(timeoutMs / 1000)}s (a script kept it running), so its scripts were terminated`,
771
- suggestion: 'The page is usable again; re-run the command',
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 returned promise did not settle within ${Math.round(timeoutMs / 1000)}s`,
803
- suggestion: 'Check that it resolves or rejects, or race it with a timeout in the script',
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 sandboxed without scripts).
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 (sandboxed frames without allow-scripts never get one). ${LIST_FRAMES_HINT}`,
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: 'No forms discovered on the page',
945
- suggestion: 'Check if forms exist with: bdg dom query "form, input, [role=textbox]" or inspect the page manually',
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
  /**