@monoes/monobrowse 1.0.9 → 1.0.12

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 (221) hide show
  1. package/README.md +1 -1
  2. package/dist/src/__tests__/adapters.test.js +5 -2
  3. package/dist/src/__tests__/adapters.test.js.map +1 -1
  4. package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
  5. package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
  6. package/dist/src/__tests__/box-model.test.js +9 -5
  7. package/dist/src/__tests__/box-model.test.js.map +1 -1
  8. package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
  9. package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
  10. package/dist/src/__tests__/browser-launch.test.js +17 -10
  11. package/dist/src/__tests__/browser-launch.test.js.map +1 -1
  12. package/dist/src/__tests__/cdp-client.test.js +9 -3
  13. package/dist/src/__tests__/cdp-client.test.js.map +1 -1
  14. package/dist/src/__tests__/close-browser.test.d.ts +13 -0
  15. package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
  16. package/dist/src/__tests__/close-browser.test.js +40 -26
  17. package/dist/src/__tests__/close-browser.test.js.map +1 -1
  18. package/dist/src/__tests__/console-log.test.js +17 -6
  19. package/dist/src/__tests__/console-log.test.js.map +1 -1
  20. package/dist/src/__tests__/find.test.js +26 -6
  21. package/dist/src/__tests__/find.test.js.map +1 -1
  22. package/dist/src/__tests__/frame-command.test.js +9 -3
  23. package/dist/src/__tests__/frame-command.test.js.map +1 -1
  24. package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
  25. package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
  26. package/dist/src/__tests__/output.test.js +1 -1
  27. package/dist/src/__tests__/output.test.js.map +1 -1
  28. package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
  29. package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
  30. package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
  31. package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
  32. package/dist/src/__tests__/ref-cache.test.js +18 -10
  33. package/dist/src/__tests__/ref-cache.test.js.map +1 -1
  34. package/dist/src/__tests__/session-state.test.d.ts +8 -0
  35. package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
  36. package/dist/src/__tests__/session-state.test.js +10 -10
  37. package/dist/src/__tests__/session-state.test.js.map +1 -1
  38. package/dist/src/__tests__/snapshot.test.js +45 -15
  39. package/dist/src/__tests__/snapshot.test.js.map +1 -1
  40. package/dist/src/__tests__/storage-eval.test.js +14 -6
  41. package/dist/src/__tests__/storage-eval.test.js.map +1 -1
  42. package/dist/src/__tests__/tabs.test.js +25 -9
  43. package/dist/src/__tests__/tabs.test.js.map +1 -1
  44. package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
  45. package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
  46. package/dist/src/browser/action-builder/analyzer.js +25 -9
  47. package/dist/src/browser/action-builder/analyzer.js.map +1 -1
  48. package/dist/src/browser/action-builder/types.d.ts.map +1 -1
  49. package/dist/src/browser/actions.d.ts +1 -1
  50. package/dist/src/browser/actions.d.ts.map +1 -1
  51. package/dist/src/browser/actions.js +16 -6
  52. package/dist/src/browser/actions.js.map +1 -1
  53. package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
  54. package/dist/src/browser/adapters/gemini.js.map +1 -1
  55. package/dist/src/browser/adapters/google.d.ts.map +1 -1
  56. package/dist/src/browser/adapters/google.js.map +1 -1
  57. package/dist/src/browser/adapters/index.d.ts.map +1 -1
  58. package/dist/src/browser/adapters/index.js +3 -3
  59. package/dist/src/browser/adapters/index.js.map +1 -1
  60. package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
  61. package/dist/src/browser/adapters/instagram.js.map +1 -1
  62. package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
  63. package/dist/src/browser/adapters/linkedin.js.map +1 -1
  64. package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
  65. package/dist/src/browser/adapters/microsoft.js.map +1 -1
  66. package/dist/src/browser/adapters/x.d.ts.map +1 -1
  67. package/dist/src/browser/adapters/x.js.map +1 -1
  68. package/dist/src/browser/browser.d.ts +30 -0
  69. package/dist/src/browser/browser.d.ts.map +1 -1
  70. package/dist/src/browser/browser.js +122 -15
  71. package/dist/src/browser/browser.js.map +1 -1
  72. package/dist/src/browser/cdp.d.ts.map +1 -1
  73. package/dist/src/browser/cdp.js +21 -8
  74. package/dist/src/browser/cdp.js.map +1 -1
  75. package/dist/src/browser/console-log.d.ts.map +1 -1
  76. package/dist/src/browser/console-log.js +2 -2
  77. package/dist/src/browser/console-log.js.map +1 -1
  78. package/dist/src/browser/dashboard/server.d.ts.map +1 -1
  79. package/dist/src/browser/dashboard/server.js +29 -19
  80. package/dist/src/browser/dashboard/server.js.map +1 -1
  81. package/dist/src/browser/dashboard/ui.html +153 -165
  82. package/dist/src/browser/dialog.d.ts.map +1 -1
  83. package/dist/src/browser/dialog.js +3 -1
  84. package/dist/src/browser/dialog.js.map +1 -1
  85. package/dist/src/browser/emulation.d.ts.map +1 -1
  86. package/dist/src/browser/emulation.js +21 -6
  87. package/dist/src/browser/emulation.js.map +1 -1
  88. package/dist/src/browser/find.d.ts +4 -4
  89. package/dist/src/browser/find.d.ts.map +1 -1
  90. package/dist/src/browser/find.js +14 -6
  91. package/dist/src/browser/find.js.map +1 -1
  92. package/dist/src/browser/har.d.ts.map +1 -1
  93. package/dist/src/browser/har.js +14 -7
  94. package/dist/src/browser/har.js.map +1 -1
  95. package/dist/src/browser/index.d.ts +16 -16
  96. package/dist/src/browser/index.d.ts.map +1 -1
  97. package/dist/src/browser/index.js +16 -16
  98. package/dist/src/browser/index.js.map +1 -1
  99. package/dist/src/browser/network.d.ts.map +1 -1
  100. package/dist/src/browser/network.js +14 -4
  101. package/dist/src/browser/network.js.map +1 -1
  102. package/dist/src/browser/pdf.d.ts.map +1 -1
  103. package/dist/src/browser/pdf.js +3 -3
  104. package/dist/src/browser/pdf.js.map +1 -1
  105. package/dist/src/browser/profiler.d.ts.map +1 -1
  106. package/dist/src/browser/profiler.js +10 -5
  107. package/dist/src/browser/profiler.js.map +1 -1
  108. package/dist/src/browser/record.d.ts +1 -1
  109. package/dist/src/browser/record.d.ts.map +1 -1
  110. package/dist/src/browser/record.js +18 -7
  111. package/dist/src/browser/record.js.map +1 -1
  112. package/dist/src/browser/ref-cache.d.ts.map +1 -1
  113. package/dist/src/browser/ref-cache.js +9 -4
  114. package/dist/src/browser/ref-cache.js.map +1 -1
  115. package/dist/src/browser/screenshot.d.ts.map +1 -1
  116. package/dist/src/browser/screenshot.js +11 -7
  117. package/dist/src/browser/screenshot.js.map +1 -1
  118. package/dist/src/browser/session.d.ts +1 -1
  119. package/dist/src/browser/session.d.ts.map +1 -1
  120. package/dist/src/browser/session.js +27 -8
  121. package/dist/src/browser/session.js.map +1 -1
  122. package/dist/src/browser/snapshot.d.ts +1 -1
  123. package/dist/src/browser/snapshot.d.ts.map +1 -1
  124. package/dist/src/browser/snapshot.js +8 -4
  125. package/dist/src/browser/snapshot.js.map +1 -1
  126. package/dist/src/browser/storage.d.ts.map +1 -1
  127. package/dist/src/browser/storage.js.map +1 -1
  128. package/dist/src/browser/tabs.d.ts.map +1 -1
  129. package/dist/src/browser/tabs.js +9 -3
  130. package/dist/src/browser/tabs.js.map +1 -1
  131. package/dist/src/browser/trace.d.ts.map +1 -1
  132. package/dist/src/browser/trace.js +3 -3
  133. package/dist/src/browser/trace.js.map +1 -1
  134. package/dist/src/browser/types.d.ts.map +1 -1
  135. package/dist/src/browser/types.js +23 -4
  136. package/dist/src/browser/types.js.map +1 -1
  137. package/dist/src/browser/vitals.d.ts.map +1 -1
  138. package/dist/src/browser/vitals.js +1 -1
  139. package/dist/src/browser/vitals.js.map +1 -1
  140. package/dist/src/browser/wait.d.ts.map +1 -1
  141. package/dist/src/browser/wait.js +11 -5
  142. package/dist/src/browser/wait.js.map +1 -1
  143. package/dist/src/cli/action.d.ts.map +1 -1
  144. package/dist/src/cli/action.js +14 -11
  145. package/dist/src/cli/action.js.map +1 -1
  146. package/dist/src/cli/commands.d.ts.map +1 -1
  147. package/dist/src/cli/commands.js +453 -159
  148. package/dist/src/cli/commands.js.map +1 -1
  149. package/dist/src/cli/output.js +5 -5
  150. package/dist/src/cli/output.js.map +1 -1
  151. package/dist/src/cli/platform.d.ts.map +1 -1
  152. package/dist/src/cli/platform.js +15 -11
  153. package/dist/src/cli/platform.js.map +1 -1
  154. package/dist/src/cli/types.d.ts +1 -1
  155. package/dist/src/cli/types.d.ts.map +1 -1
  156. package/dist/src/cli.js +21 -9
  157. package/dist/src/cli.js.map +1 -1
  158. package/dist/src/index.d.ts +2 -2
  159. package/dist/src/index.d.ts.map +1 -1
  160. package/dist/src/index.js +2 -2
  161. package/dist/src/index.js.map +1 -1
  162. package/dist/tsconfig.tsbuildinfo +1 -1
  163. package/package.json +2 -2
  164. package/src/__tests__/adapters.test.ts +15 -8
  165. package/src/__tests__/batch-eval-parsing.test.ts +4 -2
  166. package/src/__tests__/box-model.test.ts +9 -5
  167. package/src/__tests__/browser-launch.test.ts +26 -15
  168. package/src/__tests__/cdp-client.test.ts +25 -9
  169. package/src/__tests__/close-browser.test.ts +49 -26
  170. package/src/__tests__/console-log.test.ts +36 -13
  171. package/src/__tests__/find.test.ts +58 -20
  172. package/src/__tests__/frame-command.test.ts +12 -6
  173. package/src/__tests__/open-url-navigate-error.test.ts +2 -4
  174. package/src/__tests__/output.test.ts +1 -1
  175. package/src/__tests__/reap-idle-browser.test.ts +276 -0
  176. package/src/__tests__/ref-cache.test.ts +25 -12
  177. package/src/__tests__/session-state.test.ts +40 -28
  178. package/src/__tests__/snapshot.test.ts +61 -22
  179. package/src/__tests__/storage-eval.test.ts +30 -16
  180. package/src/__tests__/tabs.test.ts +26 -10
  181. package/src/browser/action-builder/analyzer.ts +39 -15
  182. package/src/browser/action-builder/types.ts +45 -8
  183. package/src/browser/actions.ts +288 -123
  184. package/src/browser/adapters/gemini.ts +7 -3
  185. package/src/browser/adapters/google.ts +5 -3
  186. package/src/browser/adapters/index.ts +5 -4
  187. package/src/browser/adapters/instagram.ts +4 -2
  188. package/src/browser/adapters/linkedin.ts +4 -2
  189. package/src/browser/adapters/microsoft.ts +9 -5
  190. package/src/browser/adapters/x.ts +7 -3
  191. package/src/browser/browser.ts +188 -42
  192. package/src/browser/cdp.ts +43 -11
  193. package/src/browser/console-log.ts +24 -7
  194. package/src/browser/dashboard/server.ts +44 -25
  195. package/src/browser/dashboard/ui.html +153 -165
  196. package/src/browser/dialog.ts +25 -8
  197. package/src/browser/emulation.ts +86 -36
  198. package/src/browser/find.ts +110 -47
  199. package/src/browser/har.ts +58 -26
  200. package/src/browser/index.ts +16 -16
  201. package/src/browser/network.ts +147 -46
  202. package/src/browser/pdf.ts +4 -4
  203. package/src/browser/profiler.ts +23 -10
  204. package/src/browser/record.ts +38 -19
  205. package/src/browser/ref-cache.ts +38 -17
  206. package/src/browser/screenshot.ts +58 -32
  207. package/src/browser/session.ts +51 -18
  208. package/src/browser/snapshot.ts +34 -20
  209. package/src/browser/storage.ts +73 -17
  210. package/src/browser/tabs.ts +29 -17
  211. package/src/browser/trace.ts +14 -10
  212. package/src/browser/types.ts +23 -4
  213. package/src/browser/vitals.ts +33 -16
  214. package/src/browser/wait.ts +47 -21
  215. package/src/cli/action.ts +52 -21
  216. package/src/cli/commands.ts +1093 -385
  217. package/src/cli/output.ts +5 -5
  218. package/src/cli/platform.ts +44 -23
  219. package/src/cli/types.ts +1 -1
  220. package/src/cli.ts +34 -18
  221. package/src/index.ts +2 -2
