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
@@ -5,7 +5,9 @@
5
5
  * cleaning up stale files, and validating command arguments.
6
6
  */
7
7
  import { buildAgentDiscoveryHelp, buildCommonTaskExamples, buildUrlExamples, buildSessionManagementReminder, } from '../formatters/helpFormatters.js';
8
- import { joinLines } from '../formatting.js';
8
+ import { formatDuration, joinLines, pluralize, truncateUrl } from '../formatting.js';
9
+ import { sessionCommand } from './sessionCommand.js';
10
+ import { truncateByLength } from '../../utils/strings.js';
9
11
  /**
10
12
  * Chrome closed by `bdg stop` (gracefully, so the profile is saved).
11
13
  *
@@ -33,14 +35,363 @@ export function orphanedDaemonsCleanedMessage(count) {
33
35
  export function domClickFallbackWarning(reason) {
34
36
  return `Element is ${reason ?? 'not reachable by the mouse'}; dispatched DOM events instead of mouse events (a user could not reach it like this)`;
35
37
  }
38
+ /** Reason of a `bdg dom form` blocker for a required field left empty */
39
+ export const REQUIRED_FIELD_EMPTY_REASON = 'Required field is empty';
40
+ /**
41
+ * Field labels as a list (without a trailing colon), the first five and how
42
+ * many more.
43
+ *
44
+ * @param labels - Field labels
45
+ * @returns e.g. "Last Name, Zip"
46
+ */
47
+ function fieldLabelList(labels) {
48
+ const shown = labels
49
+ .slice(0, 5)
50
+ .map((label) => label.replace(/\s*:\s*$/, ''))
51
+ .join(', ');
52
+ return labels.length > 5 ? `${shown} and ${labels.length - 5} more` : shown;
53
+ }
54
+ /**
55
+ * Part of the `bdg dom form` summary naming the required fields left empty.
56
+ *
57
+ * @param labels - Labels of the empty required fields
58
+ * @returns e.g. "2 required fields empty: Last Name, Zip"
59
+ */
60
+ export function requiredFieldsEmptyMessage(labels) {
61
+ const fields = labels.length === 1 ? 'field' : 'fields';
62
+ return `${labels.length} required ${fields} empty: ${fieldLabelList(labels)}`;
63
+ }
64
+ /**
65
+ * Readiness at the end of the `bdg dom form` summary.
66
+ *
67
+ * @param summary - Whether the form is ready, how many fields are filled and
68
+ * required, and the labels of the empty ones
69
+ * @returns e.g. "READY to submit", "NOT ready (no fields filled)"
70
+ */
71
+ export function formReadinessMessage(summary) {
72
+ if (!summary.readyToSubmit) {
73
+ return summary.filledFields === 0 ? 'NOT ready (no fields filled)' : 'NOT ready';
74
+ }
75
+ if (summary.requiredTotal > 0 || summary.emptyFieldLabels.length === 0)
76
+ return 'READY to submit';
77
+ return `READY to submit (no field is marked required; empty: ${fieldLabelList(summary.emptyFieldLabels)})`;
78
+ }
79
+ /** Why an action's status line is not a clean success when nothing changed */
80
+ export const NO_VISIBLE_EFFECT = 'no visible effect observed: no DOM change, requests or navigation within 300 ms';
81
+ /**
82
+ * Status line of a DOM action: a check mark only for a clean success.
83
+ *
84
+ * @param done - What was done, e.g. "Element Clicked"
85
+ * @param warned - Whether the action has warnings (shown right below)
86
+ * @param noEffect - Whether the action had no visible effect
87
+ * @returns e.g. "✓ Element Clicked", "⚠ Element Clicked (with warnings)" or
88
+ * "⚠ Element Clicked (no visible effect observed: no DOM change, requests or navigation within 300 ms)"
89
+ */
90
+ export function actionStatusLine(done, warned, noEffect = false) {
91
+ if (noEffect)
92
+ return `⚠ ${done} (${NO_VISIBLE_EFFECT})`;
93
+ return warned ? `⚠ ${done} (with warnings)` : `✓ ${done}`;
94
+ }
95
+ /**
96
+ * How an action changed the page's location, for its `Page:` row.
97
+ *
98
+ * @param navigation - Navigation the action caused
99
+ * @returns e.g. "navigated to https://example.com/secure (200)" or
100
+ * "URL changed to https://example.com/#/active (same document)"
101
+ */
102
+ export function pageNavigationText(navigation) {
103
+ if (navigation.sameDocument)
104
+ return `URL changed to ${navigation.url} (same document)`;
105
+ const status = navigation.status === undefined ? '' : ` (${navigation.status})`;
106
+ return `navigated to ${navigation.url}${status}`;
107
+ }
108
+ /**
109
+ * A message an action made appear, for its `New text:` rows.
110
+ *
111
+ * @param message - New message
112
+ * @returns e.g. `"Your password is invalid!" (div#flash.flash.error)`
113
+ */
114
+ export function newMessageText(message) {
115
+ return `"${message.text}" (${message.element})`;
116
+ }
117
+ /**
118
+ * Warning shown when a filled field's value read back is not the one given:
119
+ * cut to its maxlength, a password of another length (values never shown),
120
+ * or another value (naming the field the value went to, when one has it).
121
+ *
122
+ * @param mismatch - Value given and value found (masked for passwords)
123
+ * @returns Warning text
124
+ */
125
+ export function valueMismatchWarning(mismatch) {
126
+ if (mismatch.truncatedTo !== undefined) {
127
+ return `The value was cut to ${mismatch.truncatedTo} characters by maxlength`;
128
+ }
129
+ if (mismatch.expectedLength !== undefined) {
130
+ return `The password field's value differs from the one filled (length ${mismatch.actualLength ?? 0}, expected ${mismatch.expectedLength}); the page may have rejected or changed the input`;
131
+ }
132
+ const outcome = mismatch.movedTo === undefined
133
+ ? 'the page may have rejected or moved the input'
134
+ : `the value appeared in ${mismatch.movedTo} instead`;
135
+ return `The field's value is "${mismatch.actual}" after filling (expected "${mismatch.expected}"); ${outcome}`;
136
+ }
137
+ /**
138
+ * Warning shown when a mouse press was dispatched but the target never
139
+ * received it (e.g. a browser dialog or bubble captured the input).
140
+ */
141
+ export const CLICK_NOT_RECEIVED_WARNING = 'The click may not have reached the element: the page saw no mouse press (the browser may be showing a dialog or bubble that captures input)';
36
142
  /**
37
143
  * Note under a shortened list of matches.
38
144
  *
39
145
  * @param hidden - Matches not listed
40
- * @returns e.g. "... and 1174 more (use --json for all)"
146
+ * @param jsonLimit - How many JSON output lists, when it leaves some out too
147
+ * @returns e.g. "... and 1174 more (use --json for all)",
148
+ * "... and 8980 more (--json lists the first 100)"
149
+ */
150
+ export function moreMatchesNote(hidden, jsonLimit) {
151
+ const where = jsonLimit === undefined ? 'use --json for all' : `--json lists the first ${jsonLimit}`;
152
+ return `... and ${hidden} more (${where})`;
153
+ }
154
+ /**
155
+ * Note under a list of a11y query matches cut by `--limit`.
156
+ *
157
+ * @param omitted - Matches not listed
158
+ * @returns e.g. "... and 213 more (--limit 0 lists all; their indices work too)"
159
+ */
160
+ export function a11yMoreMatchesNote(omitted) {
161
+ return `... and ${omitted} more (--limit 0 lists all; their indices work too)`;
162
+ }
163
+ /**
164
+ * Note under a shortened list of requests an action triggered when JSON
165
+ * output left some out too (they are only in the network telemetry then).
166
+ *
167
+ * @param hidden - Requests not listed
168
+ * @returns e.g. "... and 50 more (see bdg network list)"
169
+ */
170
+ export function moreRequestsNote(hidden) {
171
+ return `... and ${hidden} more (see ${sessionCommand('bdg network list')})`;
172
+ }
173
+ /**
174
+ * Title of the list of requests that started while an action ran (they are
175
+ * attributed by time, so a page poller's requests are listed too: the title
176
+ * doesn't claim the action caused them).
177
+ *
178
+ * @param total - Requests in all
179
+ * @returns e.g. "Requests during the action (18):"
180
+ */
181
+ export function triggeredRequestsTitle(total) {
182
+ return `Requests during the action (${total}):`;
183
+ }
184
+ /**
185
+ * Line counting the static assets an action loaded instead of listing them.
186
+ *
187
+ * @param count - Asset requests
188
+ * @param types - Short type names, e.g. ["css", "js", "images"]
189
+ * @returns e.g. "+ 97 assets (css, js, fonts, images)"
41
190
  */
