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
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Sessions across the default and named session directories (`bdg sessions`):
3
+ * running ones, and ones whose daemon died and left a Chrome or files behind.
4
+ */
5
+ import * as fs from 'fs';
6
+ import { getStatus } from '../ipc/client.js';
7
+ import { isSessionChrome, readLiveDaemonPid } from './cleanup/staleSession.js';
8
+ import { probeDaemonSocket } from './daemonSocket.js';
9
+ import { SESSION_STATE_FILES, getNamedSessionDir, listSessionDirs, sessionFilePathIn, } from './paths.js';
10
+ import { readPidFromFile } from './pid.js';
11
+ import { readPortFile } from './portClaims.js';
12
+ import { isValidSessionName, normalizeSessionName } from './sessionName.js';
13
+ import { createLogger, logDebugError } from '../ui/logging/index.js';
14
+ import { removeDirCommand, sessionCommand } from '../ui/messages/sessionCommand.js';
15
+ import { isProcessAlive } from '../utils/process.js';
16
+ const log = createLogger('session');
17
+ /**
18
+ * Summarize a daemon's status response.
19
+ *
20
+ * @param name - Session name
21
+ * @param data - Status data
22
+ * @returns Session info
23
+ */
24
+ export function toRunningSession(name, data) {
25
+ const meta = data.sessionMetadata;
26
+ const state = meta ? 'active' : data.ending ? 'ending' : 'starting';
27
+ const url = data.pageState?.url ?? data.starting?.url;
28
+ return {
29
+ name,
30
+ state,
31
+ ...(url !== undefined && { url }),
32
+ ...(meta && { port: meta.port }),
33
+ daemonPid: data.daemonPid,
34
+ ...(meta?.chromePid && { chromePid: meta.chromePid }),
35
+ };
36
+ }
37
+ /**
38
+ * Describe the session of a directory: a running daemon, or what a dead
39
+ * one left behind.
40
+ *
41
+ * @param entry - Session directory
42
+ * @returns Session info, or null when there is no session
43
+ */
44
+ async function describeSession(entry) {
45
+ const { name, dir } = entry;
46
+ const socketPath = sessionFilePathIn(dir, 'DAEMON_SOCKET');
47
+ const probe = await probeDaemonSocket(socketPath);
48
+ if (probe !== 'alive')
49
+ return describeWithoutSocket(entry, probe);
50
+ try {
51
+ const response = await getStatus(socketPath);
52
+ if (response.status === 'ok' && response.data)
53
+ return toRunningSession(name, response.data);
54
+ }
55
+ catch (error) {
56
+ logDebugError(log, `get status of ${dir}`, error);
57
+ }
58
+ return { name, state: 'unresponsive' };
59
+ }
60
+ /**
61
+ * A session whose daemon socket does not answer: `starting` while its daemon
62
+ * (from daemon.pid, verified by command line) runs but has not bound the
63
+ * socket yet, otherwise see {@link describeLeftovers}.
64
+ *
65
+ * @param entry - Session directory
66
+ * @param probe - Result of probing its daemon socket
67
+ * @returns Session info, or null when the directory holds no session state
68
+ */
69
+ function describeWithoutSocket(entry, probe) {
70
+ const daemonPid = readLiveDaemonPid(entry.dir);
71
+ if (daemonPid !== null)
72
+ return { name: entry.name, state: 'starting', daemonPid };
73
+ return describeLeftovers(entry, probe);
74
+ }
75
+ /**
76
+ * A session whose daemon is gone: `crashed` while the Chrome bdg launched for
77
+ * it still runs, `stale` when only its files are left. Nothing is changed on
78
+ * disk; `cleanup` names the command that removes them.
79
+ *
80
+ * @param entry - Session directory
81
+ * @param probe - Result of probing its daemon socket
82
+ * @returns Session info, or null when the directory holds no session state
83
+ */
84
+ export function describeLeftovers({ name, dir }, probe) {
85
+ const chromePid = readPidFromFile(sessionFilePathIn(dir, 'CHROME_PID'));
86
+ const orphan = chromePid !== null && isProcessAlive(chromePid) && isSessionChrome(chromePid, dir);
87
+ const leftover = probe === 'stale' ||
88
+ SESSION_STATE_FILES.some((type) => fs.existsSync(sessionFilePathIn(dir, type)));
89
+ if (!orphan && !leftover)
90
+ return null;
91
+ const port = leftoverPort(dir);
92
+ return {
93
+ name,
94
+ state: orphan ? 'crashed' : 'stale',
95
+ ...(port !== null && { port }),
96
+ ...(orphan && { chromePid }),
97
+ cleanup: sessionCommand('bdg cleanup', name),
98
+ };
99
+ }
100
+ /**
101
+ * The port a dead session used: from its metadata, else its `port.txt`.
102
+ *
103
+ * @param dir - Session directory
104
+ * @returns Port, or null if unknown
105
+ */
106
+ function leftoverPort(dir) {
107
+ try {
108
+ const meta = JSON.parse(fs.readFileSync(sessionFilePathIn(dir, 'METADATA'), 'utf8'));
109
+ if (typeof meta.port === 'number')
110
+ return meta.port;
111
+ }
112
+ catch (error) {
113
+ logDebugError(log, `read the metadata in ${dir}`, error);
114
+ }
115
+ return readPortFile(sessionFilePathIn(dir, 'PORT'));
116
+ }
117
+ /**
118
+ * Whether a directory entry is a session `--session` can select: the
119
+ * default session, or a valid lower-case name.
120
+ *
121
+ * @param entry - Session directory
122
+ * @returns True if selectable
123
+ */
124
+ function isSelectable({ name }) {
125
+ return name === null || (isValidSessionName(name) && name === normalizeSessionName(name));
126
+ }
127
+ /**
128
+ * Whether two paths are the same directory (same inode and device), e.g.
129
+ * `sessions/ALPHA` and `sessions/alpha` on a case-insensitive file system.
130
+ *
131
+ * @param a - First path
132
+ * @param b - Second path
133
+ * @returns True if both exist and are the same directory
134
+ */
135
+ function isSameDir(a, b) {
136
+ try {
137
+ const first = fs.statSync(a);
138
+ const second = fs.statSync(b);
139
+ return first.ino === second.ino && first.dev === second.dev;
140
+ }
141
+ catch (error) {
142
+ logDebugError(log, `compare ${a} with ${b}`, error);
143
+ return false;
144
+ }
145
+ }
146
+ /**
147
+ * The session `--session` reaches in a directory: the entry itself when its
148
+ * name is selectable, the lower-cased session when the name differs only in
149
+ * case and `--session <lower-case>` resolves to this very directory (a
150
+ * case-insensitive file system), else null.
151
+ *
152
+ * @param entry - Session directory
153
+ * @returns Selectable entry, or null
154
+ */
155
+ function selectableEntry(entry) {
156
+ if (isSelectable(entry) || entry.name === null)
157
+ return entry;
158
+ const lower = normalizeSessionName(entry.name);
159
+ if (!isValidSessionName(lower))
160
+ return null;
161
+ return isSameDir(entry.dir, getNamedSessionDir(lower)) ? { name: lower, dir: entry.dir } : null;
162
+ }
163
+ /**
164
+ * A directory `--session` cannot reach (e.g. `--json`, or `ALPHA` on a
165
+ * case-sensitive file system, made by an earlier build): described like any
166
+ * session while its daemon answers, otherwise `stale` with the command that
167
+ * removes it by hand.
168
+ *
169
+ * @param entry - Session directory
170
+ * @returns Session info
171
+ */
172
+ async function describeUnselectable(entry) {
173
+ const socketPath = sessionFilePathIn(entry.dir, 'DAEMON_SOCKET');
174
+ if ((await probeDaemonSocket(socketPath)) === 'alive')
175
+ return describeSession(entry);
176
+ return { name: entry.name, state: 'stale', cleanup: removeDirCommand(entry.dir) };
177
+ }
178
+ /**
179
+ * Every session: the default one first, then named sessions by name. Includes
180
+ * crashed and stale sessions, and directories `--session` cannot reach, so
181
+ * their leftovers can be cleaned up. A directory differing only in case
182
+ * (`ALPHA`) that `--session alpha` reaches is listed once, as `alpha`.
183
+ *
184
+ * @returns Sessions
185
+ */
186
+ export async function listRunningSessions() {
187
+ const listed = new Set();
188
+ const sessions = await Promise.all(listSessionDirs().map((entry) => {
189
+ const selectable = selectableEntry(entry);
190
+ if (selectable === null)
191
+ return describeUnselectable(entry);
192
+ if (listed.has(selectable.name))
193
+ return Promise.resolve(null);
194
+ listed.add(selectable.name);
195
+ return describeSession(selectable);
196
+ }));
197
+ return sessions.filter((session) => session !== null);
198
+ }
199
+ //# sourceMappingURL=sessionList.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Named sessions (`--session <name>` / `BDG_SESSION`).
3
+ *
4
+ * A named session lives in `<base>/sessions/<name>/` with its own daemon,
5
+ * socket, Chrome and port, so several agents can run bdg side by side.
6
+ * Names are case-insensitive: they are lower-cased when selected, so `ALPHA`
7
+ * and `alpha` are the same session (and directory) on every file system.
8
+ */
9
+ /** Longest session name */
10
+ export declare const MAX_SESSION_NAME_LENGTH = 40;
11
+ /**
12
+ * Whether a name has the allowed characters and length (case-insensitive).
13
+ *
14
+ * @param name - Session name
15
+ * @returns True for a valid name
16
+ */
17
+ export declare function isValidSessionName(name: string): boolean;
18
+ /**
19
+ * The canonical form of a session name: lower case.
20
+ *
21
+ * @param name - Session name as given
22
+ * @returns Lower-cased name
23
+ */
24
+ export declare function normalizeSessionName(name: string): string;
25
+ /**
26
+ * Check a session name: allowed characters, length, and a daemon socket path
27
+ * the OS accepts. When even the shortest name would make the socket path too
28
+ * long, the base directory is blamed instead of the name.
29
+ *
30
+ * @param name - Session name
31
+ * @throws CommandError (81) for an invalid name or a too-long socket path
32
+ */
33
+ export declare function validateSessionName(name: string): void;
34
+ /**
35
+ * Select the session the command acts on.
36
+ *
37
+ * `--session` wins over `BDG_SESSION`; the choice is stored in `BDG_SESSION`
38
+ * so the rest of the process (and a daemon it spawns) resolves the same
39
+ * session directory. The name is stored lower-cased. An empty `BDG_SESSION`
40
+ * means the default session.
41
+ *
42
+ * @param optionValue - Value of `--session`, if given
43
+ * @throws CommandError (81) for an invalid name
44
+ */
45
+ export declare function selectSession(optionValue: string | undefined): void;
46
+ //# sourceMappingURL=sessionName.d.ts.map
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Named sessions (`--session <name>` / `BDG_SESSION`).
3
+ *
4
+ * A named session lives in `<base>/sessions/<name>/` with its own daemon,
5
+ * socket, Chrome and port, so several agents can run bdg side by side.
6
+ * Names are case-insensitive: they are lower-cased when selected, so `ALPHA`
7
+ * and `alpha` are the same session (and directory) on every file system.
8
+ */
9
+ import { CommandError } from '../errors/index.js';
10
+ import { invalidSessionNameError, sessionNameSocketTooLongError, socketPathTooLongError, } from '../errors/messages.js';
11
+ import { MAX_DAEMON_SOCKET_PATH_BYTES, SESSION_NAME_ENV, getNamedSessionDir, sessionFilePathIn, } from './paths.js';
12
+ import { EXIT_CODES } from '../utils/exitCodes.js';
13
+ /** Longest session name */
14
+ export const MAX_SESSION_NAME_LENGTH = 40;
15
+ /**
16
+ * Characters allowed in a session name: a letter or digit first (so a name
17
+ * never looks like an option), then letters, digits, `-` or `_`.
18
+ */
19
+ const SESSION_NAME_PATTERN = new RegExp(`^[A-Za-z0-9][A-Za-z0-9_-]{0,${MAX_SESSION_NAME_LENGTH - 1}}$`);
20
+ /**
21
+ * Whether a name has the allowed characters and length (case-insensitive).
22
+ *
23
+ * @param name - Session name
24
+ * @returns True for a valid name
25
+ */
26
+ export function isValidSessionName(name) {
27
+ return SESSION_NAME_PATTERN.test(name);
28
+ }
29
+ /**
30
+ * The canonical form of a session name: lower case.
31
+ *
32
+ * @param name - Session name as given
33
+ * @returns Lower-cased name
34
+ */
35
+ export function normalizeSessionName(name) {
36
+ return name.toLowerCase();
37
+ }
38
+ /**
39
+ * Check a session name: allowed characters, length, and a daemon socket path
40
+ * the OS accepts. When even the shortest name would make the socket path too
41
+ * long, the base directory is blamed instead of the name.
42
+ *
43
+ * @param name - Session name
44
+ * @throws CommandError (81) for an invalid name or a too-long socket path
45
+ */
46
+ export function validateSessionName(name) {
47
+ if (!isValidSessionName(name)) {
48
+ throwInvalid(invalidSessionNameError(name, MAX_SESSION_NAME_LENGTH));
49
+ }
50
+ const shortest = namedSocketPath('a');
51
+ if (Buffer.byteLength(shortest) > MAX_DAEMON_SOCKET_PATH_BYTES) {
52
+ throwInvalid(socketPathTooLongError(shortest, MAX_DAEMON_SOCKET_PATH_BYTES));
53
+ }
54
+ const socketPath = namedSocketPath(name);
55
+ if (Buffer.byteLength(socketPath) > MAX_DAEMON_SOCKET_PATH_BYTES) {
56
+ throwInvalid(sessionNameSocketTooLongError(name, socketPath, MAX_DAEMON_SOCKET_PATH_BYTES));
57
+ }
58
+ }
59
+ /**
60
+ * Daemon socket path of a named session.
61
+ *
62
+ * @param name - Session name
63
+ * @returns Socket path
64
+ */
65
+ function namedSocketPath(name) {
66
+ return sessionFilePathIn(getNamedSessionDir(name), 'DAEMON_SOCKET');
67
+ }
68
+ /**
69
+ * Throw an invalid-arguments error (81).
70
+ *
71
+ * @param err - Message and suggestion
72
+ * @throws CommandError always
73
+ */
74
+ function throwInvalid(err) {
75
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
76
+ }
77
+ /**
78
+ * Select the session the command acts on.
79
+ *
80
+ * `--session` wins over `BDG_SESSION`; the choice is stored in `BDG_SESSION`
81
+ * so the rest of the process (and a daemon it spawns) resolves the same
82
+ * session directory. The name is stored lower-cased. An empty `BDG_SESSION`
83
+ * means the default session.
84
+ *
85
+ * @param optionValue - Value of `--session`, if given
86
+ * @throws CommandError (81) for an invalid name
87
+ */
88
+ export function selectSession(optionValue) {
89
+ const name = optionValue ?? process.env[SESSION_NAME_ENV]?.trim();
90
+ if (name === undefined || (optionValue === undefined && name === '')) {
91
+ delete process.env[SESSION_NAME_ENV];
92
+ return;
93
+ }
94
+ validateSessionName(name);
95
+ process.env[SESSION_NAME_ENV] = normalizeSessionName(name);
96
+ }
97
+ //# sourceMappingURL=sessionName.js.map
@@ -28,6 +28,8 @@ export declare function collectA11yTree(): Promise<A11yTree>;
28
28
  * Queries accessibility tree by pattern (role, name, description).