@@ -6,23 +6,21 @@
6
6
  * validation logic: the CdpClient is a stub that answers the handful of CDP
7
7
  * commands the round trip issues.
8
8
  */
9
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
10
- import { mkdtemp, rm, readFile, stat, mkdir } from 'fs/promises';
11
- import { join } from 'path';
12
- import { tmpdir } from 'os';
9
+
10
+ import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises';
11
+ import { tmpdir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
13
14
  import type { CdpClient } from '../browser/cdp.js';
14
- import {
15
- saveStateFile,
16
- loadStateFile,
17
- saveSession,
18
- loadSession,
19
- listSessions,
20
- } from '../browser/session.js';
15
+ import { loadSession, loadStateFile, saveSession, saveStateFile } from '../browser/session.js';
21
16
 
22
17
  const COOKIES = [{ name: 'sid', value: 'abc', domain: 'x.test', path: '/' }];
23
18
 
24
19
  /** Records every CDP command and answers the ones session.ts depends on. */
25
- function stubClient(): { client: CdpClient; calls: Array<{ method: string; params: unknown; sid?: string }> } {
20
+ function stubClient(): {
21
+ client: CdpClient;
22
+ calls: Array<{ method: string; params: unknown; sid?: string }>;
23
+ } {
26
24
  const calls: Array<{ method: string; params: unknown; sid?: string }> = [];
27
25
  const client = {
28
26
  send: vi.fn(async (method: string, params: unknown, sid?: string) => {
@@ -30,8 +28,10 @@ function stubClient(): { client: CdpClient; calls: Array<{ method: string; param
30
28
  if (method === 'Network.getCookies') return { cookies: COOKIES };
31
29
  if (method === 'Runtime.evaluate') {
32
30
  const expr = (params as { expression: string }).expression;
33
- if (expr.includes('Object.entries(localStorage)')) return { result: { value: '{"k":"v"}' } };
34
- if (expr.includes('Object.entries(sessionStorage)')) return { result: { value: '{"s":"1"}' } };
31
+ if (expr.includes('Object.entries(localStorage)'))
32
+ return { result: { value: '{"k":"v"}' } };
33
+ if (expr.includes('Object.entries(sessionStorage)'))
34
+ return { result: { value: '{"s":"1"}' } };
35
35
  }
36
36
  return {};
37
37
  }),
@@ -69,7 +69,7 @@ describe('validateSessionName (via saveSession/loadSession)', () => {
69
69
  for (const [label, name] of bad) {
70
70
  it(`rejects ${label}`, async () => {
71
71
  await expect(saveSession(client, 'S1', 'T1', name, 'https://x.test', 'T')).rejects.toThrow(
72
- /Invalid session name/
72
+ /Invalid session name/,
73
73
  );
74
74
  await expect(loadSession(client, 'S1', name)).rejects.toThrow(/Invalid session name/);
75
75
  });
@@ -77,7 +77,7 @@ describe('validateSessionName (via saveSession/loadSession)', () => {
77
77
 
78
78
  it('accepts an ordinary name (fails later, on the missing file, not on validation)', async () => {
79
79
  await expect(loadSession(client, 'S1', 'no-such-session-xyz')).rejects.toThrow(
80
- 'Session not found: no-such-session-xyz'
80
+ 'Session not found: no-such-session-xyz',
81
81
  );
82
82
  });
83
83
 
@@ -103,7 +103,7 @@ describe('validateFilePath (via saveStateFile/loadStateFile)', () => {
103
103
  for (const [label, filePath] of bad) {
104
104
  it(`rejects ${label}`, async () => {
105
105
  await expect(
106
- saveStateFile(client, 'S1', 'T1', filePath, 'https://x.test', 'T')
106
+ saveStateFile(client, 'S1', 'T1', filePath, 'https://x.test', 'T'),
107
107
  ).rejects.toThrow(/Invalid file path/);
108
108
  await expect(loadStateFile(client, 'S1', filePath)).rejects.toThrow(/Invalid file path/);
109
109
  });
@@ -111,7 +111,9 @@ describe('validateFilePath (via saveStateFile/loadStateFile)', () => {
111
111
 
112
112
  it('allows a plain absolute path', async () => {
113
113
  const p = join(dir, 'state.json');
114
- await expect(saveStateFile(client, 'S1', 'T1', p, 'https://x.test', 'T')).resolves.toBeUndefined();
114
+ await expect(
115
+ saveStateFile(client, 'S1', 'T1', p, 'https://x.test', 'T'),
116
+ ).resolves.toBeUndefined();
115
117
  });
116
118
  });
117
119
 
@@ -164,38 +166,41 @@ describe('state file round trip', () => {
164
166
  it('rejects a state file whose cookies field is not an array', async () => {
165
167
  const { client } = stubClient();
166
168
  const p = join(dir, 'bad.json');
167
- const { writeFile } = await import('fs/promises');
169
+ const { writeFile } = await import('node:fs/promises');
168
170
  await writeFile(p, JSON.stringify({ cookies: { name: 'sid' } }));
169
171
  await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
170
- 'Invalid state file: cookies is not an array'
172
+ 'Invalid state file: cookies is not an array',
171
173
  );
172
174
  });
173
175
 
174
176
  it('rejects a state file whose localStorage is an array rather than an object', async () => {
175
177
  const { client } = stubClient();
176
178
  const p = join(dir, 'bad2.json');
177
- const { writeFile } = await import('fs/promises');
179
+ const { writeFile } = await import('node:fs/promises');
178
180
  await writeFile(p, JSON.stringify({ cookies: [], localStorage: ['nope'] }));
179
181
  await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
180
- 'Invalid state file: localStorage is not a plain object'
182
+ 'Invalid state file: localStorage is not a plain object',
181
183
  );
182
184
  });
183
185
 
184
186
  it('rejects a state file whose sessionStorage is an array', async () => {
185
187
  const { client } = stubClient();
186
188
  const p = join(dir, 'bad3.json');
187
- const { writeFile } = await import('fs/promises');
189
+ const { writeFile } = await import('node:fs/promises');
188
190
  await writeFile(p, JSON.stringify({ cookies: [], sessionStorage: ['nope'] }));
189
191
  await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
190
- 'Invalid state file: sessionStorage is not a plain object'
192
+ 'Invalid state file: sessionStorage is not a plain object',
191
193
  );
192
194
  });
193
195
 
194
196
  it('accepts a state file with neither storage map present', async () => {
195
197
  const { client, calls } = stubClient();
196
198
  const p = join(dir, 'minimal.json');
197
- const { writeFile } = await import('fs/promises');
198
- await writeFile(p, JSON.stringify({ targetId: 'T', sessionId: 'S', url: '', title: '', cookies: [] }));
199
+ const { writeFile } = await import('node:fs/promises');
200
+ await writeFile(
201
+ p,
202
+ JSON.stringify({ targetId: 'T', sessionId: 'S', url: '', title: '', cookies: [] }),
203
+ );
199
204
  await expect(loadStateFile(client, 'S1', p)).resolves.toMatchObject({ cookies: [] });
200
205
  expect(calls.filter((c) => c.method === 'Runtime.evaluate')).toEqual([]);
201
206
  });
@@ -224,7 +229,7 @@ describe('listSessions', () => {
224
229
  it('lists only .json files, with the extension stripped', async () => {
225
230
  const sessionDir = join(dir, '.monomind', 'browser-sessions');
226
231
  await mkdir(sessionDir, { recursive: true });
227
- const { writeFile } = await import('fs/promises');
232
+ const { writeFile } = await import('node:fs/promises');
228
233
  await writeFile(join(sessionDir, 'alpha.json'), '{}');
229
234
  await writeFile(join(sessionDir, 'beta.json'), '{}');
230
235
  await writeFile(join(sessionDir, 'notes.txt'), 'ignore me');
@@ -236,7 +241,14 @@ describe('listSessions', () => {
236
241
  it('saveSession writes into the session dir and loadSession reads it back', async () => {
237
242
  const mod = await withHome(dir);
238
243
  const saver = stubClient();
239
- const filePath = await mod.saveSession(saver.client, 'S1', 'T1', 'my-session', 'https://x.test', 'T');
244
+ const filePath = await mod.saveSession(
245
+ saver.client,
246
+ 'S1',
247
+ 'T1',
248
+ 'my-session',
249
+ 'https://x.test',
250
+ 'T',
251
+ );
240
252
  expect(filePath).toBe(join(dir, '.monomind', 'browser-sessions', 'my-session.json'));
241
253
  expect((await stat(filePath)).mode & 0o777).toBe(0o600);
242
254
  expect((await stat(join(dir, '.monomind', 'browser-sessions'))).mode & 0o777).toBe(0o700);
@@ -4,9 +4,14 @@
4
4
  * resolves against. That transform is pure — the only browser interaction is
5
5
  * the AX-tree fetch, which a stub answers from a literal node array.
6
6
  */
7
- import { describe, it, expect, vi } from 'vitest';
7
+ import { describe, expect, it, vi } from 'vitest';
8
8
  import type { CdpClient } from '../browser/cdp.js';
9
- import { captureSnapshot, resolveRef, getObjectIdForRef, getElementBox } from '../browser/snapshot.js';
9
+ import {
10
+ captureSnapshot,
11
+ getElementBox,
12
+ getObjectIdForRef,
13
+ resolveRef,
14
+ } from '../browser/snapshot.js';
10
15
  import type { ElementRef } from '../browser/types.js';
11
16
 
12
17
  interface AXProp {
@@ -29,7 +34,7 @@ interface AXNode {
29
34
  /** Client that serves a fixed AX tree plus location.href / document.title. */
30
35
  function axClient(
31
36
  nodes: AXNode[],
32
- extra: { url?: string; title?: string; partial?: AXNode[]; selectorNodeId?: number } = {}
37
+ extra: { url?: string; title?: string; partial?: AXNode[]; selectorNodeId?: number } = {},
33
38
  ): { client: CdpClient; calls: string[] } {
34
39
  const calls: string[] = [];
35
40
  const client = {
@@ -49,8 +54,8 @@ function axClient(
49
54
  result: {
50
55
  value:
51
56
  (params as { expression: string }).expression === 'location.href'
52
- ? extra.url ?? 'https://x.test/'
53
- : extra.title ?? 'Test Page',
57
+ ? (extra.url ?? 'https://x.test/')
58
+ : (extra.title ?? 'Test Page'),
54
59
  },
55
60
  };
56
61
  default:
@@ -80,10 +85,14 @@ describe('captureSnapshot — tree shape', () => {
80
85
  expect(snap.text).toBe(
81
86
  'RootWebArea "Test Page" [ref=e1]\n' +
82
87
  ' form "Login" [ref=e2]\n' +
83
- ' button "Submit" [ref=e3]'
88
+ ' button "Submit" [ref=e3]',
84
89
  );
85
90
  expect([...snap.refs.keys()]).toEqual(['e1', 'e2', 'e3']);
86
- expect(snap.refs.get('e3')).toMatchObject({ role: 'button', name: 'Submit', backendDOMNodeId: 30 });
91
+ expect(snap.refs.get('e3')).toMatchObject({
92
+ role: 'button',
93
+ name: 'Submit',
94
+ backendDOMNodeId: 30,
95
+ });
87
96
  expect(snap).toMatchObject({ url: 'https://x.test/', title: 'Test Page' });
88
97
  });
89
98
 
@@ -157,13 +166,13 @@ describe('captureSnapshot — options', () => {
157
166
  });
158
167
 
159
168
  it('compact drops indentation and descriptions', async () => {
160
- const { client } = axClient([
161
- button(1, 'Go', { description: { value: 'primary action' } }),
162
- ]);
169
+ const { client } = axClient([button(1, 'Go', { description: { value: 'primary action' } })]);
163
170
  const spaced = await captureSnapshot(client, 'S1');
164
171
  expect(spaced.text).toBe('button "Go" (primary action) [ref=e1]');
165
172
 
166
- const { client: c2 } = axClient([button(1, 'Go', { description: { value: 'primary action' } })]);
173
+ const { client: c2 } = axClient([
174
+ button(1, 'Go', { description: { value: 'primary action' } }),
175
+ ]);
167
176
  const compact = await captureSnapshot(c2, 'S1', { compact: true });
168
177
  expect(compact.text).toBe('button "Go" [ref=e1]');
169
178
  // The description is still available on the ref even when not printed.
@@ -198,7 +207,13 @@ describe('captureSnapshot — options', () => {
198
207
  describe('captureSnapshot — property extraction and rendering', () => {
199
208
  async function snapOf(props: AXProp[], over: Partial<AXNode> = {}) {
200
209
  const { client } = axClient([
201
- { nodeId: 1, role: { value: 'textbox' }, name: { value: 'Email' }, properties: props, ...over },
210
+ {
211
+ nodeId: 1,
212
+ role: { value: 'textbox' },
213
+ name: { value: 'Email' },
214
+ properties: props,
215
+ ...over,
216
+ },
202
217
  ]);
203
218
  return captureSnapshot(client, 'S1');
204
219
  }
@@ -210,14 +225,18 @@ describe('captureSnapshot — property extraction and rendering', () => {
210
225
  { name: 'required', value: { value: true } },
211
226
  ]);
212
227
  expect(snap.text).toBe(
213
- 'textbox "Email" [ref=e1, value="a@b.c", placeholder="you@example.com", required]'
228
+ 'textbox "Email" [ref=e1, value="a@b.c", placeholder="you@example.com", required]',
214
229
  );
215
230
  expect(snap.refs.get('e1')).toMatchObject({ value: 'a@b.c', placeholder: 'you@example.com' });
216
231
  });
217
232
 
218
233
  it('renders disabled as a bare flag only when true', async () => {
219
- expect((await snapOf([{ name: 'disabled', value: { value: true } }])).text).toContain(', disabled]');
220
- expect((await snapOf([{ name: 'disabled', value: { value: false } }])).text).not.toContain('disabled');
234
+ expect((await snapOf([{ name: 'disabled', value: { value: true } }])).text).toContain(
235
+ ', disabled]',
236
+ );
237
+ expect((await snapOf([{ name: 'disabled', value: { value: false } }])).text).not.toContain(
238
+ 'disabled',
239
+ );
221
240
  });
222
241
 
223
242
  it('accepts checked/expanded as either the boolean or the string "true"', async () => {
@@ -276,16 +295,18 @@ describe('captureSnapshot — property extraction and rendering', () => {
276
295
  });
277
296
 
278
297
  describe('resolveRef', () => {
279
- it('returns the ref when present', () => {
298
+ it('returns the ref when present', async () => {
280
299
  const refs = new Map<string, ElementRef>([
281
300
  ['e1', { ref: 'e1', role: 'button', name: 'Go', nodeId: 1 }],
282
301
  ]);
283
- expect(resolveRef({} as CdpClient, 'S1', refs, 'e1')).resolves.toMatchObject({ ref: 'e1' });
302
+ await expect(resolveRef({} as CdpClient, 'S1', refs, 'e1')).resolves.toMatchObject({
303
+ ref: 'e1',
304
+ });
284
305
  });
285
306
 
286
307
  it('throws a snapshot-first hint when the ref is unknown', async () => {
287
308
  await expect(resolveRef({} as CdpClient, 'S1', new Map(), 'e9')).rejects.toThrow(
288
- 'Element ref @e9 not found. Run snapshot first.'
309
+ 'Element ref @e9 not found. Run snapshot first.',
289
310
  );
290
311
  });
291
312
  });
@@ -295,7 +316,7 @@ describe('getObjectIdForRef', () => {
295
316
  const send = vi.fn();
296
317
  const client = { send } as unknown as CdpClient;
297
318
  await expect(
298
- getObjectIdForRef(client, 'S1', { ref: 'e1', role: 'button', name: '', nodeId: 1 })
319
+ getObjectIdForRef(client, 'S1', { ref: 'e1', role: 'button', name: '', nodeId: 1 }),
299
320
  ).resolves.toBeNull();
300
321
  expect(send).not.toHaveBeenCalled();
301
322
  });
@@ -304,7 +325,13 @@ describe('getObjectIdForRef', () => {
304
325
  const send = vi.fn(async () => ({ object: { objectId: 'OBJ' } }));
305
326
  const client = { send } as unknown as CdpClient;
306
327
  await expect(
307
- getObjectIdForRef(client, 'S1', { ref: 'e1', role: 'button', name: '', nodeId: 1, backendDOMNodeId: 77 })
328
+ getObjectIdForRef(client, 'S1', {
329
+ ref: 'e1',
330
+ role: 'button',
331
+ name: '',
332
+ nodeId: 1,
333
+ backendDOMNodeId: 77,
334
+ }),
308
335
  ).resolves.toBe('OBJ');
309
336
  expect(send.mock.calls[0]).toEqual(['DOM.resolveNode', { backendNodeId: 77 }, 'S1']);
310
337
  });
@@ -312,7 +339,13 @@ describe('getObjectIdForRef', () => {
312
339
  it('returns null when the node no longer resolves to an object', async () => {
313
340
  const client = { send: vi.fn(async () => ({ object: {} })) } as unknown as CdpClient;
314
341
  await expect(
315
- getObjectIdForRef(client, 'S1', { ref: 'e1', role: 'button', name: '', nodeId: 1, backendDOMNodeId: 77 })
342
+ getObjectIdForRef(client, 'S1', {
343
+ ref: 'e1',
344
+ role: 'button',
345
+ name: '',
346
+ nodeId: 1,
347
+ backendDOMNodeId: 77,
348
+ }),
316
349
  ).resolves.toBeNull();
317
350
  });
318
351
  });
@@ -325,7 +358,13 @@ describe('getElementBox', () => {
325
358
  })),
326
359
  } as unknown as CdpClient;
327
360
  await expect(
328
- getElementBox(client, 'S1', { ref: 'e1', role: 'button', name: '', nodeId: 1, backendDOMNodeId: 5 })
361
+ getElementBox(client, 'S1', {
362
+ ref: 'e1',
363
+ role: 'button',
364
+ name: '',
365
+ nodeId: 1,
366
+ backendDOMNodeId: 5,
367
+ }),
329
368
  ).resolves.toEqual({ x: 60, y: 40, width: 100, height: 40 });
330
369
  });
331
370
  });
@@ -5,24 +5,27 @@
5
5
  * evaluateJs's error/timeout contract is what this file pins down.
6
6
  * Fake client throughout — no browser.
7
7
  */
8
- import { describe, it, expect, vi } from 'vitest';
9
- import type { CdpClient } from '../browser/cdp.js';
8
+ import { describe, expect, it, vi } from 'vitest';
10
9
  import { evaluateJs } from '../browser/actions.js';
10
+ import type { CdpClient } from '../browser/cdp.js';
11
11
  import {
12
- getLocalStorageKey,
13
- setLocalStorageKey,
14
- removeLocalStorageKey,
15
12
  clearLocalStorage,
13
+ clearSessionStorage,
16
14
  getAllLocalStorage,
15
+ getAllSessionStorage,
16
+ getLocalStorageKey,
17
17
  getSessionStorageKey,
18
- setSessionStorageKey,
18
+ removeLocalStorageKey,
19
19
  removeSessionStorageKey,
20
- clearSessionStorage,
21
- getAllSessionStorage,
20
+ setLocalStorageKey,
21
+ setSessionStorageKey,
22
22
  } from '../browser/storage.js';
23
23
 
24
24
  /** Client stub returning a fixed Runtime.evaluate value, recording expressions. */
25
- function stubClient(value?: unknown, exceptionDetails?: unknown): {
25
+ function stubClient(
26
+ value?: unknown,
27
+ exceptionDetails?: unknown,
28
+ ): {
26
29
  client: CdpClient;
27
30
  exprs: string[];
28
31
  sids: Array<string | undefined>;
@@ -33,7 +36,9 @@ function stubClient(value?: unknown, exceptionDetails?: unknown): {
33
36
  send: vi.fn(async (_m: string, params: { expression: string }, sid?: string) => {
34
37
  exprs.push(params.expression);
35
38
  sids.push(sid);
36
- return exceptionDetails ? { result: {}, exceptionDetails } : { result: { value, type: 'string' } };
39
+ return exceptionDetails
40
+ ? { result: {}, exceptionDetails }
41
+ : { result: { value, type: 'string' } };
37
42
  }),
38
43
  } as unknown as CdpClient;
39
44
  return { client, exprs, sids };
@@ -58,19 +63,21 @@ describe('evaluateJs', () => {
58
63
  exception: { description: 'ReferenceError: foo is not defined' },
59
64
  });
60
65
  await expect(evaluateJs(client, 'S1', 'foo')).rejects.toThrow(
61
- 'JS evaluation error: ReferenceError: foo is not defined'
66
+ 'JS evaluation error: ReferenceError: foo is not defined',
62
67
  );
63
68
  });
64
69
 
65
70
  it('falls back to exceptionDetails.text when there is no description', async () => {
66
71
  const { client } = stubClient(undefined, { text: 'Uncaught SyntaxError' });
67
72
  await expect(evaluateJs(client, 'S1', '{{')).rejects.toThrow(
68
- 'JS evaluation error: Uncaught SyntaxError'
73
+ 'JS evaluation error: Uncaught SyntaxError',
69
74
  );
70
75
  });
71
76
 
72
77
  it('returns undefined for an expression with no value', async () => {
73
- const client = { send: vi.fn(async () => ({ result: { type: 'undefined' } })) } as unknown as CdpClient;
78
+ const client = {
79
+ send: vi.fn(async () => ({ result: { type: 'undefined' } })),
80
+ } as unknown as CdpClient;
74
81
  await expect(evaluateJs(client, 'S1', 'void 0')).resolves.toBeUndefined();
75
82
  });
76
83
 
@@ -79,14 +86,19 @@ describe('evaluateJs', () => {
79
86
  // without the race; a 20ms budget proves the timer is armed.
80
87
  const client = { send: vi.fn(() => new Promise(() => {})) } as unknown as CdpClient;
81
88
  await expect(evaluateJs(client, 'S1', 'new Promise(() => {})', 20)).rejects.toThrow(
82
- 'JS evaluation timed out after 20ms'
89
+ 'JS evaluation timed out after 20ms',
83
90
  );
84
91
  });
85
92
 
86
93
  it('waits indefinitely when the timeout is disabled with 0', async () => {
87
94
  let resolveSend: (v: unknown) => void = () => {};
88
95
  const client = {
89
- send: vi.fn(() => new Promise((r) => { resolveSend = r; })),
96
+ send: vi.fn(
97
+ () =>
98
+ new Promise((r) => {
99
+ resolveSend = r;
100
+ }),
101
+ ),
90
102
  } as unknown as CdpClient;
91
103
  const p = evaluateJs(client, 'S1', 'slow()', 0);
92
104
  resolveSend({ result: { value: 'done', type: 'string' } });
@@ -165,7 +177,9 @@ describe('sessionStorage helpers mirror the localStorage ones', () => {
165
177
  });
166
178
 
167
179
  it('getAll parses, and degrades to {} on garbage', async () => {
168
- await expect(getAllSessionStorage(stubClient('{"s":"1"}').client, 'S1')).resolves.toEqual({ s: '1' });
180
+ await expect(getAllSessionStorage(stubClient('{"s":"1"}').client, 'S1')).resolves.toEqual({
181
+ s: '1',
182
+ });
169
183
  await expect(getAllSessionStorage(stubClient('<html>').client, 'S1')).resolves.toEqual({});
170
184
  });
171
185
  });
@@ -2,9 +2,9 @@
2
2
  * tabs.ts — target filtering and the CDP command sequences for
3
3
  * close/activate/frame-switch. Fake fetch + fake client; no browser.
4
4
  */
5
- import { describe, it, expect, afterEach, vi } from 'vitest';
5
+ import { afterEach, describe, expect, it, vi } from 'vitest';
6
6
  import type { CdpClient } from '../browser/cdp.js';
7
- import { listTabs, newTab, closeTab, activateTab, switchToFrame } from '../browser/tabs.js';
7
+ import { activateTab, closeTab, listTabs, newTab, switchToFrame } from '../browser/tabs.js';
8
8
 
9
9
  const realFetch = globalThis.fetch;
10
10
  afterEach(() => {
@@ -55,7 +55,7 @@ describe('listTabs', () => {
55
55
 
56
56
  it('propagates a failed /json/list', async () => {
57
57
  globalThis.fetch = vi.fn(() =>
58
- Promise.resolve(new Response('', { status: 502, statusText: 'Bad Gateway' }))
58
+ Promise.resolve(new Response('', { status: 502, statusText: 'Bad Gateway' })),
59
59
  ) as unknown as typeof fetch;
60
60
  await expect(listTabs(9222)).rejects.toThrow('Failed to fetch targets: Bad Gateway');
61
61
  });
@@ -66,7 +66,7 @@ describe('newTab', () => {
66
66
  const fn = stubFetch({ id: 'T1', type: 'page' });
67
67
  await newTab(9222);
68
68
  expect(fn.mock.calls[0]![0]).toBe(
69
- 'http://127.0.0.1:9222/json/new?' + encodeURIComponent('about:blank')
69
+ `http://127.0.0.1:9222/json/new?${encodeURIComponent('about:blank')}`,
70
70
  );
71
71
  });
72
72
 
@@ -81,7 +81,9 @@ describe('closeTab', () => {
81
81
  it('sends Target.closeTarget for the given targetId', async () => {
82
82
  const { client, calls } = stubClient();
83
83
  await closeTab(client, 'S1', 'T9');
84
- expect(calls).toEqual([{ method: 'Target.closeTarget', params: { targetId: 'T9' }, sid: undefined }]);
84
+ expect(calls).toEqual([
85
+ { method: 'Target.closeTarget', params: { targetId: 'T9' }, sid: undefined },
86
+ ]);
85
87
  });
86
88
  });
87
89
 
@@ -129,7 +131,10 @@ describe('switchToFrame', () => {
129
131
  'Runtime.evaluate': { result: { result: { value: null } } },
130
132
  'Page.getFrameTree': { frameTree: {} },
131
133
  });
132
- await expect(switchToFrame(client, 'S1', 'div.not-a-frame')).resolves.toEqual({ url: null, sessionId: null });
134
+ await expect(switchToFrame(client, 'S1', 'div.not-a-frame')).resolves.toEqual({
135
+ url: null,
136
+ sessionId: null,
137
+ });
133
138
  // The selector is embedded as a JSON string literal, never interpolated raw.
134
139
  const expr = (calls[0]!.params as { expression: string }).expression;
135
140
  expect(expr).toContain('document.querySelector("div.not-a-frame")');
@@ -149,9 +154,14 @@ describe('switchToFrame', () => {
149
154
  const { client, calls } = stubClient({
150
155
  'Runtime.evaluate': { result: { result: { value: 'https://f.test/frame' } } },
151
156
  'Page.getFrameTree': frameTreeWith('https://f.test/frame'),
152
- 'Target.getTargets': { targetInfos: [{ targetId: 'T1', type: 'page', url: 'https://x.test' }] },
157
+ 'Target.getTargets': {
158
+ targetInfos: [{ targetId: 'T1', type: 'page', url: 'https://x.test' }],
159
+ },
160
+ });
161
+ await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({
162
+ url: 'https://f.test/frame',
163
+ sessionId: null,
153
164
  });
154
- await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({ url: 'https://f.test/frame', sessionId: null });
155
165
  expect(calls.map((c) => c.method)).not.toContain('Target.attachToTarget');
156
166
  });
157
167
 
@@ -167,7 +177,10 @@ describe('switchToFrame', () => {
167
177
  },
168
178
  'Target.attachToTarget': { sessionId: 'S-FRAME' },
169
179
  });
170
- await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({ url: 'https://f.test/frame', sessionId: 'S-FRAME' });
180
+ await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({
181
+ url: 'https://f.test/frame',
182
+ sessionId: 'S-FRAME',
183
+ });
171
184
  const attach = calls.find((c) => c.method === 'Target.attachToTarget');
172
185
  expect(attach!.params).toEqual({ targetId: 'T-OOPIF', flatten: true });
173
186
  });
@@ -177,7 +190,10 @@ describe('switchToFrame', () => {
177
190
  'Runtime.evaluate': { result: { result: { value: 'https://f.test/frame' } } },
178
191
  'Page.getFrameTree': frameTreeWith('https://other.test/elsewhere'),
179
192
  });
180
- await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({ url: 'https://f.test/frame', sessionId: null });
193
+ await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({
194
+ url: 'https://f.test/frame',
195
+ sessionId: null,
196
+ });
181
197
  expect(calls.map((c) => c.method)).not.toContain('Target.getTargets');
182
198
  });
183
199
  });
@@ -1,5 +1,5 @@
1
1
  // src/browser/action-builder/analyzer.ts
2
- import { spawn } from 'child_process';
2
+ import { spawn } from 'node:child_process';
3
3
  import type { ActionDef } from './types.js';
4
4
 
5
5
  export interface AnalyzerPage {
@@ -45,7 +45,15 @@ function claudeCliCall(prompt: string): Promise<string> {
45
45
  return new Promise((resolve, reject) => {
46
46
  const child = spawn(
47
47
  'claude',
48
- ['--print', '--model', 'haiku', '--strict-mcp-config', '--no-session-persistence', '--', prompt],
48
+ [
49
+ '--print',
50
+ '--model',
51
+ 'haiku',
52
+ '--strict-mcp-config',
53
+ '--no-session-persistence',
54
+ '--',
55
+ prompt,
56
+ ],
49
57
  { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true },
50
58
  );
51
59
  let stdout = '';
@@ -54,11 +62,19 @@ function claudeCliCall(prompt: string): Promise<string> {
54
62
  const timer = setTimeout(() => {
55
63
  if (settled) return;
56
64
  settled = true;
57
- try { child.kill('SIGTERM'); } catch { /* ignore */ }
65
+ try {
66
+ child.kill('SIGTERM');
67
+ } catch {
68
+ /* ignore */
69
+ }
58
70
  reject(new Error('claude --print timed out after 60s'));
59
71
  }, 60_000);
60
- child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
61
- child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
72
+ child.stdout.on('data', (d: Buffer) => {
73
+ stdout += d.toString();
74
+ });
75
+ child.stderr.on('data', (d: Buffer) => {
76
+ stderr += d.toString();
77
+ });
62
78
  child.on('close', (code) => {
63
79
  if (settled) return;
64
80
  settled = true;
@@ -70,7 +86,11 @@ function claudeCliCall(prompt: string): Promise<string> {
70
86
  if (settled) return;
71
87
  settled = true;
72
88
  clearTimeout(timer);
73
- reject(new Error(`claude CLI not found — is Claude Code installed? (${(err as NodeJS.ErrnoException).code})`));
89
+ reject(
90
+ new Error(
91
+ `claude CLI not found — is Claude Code installed? (${(err as NodeJS.ErrnoException).code})`,
92
+ ),
93
+ );
74
94
  });
75
95
  });
76
96
  }
@@ -103,7 +123,7 @@ ActionDef schema:
103
123
  export async function analyzePageForAction(
104
124
  page: AnalyzerPage,
105
125
  task: string,
106
- options: AnalyzerOptions = {},
126
+ _options: AnalyzerOptions = {},
107
127
  ): Promise<ActionDef> {
108
128
  const url = await page.url();
109
129
  const title = await page.evaluate<string>('document.title');
@@ -119,13 +139,15 @@ export async function analyzePageForAction(
119
139
  const domContext = `URL: ${url}
120
140
  Title: ${title}
121
141
  Interactive elements (${elements.length}):
122
- ${elements.map((el, i) => {
123
- const attrs = Object.entries(el.attrs)
124
- .filter(([, v]) => v)
125
- .map(([k, v]) => `${k}="${v}"`)
126
- .join(' ');
127
- return `${i + 1}. <${el.tag}${attrs ? ' ' + attrs : ''}>${el.text}</${el.tag}>`;
128
- }).join('\n')}`;
142
+ ${elements
143
+ .map((el, i) => {
144
+ const attrs = Object.entries(el.attrs)
145
+ .filter(([, v]) => v)
146
+ .map(([k, v]) => `${k}="${v}"`)
147
+ .join(' ');
148
+ return `${i + 1}. <${el.tag}${attrs ? ` ${attrs}` : ''}>${el.text}</${el.tag}>`;
149
+ })
150
+ .join('\n')}`;
129
151
 
130
152
  const fullPrompt = `${SYSTEM_PROMPT}\n\nTask: ${task}\n\nPage context:\n${domContext}`;
131
153
  const responseText = await claudeCliCall(fullPrompt);
@@ -138,7 +160,9 @@ ${elements.map((el, i) => {
138
160
  }
139
161
 
140
162
  if (!actionDef.id || !actionDef.steps || !Array.isArray(actionDef.steps)) {
141
- throw new Error(`Claude returned invalid ActionDef: missing id or steps\n${responseText.slice(0, 500)}`);
163
+ throw new Error(
164
+ `Claude returned invalid ActionDef: missing id or steps\n${responseText.slice(0, 500)}`,
165
+ );
142
166
  }
143
167
 
144
168
  return actionDef;