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
@@ -3,29 +3,37 @@
3
3
  * `:visible`.
4
4
  *
5
5
  * They are not CSS, so the selector is split here (in Node) into the CSS the
6
- * browser runs and the filters the page applies to its matches. They are only
7
- * supported at the end of the last compound of each selector in a list
8
- * (`li.item:has-text("x"):visible`, `a:text-is("Home"), button:visible`):
9
- * there they narrow down the elements the CSS part matched. Anywhere else
10
- * (`div:has-text("x") > button`, `:not(:visible)`) they would have to be
11
- * evaluated inside the browser's selector matching, which bdg cannot do.
6
+ * browser runs and the filters the page applies to its matches. A filter
7
+ * applies to the compound it is written in (`li.item:has-text("x"):visible`,
8
+ * `a:visible.active`). On the last compound it narrows down the matches; on an
9
+ * earlier one it scopes the rest of the selector, which is then matched under
10
+ * each element passing the filter: `li:has-text("Buy milk") .toggle` is the
11
+ * `.toggle` inside the row that says "Buy milk". Only descendant and child
12
+ * combinators can follow a filtered compound. Inside `:has()` filters test
13
+ * what an element contains (`li:has(label:text-is("x"))`); inside other
14
+ * pseudo-classes (`:not(:visible)`) they would have to be evaluated by the
15
+ * browser's selector matching, which bdg cannot do.
12
16
  */
13
17
  import { CommandError } from '../errors/index.js';
14
- import { invalidSelectorFilterError, misplacedSelectorFilterError } from '../errors/messages.js';
18
+ import { emptyTextFilterError, malformedSelectorError, invalidSelectorFilterError, misplacedSelectorFilterError, siblingAfterFilterError, siblingInHasError, } from '../errors/messages.js';
15
19
  import { EXIT_CODES } from './exitCodes.js';
16
20
  /** Cheap pre-check: selectors without any of the names are plain CSS. */
17
21
  const FILTER_NAME_HINT = /:(?:has-text|text-is|visible)/i;
18
22
  /** A filter name at a given position (sticky). */
19
23
  const FILTER_NAME = /:(has-text|text-is|visible)(?![\w-])/iy;