29
29
  *
30
30
  * Performs case-insensitive matching with AND logic for multiple fields.
31
+ * An element reported more than once (by the page's tree and its frame's)
32
+ * is listed once.
31
33
  *
32
34
  * @param tree - Accessibility tree to search
33
35
  * @param pattern - Query pattern with optional role, name, description
@@ -50,18 +52,21 @@ export declare function queryA11yTree(tree: A11yTree, pattern: A11yQueryPattern)
50
52
  * Parses query pattern string into A11yQueryPattern object.
51
53
  *
52
54
  * Fields are `key:value` or `key=value`, separated by spaces or commas.
53
- * Values may contain spaces (quote them if they contain `key:`-like text).
54
- * Keys: role, name, description (desc). Case-insensitive.
55
+ * Keys: role, name, description (desc). Case-insensitive. A role ends at the
56
+ * next `key:`; a name or description runs to the next role/name/description
57
+ * field with a value, or to the end, so it may contain spaces and colons
58
+ * (`name=E-mail address:`). Quote a value to end it explicitly.
55
59
  *
56
60
  * @param patternString - Query pattern string
57
61
  * @returns Parsed query pattern
58
- * @throws CommandError (81) for unknown keys
62
+ * @throws CommandError (81) for unknown keys, also a misspelled one inside a name
59
63
  *
