browser-debugger-cli 0.8.0 → 0.10.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 (304) hide show
  1. package/README.md +7 -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 +29 -6
  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 +189 -119
  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/keyAttributes.d.ts +20 -0
  27. package/dist/commands/dom/helpers/keyAttributes.js +54 -0
  28. package/dist/commands/dom/helpers/query.d.ts +44 -17
  29. package/dist/commands/dom/helpers/query.js +300 -106
  30. package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
  31. package/dist/commands/dom/helpers/runElementCommand.js +98 -30
  32. package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
  33. package/dist/commands/dom/helpers/screenshot.js +239 -51
  34. package/dist/commands/dom/index.d.ts +4 -1
  35. package/dist/commands/dom/index.js +22 -7
  36. package/dist/commands/dom/inspect.d.ts +15 -0
  37. package/dist/commands/dom/inspect.js +82 -0
  38. package/dist/commands/dom/layout.d.ts +14 -0
  39. package/dist/commands/dom/layout.js +54 -0
  40. package/dist/commands/dom/listeners.d.ts +5 -1
  41. package/dist/commands/dom/listeners.js +15 -5
  42. package/dist/commands/dom/query.js +2 -3
  43. package/dist/commands/dom/screenshot.d.ts +12 -2
  44. package/dist/commands/dom/screenshot.js +27 -3
  45. package/dist/commands/dom/semanticUtils.d.ts +16 -10
  46. package/dist/commands/dom/semanticUtils.js +53 -16
  47. package/dist/commands/dom/wait.d.ts +13 -0
  48. package/dist/commands/dom/wait.js +83 -0
  49. package/dist/commands/helpJson.js +2 -2
  50. package/dist/commands/network/list.js +17 -13
  51. package/dist/commands/optionBehaviors.js +154 -21
  52. package/dist/commands/page.d.ts +3 -2
  53. package/dist/commands/page.js +100 -5
  54. package/dist/commands/peek.js +4 -11
  55. package/dist/commands/sessions.d.ts +8 -0
  56. package/dist/commands/sessions.js +19 -0
  57. package/dist/commands/shared/CommandRunner.js +4 -4
  58. package/dist/commands/shared/commonOptions.d.ts +4 -0
  59. package/dist/commands/shared/commonOptions.js +9 -0
  60. package/dist/commands/shared/dataFetcher.js +2 -2
  61. package/dist/commands/shared/followMode.d.ts +21 -1
  62. package/dist/commands/shared/followMode.js +29 -2
  63. package/dist/commands/shared/handleValidationError.d.ts +2 -2
  64. package/dist/commands/shared/handleValidationError.js +12 -3
  65. package/dist/commands/shared/optionTypes.d.ts +61 -5
  66. package/dist/commands/shared/startHelpers.d.ts +66 -0
  67. package/dist/commands/shared/startHelpers.js +103 -13
  68. package/dist/commands/shared/validation.d.ts +14 -2
  69. package/dist/commands/shared/validation.js +20 -3
  70. package/dist/commands/start.d.ts +63 -0
  71. package/dist/commands/start.js +115 -15
  72. package/dist/commands/status.js +29 -7
  73. package/dist/commands/stop.js +7 -6
  74. package/dist/commands/tail.js +4 -11
  75. package/dist/commands/types.d.ts +2 -0
  76. package/dist/commands.js +2 -0
  77. package/dist/connection/chromeIdentity.d.ts +65 -0
  78. package/dist/connection/chromeIdentity.js +143 -0
  79. package/dist/connection/launcher/profilePreferences.d.ts +47 -0
  80. package/dist/connection/launcher/profilePreferences.js +151 -0
  81. package/dist/connection/launcher.d.ts +21 -2
  82. package/dist/connection/launcher.js +42 -16
  83. package/dist/connection/portReservation.d.ts +14 -4
  84. package/dist/connection/portReservation.js +21 -6
  85. package/dist/connection/startupExit.d.ts +8 -0
  86. package/dist/connection/startupExit.js +15 -6
  87. package/dist/constants.d.ts +6 -2
  88. package/dist/constants.js +9 -2
  89. package/dist/daemon/SessionController.js +23 -7
  90. package/dist/daemon/errors.d.ts +1 -1
  91. package/dist/daemon/errors.js +1 -1
  92. package/dist/daemon/launcher.d.ts +10 -2
  93. package/dist/daemon/launcher.js +8 -7
  94. package/dist/daemon/server/SocketServer.js +1 -2
  95. package/dist/daemon/session/Session.d.ts +20 -0
  96. package/dist/daemon/session/Session.js +80 -9
  97. package/dist/daemon/session/chromeConnection.d.ts +9 -0
  98. package/dist/daemon/session/chromeConnection.js +45 -8
  99. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  100. package/dist/daemon/session/commandRegistry.js +113 -67
  101. package/dist/daemon/session/interactions.d.ts +48 -9
  102. package/dist/daemon/session/interactions.js +46 -9
  103. package/dist/daemon/session/triggeredRequests.d.ts +67 -0
  104. package/dist/daemon/session/triggeredRequests.js +157 -0
  105. package/dist/daemon/session/types.d.ts +5 -1
  106. package/dist/daemon.js +10630 -3601
  107. package/dist/errors/messages.d.ts +456 -24
  108. package/dist/errors/messages.js +862 -67
  109. package/dist/index.js +6915 -3401
  110. package/dist/ipc/client.d.ts +21 -1
  111. package/dist/ipc/client.js +35 -3
  112. package/dist/ipc/protocol/commands.d.ts +145 -5
  113. package/dist/ipc/protocol/commands.js +4 -0
  114. package/dist/ipc/protocol/domTypes.d.ts +291 -7
  115. package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
  116. package/dist/ipc/protocol/inspectTypes.js +10 -0
  117. package/dist/ipc/session/lifecycle.d.ts +8 -1
  118. package/dist/ipc/session/queries.d.ts +5 -1
  119. package/dist/ipc/session/types.d.ts +5 -0
  120. package/dist/ipc/transport/index.d.ts +2 -1
  121. package/dist/ipc/transport/index.js +2 -2
  122. package/dist/runtime/dom/actionEffects.d.ts +185 -0
  123. package/dist/runtime/dom/actionEffects.js +402 -0
  124. package/dist/runtime/dom/actionEffectsScripts.d.ts +90 -0
  125. package/dist/runtime/dom/actionEffectsScripts.js +426 -0
  126. package/dist/runtime/dom/elementGeometry.d.ts +170 -0
  127. package/dist/runtime/dom/elementGeometry.js +553 -0
  128. package/dist/runtime/dom/elementInfo.d.ts +103 -0
  129. package/dist/runtime/dom/elementInfo.js +256 -0
  130. package/dist/runtime/dom/evalHelpers.d.ts +51 -6
  131. package/dist/runtime/dom/evalHelpers.js +136 -26
  132. package/dist/runtime/dom/eventListeners.d.ts +2 -1
  133. package/dist/runtime/dom/eventListeners.js +184 -47
  134. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  135. package/dist/runtime/dom/formDiscovery.js +116 -16
  136. package/dist/runtime/dom/formFillHelpers/fill.d.ts +9 -0
  137. package/dist/runtime/dom/formFillHelpers/fill.js +178 -14
  138. package/dist/runtime/dom/formFillHelpers/index.d.ts +2 -2
  139. package/dist/runtime/dom/formFillHelpers/index.js +2 -2
  140. package/dist/runtime/dom/formFillHelpers/pressKey.js +14 -3
  141. package/dist/runtime/dom/formFillHelpers/scroll.d.ts +3 -0
  142. package/dist/runtime/dom/formFillHelpers/scroll.js +60 -18
  143. package/dist/runtime/dom/formFillHelpers/shared.d.ts +17 -0
  144. package/dist/runtime/dom/formFillHelpers/shared.js +25 -1
  145. package/dist/runtime/dom/formFillHelpers/stability.d.ts +20 -6
  146. package/dist/runtime/dom/formFillHelpers/stability.js +50 -19
  147. package/dist/runtime/dom/formSubmitHelpers.d.ts +3 -0
  148. package/dist/runtime/dom/formSubmitHelpers.js +89 -15
  149. package/dist/runtime/dom/frameLayout.d.ts +60 -0
  150. package/dist/runtime/dom/frameLayout.js +140 -0
  151. package/dist/runtime/dom/frameOrigin.d.ts +50 -0
  152. package/dist/runtime/dom/frameOrigin.js +62 -0
  153. package/dist/runtime/dom/frameScopedConnection.d.ts +92 -0
  154. package/dist/runtime/dom/frameScopedConnection.js +252 -0
  155. package/dist/runtime/dom/frameSelection.d.ts +12 -1
  156. package/dist/runtime/dom/frameSelection.js +22 -3
  157. package/dist/runtime/dom/frames.d.ts +61 -5
  158. package/dist/runtime/dom/frames.js +329 -75
  159. package/dist/runtime/dom/inspect.d.ts +28 -0
  160. package/dist/runtime/dom/inspect.js +557 -0
  161. package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
  162. package/dist/runtime/dom/inspectAllStyles.js +385 -0
  163. package/dist/runtime/dom/inspectCascade.d.ts +94 -0
  164. package/dist/runtime/dom/inspectCascade.js +371 -0
  165. package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
  166. package/dist/runtime/dom/inspectCascadeModel.js +232 -0
  167. package/dist/runtime/dom/inspectHints.d.ts +62 -0
  168. package/dist/runtime/dom/inspectHints.js +305 -0
  169. package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
  170. package/dist/runtime/dom/inspectLayoutModel.js +346 -0
  171. package/dist/runtime/dom/inspectModel.d.ts +74 -0
  172. package/dist/runtime/dom/inspectModel.js +184 -0
  173. package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
  174. package/dist/runtime/dom/inspectPaintModel.js +461 -0
  175. package/dist/runtime/dom/inspectRules.d.ts +37 -0
  176. package/dist/runtime/dom/inspectRules.js +101 -0
  177. package/dist/runtime/dom/inspectScripts.d.ts +132 -0
  178. package/dist/runtime/dom/inspectScripts.js +263 -0
  179. package/dist/runtime/dom/inspectTree.d.ts +40 -0
  180. package/dist/runtime/dom/inspectTree.js +134 -0
  181. package/dist/runtime/dom/inspectVariables.d.ts +33 -0
  182. package/dist/runtime/dom/inspectVariables.js +94 -0
  183. package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
  184. package/dist/runtime/dom/inspectWhyModel.js +134 -0
  185. package/dist/runtime/dom/layout.d.ts +71 -0
  186. package/dist/runtime/dom/layout.js +340 -0
  187. package/dist/runtime/dom/listenerPageScripts.d.ts +72 -0
  188. package/dist/runtime/dom/listenerPageScripts.js +365 -0
  189. package/dist/runtime/dom/listenerSummary.d.ts +136 -11
  190. package/dist/runtime/dom/listenerSummary.js +361 -22
  191. package/dist/runtime/dom/pageActivity.d.ts +41 -0
  192. package/dist/runtime/dom/pageActivity.js +123 -0
  193. package/dist/runtime/dom/reactEventHelpers.d.ts +63 -2
  194. package/dist/runtime/dom/reactEventHelpers.js +220 -41
  195. package/dist/runtime/dom/targetNode.d.ts +80 -27
  196. package/dist/runtime/dom/targetNode.js +249 -33
  197. package/dist/runtime/dom/wait.d.ts +25 -0
  198. package/dist/runtime/dom/wait.js +199 -0
  199. package/dist/runtime/dom/waitCondition.d.ts +71 -0
  200. package/dist/runtime/dom/waitCondition.js +75 -0
  201. package/dist/runtime/page/emulation.d.ts +71 -0
  202. package/dist/runtime/page/emulation.js +117 -0
  203. package/dist/runtime/page/loadingState.d.ts +36 -0
  204. package/dist/runtime/page/loadingState.js +86 -0
  205. package/dist/runtime/page/navigation.d.ts +46 -2
  206. package/dist/runtime/page/navigation.js +69 -33
  207. package/dist/session/QueryCacheManager.d.ts +11 -1
  208. package/dist/session/QueryCacheManager.js +25 -3
  209. package/dist/session/chromeOwners.d.ts +34 -0
  210. package/dist/session/chromeOwners.js +51 -0
  211. package/dist/session/cleanup/staleSession.d.ts +11 -1
  212. package/dist/session/cleanup/staleSession.js +17 -6
  213. package/dist/session/cleanup/userCommands.js +2 -4
  214. package/dist/session/metadata.d.ts +5 -1
  215. package/dist/session/metadata.js +2 -1
  216. package/dist/session/paths.d.ts +77 -3
  217. package/dist/session/paths.js +111 -5
  218. package/dist/session/port.d.ts +31 -7
  219. package/dist/session/port.js +50 -43
  220. package/dist/session/portClaims.d.ts +66 -0
  221. package/dist/session/portClaims.js +284 -0
  222. package/dist/session/sessionList.d.ts +58 -0
  223. package/dist/session/sessionList.js +199 -0
  224. package/dist/session/sessionName.d.ts +46 -0
  225. package/dist/session/sessionName.js +97 -0
  226. package/dist/telemetry/a11y.d.ts +18 -3
  227. package/dist/telemetry/a11y.js +170 -29
  228. package/dist/telemetry/console.d.ts +1 -0
  229. package/dist/telemetry/console.js +100 -5
  230. package/dist/telemetry/network.js +3 -1
  231. package/dist/telemetry/requestKinds.d.ts +32 -0
  232. package/dist/telemetry/requestKinds.js +61 -0
  233. package/dist/telemetry/requestState.d.ts +31 -0
  234. package/dist/telemetry/requestState.js +38 -0
  235. package/dist/types.d.ts +112 -3
  236. package/dist/ui/formatters/a11y.js +3 -0
  237. package/dist/ui/formatters/console/chronological.d.ts +8 -0
  238. package/dist/ui/formatters/console/chronological.js +17 -4
  239. package/dist/ui/formatters/console/json.js +3 -4
  240. package/dist/ui/formatters/console/shared.d.ts +12 -0
  241. package/dist/ui/formatters/console.d.ts +2 -2
  242. package/dist/ui/formatters/console.js +1 -1
  243. package/dist/ui/formatters/details.d.ts +8 -0
  244. package/dist/ui/formatters/details.js +61 -4
  245. package/dist/ui/formatters/dom.d.ts +27 -14
  246. package/dist/ui/formatters/dom.js +88 -59
  247. package/dist/ui/formatters/form.js +29 -18
  248. package/dist/ui/formatters/inspect.d.ts +39 -0
  249. package/dist/ui/formatters/inspect.js +596 -0
  250. package/dist/ui/formatters/keyAttributes.d.ts +19 -0
  251. package/dist/ui/formatters/keyAttributes.js +84 -0
  252. package/dist/ui/formatters/layout.d.ts +31 -0
  253. package/dist/ui/formatters/layout.js +53 -0
  254. package/dist/ui/formatters/listeners.d.ts +3 -2
  255. package/dist/ui/formatters/listeners.js +73 -9
  256. package/dist/ui/formatters/networkHeaders.d.ts +13 -0
  257. package/dist/ui/formatters/networkHeaders.js +36 -3
  258. package/dist/ui/formatters/networkList.d.ts +29 -1
  259. package/dist/ui/formatters/networkList.js +86 -20
  260. package/dist/ui/formatters/preview.js +2 -1
  261. package/dist/ui/formatters/requestStatus.d.ts +1 -17
  262. package/dist/ui/formatters/requestStatus.js +2 -30
  263. package/dist/ui/formatters/sessions.d.ts +12 -0
  264. package/dist/ui/formatters/sessions.js +40 -0
  265. package/dist/ui/formatters/status.d.ts +21 -2
  266. package/dist/ui/formatters/status.js +47 -10
  267. package/dist/ui/formatters/triggeredRequests.d.ts +36 -0
  268. package/dist/ui/formatters/triggeredRequests.js +65 -0
  269. package/dist/ui/formatting.d.ts +19 -0
  270. package/dist/ui/formatting.js +31 -36
  271. package/dist/ui/messages/chrome.d.ts +9 -0
  272. package/dist/ui/messages/chrome.js +17 -5
  273. package/dist/ui/messages/commands.d.ts +504 -14
  274. package/dist/ui/messages/commands.js +835 -21
  275. package/dist/ui/messages/consoleMessages.d.ts +10 -0
  276. package/dist/ui/messages/consoleMessages.js +17 -0
  277. package/dist/ui/messages/hints.js +2 -1
  278. package/dist/ui/messages/networkMessages.d.ts +14 -0
  279. package/dist/ui/messages/networkMessages.js +18 -0
  280. package/dist/ui/messages/preview.js +5 -4
  281. package/dist/ui/messages/session.d.ts +30 -21
  282. package/dist/ui/messages/session.js +48 -26
  283. package/dist/ui/messages/sessionCommand.d.ts +43 -0
  284. package/dist/ui/messages/sessionCommand.js +52 -0
  285. package/dist/utils/async.d.ts +17 -0
  286. package/dist/utils/async.js +36 -0
  287. package/dist/utils/color.d.ts +84 -0
  288. package/dist/utils/color.js +376 -0
  289. package/dist/utils/cssValues.d.ts +109 -0
  290. package/dist/utils/cssValues.js +236 -0
  291. package/dist/utils/http.d.ts +22 -1
  292. package/dist/utils/http.js +28 -9
  293. package/dist/utils/selectorFilters.d.ts +48 -8
  294. package/dist/utils/selectorFilters.js +296 -53
  295. package/dist/utils/shellDetection.d.ts +8 -2
  296. package/dist/utils/shellDetection.js +120 -33
  297. package/dist/utils/suggestions.d.ts +26 -0
  298. package/dist/utils/suggestions.js +73 -0
  299. package/dist/utils/taskMappings.js +10 -0
  300. package/dist/utils/url.d.ts +12 -2
  301. package/dist/utils/url.js +69 -7
  302. package/package.json +1 -1
  303. package/dist/ui/formatters/sessionFormatters.d.ts +0 -58
  304. 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,475 @@ 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 state - Whether the action has warnings (shown right below), had no