20
- /** CSS ending in a (not escaped) combinator still needs an element selector. */
21
- const ENDS_IN_COMBINATOR = /(^|[^\\])[\s>+~]$/;
24
+ /** `:has(` at a given position (sticky). */
25
+ const HAS_OPEN = /:has\(/iy;
26
+ /** `:scope`, which a `:has()` with filters adds itself. */
27
+ const SCOPE_PSEUDO = /:scope(?![\w-])/i;
28
+ /** Characters that form combinators between compounds. */
29
+ const COMBINATOR_CHAR = /[\s>+~]/;
22
30
  /**
23
31
  * Split a selector into the CSS part and filters of each selector of its
24
32
  * list.
25
33
  *
26
34
  * @param selector - Selector as the user gave it
27
35
  * @returns The parts, or null for plain CSS (no filters anywhere), which runs unchanged
28
- * @throws CommandError (81) for a filter that is not at the end, or a malformed one
36
+ * @throws CommandError (81) for a misplaced or malformed filter
29
37
  */
30
38
  export function parseSelectorFilters(selector) {
31
39
  if (!FILTER_NAME_HINT.test(selector))
@@ -33,6 +41,21 @@ export function parseSelectorFilters(selector) {
33
41
  const parts = splitSelectorList(selector).map((part) => parsePart(part, selector));
34
42
  return parts.some((part) => part.filters.length > 0) ? parts : null;
35
43
  }
44
+ /**
45
+ * The parts with every `:visible` filter removed, to count what only
46
+ * visibility excluded.
47
+ *
48
+ * @param parts - Parsed selector parts
49
+ * @returns Parts without `:visible`, or null when they have none
50
+ */
51
+ export function withoutVisibleFilters(parts) {
52
+ const json = JSON.stringify(parts);
53
+ if (!json.includes('"kind":"visible"'))
54
+ return null;
55
+ return JSON.parse(json, (key, value) => key === 'filters' && Array.isArray(value)
56
+ ? value.filter((filter) => filter.kind !== 'visible')
57
+ : value);
58
+ }
36
59
  /**
37
60
  * Split a selector list at its top-level commas (not inside quotes,
38
61
  * attribute brackets or parentheses).
@@ -54,35 +77,32 @@ export function splitSelectorList(selector) {
54
77
  return parts;
55
78
  }
56
79
  /**
57
- * Walk a selector, calling `visit` for every character outside quotes,
58
- * escapes and attribute brackets, with the parenthesis depth.
80
+ * Walk a selector, calling `visit` at every token start outside quoted
81
+ * strings, escapes and attribute brackets (whose insides are skipped), with
82
+ * the parenthesis depth (for `)`, the depth inside it).
59
83
  *
60
84
  * @param text - Selector text
61
- * @param visit - Returns the index to continue after (to skip what it consumed)
85
+ * @param visit - Returns the index to continue after (`index` to let scan handle the character)
62
86
  */
63
87
  function scan(text, visit) {
64
88
  let depth = 0;
65
- let inBrackets = false;
66
89
  for (let i = 0; i < text.length; i++) {
90
+ const next = visit(i, depth);
91
+ if (next !== i) {
92
+ i = next;
93
+ continue;
94
+ }
67
95
  const char = text[i];
68
- if (char === '\\') {
96
+ if (char === '\\')
69
97
  i++;
70
- }
71
- else if (char === '"' || char === "'") {
98
+ else if (char === '"' || char === "'")
72
99
  i = quotedEnd(text, i) - 1;
73
- }
74
- else if (inBrackets) {
75
- inBrackets = char !== ']';
76
- }
77
- else if (char === '[') {
78
- inBrackets = true;
79
- }
80
- else if (char === '(' || char === ')') {
81
- depth += char === '(' ? 1 : -1;
82
- }
83
- else {
84
- i = visit(i, depth);
85
- }
100
+ else if (char === '[')
101
+ i = bracketEnd(text, i) - 1;
102
+ else if (char === '(')
103
+ depth++;
104
+ else if (char === ')')
105
+ depth--;
86
106
  }
87
107
  }
88
108
  /**
@@ -96,6 +116,25 @@ function quotedEnd(text, start) {
96
116
  const end = closingQuote(text, start);
97
117
  return end === -1 ? text.length : end + 1;
98
118
  }
119
+ /**
120
+ * End of an attribute selector (quotes and escapes inside skipped).
121
+ *
122
+ * @param text - Text containing it
123
+ * @param start - Index of the `[`
124
+ * @returns Index after the `]`, or the text length when unterminated
125
+ */
126
+ function bracketEnd(text, start) {
127
+ for (let i = start + 1; i < text.length; i++) {
128
+ const char = text[i];
129
+ if (char === '\\')
130
+ i++;
131
+ else if (char === '"' || char === "'")
132
+ i = quotedEnd(text, i) - 1;
133
+ else if (char === ']')
134
+ return i + 1;
135
+ }
136
+ return text.length;
137
+ }
99
138
  /**
100
139
  * Index of the quote closing the string opened at `start` (backslash escapes skipped).
101
140
  *
@@ -113,35 +152,210 @@ function closingQuote(text, start) {
113
152
  return -1;
114
153
  }
115
154
  /**
116
- * Split one selector of a list into its CSS part and trailing filters.
155
+ * Index of the parenthesis closing the one at `open` (quotes, escapes and
156
+ * attribute brackets skipped).
157
+ *
158
+ * @param text - Selector text
159
+ * @param open - Index of the `(`
160
+ * @returns Index of the `)`, or -1 when it is not closed
161
+ */
162
+ function closingParenthesis(text, open) {
163
+ let close = -1;
164
+ scan(text.slice(open), (index, depth) => {
165
+ if (close !== -1 || text[open + index] !== ')' || depth !== 1)
166
+ return index;
167
+ close = open + index;
168
+ return text.length;
169
+ });
170
+ return close;
171
+ }
172
+ /**
173
+ * Throw an exit-81 error.
174
+ *
175
+ * @param err - Message and suggestion
176
+ * @throws CommandError (81)
177
+ */
178
+ function reject(err) {
179
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
180
+ }
181
+ /**
182
+ * Parse one selector of a list.
117
183
  *
118
184
  * @param part - One selector of the list
119
185
  * @param selector - Whole selector, for error messages
120
- * @returns CSS part (`*` when only filters are given) and filters
121
- * @throws CommandError (81) when a filter is not at the end
186
+ * @returns CSS and filters of its first step, and the scoped steps after it
187
+ * @throws CommandError (81) for an empty selector or misplaced filters
122
188
  */
123
189
  function parsePart(part, selector) {
124
- const matches = findFilters(part, selector);
125
- const first = matches[0];
126
- if (!first)
190
+ if (part.trim() === '')
191
+ reject(malformedSelectorError(selector, 'empty-in-list'));
192
+ if (!FILTER_NAME_HINT.test(part))
127
193
  return { css: part.trim(), filters: [] };
128
- const misplaced = matches.find((match, i) => match.depth > 0 || match.end !== (matches[i + 1]?.start ?? part.trimEnd().length));
129
- if (misplaced) {
130
- const err = misplacedSelectorFilterError(selector, part.slice(misplaced.start, misplaced.end));
131
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
132
- }
133
- const css = part.slice(0, first.start);
134
- const needsElement = css.trim() === '' || ENDS_IN_COMBINATOR.test(css);
194
+ const { leading, steps } = parseChain(part, selector);
195
+ if (leading)
196
+ reject(malformedSelectorError(selector, 'leading-combinator', leading));
197
+ const [first, ...rest] = steps;
135
198
  return {
136
- css: (needsElement ? `${css}*` : css).trim(),
137
- filters: matches.map((match) => match.filter),
199
+ css: first?.css ?? '*',
200
+ filters: first?.filters ?? [],
201
+ ...(rest.length > 0 && { steps: rest }),
138
202
  };
139
203
  }
140
204
  /**
141
- * Find every filter in a selector (at any parenthesis depth, outside quotes
142
- * and attribute brackets).
205
+ * Parse a complex selector into steps that end at each filtered compound.
206
+ *
207
+ * @param text - Selector (may start with a combinator inside `:has()`)
208
+ * @param selector - Whole selector, for error messages
209
+ * @returns The leading combinator (`''` for none) and the steps
210
+ * @throws CommandError (81) for a sibling combinator after a filter, or a trailing combinator
211
+ */
212
+ function parseChain(text, selector) {
213
+ const compounds = splitCompounds(text.trim(), selector);
214
+ const leading = compounds[0]?.combinator ?? '';
215
+ const steps = [];
216
+ let combinator = ' ';
217
+ let css = '';
218
+ compounds.forEach((compound, i) => {
219
+ const parsed = parseCompound(compound.text, selector);
220
+ css += css ? joinCombinator(compound.combinator) + parsed.css : parsed.css;
221
+ const next = compounds[i + 1];
222
+ if (parsed.filters.length === 0 && next)
223
+ return;
224
+ steps.push({ combinator, css, filters: parsed.filters });
225
+ css = '';
226
+ if (!next)
227
+ return;
228
+ if (next.combinator !== ' ' && next.combinator !== '>') {
229
+ reject(siblingAfterFilterError(selector, next.combinator));
230
+ }
231
+ combinator = next.combinator;
232
+ });
233
+ return { leading, steps };
234
+ }
235
+ /**
236
+ * Combinator as written between two compounds of the CSS.
237
+ *
238
+ * @param combinator - ` `, `>`, `+` or `~`
239
+ * @returns The combinator with surrounding spaces
240
+ */
241
+ function joinCombinator(combinator) {
242
+ return combinator === ' ' ? ' ' : ` ${combinator} `;
243
+ }
244
+ /**
245
+ * Split a complex selector into its compounds at top-level combinators.
143
246
  *
144
- * @param part - One selector of a list
247
+ * @param text - Trimmed selector
248
+ * @param selector - Whole selector, for error messages
249
+ * @returns Compounds with the combinator before each (`''` for the first, unless it leads)
250
+ * @throws CommandError (81) when the selector ends with a combinator
251
+ */
252
+ function splitCompounds(text, selector) {
253
+ const compounds = [];
254
+ let combinator = '';
255
+ let start = -1;
256
+ scan(text, (index, depth) => {
257
+ const char = text[index] ?? '';
258
+ if (depth > 0 || !COMBINATOR_CHAR.test(char)) {
259
+ if (start === -1)
260
+ start = index;
261
+ return index;
262
+ }
263
+ if (start !== -1) {
264
+ compounds.push({ combinator, text: text.slice(start, index) });
265
+ start = -1;
266
+ combinator = ' ';
267
+ }
268
+ if (char.trim() && !combinator.trim())
269
+ combinator = char;
270
+ return index;
271
+ });
272
+ if (start === -1)
273
+ reject(malformedSelectorError(selector, 'trailing-combinator'));
274
+ compounds.push({ combinator, text: text.slice(start) });
275
+ return compounds;
276
+ }
277
+ /**
278
+ * Take the filters (and `:has()` containing filters) out of a compound.
279
+ *
280
+ * @param text - Compound selector
281
+ * @param selector - Whole selector, for error messages
282
+ * @returns The compound's CSS (`*` when only filters are given) and its filters
283
+ * @throws CommandError (81) for a filter inside another pseudo-class
284
+ */
285
+ function parseCompound(text, selector) {
286
+ if (!FILTER_NAME_HINT.test(text))
287
+ return { css: text, filters: [] };
288
+ const hasFilters = findHasFilters(text, selector);
289
+ const inHas = (match) => hasFilters.some((has) => match.start >= has.start && match.end <= has.end);
290
+ const filters = findFilters(text, selector).filter((match) => !inHas(match));
291
+ const misplaced = filters.find((match) => match.depth > 0);
292
+ if (misplaced) {
293
+ reject(misplacedSelectorFilterError(selector, text.slice(misplaced.start, misplaced.end)));
294
+ }
295
+ const removed = [...hasFilters, ...filters].sort((a, b) => a.start - b.start);
296
+ let css = '';
297
+ let position = 0;
298
+ for (const match of removed) {
299
+ css += text.slice(position, match.start);
300
+ position = match.end;
301
+ }
302
+ css += text.slice(position);
303
+ return { css: css || '*', filters: removed.map((match) => match.filter) };
304
+ }
305
+ /**
306
+ * Find the top-level `:has()` of a compound whose argument uses filters.
307
+ *
308
+ * @param text - Compound selector
309
+ * @param selector - Whole selector, for error messages
310
+ * @returns The `:has()` filters with their positions
311
+ */
312
+ function findHasFilters(text, selector) {
313
+ const matches = [];
314
+ scan(text, (index, depth) => {
315
+ HAS_OPEN.lastIndex = index;
316
+ if (depth > 0 || !HAS_OPEN.test(text))
317
+ return index;
318
+ const open = HAS_OPEN.lastIndex - 1;
319
+ const close = closingParenthesis(text, open);
320
+ if (close === -1)
321
+ return index;
322
+ const filter = parseHasArgument(text.slice(open + 1, close), selector);
323
+ if (filter)
324
+ matches.push({ start: index, end: close + 1, depth, filter });
325
+ return filter ? close : index;
326
+ });
327
+ return matches;
328
+ }
329
+ /**
330
+ * Parse the relative selector list of a `:has()` that may use filters.
331
+ *
332
+ * @param argument - Text between the parentheses
333
+ * @param selector - Whole selector, for error messages
334
+ * @returns The `has` filter, or null when the argument is plain CSS
335
+ * @throws CommandError (81) for a sibling combinator (`:has(+ x)`)
336
+ */
337
+ function parseHasArgument(argument, selector) {
338
+ if (!FILTER_NAME_HINT.test(argument))
339
+ return null;
340
+ const selectors = splitSelectorList(argument).map((relative) => {
341
+ if (relative.trim() === '')
342
+ reject(malformedSelectorError(selector, 'empty-has'));
343
+ if (SCOPE_PSEUDO.test(relative))
344
+ reject(malformedSelectorError(selector, 'scope-in-has'));
345
+ const { leading, steps } = parseChain(relative, selector);
346
+ if (leading && leading !== '>')
347
+ reject(siblingInHasError(selector, leading));
348
+ const [first, ...rest] = steps;
349
+ return first ? [{ ...first, combinator: (leading || ' ') }, ...rest] : [];
350
+ });
351
+ const usesFilters = selectors.some((chain) => chain.some((step) => step.filters.length > 0));
352
+ return usesFilters ? { kind: 'has', selectors } : null;
353
+ }
354
+ /**
355
+ * Find every text and visibility filter in a compound (at any parenthesis
356
+ * depth, outside quotes and attribute brackets).
357
+ *
358
+ * @param part - Compound selector
145
359
  * @param selector - Whole selector, for error messages
146
360
  * @returns Filters in order
147
361
  */
@@ -169,15 +383,15 @@ function findFilters(part, selector) {
169
383
  * @param name - `has-text` or `text-is`
170
384
  * @param selector - Whole selector, for error messages
171
385
  * @returns The filter (text whitespace-normalized) and the index after it
172
- * @throws CommandError (81) when the argument is missing or not closed
386
+ * @throws CommandError (81) when the argument is missing or not closed, or `:has-text` is empty
173
387
  */
174
388
  function readTextFilter(part, start, name, selector) {
175
389
  const argument = part[start] === '(' ? readArgument(part, start + 1) : null;
176
- if (!argument) {
177
- const err = invalidSelectorFilterError(selector, `:${name}`);
178
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
179
- }
390
+ if (!argument)
391
+ reject(invalidSelectorFilterError(selector, `:${name}`));
180
392
  const text = argument.text.replace(/\s+/g, ' ').trim();
393
+ if (name === 'has-text' && text === '')
394
+ reject(emptyTextFilterError(selector));
181
395
  const filter = name === 'has-text'
182
396
  ? { kind: 'has-text', text: text.toLowerCase() }
183
397
  : { kind: 'text-is', text };
@@ -25,10 +25,16 @@ export declare function hasAttributeSelector(selector: string): boolean;
25
25
  */
26
26
  export declare function detectSelectorQuoteDamage(selector: string): ShellDamageResult;
27
27
  /**
28
- * Detects shell quote damage in JavaScript expressions.
28
+ * Detects shell quote damage in JavaScript expressions. Conservative: only an
29
+ * error stripped quotes cause (`x is not defined`,
30
+ * `missing ) after argument list`, `Unexpected identifier`,
31
+ * `Unexpected token '.'`, `Invalid or unexpected token`) counts, about an
32
+ * argument of a DOM method that takes a string (`querySelector(input)`),
33
+ * which the script does not declare itself.
29
34
  *
30
35
  * @param script - The script as received by the command
36
+ * @param errorMessage - The error the script raised
31
37
  * @returns Detection result with details and suggestions
32
38
  */
33
- export declare function detectScriptQuoteDamage(script: string): ShellDamageResult;
39
+ export declare function detectScriptQuoteDamage(script: string, errorMessage: string): ShellDamageResult;
34
40
  //# sourceMappingURL=shellDetection.d.ts.map
@@ -4,16 +4,52 @@
4
4
  * Detects when shell quote handling has corrupted selectors or scripts,
5
5
  * providing actionable suggestions for recovery.
6
6
  */
7
+ import { sessionCommand } from '../ui/messages/sessionCommand.js';
7
8
  const ATTRIBUTE_SELECTOR_PATTERN = /\[[\w-]+=/.source;
8
9
  const QUOTED_ATTRIBUTE_PATTERN = /\[[\w-]+=['"][^'"]*['"]\]/;
9
10
  const UNQUOTED_ATTRIBUTE_PATTERN = /\[([\w-]+)=([\w-]+)\]/;
10
- const BARE_ARGUMENT_PATTERN = /(\w+)\(\s*([a-zA-Z][\w-]*)\s*\)/;
11
- const UNEXPECTED_IDENTIFIER_PATTERN = /Unexpected identifier ['"]?(\w+)['"]?/;
11
+ /** DOM methods whose one argument is a string (a selector, id, name or class) */
12
+ const STRING_ARGUMENT_METHODS = new Set([
13
+ 'querySelector',
14
+ 'querySelectorAll',
15
+ 'getElementById',
16
+ 'getElementsByClassName',
17
+ 'getElementsByTagName',
18
+ 'getElementsByName',
19
+ 'closest',
20
+ 'matches',
21
+ 'getAttribute',
22
+ 'hasAttribute',
23
+ 'removeAttribute',
24
+ 'toggleAttribute',
25
+ 'createElement',
26
+ 'add',
27
+ 'remove',
28
+ 'toggle',
29
+ ]);
30
+ /** A call with one argument and no quotes, parentheses or commas in it */
31
+ const UNQUOTED_CALL_PATTERN = /([\w$]+)\(\s*([^()'"`,]+?)\s*\)/g;
32
+ /** `x is not defined`: the shell may have turned `"x"` into `x` */
33
+ const NOT_DEFINED_PATTERN = /^ReferenceError: ([\w$]+) is not defined/;
34
+ /**
35
+ * The syntax errors V8 raises for `fn(div p)`: the shell may have turned
36
+ * `"div p"` into `div p`
37
+ */
38
+ const SEVERAL_WORDS_PATTERN = /^SyntaxError: (?:missing \) after argument list|Unexpected identifier '([\w$]+)')/;
39
+ /**
40
+ * The syntax errors V8 raises for a selector starting with punctuation:
41
+ * `fn(.btn)`, `fn(#main)`, `fn(> p)`
42
+ */
43
+ const SELECTOR_SYNTAX_PATTERN = /^SyntaxError: (?:Unexpected token '[^']*'|Invalid or unexpected token|Private field '#[^']*' must be declared)/;
44
+ /** Start of a CSS selector that is not a JavaScript expression */
45
+ const SELECTOR_PUNCTUATION = /^[.#>~+*[:]/;
46
+ /** Words that may start a valid expression of several words */
47
+ const EXPRESSION_KEYWORDS = /^(?:new|typeof|void|delete|await|yield)\b/;
12
48
  function noDamage() {
13
49
  return { damaged: false };
14
50
  }
15
51
  function buildSelectorSuggestion(selector) {
16
- return `Use the two-step pattern:\n 1. bdg dom query '${selector}'\n 2. bdg dom a11y describe 0`;
52
+ return `Use the two-step pattern:\n 1. ${sessionCommand(`bdg dom query '${selector}'`)}\n 2. ${sessionCommand('bdg dom a11y describe 0')}`;
17
53
  }
18
54
  function checkUnquotedAttribute(selector) {
19
55
  if (QUOTED_ATTRIBUTE_PATTERN.test(selector)) {
@@ -31,31 +67,66 @@ function checkUnquotedAttribute(selector) {
31
67
  suggestion: buildSelectorSuggestion(selector),
32
68
  };
33
69
  }
34
- function checkBareArgument(script) {
35
- const match = BARE_ARGUMENT_PATTERN.exec(script);
36
- if (!match) {
37
- return noDamage();
38
- }
39
- const [matchedPart, funcName, bareArg] = match;
40
- const fixedPart = `${funcName}("${bareArg}")`;
41
- const fixedScript = script.replace(matchedPart, fixedPart);
42
- return {
43
- damaged: true,
44
- type: 'unquoted-argument',
45
- details: `${funcName}(${bareArg}) - quotes stripped by shell`,
46
- suggestion: `Try: bdg dom eval '${fixedScript}'`,
47
- };
70
+ /**
71
+ * Whether the script declares a name itself (then it is a variable, not a
72
+ * string that lost its quotes).
73
+ *
74
+ * @param script - The script
75
+ * @param name - Identifier
76
+ * @returns True for a `const`/`let`/`var`/`function`/`class` declaration of it
77
+ */
78
+ function declares(script, name) {
79
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
80
+ return new RegExp(`\\b(?:const|let|var|function|class)\\s+${escaped}\\b`).test(script);
48
81
  }
49
- function checkUnexpectedIdentifier(script) {
50
- if (!UNEXPECTED_IDENTIFIER_PATTERN.test(script)) {
51
- return noDamage();
52
- }
53
- return {
54
- damaged: true,
55
- type: 'unquoted-argument',
56
- details: 'Unexpected identifier suggests quotes were stripped',
57
- suggestion: "Use single quotes around the script: bdg dom eval '...'",
58
- };
82
+ /**
83
+ * Whether text starts with an identifier (and not with a longer one).
84
+ *
85
+ * @param text - Text
86
+ * @param name - Identifier
87
+ * @returns True when `text` is `name` or `name` followed by a non-identifier character
88
+ */
89
+ function startsWithName(text, name) {
90
+ return text.startsWith(name) && !/^[\w$]/.test(text.slice(name.length));
91
+ }
92
+ /**
93
+ * Whether an unquoted argument is what the error complains about: for
94
+ * `ReferenceError` the argument starts with the undefined name (`input`,
95
+ * `my-id`, `button.primary`); for `SyntaxError` it is several bare words
96
+ * (`div p`), the unexpected one (when named) among the later ones; for a
97
+ * syntax error at punctuation it starts like a selector (`.btn`, `#main`).
98
+ *
99
+ * @param argument - Unquoted argument
100
+ * @param error - What the error says
101
+ * @returns True when quotes stripped from it explain the error
102
+ */
103
+ function explainsError(argument, error) {
104
+ if (error.kind === 'undefined')
105
+ return startsWithName(argument, error.name);
106
+ if (error.kind === 'selector')
107
+ return SELECTOR_PUNCTUATION.test(argument);
108
+ const [, ...laterWords] = argument.split(/\s+/);
109
+ return (!EXPRESSION_KEYWORDS.test(argument) &&
110
+ laterWords.length > 0 &&
111
+ (error.name === undefined || laterWords.some((word) => startsWithName(word, error.name ?? ''))));
112
+ }
113
+ /**
114
+ * Read the identifier an error is about, when the error is one stripped
115
+ * quotes cause.
116
+ *
117
+ * @param errorMessage - The JavaScript error
118
+ * @returns The identifier and kind of error, or undefined for other errors
119
+ */
120
+ function quoteRelatedError(errorMessage) {
121
+ const notDefined = NOT_DEFINED_PATTERN.exec(errorMessage);
122
+ if (notDefined?.[1])
123
+ return { kind: 'undefined', name: notDefined[1] };
124
+ if (SELECTOR_SYNTAX_PATTERN.test(errorMessage))
125
+ return { kind: 'selector' };
126
+ const syntax = SEVERAL_WORDS_PATTERN.exec(errorMessage);
127
+ if (!syntax)
128
+ return undefined;
129
+ return syntax[1] ? { kind: 'syntax', name: syntax[1] } : { kind: 'syntax' };
59
130
  }
60
131
  /**
61
132
  * Checks if a selector contains attribute syntax.
@@ -79,16 +150,32 @@ export function detectSelectorQuoteDamage(selector) {
79
150
  return checkUnquotedAttribute(selector);
80
151
  }
81
152
  /**
82
- * Detects shell quote damage in JavaScript expressions.
153
+ * Detects shell quote damage in JavaScript expressions. Conservative: only an
154
+ * error stripped quotes cause (`x is not defined`,
155
+ * `missing ) after argument list`, `Unexpected identifier`,
156
+ * `Unexpected token '.'`, `Invalid or unexpected token`) counts, about an
157
+ * argument of a DOM method that takes a string (`querySelector(input)`),
158
+ * which the script does not declare itself.
83
159
  *
84
160
  * @param script - The script as received by the command
161
+ * @param errorMessage - The error the script raised
85
162
  * @returns Detection result with details and suggestions
86
163
  */
87
- export function detectScriptQuoteDamage(script) {
88
- const bareArgResult = checkBareArgument(script);
89
- if (bareArgResult.damaged) {
90
- return bareArgResult;
164
+ export function detectScriptQuoteDamage(script, errorMessage) {
165
+ const error = quoteRelatedError(errorMessage);
166
+ if (!error || (error.name && declares(script, error.name)))
167
+ return noDamage();
168
+ for (const [call, method = '', argument = ''] of script.matchAll(UNQUOTED_CALL_PATTERN)) {
169
+ if (!STRING_ARGUMENT_METHODS.has(method) || !explainsError(argument, error))
170
+ continue;
171
+ const fixedScript = script.replace(call, `${method}("${argument}")`);
172
+ return {
173
+ damaged: true,
174
+ type: 'unquoted-argument',
175
+ details: `${method}(${argument}) - quotes stripped by shell`,
176
+ suggestion: `Try: ${sessionCommand(`bdg dom eval '${fixedScript}'`)}`,
177
+ };
91
178
  }
92
- return checkUnexpectedIdentifier(script);
179
+ return noDamage();
93
180
  }
94
181
  //# sourceMappingURL=shellDetection.js.map
@@ -15,5 +15,31 @@ export declare function getSuggestion(input: string, candidates: readonly string
15
15
  prefix?: string;
16
16
  suffix?: string;
17
17
  }): string;
18
+ /**
19
+ * Names (ids or classes) similar to one that matched nothing, best first, at
20
+ * most three: near-typos first (Levenshtein distance up to a fifth of the
21
+ * length, at least 2), then names sharing a long end, then names sharing a
22
+ * long start (at least a third of the name, at least 4 characters). The end
23
+ * ranks before the start because ids tend to name an action before the item
24
+ * (`add-to-cart-backpack` becomes `remove-backpack` once clicked).
25
+ *
26
+ * @param name - Id or class that matched nothing
27
+ * @param candidates - Ids or classes on the page
28
+ * @returns Up to three similar names
29
+ */
30
+ export declare function findSimilarNames(name: string, candidates: readonly string[]): string[];
31
+ /** A selector that is just one id or one class, e.g. `#save` or `.btn-primary` */
32
+ export interface SingleNameSelector {
33
+ kind: 'id' | 'class';
34
+ name: string;
35
+ }
36
+ /**
37
+ * Whether a selector is a single id or class (the kind "did you mean" can
38
+ * suggest for).
39
+ *
40
+ * @param selector - CSS selector
41
+ * @returns The id or class, or null for any other selector
42
+ */
43
+ export declare function parseSingleNameSelector(selector: string): SingleNameSelector | null;
18
44
  export {};
19
45
  //# sourceMappingURL=suggestions.d.ts.map