60
64
  * @example
61
65
  * ```typescript
62
66
  * parseQueryPattern('role:button name:Submit') // { role: 'button', name: 'Submit' }
63
67
  * parseQueryPattern('role=link,name=Google Chrome') // { role: 'link', name: 'Google Chrome' }
64
68
  * parseQueryPattern('name:"Sign in" role:button') // { name: 'Sign in', role: 'button' }
69
+ * parseQueryPattern('name=E-mail address:') // { name: 'E-mail address:' }
65
70
  * ```
66
71
  */
67
72
  export declare function parseQueryPattern(patternString: string): A11yQueryPattern;
@@ -1,7 +1,9 @@
1
1
  import { CommandError } from '../errors/index.js';
2
2
  import { unknownQueryFieldError } from '../errors/messages.js';
3
3
  import { callCDP } from '../ipc/client.js';
4
+ import { childFrameIds } from '../runtime/dom/frameLayout.js';
4
5
  import { EXIT_CODES } from '../utils/exitCodes.js';
6
+ import { levenshteinDistance } from '../utils/levenshtein.js';
5
7
  /**
6
8
  * Builds accessibility tree from raw CDP nodes.
7
9
  *
@@ -98,22 +100,13 @@ async function collectFrameNodes(pageNodes) {
98
100
  const tree = (await callCDP('Page.getFrameTree', {})).data?.result;
99
101
  const known = [...pageNodes];
100
102
  const collected = [];
101
- for (const [index, frameId] of childFrames(tree?.frameTree).entries()) {
103
+ for (const [index, frameId] of childFrameIds(tree?.frameTree).entries()) {
102
104
  const nodes = await frameNodes(frameId, `f${index}:`, known);
103
105
  known.push(...nodes);
104
106
  collected.push(...nodes);
105
107
  }
106
108
  return collected;
107
109
  }
108
- /**
109
- * Ids of all frames below a frame tree's root.
110
- *
111
- * @param tree - Frame tree
112
- * @returns Frame ids, depth-first
113
- */
114
- function childFrames(tree) {
115
- return (tree?.childFrames ?? []).flatMap((child) => [child.frame.id, ...childFrames(child)]);
116
- }
117
110
  /**
118
111
  * One frame's nodes, with ids prefixed, its root attached to the node of its
119
112
  * `<iframe>` element.
@@ -234,6 +227,8 @@ const TEXT_ROLES = new Set(['statictext', 'inlinetextbox']);
234
227
  * Queries accessibility tree by pattern (role, name, description).
235
228
  *
236
229
  * Performs case-insensitive matching with AND logic for multiple fields.
230
+ * An element reported more than once (by the page's tree and its frame's)
231
+ * is listed once.
237
232
  *
238
233
  * @param tree - Accessibility tree to search
239
234
  * @param pattern - Query pattern with optional role, name, description
@@ -253,13 +248,19 @@ const TEXT_ROLES = new Set(['statictext', 'inlinetextbox']);
253
248
  */