86
+ * visible effect, or returned while the page was still changing
87
+ * @returns e.g. "✓ Element Clicked", "⚠ Element Clicked (with warnings)",
88
+ * "⚠ Element Clicked (page still changing)" or
89
+ * "⚠ Element Clicked (no visible effect observed: no DOM change, requests or navigation within 300 ms)"
90
+ */
91
+ export function actionStatusLine(done, state) {
92
+ if (state.noEffect)
93
+ return `⚠ ${done} (${NO_VISIBLE_EFFECT})`;
94
+ const notes = [state.warned && 'with warnings', state.stillChanging && 'page still changing'];
95
+ const shown = notes.filter((note) => typeof note === 'string');
96
+ return shown.length > 0 ? `⚠ ${done} (${shown.join('; ')})` : `✓ ${done}`;
97
+ }
98
+ /**
99
+ * Note under the status line of an action that returned while the page was
100
+ * still changing.
101
+ *
102
+ * @param action - What returned, e.g. "click", "key press"
103
+ * @param pending - What the page was still working on
104
+ * @returns e.g. "The page was still changing when the click returned (2 requests pending); wait for the result with bdg dom wait <selector>"
105
+ */
106
+ export function stillChangingNote(action, pending) {
107
+ const parts = [
108
+ pending.requests !== undefined && `${pluralize(pending.requests, 'request')} pending`,
109
+ pending.navigation && 'a new page still loading',
110
+ pending.loading !== undefined && `loading indicator ${pending.loading} shown`,
111
+ pending.domChanging && 'DOM still changing',
112
+ pending.busy && 'page busy running a script',
113
+ ].filter((part) => typeof part === 'string');
114
+ const wait = sessionCommand('bdg dom wait <selector>');
115
+ return `The page was still changing when the ${action} returned (${parts.join(', ')}); wait for the result with ${wait}`;
116
+ }
117
+ /**
118
+ * An element an action showed, for its `Shown:` rows.
119
+ *
120
+ * @param element - Shown element
121
+ * @returns e.g. `div.figcaption "name: user2 View profile"`
122
+ */
123
+ export function shownElementText(element) {
124
+ return `${element.element} "${element.text}"`;
125
+ }
126
+ /**
127
+ * How an action changed the page's location, for its `Page:` row.
128
+ *
129
+ * @param navigation - Navigation the action caused
130
+ * @returns e.g. "navigated to https://example.com/secure (200)" or
131
+ * "URL changed to https://example.com/#/active (same document)"
132
+ */
133
+ export function pageNavigationText(navigation) {
134
+ if (navigation.sameDocument)
135
+ return `URL changed to ${navigation.url} (same document)`;
136
+ const status = navigation.status === undefined ? '' : ` (${navigation.status})`;
137
+ return `navigated to ${navigation.url}${status}`;
138
+ }
139
+ /**
140
+ * A message an action made appear, for its `New text:` rows.
141
+ *
142
+ * @param message - New message
143
+ * @returns e.g. `"Your password is invalid!" (div#flash.flash.error)`
144
+ */
145
+ export function newMessageText(message) {
146
+ return `"${message.text}" (${message.element})`;
147
+ }
148
+ /**
149
+ * Warning shown when a filled field's value read back is not the one given:
150
+ * cut to its maxlength, a password of another length (values never shown),
151
+ * or another value (naming the field the value went to, when one has it).
152
+ *
153
+ * @param mismatch - Value given and value found (masked for passwords)
154
+ * @returns Warning text
155
+ */
156
+ export function valueMismatchWarning(mismatch) {
157
+ if (mismatch.truncatedTo !== undefined) {
158
+ return `The value was cut to ${mismatch.truncatedTo} characters by maxlength`;
159
+ }
160
+ if (mismatch.expectedLength !== undefined) {
161
+ 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`;
162
+ }
163
+ const outcome = mismatch.movedTo === undefined
164
+ ? 'the page may have rejected or moved the input'
165
+ : `the value appeared in ${mismatch.movedTo} instead`;
166
+ return `The field's value is "${mismatch.actual}" after filling (expected "${mismatch.expected}"); ${outcome}`;
167
+ }
168
+ /**
169
+ * Warning shown when a mouse press was dispatched but the target never
170
+ * received it (e.g. a browser dialog or bubble captured the input).
171
+ */
172
+ 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
173
  /**
37
174
  * Note under a shortened list of matches.
38
175
  *
39
176
  * @param hidden - Matches not listed
40
- * @returns e.g. "... and 1174 more (use --json for all)"
177
+ * @param jsonLimit - How many JSON output lists, when it leaves some out too
178
+ * @returns e.g. "... and 1174 more (use --json for all)",
179
+ * "... and 8980 more (--json lists the first 100)"
180
+ */
181
+ export function moreMatchesNote(hidden, jsonLimit) {
182
+ const where = jsonLimit === undefined ? 'use --json for all' : `--json lists the first ${jsonLimit}`;
183
+ return `... and ${hidden} more (${where})`;
184
+ }
185
+ /**
186
+ * Note under a list of a11y query matches cut by `--limit`.
187
+ *
188
+ * @param omitted - Matches not listed
189
+ * @returns e.g. "... and 213 more (--limit 0 lists all; their indices work too)"
190
+ */
191
+ export function a11yMoreMatchesNote(omitted) {
192
+ return `... and ${omitted} more (--limit 0 lists all; their indices work too)`;
193
+ }
194
+ /**
195
+ * Note under a shortened list of requests an action triggered when JSON
196
+ * output left some out too (they are only in the network telemetry then).
197
+ *
198
+ * @param hidden - Requests not listed
199
+ * @returns e.g. "... and 50 more (see bdg network list)"
200
+ */
201
+ export function moreRequestsNote(hidden) {
202
+ return `... and ${hidden} more (see ${sessionCommand('bdg network list')})`;
203
+ }
204
+ /**
205
+ * Title of the list of requests that started while an action ran (they are
206
+ * attributed by time, so a page poller's requests are listed too: the title
207
+ * doesn't claim the action caused them).
208
+ *
209
+ * @param total - Requests in all
210
+ * @returns e.g. "Requests during the action (18):"
211
+ */
212
+ export function triggeredRequestsTitle(total) {
213
+ return `Requests during the action (${total}):`;
214
+ }
215
+ /**
216
+ * Line counting the static assets an action loaded instead of listing them.
217
+ *
218
+ * @param count - Asset requests
219
+ * @param types - Short type names, e.g. ["css", "js", "images"]
220
+ * @returns e.g. "+ 97 assets (css, js, fonts, images)"
221
+ */
222
+ export function assetRequestsNote(count, types) {
223
+ return `+ ${count} ${count === 1 ? 'asset' : 'assets'} (${types.join(', ')})`;
224
+ }
225
+ /**
226
+ * Pieces of the reasons `bdg dom layout` gives for hidden and invisible
227
+ * elements. The page-side measurement builds the reasons from them, e.g.
228
+ * `clipped by div#acc: zero height`, `opacity: 0 on div#menu`.
229
+ */
230
+ export const LAYOUT_REASONS = {
231
+ /** Hidden: an `<option>` of a closed `<select>` has no box */
232
+ option: 'not rendered (an <option> is shown by its <select>)',
233
+ /** Hidden: start of the reason for a clipping container with no area, followed by it */
234
+ clippedBy: 'clipped by ',
235
+ /** Which size of that container is zero */
236
+ zeroHeight: 'zero height',
237
+ zeroWidth: 'zero width',
238
+ /** Invisible: fully transparent */
239
+ transparent: 'opacity: 0',
240
+ /** Joins an invisible reason to the ancestor causing it */
241
+ on: ' on ',
242
+ };
243
+ /** Start of the off-screen reason of an element a scroll-locked page hides ({@link scrollLockedReason}) */
244
+ const SCROLL_LOCKED_PREFIX = 'page scrolling is locked';
245
+ /**
246
+ * Off-screen reason for an element out of view on a page whose scrolling is
247
+ * locked, naming the visible dialog that likely locked it when there is one.
248
+ *
249
+ * @param lock - What locks it, e.g. `overflow: hidden on body`
250
+ * @param dialog - Visible dialog on the page, e.g. `div#consent` (null: none)
251
+ * @returns e.g. `page scrolling is locked (overflow: hidden on body), likely by dialog div#consent`,
252
+ * or `page scrolling is locked (overflow: hidden on body)`
253
+ */
254
+ export function scrollLockedReason(lock, dialog = null) {
255
+ const cause = dialog ? `, likely by dialog ${dialog}` : '';
256
+ return `${SCROLL_LOCKED_PREFIX} (${lock})${cause}`;
257
+ }
258
+ /** Short location hints for elements a user cannot see without scrolling */
259
+ const VIEWPORT_POSITION_HINTS = {
260
+ above: 'above viewport',
261
+ below: 'below fold',
262
+ left: 'left of viewport',
263
+ right: 'right of viewport',
264
+ hidden: 'hidden',
265
+ };
266
+ /**
267
+ * Location hint for an element outside the viewport or hidden.
268
+ *
269
+ * @param position - Where the element is
270
+ * @param clippedBy - Ancestor or iframe cutting it off, if any
271
+ * @returns e.g. "below fold", "out of view in ul#list"; undefined for
272
+ * elements (partly) in view
273
+ */
274
+ export function viewportPositionHint(position, clippedBy) {
275
+ const outside = position !== 'visible' && position !== 'partly' && position !== 'hidden';
276
+ if (outside && clippedBy)
277
+ return `out of view in ${clippedBy}`;
278
+ return VIEWPORT_POSITION_HINTS[position];
279
+ }
280
+ /**
281
+ * Page scroll that would bring an element into view, in words.
282
+ *
283
+ * @param scrollBy - Scroll amounts
284
+ * @param purpose - What the scroll does, e.g. "centre it"
285
+ * @returns e.g. "scroll down 760px to centre it"
286
+ */
287
+ function scrollAdvice(scrollBy, purpose) {
288
+ const steps = [
289
+ scrollBy.y !== 0 && `${scrollBy.y > 0 ? 'down' : 'up'} ${Math.abs(scrollBy.y)}px`,
290
+ scrollBy.x !== 0 && `${scrollBy.x > 0 ? 'right' : 'left'} ${Math.abs(scrollBy.x)}px`,
291
+ ].filter(Boolean);
292
+ return `scroll ${steps.join(', ')} to ${purpose}`;
293
+ }
294
+ /**
295
+ * Whether an element fits the viewport (the scroll then centres it; a larger
296
+ * one gets its start aligned).
297
+ *
298
+ * @param element - Element layout
299
+ * @param viewport - Viewport size, when known
300
+ * @returns True when it fits, or the viewport is unknown
301
+ */
302
+ function fitsViewport(element, viewport) {
303
+ const { width, height } = element.bounds;
304
+ return !viewport || (width <= viewport.width && height <= viewport.height);
305
+ }
306
+ /**
307
+ * How to bring an element that is out of view into view, in words.
308
+ *
309
+ * @param element - Element layout
310
+ * @param viewport - Viewport size (an element larger than it is not centred)
311
+ * @returns e.g. "scroll down 760px to centre it", "off-screen: fixed position, page scroll
312
+ * does not move it"; undefined when neither applies
313
+ */
314
+ function layoutScrollNote(element, viewport) {
315
+ if (element.scrollBy) {
316
+ const purpose = fitsViewport(element, viewport) ? 'centre it' : 'bring it into view';
317
+ return scrollAdvice(element.scrollBy, purpose);
318
+ }
319
+ return element.offScreenReason && `off-screen: ${element.offScreenReason}`;
320
+ }
321
+ /**
322
+ * How to see all of an element that is partly in view, in words.
323
+ *
324
+ * @param element - Element layout
325
+ * @param viewport - Viewport size (for an element larger than it, the scroll shows its start)
326
+ * @returns e.g. "scroll down 302px to see all of it", "sticky position, page scroll moves it
327
+ * only until it sticks"; undefined when neither applies
328
+ */
329
+ function partlyVisibleNote(element, viewport) {
330
+ if (element.scrollBy) {
331
+ const purpose = fitsViewport(element, viewport) ? 'see all of it' : 'show it from its start';
332
+ return scrollAdvice(element.scrollBy, purpose);
333
+ }
334
+ return element.offScreenReason;
335
+ }
336
+ /**
337
+ * Where an element is relative to the viewport, for `bdg dom layout`.
338
+ *
339
+ * @param element - Element layout
340
+ * @param viewport - Viewport size, when known
341
+ * @returns e.g. "visible", "partly visible (40%); scroll down 302px to see all of it",
342
+ * "below fold (scroll down 760px to centre it)",
343
+ * "out of view in ul#list (below)", "hidden (display: none)",
344
+ * "left of viewport (off-screen: beyond the page's scroll range)",
345
+ * "below fold; page scrolling is locked (overflow: hidden on body), likely by dialog div#consent"
346
+ */
347
+ export function layoutPositionLabel(element, viewport) {
348
+ const { inViewport, percentVisible, hiddenReason, clippedBy, offScreenReason } = element;
349
+ if (inViewport === 'visible')
350
+ return 'visible';
351
+ const locked = !element.scrollBy && offScreenReason?.startsWith(SCROLL_LOCKED_PREFIX);
352
+ if (inViewport === 'partly') {
353
+ const clipped = clippedBy ? `, clipped by ${clippedBy}` : '';
354
+ const label = `partly visible (${percentVisible ?? 0}%${clipped})`;
355
+ const note = partlyVisibleNote(element, viewport);
356
+ return note ? `${label}; ${note}` : label;
357
+ }
358
+ const note = locked ? undefined : layoutScrollNote(element, viewport);
359
+ const label = viewportPositionHint(inViewport, clippedBy) ?? inViewport;
360
+ if (hiddenReason)
361
+ return `${label} (${hiddenReason})`;
362
+ if (clippedBy)
363
+ return `${label} (${inViewport})`;
364
+ if (locked)
365
+ return `${label}; ${offScreenReason}`;
366
+ return note ? `${label} (${note})` : label;
367
+ }
368
+ /**
369
+ * The `prefers-color-scheme` media feature the page sees, labelled as the
370
+ * preference it is (the page may still render its own theme), and where it
371
+ * comes from.
372
+ *
373
+ * @param scheme - Light or dark
374
+ * @param emulated - Set with `--color-scheme` or `page emulate` (otherwise the system setting)
375
+ * @returns e.g. `prefers-color-scheme: dark (from the system setting)`
376
+ */
377
+ export function colorSchemeLabel(scheme, emulated) {
378
+ const source = emulated ? 'emulated' : 'from the system setting';
379
+ return `prefers-color-scheme: ${scheme} (${source})`;
380
+ }
381
+ /**
382
+ * First line of `bdg status` for a running session.
383
+ *
384
+ * @param page - URL and title of the page, when the session reported them
385
+ * @returns e.g. `Session active: https://example.com/ — Example Domain`
386
+ */
387
+ export function sessionActiveLine(page) {
388
+ if (!page)
389
+ return 'Session active';
390
+ return `Session active: ${page.url}${page.title ? ` — ${page.title}` : ''}`;
391
+ }
392
+ /**
393
+ * Page dimensions line of `bdg dom layout`.
394
+ *
395
+ * @param page - Viewport, scroll position, document size and the color scheme the page is told to prefer
396
+ * @returns e.g. "Page: viewport 1280×720, scrolled to 0,0, document 1280×2400, prefers-color-scheme: dark"
397
+ */
398
+ export function pageLayoutLine(page) {
399
+ const { viewport, scroll, document, colorScheme } = page;
400
+ const scheme = colorScheme ? `, prefers-color-scheme: ${colorScheme}` : '';
401
+ return `Page: viewport ${viewport.width}×${viewport.height}, scrolled to ${scroll.x},${scroll.y}, document ${document.width}×${document.height}${scheme}`;
402
+ }
403
+ /**
404
+ * Headline of `bdg dom layout`.
405
+ *
406
+ * @param count - Elements matched
407
+ * @param listed - Elements reported (fewer with --index)
408
+ * @param selector - Selector they matched
409
+ * @returns e.g. '3 elements match "button" (page x,y and size in CSS px):',
410
+ * '1 of 3 elements matching "button" (...)'
411
+ */
412
+ export function layoutHeadline(count, listed, selector) {
413
+ const matched = listed < count
414
+ ? `${listed} of ${count} elements matching "${selector}"`
415
+ : `${count} element${count === 1 ? ' matches' : 's match'} "${selector}"`;
416
+ return `${matched} (page x,y and size in CSS px):`;
417
+ }
418
+ /**
419
+ * Headline of `bdg dom layout` for a cached index.
420
+ *
421
+ * @param target - The index and the list it refers to, in words
422
+ * @returns e.g. `Element at index 0 of the last dom query "h3" (page x,y and size in CSS px):`
423
+ */
424
+ export function indexLayoutHeadline(target) {
425
+ return `Element at ${target} (page x,y and size in CSS px):`;
426
+ }
427
+ /** Help text explaining `bdg dom inspect`'s output notation */
428
+ export const INSPECT_OUTPUT_LEGEND = `
429
+ Output notation:
430
+ WxH @x,y rendered border box size and page position (CSS px, no unit)
431
+ m / p / b margin / padding / border widths, 1-4 values in CSS order (top right bottom left)
432
+ in-parent distances to the parent's content edges (l t r b); sib: gaps to the sibling on each side
433
+ scroll WxH the content (pseudo-elements too) is larger than the box
434
+ 16/24 font size / line height; 'webfont loaded' = drawn with a downloaded font;
435
+ (rendered "X") = drawn with another font than declared (a fallback)
436
+ contrast 4.47 WCAG ratio, rounded down, against the background behind the text
437
+ (+N not rendered) children with display: none (or not in the layout)
438
+ hints declarations on this element that have no effect, why, the fix and where they are
439
+ ('none': checked, nothing found)
440
+ ← sel (file:N) --rules: the declaration that sets the value (file:line, or file:line:column in
441
+ minified files); 'over X': rules it beats; '= v': the value of a var() expression
442
+ ✓ / ✗ --why: the winning declaration / ones it beats, highest precedence first;
443
+ [0,2,0]: selector specificity (ids, classes, types); indented --name lines: where
444
+ the winner's custom properties are set
445
+ Sessions follow the system color scheme; start with --color-scheme light|dark to choose.`;
446
+ /**
447
+ * What covers an element: a cover that paints nothing at that point (a
448
+ * transparent box over it) does not hide it, but takes its clicks.
449
+ *
450
+ * @param cover - Description of the covering element
451
+ * @param transparent - The cover paints nothing there
452
+ * @returns e.g. `covered by div#modal`, `under transparent ul.filters (clicks land on it)`
453
+ */
454
+ export function coverText(cover, transparent) {
455
+ return transparent ? `under transparent ${cover} (clicks land on it)` : `covered by ${cover}`;
456
+ }
457
+ /**
458
+ * Note when `bdg dom inspect` could not read the element's matched rules, so
459
+ * no hints, rules or why were computed.
460
+ *
461
+ * @param reason - `timeout` (very large stylesheets) or `failed` (Chrome reported an error)
462
+ * @returns Note
41
463
  */
42
- export function moreMatchesNote(hidden) {
43
- return `... and ${hidden} more (use --json for all)`;
464
+ export function inspectCascadeNote(reason) {
465
+ return reason === 'timeout'
466
+ ? "CSS rules not read: the page's stylesheets took too long (hints wait 1 s; --rules and --why 5 s)"
467
+ : 'CSS rules not read: Chrome could not report the rules matching this element';
468
+ }
469
+ /**
470
+ * Header badge of `bdg dom inspect` when the page is shown in its dark theme
471
+ * because the session follows the system's dark preference: the colors are
472
+ * the dark theme's, not what a light-mode visitor sees.
473
+ *
474
+ * @returns Badge
475
+ */
476
+ export function inspectDarkThemeBadge() {
477
+ return '[dark theme from system; --color-scheme light for light]';
478
+ }
479
+ /**
480
+ * Header badges of `bdg dom inspect` for what keeps an element from being seen.
481
+ *
482
+ * @param visibility - Not rendered, hidden, offscreen, covered
483
+ * @returns e.g. `[not rendered: display: none]`, `[offscreen: below]`, `[covered by div#modal]`
484
+ */
485
+ export function inspectVisibilityBadges(visibility) {
486
+ const reason = (text) => (text ? `: ${text}` : '');
487
+ if (visibility.notRendered) {
488
+ return [`[not rendered${reason(visibility.hidden?.replace(/^not rendered \((.*)\)$/, '$1'))}]`];
489
+ }
490
+ return [
491
+ visibility.hidden && `[hidden: ${visibility.hidden}]`,
492
+ visibility.offscreen && `[offscreen: ${visibility.offscreen}]`,
493
+ visibility.coveredBy && `[${coverText(visibility.coveredBy, visibility.coverTransparent)}]`,
494
+ ].filter((badge) => Boolean(badge));
495
+ }
496
+ /**
497
+ * What `bdg dom inspect` did when several elements matched and no --index was given.
498
+ *
499
+ * @param picked - How the match was chosen
500
+ * @param index - Index of the inspected match
501
+ * @returns e.g. "inspected the first visible one ([2])"
502
+ */
503
+ export function inspectedMatchAction(picked, index) {
504
+ return picked === 'first-visible'
505
+ ? `inspected the first visible one ([${index}])`
506
+ : 'inspected the first';
44
507
  }
45
508
  /**
46
509
  * Warning when a selector matched several elements and no --index was given.
@@ -53,14 +516,55 @@ export function multipleMatchesWarning(count, action) {
53
516
  return `${count} elements match; ${action} (use --index or a more specific selector)`;
54
517
  }
55
518
  /**
56
- * Headline of `bdg dom listeners`.
519
+ * Headline of `bdg dom listeners`, saying where the counted listeners are:
520
+ * the list covers the element, its ancestors, its document and window.
57
521
  *
58
522
  * @param element - Inspected element, e.g. "button#save"
59
- * @param count - Listeners found
60
- * @returns e.g. "Event listeners for button#save (3)"
523
+ * @param counts - Listeners found, by placement
524
+ * @param context - Index of the element and the iframe it is in, if any
525
+ * @returns e.g. "Event listeners for button#save (156: 3 on the element, 120 on ancestors, 33 on document and window)"
61
526
  */
62
- export function listenersHeadline(element, count) {
63
- return `Event listeners for ${element} (${count})`;
527
+ export function listenersHeadline(element, counts, context = {}) {
528
+ const index = context.index === undefined ? '' : ` [${context.index}]`;
529
+ const frame = context.frame ? ` in ${context.frame}` : '';
530
+ const total = counts.target + counts.ancestor + counts.global;
531
+ const where = [
532
+ counts.target > 0 && `${counts.target} on the element`,
533
+ counts.ancestor > 0 && `${counts.ancestor} on ancestors`,
534
+ counts.global > 0 && `${counts.global} on document and window`,
535
+ ].filter(Boolean);
536
+ return `Event listeners for ${element}${index}${frame} (${total}: ${where.join(', ')})`;
537
+ }
538
+ /** Heading of the collapsed framework root listeners */
539
+ export const COLLAPSED_LISTENERS_HEADING = 'Framework roots (one line per node; --all lists each listener):';
540
+ /**
541
+ * One collapsed framework root.
542
+ *
543
+ * @param root - Framework label, event types, phases and dispatcher names
544
+ * @returns e.g. "React root: 90 event types, capture and bubble (dispatchEvent, …)"
545
+ */
546
+ export function collapsedListenersSummary(root) {
547
+ const phases = [root.capture && 'capture', root.bubble && 'bubble'].filter(Boolean).join(' and ');
548
+ const label = root.framework ?? 'Dispatcher';
549
+ return `${label}: ${root.types.length} event types, ${phases} (${root.handlers.join(', ')})`;
550
+ }
551
+ /**
552
+ * jQuery handlers left unresolved because there were too many.
553
+ *
554
+ * @param count - Handlers not resolved
555
+ * @returns One-line note
556
+ */
557
+ export function jqueryHandlersSkippedNote(count) {
558
+ return `Note: ${count} more jQuery handler${count === 1 ? '' : 's'} not resolved; their jQuery dispatcher is listed instead (narrow down with --type)`;
559
+ }
560
+ /**
561
+ * Event types a mistyped `--type` probably meant.
562
+ *
563
+ * @param types - Suggested types
564
+ * @returns e.g. "Did you mean: --type click?"
565
+ */
566
+ export function eventTypeSuggestion(types) {
567
+ return `Did you mean: --type ${types.join(',')}? (event types are case-sensitive, without "on")`;
64
568
  }
65
569
  /**
66
570
  * `bdg dom listeners` found nothing.
@@ -75,14 +579,57 @@ export function noListenersMessage(element, types) {
75
579
  }
76
580
  /** What `bdg dom listeners` covers, shown when it found nothing */
77
581
  export const NO_LISTENERS_HINT = 'Inline on… attributes and on… properties are included, and so are handlers frameworks delegate to ancestors (React, jQuery)';
582
+ /** Event types named in a note; the rest are counted */
583
+ const NOTE_TYPES_SHOWN = 5;
584
+ /**
585
+ * "click", "click and keydown", "click, keydown and input", "a, b, c, d, e
586
+ * and 3 more".
587
+ *
588
+ * @param types - Event types
589
+ * @returns The types as a list in prose
590
+ */
591
+ function typeList(types) {
592
+ if (types.length > NOTE_TYPES_SHOWN + 1) {
593
+ return `${types.slice(0, NOTE_TYPES_SHOWN).join(', ')} and ${types.length - NOTE_TYPES_SHOWN} more`;
594
+ }
595
+ return types.length > 1
596
+ ? `${types.slice(0, -1).join(', ')} and ${types[types.length - 1]}`
597
+ : (types[0] ?? '');
598
+ }
599
+ /**
600
+ * Note for interaction event types the element has no listener of its own for.
601
+ *
602
+ * @param note - How the types reach their handlers
603
+ * @returns One-line note, or undefined when the listed handlers say it all
604
+ */
605
+ export function delegationNote(note) {
606
+ const types = typeList(note.types);
607
+ const has = note.types.length === 1 ? 'has' : 'have';
608
+ const placeholders = typeList(note.placeholderTypes);
609
+ const placeholder = `the element's own ${placeholders} listener is only React's no-op placeholder`;
610
+ switch (note.kind) {
611
+ case 'react':
612
+ if (note.placeholderTypes.length === 0)
613
+ return undefined;
614
+ return `Note: ${placeholder}; the React on… handlers listed above for ${placeholders} run from React's root container`;
615
+ case 'react-root':
616
+ 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}` : ''}`;
617
+ case 'jquery':
618
+ return `Note: ${types} ${has} no listener on the element itself; jQuery runs the handlers listed above by delegation from ${note.node ?? 'an ancestor'}`;
619
+ case 'delegated': {
620
+ const own = note.placeholderTypes.length > 0 ? 'no listener that does anything' : 'no listener';
621
+ 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)`;
622
+ }
623
+ }
624
+ }
78
625
  /**
79
- * Note for event types handled only by ancestors, document or window.
626
+ * React prop handlers left unresolved because there were too many.
80
627
  *
81
- * @param types - Event types without a listener on the element itself
628
+ * @param count - Handlers not resolved
82
629
  * @returns One-line note
83
630
  */
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`;
631
+ export function reactHandlersSkippedNote(count) {
632
+ return `Note: ${count} more React handler prop${count === 1 ? '' : 's'} not listed (narrow down with --type)`;
86
633
  }
87
634
  /**
88
635
  * A JavaScript dialog bdg accepted, as one line.
@@ -101,6 +648,13 @@ export const POINTER_ACTION_DONE = {
101
648
  right: 'Right-clicked',
102
649
  hover: 'Hovered',
103
650
  };
651
+ /** What each pointer action is called in notes ("when the click returned") */
652
+ export const POINTER_ACTION_NOUN = {
653
+ click: 'click',
654
+ double: 'double-click',
655
+ right: 'right-click',
656
+ hover: 'hover',
657
+ };
104
658
  /** Headline of each `bdg page` action */
105
659
  export const PAGE_ACTION_DONE = {
106
660
  navigate: 'Navigated',
@@ -108,6 +662,10 @@ export const PAGE_ACTION_DONE = {
108
662
  back: 'Went back',
109
663
  forward: 'Went forward',
110
664
  };
665
+ /** Description of `bdg page info` */
666
+ export const PAGE_INFO_DESCRIPTION = 'Show the URL and title of the session page';
667
+ /** Description of `bdg page emulate` */
668
+ export const PAGE_EMULATE_DESCRIPTION = 'Change the viewport or color scheme mid-session (like --viewport and --color-scheme at start), or --reset both';
111
669
  /** Help text of the `bdg page` history commands */
112
670
  export const PAGE_ACTION_DESCRIPTIONS = {
113
671
  reload: 'Reload the page',
@@ -115,13 +673,21 @@ export const PAGE_ACTION_DESCRIPTIONS = {
115
673
  forward: 'Go forward one page (like the browser button)',
116
674
  };
117
675
  /**
118
- * `bdg page navigate` to a page that answered with an HTTP error.
676
+ * `bdg page navigate|reload|back|forward` when the document answered with an
677
+ * error status, or the page then loaded another document that answered
678
+ * differently (a 404 page whose script loads the app).
119
679
  *
120
- * @param status - HTTP status
121
- * @returns Warning
680
+ * @param status - HTTP status of the navigation's document
681
+ * @param later - The last document the page loaded afterwards, if any
682
+ * @returns Warning, or undefined when there is nothing to report
122
683
  */
123
- export function httpErrorWarning(status) {
124
- return `The page responded with HTTP ${status}`;
684
+ export function documentStatusWarning(status, later) {
685
+ const failed = status >= 400;
686
+ const laterFailed = later !== undefined && later.status >= 400;
687
+ if (later && (failed || laterFailed)) {
688
+ return `The page responded with HTTP ${status}, then loaded ${later.url} (HTTP ${later.status})`;
689
+ }
690
+ return failed ? `The page responded with HTTP ${status}` : undefined;
125
691
  }
126
692
  /**
127
693
  * `bdg page navigate` to a URL that loaded no page.
@@ -138,7 +704,118 @@ export function notAPageWarning() {
138
704
  * @returns Warning
139
705
  */
140
706
  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)`;
707
+ return `The new page has not answered within ${Math.round(ms / 1000)}s; it is still loading (check with ${sessionCommand('bdg status')})`;
708
+ }
709
+ /**
710
+ * A request the page is still waiting for.
711
+ *
712
+ * @param request - Pending request
713
+ * @returns e.g. `GET code.jquery.com/ui/.../jquery-ui.js (pending 30s)`
714
+ */
715
+ function pendingRequestLabel(request) {
716
+ return `${request.method} ${truncateUrl(request.url)} (pending ${formatDuration(request.pendingMs)})`;
717
+ }
718
+ /**
719
+ * Requests still running, the longest-running first, in words.
720
+ *
721
+ * @param pending - The requests to name
722
+ * @param count - All requests still running
723
+ * @returns e.g. `GET …/app.js (pending 30s) and 2 more`
724
+ */
725
+ export function pendingRequestsText(pending, count) {
726
+ const more = count - pending.length;
727
+ return `${pending.map(pendingRequestLabel).join(', ')}${more > 0 ? ` and ${more} more` : ''}`;
728
+ }
729
+ /**
730
+ * How far a page request got, in words.
731
+ *
732
+ * @param request - The page request
733
+ * @returns e.g. `POST …/authenticate pending for 30s`, `POST …/authenticate returned 503 Service Unavailable`,
734
+ * `POST …/authenticate failed (net::ERR_CONNECTION_REFUSED)`
735
+ */
736
+ export function documentRequestText(request) {
737
+ const target = `${request.method} ${truncateUrl(request.url)}`;
738
+ if (request.errorText !== undefined)
739
+ return `${target} failed (${request.errorText})`;
740
+ if (request.status !== undefined) {
741
+ return `${target} returned ${request.status}${request.statusText ? ` ${request.statusText}` : ''}`;
742
+ }
743
+ return `${target} pending for ${formatDuration(request.pendingMs ?? 0)}`;
744
+ }
745
+ /**
746
+ * Start and `bdg page` when the document has not finished loading within
747
+ * the readiness wait: its readyState and the requests it is waiting on.
748
+ *
749
+ * @param state - Loading state of the page
750
+ * @returns Warning, e.g. `The page is still loading (document.readyState: loading); waiting on: GET …/jquery-ui.js (pending 30s)`
751
+ */
752
+ export function pageLoadingWarning(state) {
753
+ const waitingOn = state.pending.length > 0
754
+ ? `; waiting on: ${pendingRequestsText(state.pending, state.pendingCount)}`
755
+ : '';
756
+ 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`;
757
+ }
758
+ /**
759
+ * Help of `dom click`/`submit`: they wait for the network only, so results a
760
+ * page shows later (timers, spinners, animations) are waited for with `dom wait`.
761
+ */
762
+ 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); the result says', '"page still changing" when it saw such work pending. Wait for those with:', " bdg dom wait '#result' --visible # or --text 'Saved', or '.spinner' --gone");
763
+ /** Examples in the help of `bdg dom wait` */
764
+ 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');
765
+ /**
766
+ * The elements `bdg dom wait` waits for.
767
+ *
768
+ * @param condition - What is waited for
769
+ * @returns e.g. `#finish with text "hello world"`
770
+ */
771
+ export function waitTargetLabel(condition) {
772
+ const text = condition.text !== undefined ? ` with text "${condition.text}"` : '';
773
+ return `${condition.selector ?? 'the page'}${text}`;
774
+ }
775
+ /**
776
+ * One-line result of `bdg dom wait`.
777
+ *
778
+ * @param condition - What was waited for
779
+ * @param elapsedMs - How long it took
780
+ * @returns e.g. `✓ div#finish visible after 5.1s`
781
+ */
782
+ export function waitMetMessage(condition, elapsedMs) {
783
+ const after = `after ${(elapsedMs / 1000).toFixed(1)}s`;
784
+ if (condition.selector === undefined)
785
+ return `✓ Page loaded ${after}`;
786
+ const state = condition.gone
787
+ ? condition.visible
788
+ ? 'hidden'
789
+ : 'gone'
790
+ : condition.visible
791
+ ? 'visible'
792
+ : 'found';
793
+ const loaded = condition.load ? ' and page loaded' : '';
794
+ return `✓ ${waitTargetLabel(condition)} ${state}${loaded} ${after}`;
795
+ }
796
+ /**
797
+ * What the page showed, for a `bdg dom wait` that timed out.
798
+ *
799
+ * @param snapshot - Last thing the page reported
800
+ * @param condition - What was waited for
801
+ * @returns e.g. `2 matches, none visible` or `document.readyState: loading`
802
+ */
803
+ export function waitSnapshotSummary(snapshot, condition) {
804
+ const some = (count) => (count === 0 ? 'none' : String(count));
805
+ const parts = [];
806
+ if (condition.selector !== undefined) {
807
+ parts.push(snapshot.count === 0 ? 'no matches' : pluralize(snapshot.count, 'match', 'matches'));
808
+ if (condition.text !== undefined && snapshot.count > 0) {
809
+ parts.push(`${some(snapshot.textCount)} with text "${condition.text}"`);
810
+ }
811
+ if (condition.visible && snapshot.textCount > 0) {
812
+ parts.push(`${some(snapshot.visibleCount)} visible`);
813
+ }
814
+ }
815
+ if (condition.load || snapshot.readyState !== 'complete') {
816
+ parts.push(`document.readyState: ${snapshot.readyState}`);
817
+ }
818
+ return parts.join(', ');
142
819
  }
143
820
  /**
144
821
  * One line describing an iframe: index, URL, name/id, and how it is isolated.
@@ -152,7 +829,19 @@ export function frameLabel(frame) {
152
829
  frame.crossOrigin ? 'cross-origin' : 'same-origin',
153
830
  frame.outOfProcess && 'out-of-process',
154
831
  ].filter(Boolean);
155
- return [`[${frame.index}] ${frame.url}`, ...names, isolation.join(', ')].join(' ');
832
+ const label = `[${frame.index}] ${frameUrlLabel(frame.url)}`;
833
+ return [label, ...names, isolation.join(', ')].join(' ');
834
+ }
835
+ /** Longest frame URL shown in human output (JSON has the full URL) */
836
+ const FRAME_URL_MAX_LENGTH = 100;
837
+ /**
838
+ * A frame URL for human output: shortened, and named when empty.
839
+ *
840
+ * @param url - Frame URL
841
+ * @returns e.g. `https://pay.example/checkout?…`, or `(no URL)`
842
+ */
843
+ export function frameUrlLabel(url) {
844
+ return url ? truncateByLength(url, FRAME_URL_MAX_LENGTH) : '(no URL)';
156
845
  }
157
846
  /**
158
847
  * `bdg dom frames` on a page without iframes.
@@ -162,6 +851,63 @@ export function frameLabel(frame) {
162
851
  export function noFramesMessage() {
163
852
  return 'The page has no iframes';
164
853
  }
854
+ /**
855
+ * `bdg dom frames` while the page is still loading: its iframes may not
856
+ * exist yet.
857
+ *
858
+ * @param empty - No iframe was found
859
+ * @returns e.g. `No iframes yet; the page is still loading, so the list may be incomplete (bdg dom wait --load)`
860
+ */
861
+ export function framesStillLoadingNote(empty) {
862
+ const wait = `(${sessionCommand('bdg dom wait --load')})`;
863
+ return empty
864
+ ? `No iframes yet; the page is still loading, so the list may be incomplete ${wait}`
865
+ : `Note: the page is still loading, so the list may be incomplete ${wait}`;
866
+ }
867
+ /**
868
+ * Text of an element in `bdg dom get` output.
869
+ *
870
+ * @param text - Collapsed text, ending in `...` when it was cut
871
+ * @returns e.g. `Text: Welcome to ... (cut at 500 characters; --full shows all of it)`
872
+ */
873
+ export function elementTextLine(text) {
874
+ const cut = text.endsWith('...') ? ' (cut at 500 characters; --full shows all of it)' : '';
875
+ return `Text: ${text}${cut}`;
876
+ }
877
+ /**
878
+ * What an element without text holds, in `bdg dom get` output.
879
+ *
880
+ * @param children - First child elements, e.g. `iframe#app`
881
+ * @param count - Number of child elements
882
+ * @returns e.g. `No text; holds 1 element: iframe (see its HTML with --raw)`
883
+ */
884
+ export function emptyElementLine(children, count) {
885
+ if (count === 0)
886
+ return 'No text and no child elements';
887
+ const more = count > children.length ? `, … ${count - children.length} more` : '';
888
+ return `No text; holds ${pluralize(count, 'element')}: ${children.join(', ')}${more} (see its HTML with --raw)`;
889
+ }
890
+ /**
891
+ * Note on an element screenshot that captured more than the element's border
892
+ * box, because content (floats, positioned children) overflows it.
893
+ *
894
+ * @param box - Border box
895
+ * @param captured - Area captured
896
+ * @returns e.g. `grown from 940×37 to 940×285 to include content overflowing the element`
897
+ */
898
+ export function screenshotGrownNote(box, captured) {
899
+ return `grown from ${box.width}×${box.height} to ${captured.width}×${captured.height} to include content overflowing the element`;
900
+ }
901
+ /**
902
+ * Next commands after `bdg dom query`, by index so they reach matches in
903
+ * shadow roots and iframes too.
904
+ *
905
+ * @param index - Match to use in the examples
906
+ * @returns One line
907
+ */
908
+ export function queryNextSteps(index) {
909
+ return `Next: bdg dom get ${index} (text), bdg dom get ${index} --raw (HTML), bdg dom layout ${index} (position)`;
910
+ }
165
911
  /**
166
912
  * Header of `bdg dom eval --frame` output naming the frame the script ran in.
167
913
  *
@@ -169,7 +915,7 @@ export function noFramesMessage() {
169
915
  * @returns e.g. `Frame: https://pay.example/`
170
916
  */
171
917
  export function evalFrameLine(url) {
172
- return `Frame: ${url}`;
918
+ return `Frame: ${frameUrlLabel(url)}`;
173
919
  }
174
920
  /**
175
921
  * Generate warning message.
@@ -204,6 +950,15 @@ export function sessionOutputRemovedMessage() {
204
950
  export function sessionDirectoryCleanMessage() {
205
951
  return 'Session directory is now clean';
206
952
  }
953
+ /**
954
+ * `bdg cleanup --purge` deleted a named session's directory.
955
+ *
956
+ * @param dir - Deleted directory
957
+ * @returns Message
958
+ */
959
+ export function sessionDirectoryPurgedMessage(dir) {
960
+ return `Session directory removed: ${dir}`;
961
+ }
207
962
  /**
208
963
  * Generate no session files found message.
209
964
  *
@@ -221,6 +976,15 @@ export function noSessionFilesMessage() {
221
976
  export function sessionStillActiveError(pid) {
222
977
  return `Session is still active (PID ${pid})`;
223
978
  }
979
+ /**
980
+ * How to clean up a session that is still running.
981
+ *
982
+ * @param session - Name of a named session, or null for the default session
983
+ * @returns Suggestion lines
984
+ */
985
+ export function sessionStillActiveSuggestion(session) {
986
+ return `Stop gracefully: ${sessionCommand('bdg stop', session)}\nForce cleanup: ${sessionCommand('bdg cleanup --force', session)}`;
987
+ }
224
988
  /**
225
989
  * Generate help message when no URL is provided to start command.
226
990
  *
@@ -238,4 +1002,54 @@ export function sessionStillActiveError(pid) {
238
1002
  export function startCommandHelpMessage() {
239
1003
  return joinLines('', buildAgentDiscoveryHelp(), '', buildCommonTaskExamples(), '', buildUrlExamples(), '', buildSessionManagementReminder(), '', 'Not sure which command? Start a session to see all available commands:', ' bdg <url>', '');
240
1004
  }
1005
+ /**
1006
+ * `bdg page emulate` without anything to change.
1007
+ *
1008
+ * @returns Message and suggestion
1009
+ */
1010
+ export function pageEmulateNothingError() {
1011
+ return {
1012
+ message: 'Nothing to emulate',
1013
+ suggestion: 'Give --viewport <WxH>, --color-scheme light|dark, or --reset, e.g. bdg page emulate --viewport 900x700',
1014
+ };
1015
+ }
1016
+ /**
1017
+ * Lines of `bdg page emulate`: what is emulated and what the page now has.
1018
+ *
1019
+ * @param result - Emulation and page appearance
1020
+ * @returns Label/value pairs
1021
+ */
1022
+ export function pageEmulationLines(result) {
1023
+ const size = (v) => `${v.width}x${v.height}`;
1024
+ const { emulated } = result;
1025
+ return [
1026
+ ['Viewport', emulated.viewport ? size(emulated.viewport) : 'the browser window'],
1027
+ ...(result.viewport
1028
+ ? [['Layout', `${size(result.viewport)} (without scrollbars)`]]
1029
+ : []),
1030
+ [
1031
+ 'Scheme',
1032
+ emulated.colorScheme ??
1033
+ `system setting${result.colorScheme ? ` (${result.colorScheme})` : ''}`,
1034
+ ],
1035
+ ];
1036
+ }
1037
+ /**
1038
+ * Header badge of `bdg dom inspect` while CSS transitions or animations run
1039
+ * on the element: the values read are mid-way.
1040
+ *
1041
+ * @param animating - Transitioned properties and animation names
1042
+ * @returns e.g. `[animating: background-color; values are mid-way, inspect again]`
1043
+ */
1044
+ export function inspectAnimatingBadge(animating) {
1045
+ return `[animating: ${animating.join(', ')}; values are mid-way, inspect again]`;
1046
+ }
1047
+ /**
1048
+ * `--why` note on a computed value read during its transition.
1049
+ *
1050
+ * @returns Note
1051
+ */
1052
+ export function inspectMidTransitionNote() {
1053
+ return '(mid-transition: inspect again for the final value)';
1054
+ }
241
1055
  //# sourceMappingURL=commands.js.map