42
- export function moreMatchesNote(hidden) {
43
- return `... and ${hidden} more (use --json for all)`;
191
+ export function assetRequestsNote(count, types) {
192
+ return `+ ${count} ${count === 1 ? 'asset' : 'assets'} (${types.join(', ')})`;
193
+ }
194
+ /**
195
+ * Pieces of the reasons `bdg dom layout` gives for hidden and invisible
196
+ * elements. The page-side measurement builds the reasons from them, e.g.
197
+ * `clipped by div#acc: zero height`, `opacity: 0 on div#menu`.
198
+ */
199
+ export const LAYOUT_REASONS = {
200
+ /** Hidden: an `<option>` of a closed `<select>` has no box */
201
+ option: 'not rendered (an <option> is shown by its <select>)',
202
+ /** Hidden: start of the reason for a clipping container with no area, followed by it */
203
+ clippedBy: 'clipped by ',
204
+ /** Which size of that container is zero */
205
+ zeroHeight: 'zero height',
206
+ zeroWidth: 'zero width',
207
+ /** Invisible: fully transparent */
208
+ transparent: 'opacity: 0',
209
+ /** Joins an invisible reason to the ancestor causing it */
210
+ on: ' on ',
211
+ };
212
+ /** Start of the off-screen reason of an element a scroll-locked page hides ({@link scrollLockedReason}) */
213
+ const SCROLL_LOCKED_PREFIX = 'page scrolling is locked';
214
+ /**
215
+ * Off-screen reason for an element out of view on a page whose scrolling is
216
+ * locked, naming the visible dialog that likely locked it when there is one.
217
+ *
218
+ * @param lock - What locks it, e.g. `overflow: hidden on body`
219
+ * @param dialog - Visible dialog on the page, e.g. `div#consent` (null: none)
220
+ * @returns e.g. `page scrolling is locked (overflow: hidden on body), likely by dialog div#consent`,
221
+ * or `page scrolling is locked (overflow: hidden on body)`
222
+ */
223
+ export function scrollLockedReason(lock, dialog = null) {
224
+ const cause = dialog ? `, likely by dialog ${dialog}` : '';
225
+ return `${SCROLL_LOCKED_PREFIX} (${lock})${cause}`;
226
+ }
227
+ /** Short location hints for elements a user cannot see without scrolling */
228
+ const VIEWPORT_POSITION_HINTS = {
229
+ above: 'above viewport',
230
+ below: 'below fold',
231
+ left: 'left of viewport',
232
+ right: 'right of viewport',
233
+ hidden: 'hidden',
234
+ };
235
+ /**
236
+ * Location hint for an element outside the viewport or hidden.
237
+ *
238
+ * @param position - Where the element is
239
+ * @param clippedBy - Ancestor or iframe cutting it off, if any
240
+ * @returns e.g. "below fold", "out of view in ul#list"; undefined for
241
+ * elements (partly) in view
242
+ */
243
+ export function viewportPositionHint(position, clippedBy) {
244
+ const outside = position !== 'visible' && position !== 'partly' && position !== 'hidden';
245
+ if (outside && clippedBy)
246
+ return `out of view in ${clippedBy}`;
247
+ return VIEWPORT_POSITION_HINTS[position];
248
+ }
249
+ /**
250
+ * Page scroll that would bring an element into view, in words.
251
+ *
252
+ * @param scrollBy - Scroll amounts
253
+ * @param purpose - What the scroll does, e.g. "centre it"
254
+ * @returns e.g. "scroll down 760px to centre it"
255
+ */
256
+ function scrollAdvice(scrollBy, purpose) {
257
+ const steps = [
258
+ scrollBy.y !== 0 && `${scrollBy.y > 0 ? 'down' : 'up'} ${Math.abs(scrollBy.y)}px`,
259
+ scrollBy.x !== 0 && `${scrollBy.x > 0 ? 'right' : 'left'} ${Math.abs(scrollBy.x)}px`,
260
+ ].filter(Boolean);
261
+ return `scroll ${steps.join(', ')} to ${purpose}`;
262
+ }
263
+ /**
264
+ * Whether an element fits the viewport (the scroll then centres it; a larger
265
+ * one gets its start aligned).
266
+ *
267
+ * @param element - Element layout
268
+ * @param viewport - Viewport size, when known
269
+ * @returns True when it fits, or the viewport is unknown
270
+ */
271
+ function fitsViewport(element, viewport) {
272
+ const { width, height } = element.bounds;
273
+ return !viewport || (width <= viewport.width && height <= viewport.height);
274
+ }
275
+ /**
276
+ * How to bring an element that is out of view into view, in words.
277
+ *
278
+ * @param element - Element layout
279
+ * @param viewport - Viewport size (an element larger than it is not centred)
280
+ * @returns e.g. "scroll down 760px to centre it", "off-screen: fixed position, page scroll
281
+ * does not move it"; undefined when neither applies
282
+ */
283
+ function layoutScrollNote(element, viewport) {
284
+ if (element.scrollBy) {
285
+ const purpose = fitsViewport(element, viewport) ? 'centre it' : 'bring it into view';
286
+ return scrollAdvice(element.scrollBy, purpose);
287
+ }
288
+ return element.offScreenReason && `off-screen: ${element.offScreenReason}`;
289
+ }
290
+ /**
291
+ * How to see all of an element that is partly in view, in words.
292
+ *
293
+ * @param element - Element layout
294
+ * @param viewport - Viewport size (for an element larger than it, the scroll shows its start)
295
+ * @returns e.g. "scroll down 302px to see all of it", "sticky position, page scroll moves it
296
+ * only until it sticks"; undefined when neither applies
297
+ */
298
+ function partlyVisibleNote(element, viewport) {
299
+ if (element.scrollBy) {
300
+ const purpose = fitsViewport(element, viewport) ? 'see all of it' : 'show it from its start';
301
+ return scrollAdvice(element.scrollBy, purpose);
302
+ }
303
+ return element.offScreenReason;
304
+ }
305
+ /**
306
+ * Where an element is relative to the viewport, for `bdg dom layout`.
307
+ *
308
+ * @param element - Element layout
309
+ * @param viewport - Viewport size, when known
310
+ * @returns e.g. "visible", "partly visible (40%); scroll down 302px to see all of it",
311
+ * "below fold (scroll down 760px to centre it)",
312
+ * "out of view in ul#list (below)", "hidden (display: none)",
313
+ * "left of viewport (off-screen: beyond the page's scroll range)",
314
+ * "below fold; page scrolling is locked (overflow: hidden on body), likely by dialog div#consent"
315
+ */
316
+ export function layoutPositionLabel(element, viewport) {
317
+ const { inViewport, percentVisible, hiddenReason, clippedBy, offScreenReason } = element;
318
+ if (inViewport === 'visible')
319
+ return 'visible';
320
+ const locked = !element.scrollBy && offScreenReason?.startsWith(SCROLL_LOCKED_PREFIX);
321
+ if (inViewport === 'partly') {
322
+ const clipped = clippedBy ? `, clipped by ${clippedBy}` : '';
323
+ const label = `partly visible (${percentVisible ?? 0}%${clipped})`;
324
+ const note = partlyVisibleNote(element, viewport);
325
+ return note ? `${label}; ${note}` : label;
326
+ }
327
+ const note = locked ? undefined : layoutScrollNote(element, viewport);
328
+ const label = viewportPositionHint(inViewport, clippedBy) ?? inViewport;
329
+ if (hiddenReason)
330
+ return `${label} (${hiddenReason})`;
331
+ if (clippedBy)
332
+ return `${label} (${inViewport})`;
333
+ if (locked)
334
+ return `${label}; ${offScreenReason}`;
335
+ return note ? `${label} (${note})` : label;
336
+ }
337
+ /**
338
+ * The `prefers-color-scheme` media feature the page sees, labelled as the
339
+ * preference it is (the page may still render its own theme), and where it
340
+ * comes from.
341
+ *
342
+ * @param scheme - Light or dark
343
+ * @param emulated - Set with `--color-scheme` (otherwise the system setting)
344
+ * @returns e.g. `prefers-color-scheme: dark (from the system setting)`
345
+ */
346
+ export function colorSchemeLabel(scheme, emulated) {
347
+ const source = emulated ? 'emulated with --color-scheme' : 'from the system setting';
348
+ return `prefers-color-scheme: ${scheme} (${source})`;
349
+ }
350
+ /**
351
+ * First line of `bdg status` for a running session.
352
+ *
353
+ * @param page - URL and title of the page, when the session reported them
354
+ * @returns e.g. `Session active: https://example.com/ — Example Domain`
355
+ */
356
+ export function sessionActiveLine(page) {
357
+ if (!page)
358
+ return 'Session active';
359
+ return `Session active: ${page.url}${page.title ? ` — ${page.title}` : ''}`;
360
+ }
361
+ /**
362
+ * Page dimensions line of `bdg dom layout`.
363
+ *
364
+ * @param page - Viewport, scroll position, document size and the color scheme the page is told to prefer
365
+ * @returns e.g. "Page: viewport 1280×720, scrolled to 0,0, document 1280×2400, prefers-color-scheme: dark"
366
+ */
367
+ export function pageLayoutLine(page) {
368
+ const { viewport, scroll, document, colorScheme } = page;
369
+ const scheme = colorScheme ? `, prefers-color-scheme: ${colorScheme}` : '';
370
+ return `Page: viewport ${viewport.width}×${viewport.height}, scrolled to ${scroll.x},${scroll.y}, document ${document.width}×${document.height}${scheme}`;
371
+ }
372
+ /**
373
+ * Headline of `bdg dom layout`.
374
+ *
375
+ * @param count - Elements matched
376
+ * @param listed - Elements reported (fewer with --index)
377
+ * @param selector - Selector they matched
378
+ * @returns e.g. '3 elements match "button" (page x,y and size in CSS px):',
379
+ * '1 of 3 elements matching "button" (...)'
380
+ */
381
+ export function layoutHeadline(count, listed, selector) {
382
+ const matched = listed < count
383
+ ? `${listed} of ${count} elements matching "${selector}"`
384
+ : `${count} element${count === 1 ? ' matches' : 's match'} "${selector}"`;
385
+ return `${matched} (page x,y and size in CSS px):`;
386
+ }
387
+ /**
388
+ * Headline of `bdg dom layout` for a cached index.
389
+ *
390
+ * @param target - The index and the list it refers to, in words
391
+ * @returns e.g. `Element at index 0 of the last dom query "h3" (page x,y and size in CSS px):`
392
+ */
393
+ export function indexLayoutHeadline(target) {
394
+ return `Element at ${target} (page x,y and size in CSS px):`;
44
395
  }
45
396
  /**
46
397
  * Warning when a selector matched several elements and no --index was given.
@@ -53,14 +404,55 @@ export function multipleMatchesWarning(count, action) {
53
404
  return `${count} elements match; ${action} (use --index or a more specific selector)`;
54
405
  }
55
406
  /**
56
- * Headline of `bdg dom listeners`.
407
+ * Headline of `bdg dom listeners`, saying where the counted listeners are:
408
+ * the list covers the element, its ancestors, its document and window.
57
409
  *
58
410
  * @param element - Inspected element, e.g. "button#save"
59
- * @param count - Listeners found
60
- * @returns e.g. "Event listeners for button#save (3)"
411
+ * @param counts - Listeners found, by placement
412
+ * @param context - Index of the element and the iframe it is in, if any
413
+ * @returns e.g. "Event listeners for button#save (156: 3 on the element, 120 on ancestors, 33 on document and window)"
414
+ */
415
+ export function listenersHeadline(element, counts, context = {}) {
416
+ const index = context.index === undefined ? '' : ` [${context.index}]`;
417
+ const frame = context.frame ? ` in ${context.frame}` : '';
418
+ const total = counts.target + counts.ancestor + counts.global;
419
+ const where = [
420
+ counts.target > 0 && `${counts.target} on the element`,
421
+ counts.ancestor > 0 && `${counts.ancestor} on ancestors`,
422
+ counts.global > 0 && `${counts.global} on document and window`,
423
+ ].filter(Boolean);
424
+ return `Event listeners for ${element}${index}${frame} (${total}: ${where.join(', ')})`;
425
+ }
426
+ /** Heading of the collapsed framework root listeners */
427
+ export const COLLAPSED_LISTENERS_HEADING = 'Framework roots (one line per node; --all lists each listener):';
428
+ /**
429
+ * One collapsed framework root.
430
+ *
431
+ * @param root - Framework label, event types, phases and dispatcher names
432
+ * @returns e.g. "React root: 90 event types, capture and bubble (dispatchEvent, …)"
433
+ */
434
+ export function collapsedListenersSummary(root) {
435
+ const phases = [root.capture && 'capture', root.bubble && 'bubble'].filter(Boolean).join(' and ');
436
+ const label = root.framework ?? 'Dispatcher';
437
+ return `${label}: ${root.types.length} event types, ${phases} (${root.handlers.join(', ')})`;
438
+ }
439
+ /**
440
+ * jQuery handlers left unresolved because there were too many.
441
+ *
442
+ * @param count - Handlers not resolved
443
+ * @returns One-line note
444
+ */
445
+ export function jqueryHandlersSkippedNote(count) {
446
+ return `Note: ${count} more jQuery handler${count === 1 ? '' : 's'} not resolved; their jQuery dispatcher is listed instead (narrow down with --type)`;
447
+ }
448
+ /**
449
+ * Event types a mistyped `--type` probably meant.
450
+ *
451
+ * @param types - Suggested types
452
+ * @returns e.g. "Did you mean: --type click?"
61
453
  */
62
- export function listenersHeadline(element, count) {
63
- return `Event listeners for ${element} (${count})`;
454
+ export function eventTypeSuggestion(types) {
455
+ return `Did you mean: --type ${types.join(',')}? (event types are case-sensitive, without "on")`;
64
456
  }
65
457
  /**
66
458
  * `bdg dom listeners` found nothing.
@@ -75,14 +467,57 @@ export function noListenersMessage(element, types) {
75
467
  }
76
468
  /** What `bdg dom listeners` covers, shown when it found nothing */
77
469
  export const NO_LISTENERS_HINT = 'Inline on… attributes and on… properties are included, and so are handlers frameworks delegate to ancestors (React, jQuery)';
470
+ /** Event types named in a note; the rest are counted */
471
+ const NOTE_TYPES_SHOWN = 5;
472
+ /**
473
+ * "click", "click and keydown", "click, keydown and input", "a, b, c, d, e
474
+ * and 3 more".
475
+ *
476
+ * @param types - Event types
477
+ * @returns The types as a list in prose
478
+ */
479
+ function typeList(types) {
480
+ if (types.length > NOTE_TYPES_SHOWN + 1) {
481
+ return `${types.slice(0, NOTE_TYPES_SHOWN).join(', ')} and ${types.length - NOTE_TYPES_SHOWN} more`;
482
+ }
483
+ return types.length > 1
484
+ ? `${types.slice(0, -1).join(', ')} and ${types[types.length - 1]}`
485
+ : (types[0] ?? '');
486
+ }
78
487
  /**
79
- * Note for event types handled only by ancestors, document or window.
488
+ * Note for interaction event types the element has no listener of its own for.
80
489
  *
81
- * @param types - Event types without a listener on the element itself
490
+ * @param note - How the types reach their handlers
491
+ * @returns One-line note, or undefined when the listed handlers say it all
492
+ */
493
+ export function delegationNote(note) {
494
+ const types = typeList(note.types);
495
+ const has = note.types.length === 1 ? 'has' : 'have';
496
+ const placeholders = typeList(note.placeholderTypes);
497
+ const placeholder = `the element's own ${placeholders} listener is only React's no-op placeholder`;
498
+ switch (note.kind) {
499
+ case 'react':
500
+ if (note.placeholderTypes.length === 0)
501
+ return undefined;
502
+ return `Note: ${placeholder}; the React on… handlers listed above for ${placeholders} run from React's root container`;
503
+ case 'react-root':
504
+ return `Note: React's root container (${note.node ?? 'its root'}) handles ${types}, but no React on… prop for ${note.types.length === 1 ? 'it' : 'them'} was found on the element or its ancestors${note.placeholderTypes.length > 0 ? `; ${placeholder}` : ''}`;
505
+ case 'jquery':
506
+ return `Note: ${types} ${has} no listener on the element itself; jQuery runs the handlers listed above by delegation from ${note.node ?? 'an ancestor'}`;
507
+ case 'delegated': {
508
+ const own = note.placeholderTypes.length > 0 ? 'no listener that does anything' : 'no listener';
509
+ return `Note: ${types} ${has} ${own} on the element itself; the listeners on its ancestors, document or window listed above still run for it (event delegation)`;
510
+ }
511
+ }
512
+ }
513
+ /**
514
+ * React prop handlers left unresolved because there were too many.
515
+ *
516
+ * @param count - Handlers not resolved
82
517
  * @returns One-line note
83
518
  */
84
- export function delegatedListenersNote(types) {
85
- return `Note: ${types.join(', ')} ${types.length === 1 ? 'has' : 'have'} no listener on the element itself; frameworks like React and jQuery delegate events to a root container, document or window, so these still run for it`;
519
+ export function reactHandlersSkippedNote(count) {
520
+ return `Note: ${count} more React handler prop${count === 1 ? '' : 's'} not listed (narrow down with --type)`;
86
521
  }
87
522
  /**
88
523
  * A JavaScript dialog bdg accepted, as one line.
@@ -109,19 +544,29 @@ export const PAGE_ACTION_DONE = {
109
544
  forward: 'Went forward',
110
545
  };
111
546
  /** Help text of the `bdg page` history commands */
547
+ /** Description of `bdg page info` */
548
+ export const PAGE_INFO_DESCRIPTION = 'Show the URL and title of the session page';
112
549
  export const PAGE_ACTION_DESCRIPTIONS = {
113
550
  reload: 'Reload the page',
114
551
  back: 'Go back one page (like the browser button)',
115
552
  forward: 'Go forward one page (like the browser button)',
116
553
  };
117
554
  /**
118
- * `bdg page navigate` to a page that answered with an HTTP error.
555
+ * `bdg page navigate|reload|back|forward` when the document answered with an
556
+ * error status, or the page then loaded another document that answered
557
+ * differently (a 404 page whose script loads the app).
119
558
  *
120
- * @param status - HTTP status
121
- * @returns Warning
559
+ * @param status - HTTP status of the navigation's document
560
+ * @param later - The last document the page loaded afterwards, if any
561
+ * @returns Warning, or undefined when there is nothing to report
122
562
  */
123
- export function httpErrorWarning(status) {
124
- return `The page responded with HTTP ${status}`;
563
+ export function documentStatusWarning(status, later) {
564
+ const failed = status >= 400;
565
+ const laterFailed = later !== undefined && later.status >= 400;
566
+ if (later && (failed || laterFailed)) {
567
+ return `The page responded with HTTP ${status}, then loaded ${later.url} (HTTP ${later.status})`;
568
+ }
569
+ return failed ? `The page responded with HTTP ${status}` : undefined;
125
570
  }
126
571
  /**
127
572
  * `bdg page navigate` to a URL that loaded no page.
@@ -138,7 +583,118 @@ export function notAPageWarning() {
138
583
  * @returns Warning
139
584
  */
140
585
  export function stillLoadingWarning(ms) {
141
- return `The new page has not answered within ${Math.round(ms / 1000)}s; it is still loading (check with bdg status)`;
586
+ return `The new page has not answered within ${Math.round(ms / 1000)}s; it is still loading (check with ${sessionCommand('bdg status')})`;
587
+ }
588
+ /**
589
+ * A request the page is still waiting for.
590
+ *
591
+ * @param request - Pending request
592
+ * @returns e.g. `GET code.jquery.com/ui/.../jquery-ui.js (pending 30s)`
593
+ */
594
+ function pendingRequestLabel(request) {
595
+ return `${request.method} ${truncateUrl(request.url)} (pending ${formatDuration(request.pendingMs)})`;
596
+ }
597
+ /**
598
+ * Requests still running, the longest-running first, in words.
599
+ *
600
+ * @param pending - The requests to name
601
+ * @param count - All requests still running
602
+ * @returns e.g. `GET …/app.js (pending 30s) and 2 more`
603
+ */
604
+ export function pendingRequestsText(pending, count) {
605
+ const more = count - pending.length;
606
+ return `${pending.map(pendingRequestLabel).join(', ')}${more > 0 ? ` and ${more} more` : ''}`;
607
+ }
608
+ /**
609
+ * How far a page request got, in words.
610
+ *
611
+ * @param request - The page request
612
+ * @returns e.g. `POST …/authenticate pending for 30s`, `POST …/authenticate returned 503 Service Unavailable`,
613
+ * `POST …/authenticate failed (net::ERR_CONNECTION_REFUSED)`
614
+ */
615
+ export function documentRequestText(request) {
616
+ const target = `${request.method} ${truncateUrl(request.url)}`;
617
+ if (request.errorText !== undefined)
618
+ return `${target} failed (${request.errorText})`;
619
+ if (request.status !== undefined) {
620
+ return `${target} returned ${request.status}${request.statusText ? ` ${request.statusText}` : ''}`;
621
+ }
622
+ return `${target} pending for ${formatDuration(request.pendingMs ?? 0)}`;
623
+ }
624
+ /**
625
+ * Start and `bdg page` when the document has not finished loading within
626
+ * the readiness wait: its readyState and the requests it is waiting on.
627
+ *
628
+ * @param state - Loading state of the page
629
+ * @returns Warning, e.g. `The page is still loading (document.readyState: loading); waiting on: GET …/jquery-ui.js (pending 30s)`
630
+ */
631
+ export function pageLoadingWarning(state) {
632
+ const waitingOn = state.pending.length > 0
633
+ ? `; waiting on: ${pendingRequestsText(state.pending, state.pendingCount)}`
634
+ : '';
635
+ return `The page is still loading (document.readyState: ${state.readyState})${waitingOn}. Elements may be missing until it finishes: ${sessionCommand('bdg dom wait <selector>')} waits for one`;
636
+ }
637
+ /**
638
+ * Help of `dom click`/`submit`: they wait for the network only, so results a
639
+ * page shows later (timers, spinners, animations) are waited for with `dom wait`.
640
+ */
641
+ export const CLICK_RESULT_WAIT_HELP = joinLines('', 'Waits only for the requests the action starts (150 ms idle, up to 2 s), not for', 'results the page shows later (timers, spinners, animations). Wait for those with:', " bdg dom wait '#result' --visible # or --text 'Saved', or '.spinner' --gone");
642
+ /** Examples in the help of `bdg dom wait` */
643
+ export const WAIT_HELP_EXAMPLES = joinLines('', 'Examples:', " bdg dom wait '#finish' --visible # timer-based loading (a spinner, then the result)", " bdg dom wait '.toast' --text 'Saved' # a match containing the text", " bdg dom wait '#loading' --gone # the spinner went away", ' bdg dom wait --load # the page finished loading');
644
+ /**
645
+ * The elements `bdg dom wait` waits for.
646
+ *
647
+ * @param condition - What is waited for
648
+ * @returns e.g. `#finish with text "hello world"`
649
+ */
650
+ export function waitTargetLabel(condition) {
651
+ const text = condition.text !== undefined ? ` with text "${condition.text}"` : '';
652
+ return `${condition.selector ?? 'the page'}${text}`;
653
+ }
654
+ /**
655
+ * One-line result of `bdg dom wait`.
656
+ *
657
+ * @param condition - What was waited for
658
+ * @param elapsedMs - How long it took
659
+ * @returns e.g. `✓ div#finish visible after 5.1s`
660
+ */
661
+ export function waitMetMessage(condition, elapsedMs) {
662
+ const after = `after ${(elapsedMs / 1000).toFixed(1)}s`;
663
+ if (condition.selector === undefined)
664
+ return `✓ Page loaded ${after}`;
665
+ const state = condition.gone
666
+ ? condition.visible
667
+ ? 'hidden'
668
+ : 'gone'
669
+ : condition.visible
670
+ ? 'visible'
671
+ : 'found';
672
+ const loaded = condition.load ? ' and page loaded' : '';
673
+ return `✓ ${waitTargetLabel(condition)} ${state}${loaded} ${after}`;
674
+ }
675
+ /**
676
+ * What the page showed, for a `bdg dom wait` that timed out.
677
+ *
678
+ * @param snapshot - Last thing the page reported
679
+ * @param condition - What was waited for
680
+ * @returns e.g. `2 matches, none visible` or `document.readyState: loading`
681
+ */
682
+ export function waitSnapshotSummary(snapshot, condition) {
683
+ const some = (count) => (count === 0 ? 'none' : String(count));
684
+ const parts = [];
685
+ if (condition.selector !== undefined) {
686
+ parts.push(snapshot.count === 0 ? 'no matches' : pluralize(snapshot.count, 'match', 'matches'));
687
+ if (condition.text !== undefined && snapshot.count > 0) {
688
+ parts.push(`${some(snapshot.textCount)} with text "${condition.text}"`);
689
+ }
690
+ if (condition.visible && snapshot.textCount > 0) {
691
+ parts.push(`${some(snapshot.visibleCount)} visible`);
692
+ }
693
+ }
694
+ if (condition.load || snapshot.readyState !== 'complete') {
695
+ parts.push(`document.readyState: ${snapshot.readyState}`);
696
+ }
697
+ return parts.join(', ');
142
698
  }
143
699
  /**
144
700
  * One line describing an iframe: index, URL, name/id, and how it is isolated.
@@ -152,7 +708,19 @@ export function frameLabel(frame) {
152
708
  frame.crossOrigin ? 'cross-origin' : 'same-origin',
153
709
  frame.outOfProcess && 'out-of-process',
154
710
  ].filter(Boolean);
155
- return [`[${frame.index}] ${frame.url}`, ...names, isolation.join(', ')].join(' ');
711
+ const label = `[${frame.index}] ${frameUrlLabel(frame.url)}`;
712
+ return [label, ...names, isolation.join(', ')].join(' ');
713
+ }
714
+ /** Longest frame URL shown in human output (JSON has the full URL) */
715
+ const FRAME_URL_MAX_LENGTH = 100;
716
+ /**
717
+ * A frame URL for human output: shortened, and named when empty.
718
+ *
719
+ * @param url - Frame URL
720
+ * @returns e.g. `https://pay.example/checkout?…`, or `(no URL)`
721
+ */
722
+ export function frameUrlLabel(url) {
723
+ return url ? truncateByLength(url, FRAME_URL_MAX_LENGTH) : '(no URL)';
156
724
  }
157
725
  /**
158
726
  * `bdg dom frames` on a page without iframes.
@@ -162,6 +730,63 @@ export function frameLabel(frame) {
162
730
  export function noFramesMessage() {
163
731
  return 'The page has no iframes';
164
732
  }
733
+ /**
734
+ * `bdg dom frames` while the page is still loading: its iframes may not
735
+ * exist yet.
736
+ *
737
+ * @param empty - No iframe was found
738
+ * @returns e.g. `No iframes yet; the page is still loading, so the list may be incomplete (bdg dom wait --load)`
739
+ */
740
+ export function framesStillLoadingNote(empty) {
741
+ const wait = `(${sessionCommand('bdg dom wait --load')})`;
742
+ return empty
743
+ ? `No iframes yet; the page is still loading, so the list may be incomplete ${wait}`
744
+ : `Note: the page is still loading, so the list may be incomplete ${wait}`;
745
+ }
746
+ /**
747
+ * Text of an element in `bdg dom get` output.
748
+ *
749
+ * @param text - Collapsed text, ending in `...` when it was cut
750
+ * @returns e.g. `Text: Welcome to ... (cut at 500 characters; --full shows all of it)`
751
+ */
752
+ export function elementTextLine(text) {
753
+ const cut = text.endsWith('...') ? ' (cut at 500 characters; --full shows all of it)' : '';
754
+ return `Text: ${text}${cut}`;
755
+ }
756
+ /**
757
+ * What an element without text holds, in `bdg dom get` output.
758
+ *
759
+ * @param children - First child elements, e.g. `iframe#app`
760
+ * @param count - Number of child elements
761
+ * @returns e.g. `No text; holds 1 element: iframe (see its HTML with --raw)`
762
+ */
763
+ export function emptyElementLine(children, count) {
764
+ if (count === 0)
765
+ return 'No text and no child elements';
766
+ const more = count > children.length ? `, … ${count - children.length} more` : '';
767
+ return `No text; holds ${pluralize(count, 'element')}: ${children.join(', ')}${more} (see its HTML with --raw)`;
768
+ }
769
+ /**
770
+ * Note on an element screenshot that captured more than the element's border
771
+ * box, because content (floats, positioned children) overflows it.
772
+ *
773
+ * @param box - Border box
774
+ * @param captured - Area captured
775
+ * @returns e.g. `grown from 940×37 to 940×285 to include content overflowing the element`
776
+ */
777
+ export function screenshotGrownNote(box, captured) {
778
+ return `grown from ${box.width}×${box.height} to ${captured.width}×${captured.height} to include content overflowing the element`;
779
+ }
780
+ /**
781
+ * Next commands after `bdg dom query`, by index so they reach matches in
782
+ * shadow roots and iframes too.
783
+ *
784
+ * @param index - Match to use in the examples
785
+ * @returns One line
786
+ */
787
+ export function queryNextSteps(index) {
788
+ return `Next: bdg dom get ${index} (text), bdg dom get ${index} --raw (HTML), bdg dom layout ${index} (position)`;
789
+ }
165
790
  /**
166
791
  * Header of `bdg dom eval --frame` output naming the frame the script ran in.
167
792
  *
@@ -169,7 +794,7 @@ export function noFramesMessage() {
169
794
  * @returns e.g. `Frame: https://pay.example/`
170
795
  */
171
796
  export function evalFrameLine(url) {
172
- return `Frame: ${url}`;
797
+ return `Frame: ${frameUrlLabel(url)}`;
173
798
  }
174
799
  /**
175
800
  * Generate warning message.
@@ -204,6 +829,15 @@ export function sessionOutputRemovedMessage() {
204
829
  export function sessionDirectoryCleanMessage() {
205
830
  return 'Session directory is now clean';
206
831
  }
832
+ /**
833
+ * `bdg cleanup --purge` deleted a named session's directory.
834
+ *
835
+ * @param dir - Deleted directory
836
+ * @returns Message
837
+ */
838
+ export function sessionDirectoryPurgedMessage(dir) {
839
+ return `Session directory removed: ${dir}`;
840
+ }
207
841
  /**
208
842
  * Generate no session files found message.
209
843
  *
@@ -221,6 +855,15 @@ export function noSessionFilesMessage() {
221
855
  export function sessionStillActiveError(pid) {
222
856
  return `Session is still active (PID ${pid})`;
223
857
  }
858
+ /**
859
+ * How to clean up a session that is still running.
860
+ *
861
+ * @param session - Name of a named session, or null for the default session
862
+ * @returns Suggestion lines
863
+ */
864
+ export function sessionStillActiveSuggestion(session) {
865
+ return `Stop gracefully: ${sessionCommand('bdg stop', session)}\nForce cleanup: ${sessionCommand('bdg cleanup --force', session)}`;
866
+ }
224
867
  /**
225
868
  * Generate help message when no URL is provided to start command.
226
869
  *