254
249
  export function queryA11yTree(tree, pattern) {
255
250
  const matches = [];
251
+ const seenElements = new Set();
256
252
  const wantsText = pattern.role !== undefined && TEXT_ROLES.has(pattern.role.toLowerCase());
257
253
  for (const node of tree.nodes.values()) {
258
254
  if (!wantsText && TEXT_ROLES.has(node.role.toLowerCase()))
259
255
  continue;
260
- if (matchesPattern(node, pattern)) {
261
- matches.push(node);
256
+ if (!matchesPattern(node, pattern))
257
+ continue;
258
+ if (node.backendDOMNodeId !== undefined) {
259
+ if (seenElements.has(node.backendDOMNodeId))
260
+ continue;
261
+ seenElements.add(node.backendDOMNodeId);
262
262
  }
263
+ matches.push(node);
263
264
  }
264
265
  return {
265
266
  nodes: matches,
@@ -317,47 +318,110 @@ const QUERY_FIELDS = {
317
318
  description: 'description',
318
319
  desc: 'description',
319
320
  };
321
+ /** The next `key:`/`key=` anywhere (fields may follow other text). */
322
+ const QUERY_KEY = /([a-z]+)\s*[:=]/i;
323
+ /** A quoted value (it may contain anything but its own quote). */
324
+ const QUOTED_QUERY_VALUE = /^\s*("[^"]*"|'[^']*')/;
325
+ /** Where a role value ends: at the next `key:` after a space or comma. */
326
+ const NEXT_QUERY_FIELD = /[\s,]+[a-z]+\s*[:=]/i;
320
327
  /**
321
- * One `key:value` (or `key=value`) field: the value is quoted, or runs until
322
- * the next `key:`/`key=` after a space or comma.
328
+ * Where a name or description ends: only at a known field that has a value,
329
+ * so names keep their spaces and colons (`name=E-mail address:`).
323
330
  */
324
- const QUERY_FIELD = /([a-z]+)\s*[:=](?:\s*("[^"]*"|'[^']*')|((?:(?![\s,]+[a-z]+\s*[:=]).)*))/gis;
331
+ const NEXT_KNOWN_QUERY_FIELD = /[\s,]+(?:role|name|description|desc)\s*[:=](?=\s*[^\s,])/i;
332
+ /**
333
+ * Read one field value.
334
+ *
335
+ * @param text - Text after `key:`
336
+ * @param field - Field the value is for
337
+ * @returns The value, where it ends in `text`, and whether it was quoted
338
+ */
339
+ function readQueryValue(text, field) {
340
+ const quoted = QUOTED_QUERY_VALUE.exec(text);
341
+ if (quoted?.[1])
342
+ return { value: quoted[1].slice(1, -1), end: quoted[0].length, quoted: true };
343
+ const next = (field === 'role' ? NEXT_QUERY_FIELD : NEXT_KNOWN_QUERY_FIELD).exec(text);
344
+ const end = next ? next.index : text.length;
345
+ const value = text
346
+ .slice(0, end)
347
+ .trim()
348
+ .replace(/,+$/, '')
349
+ .replace(/^(["'])(.*)\1$/s, '$2');
350
+ return { value, end, quoted: false };
351
+ }
325
352
  /**
326
353
  * Parses query pattern string into A11yQueryPattern object.
327
354
  *
328
355
  * Fields are `key:value` or `key=value`, separated by spaces or commas.
329
- * Values may contain spaces (quote them if they contain `key:`-like text).
330
- * Keys: role, name, description (desc). Case-insensitive.
356
+ * Keys: role, name, description (desc). Case-insensitive. A role ends at the
357
+ * next `key:`; a name or description runs to the next role/name/description
358
+ * field with a value, or to the end, so it may contain spaces and colons
359
+ * (`name=E-mail address:`). Quote a value to end it explicitly.
331
360
  *
332
361
  * @param patternString - Query pattern string
333
362
  * @returns Parsed query pattern
334
- * @throws CommandError (81) for unknown keys
363
+ * @throws CommandError (81) for unknown keys, also a misspelled one inside a name
335
364
  *
336
365
  * @example
337
366
  * ```typescript
338
367
  * parseQueryPattern('role:button name:Submit') // { role: 'button', name: 'Submit' }
339
368
  * parseQueryPattern('role=link,name=Google Chrome') // { role: 'link', name: 'Google Chrome' }
340
369
  * parseQueryPattern('name:"Sign in" role:button') // { name: 'Sign in', role: 'button' }
370
+ * parseQueryPattern('name=E-mail address:') // { name: 'E-mail address:' }
341
371
  * ```
342
372
  */
343
373
  export function parseQueryPattern(patternString) {
344
374
  const pattern = {};
345
- for (const [, rawKey = '', quoted, plain = ''] of patternString.matchAll(QUERY_FIELD)) {
346
- const rawValue = quoted ?? plain;
375
+ let rest = patternString;
376
+ for (let key = QUERY_KEY.exec(rest); key; key = QUERY_KEY.exec(rest)) {
377
+ const rawKey = key[1] ?? '';
347
378
  const field = QUERY_FIELDS[rawKey.toLowerCase()];
348
- if (!field) {
349
- const err = unknownQueryFieldError(rawKey);
350
- throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
351
- }
352
- const value = rawValue
353
- .trim()
354
- .replace(/,+$/, '')
355
- .replace(/^(["'])(.*)\1$/s, '$2');
379
+ if (!field)
380
+ throwUnknownField(rawKey);
381
+ const valueText = rest.slice(key.index + key[0].length);
382
+ const { value, end, quoted } = readQueryValue(valueText, field);
383
+ if (!quoted && field !== 'role')
384
+ checkMisspelledField(value, field);
356
385
  if (value)
357
386
  pattern[field] = value;
387
+ rest = valueText.slice(end);
358
388
  }
359
389
  return pattern;
360
390
  }
391
+ /**
392
+ * Throw the exit-81 error for an unknown query field.
393
+ *
394
+ * @param field - The unrecognized key
395
+ * @param similar - A known field it looks like a typo of
396
+ * @param value - The name or description that absorbed it
397
+ * @throws CommandError (81)
398
+ */
399
+ function throwUnknownField(field, similar, value) {
400
+ const err = unknownQueryFieldError(field, similar, value);
401
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
402
+ }
403
+ /** A `word:`/`word=` inside a name or description. */
404
+ const ABSORBED_KEY = /[\s,]([a-z]+)\s*[:=]/gi;
405
+ /**
406
+ * Reject a name or description that swallowed a misspelled field
407
+ * (`name:Save rol:button`): a `word:` within edit distance 1 of role, name or
408
+ * desc (2 of description). Wider distances would catch common label words
409
+ * (`Date:`, `Note:`, `Code:`).
410
+ *
411
+ * @param value - Unquoted name or description
412
+ * @param field - Field it is the value of
413
+ * @throws CommandError (81) with a "did you mean" suggestion
414
+ */
415
+ function checkMisspelledField(value, field) {
416
+ for (const [, word = ''] of value.matchAll(ABSORBED_KEY)) {
417
+ const lower = word.toLowerCase();
418
+ if (QUERY_FIELDS[lower])
419
+ continue;
420
+ const similar = Object.keys(QUERY_FIELDS).find((name) => levenshteinDistance(lower, name) <= (name.length > 4 ? 2 : 1));
421
+ if (similar)
422
+ throwUnknownField(word, similar, `${field}=${value}`);
423
+ }
424
+ }
361
425
  /**
362
426
  * Resolve accessibility properties for a DOM node via IPC.
363
427
  *
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Which requests an action is about (pages, API calls, sockets) and which
3
+ * are static assets loaded along the way (stylesheets, scripts, fonts,
4
+ * images), by CDP resource type.
5
+ */
6
+ /** Fields a request needs to be classified */
7
+ interface ClassifiedRequest {
8
+ method?: string | undefined;
9
+ resourceType?: string | undefined;
10
+ status?: number | undefined;
11
+ failed?: true | undefined;
12
+ }
13
+ /**
14
+ * Whether a request is worth its own line after an action: one of the
15
+ * {@link ACTIVITY_RESOURCE_TYPES}, one of unknown type, any request that is
16
+ * not a GET (a `sendBeacon` POST typed `Other`), or an asset that failed (an
17
+ * HTTP error or no response).
18
+ *
19
+ * @param request - Request with its method and CDP resource type
20
+ * @returns False for assets fetched with GET that loaded normally
21
+ */
22
+ export declare function isNotableRequest(request: ClassifiedRequest): boolean;
23
+ /**
24
+ * Short names of the asset types among requests, e.g. `["css", "js", "images"]`
25
+ * (types without a short name are lowercased, after the known ones).
26
+ *
27
+ * @param requests - Asset requests
28
+ * @returns Distinct names, known types first
29
+ */
30
+ export declare function assetTypeNames(requests: ClassifiedRequest[]): string[];
31
+ export {};
32
+ //# sourceMappingURL=requestKinds.d.ts.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Which requests an action is about (pages, API calls, sockets) and which
3
+ * are static assets loaded along the way (stylesheets, scripts, fonts,
4
+ * images), by CDP resource type.
5
+ */
6
+ /**
7
+ * Resource types listed one by one after an action: pages, API calls,
8
+ * sockets, and the requests a page sends about itself (pings, beacons,
9
+ * preflights, CSP reports)
10
+ */
11
+ const ACTIVITY_RESOURCE_TYPES = new Set([
12
+ 'Document',
13
+ 'XHR',
14
+ 'Fetch',
15
+ 'WebSocket',
16
+ 'EventSource',
17
+ 'Ping',
18
+ 'Preflight',
19
+ 'CSPViolationReport',
20
+ ]);
21
+ /** Short names of asset types, in the order summaries list them */
22
+ const ASSET_TYPE_NAMES = [
23
+ ['Stylesheet', 'css'],
24
+ ['Script', 'js'],
25
+ ['Font', 'fonts'],
26
+ ['Image', 'images'],
27
+ ['Media', 'media'],
28
+ ];
29
+ /**
30
+ * Whether a request is worth its own line after an action: one of the
31
+ * {@link ACTIVITY_RESOURCE_TYPES}, one of unknown type, any request that is
32
+ * not a GET (a `sendBeacon` POST typed `Other`), or an asset that failed (an
33
+ * HTTP error or no response).
34
+ *
35
+ * @param request - Request with its method and CDP resource type
36
+ * @returns False for assets fetched with GET that loaded normally
37
+ */
38
+ export function isNotableRequest(request) {
39
+ if (request.resourceType === undefined || ACTIVITY_RESOURCE_TYPES.has(request.resourceType)) {
40
+ return true;
41
+ }
42
+ if (request.method !== undefined && request.method.toUpperCase() !== 'GET')
43
+ return true;
44
+ return request.failed === true || (request.status ?? 0) >= 400;
45
+ }
46
+ /**
47
+ * Short names of the asset types among requests, e.g. `["css", "js", "images"]`
48
+ * (types without a short name are lowercased, after the known ones).
49
+ *
50
+ * @param requests - Asset requests
51
+ * @returns Distinct names, known types first
52
+ */
53
+ export function assetTypeNames(requests) {
54
+ const types = new Set(requests.map((request) => request.resourceType ?? 'Other'));
55
+ const known = ASSET_TYPE_NAMES.filter(([type]) => types.has(type)).map(([, name]) => name);
56
+ const others = [...types]
57
+ .filter((type) => !ASSET_TYPE_NAMES.some(([known]) => known === type))
58
+ .map((type) => type.toLowerCase());
59
+ return [...known, ...others];
60
+ }
61
+ //# sourceMappingURL=requestKinds.js.map