@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monobrowse",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
4
4
  "description": "Native browser automation via Chrome DevTools Protocol — the engine powering monomind browse",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "src"
32
32
  ],
33
33
  "engines": {
34
- "node": ">=18.0.0"
34
+ "node": ">=22.12.0"
35
35
  },
36
36
  "keywords": [
37
37
  "browser-automation",
@@ -2,7 +2,7 @@
2
2
  * Platform adapters are pure declarations plus two tiny page-probing methods.
3
3
  * A fake PageInterface (evaluate + url) is all that's needed — no browser.
4
4
  */
5
- import { describe, it, expect, vi } from 'vitest';
5
+ import { describe, expect, it, vi } from 'vitest';
6
6
  import { adapters, getAdapter, type PageInterface } from '../browser/adapters/index.js';
7
7
 
8
8
  const PLATFORMS = ['linkedin', 'instagram', 'x', 'gemini', 'google', 'microsoft'] as const;
@@ -32,7 +32,7 @@ describe('getAdapter', () => {
32
32
 
33
33
  it('throws for an unknown platform, listing the supported ones', () => {
34
34
  expect(() => getAdapter('myspace')).toThrow(
35
- `Unknown platform: myspace. Supported: ${PLATFORMS.join(', ')}`
35
+ `Unknown platform: myspace. Supported: ${PLATFORMS.join(', ')}`,
36
36
  );
37
37
  });
38
38
 
@@ -72,12 +72,19 @@ describe('adapter invariants', () => {
72
72
 
73
73
  describe('isLoggedIn', () => {
74
74
  it('coerces the page probe to the boolean the caller expects', async () => {
75
- await expect(getAdapter('linkedin').isLoggedIn(fakePage({ evaluate: true }))).resolves.toBe(true);
76
- await expect(getAdapter('linkedin').isLoggedIn(fakePage({ evaluate: false }))).resolves.toBe(false);
75
+ await expect(getAdapter('linkedin').isLoggedIn(fakePage({ evaluate: true }))).resolves.toBe(
76
+ true,
77
+ );
78
+ await expect(getAdapter('linkedin').isLoggedIn(fakePage({ evaluate: false }))).resolves.toBe(
79
+ false,
80
+ );
77
81
  });
78
82
 
79
83
  it('google short-circuits to false on the sign-in page without probing the DOM', async () => {
80
- const page = fakePage({ url: 'https://accounts.google.com/signin/v2/identifier', evaluate: true });
84
+ const page = fakePage({
85
+ url: 'https://accounts.google.com/signin/v2/identifier',
86
+ evaluate: true,
87
+ });
81
88
  await expect(getAdapter('google').isLoggedIn(page)).resolves.toBe(false);
82
89
  expect(page.evaluated).toEqual([]);
83
90
  });
@@ -109,9 +116,9 @@ describe('isLoggedIn', () => {
109
116
  describe('extractUsername', () => {
110
117
  it('returns whatever the page evaluation yields', async () => {
111
118
  for (const key of PLATFORMS) {
112
- await expect(getAdapter(key).extractUsername(fakePage({ evaluate: 'someuser' }))).resolves.toBe(
113
- 'someuser'
114
- );
119
+ await expect(
120
+ getAdapter(key).extractUsername(fakePage({ evaluate: 'someuser' })),
121
+ ).resolves.toBe('someuser');
115
122
  }
116
123
  });
117
124
 
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, expect, it } from 'vitest';
2
2
  import { parseBatchCommandLine } from '../cli/commands.js';
3
3
 
4
4
  describe('parseBatchCommandLine — eval expressions inside a batch command string', () => {
@@ -25,7 +25,9 @@ describe('parseBatchCommandLine — eval expressions inside a batch command stri
25
25
  const input = "eval [...document.querySelectorAll('li')].map(x => x.textContent.trim())";
26
26
  const { subName, subArgs } = parseBatchCommandLine(input);
27
27
  expect(subName).toBe('eval');
28
- expect(subArgs).toEqual(["[...document.querySelectorAll('li')].map(x => x.textContent.trim())"]);
28
+ expect(subArgs).toEqual([
29
+ "[...document.querySelectorAll('li')].map(x => x.textContent.trim())",
30
+ ]);
29
31
  });
30
32
 
31
33
  it('still recognizes --json and --max-output flags placed before the expression', () => {
@@ -1,10 +1,10 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { deriveBoxOutput } from '../cli/commands.js';
1
+ import { describe, expect, it, vi } from 'vitest';
3
2
  import { getElementBox } from '../browser/snapshot.js';
4
3
  import type { ElementRef } from '../browser/types.js';
4
+ import { deriveBoxOutput } from '../cli/commands.js';
5
5
 
6
6
  describe('getElementBox (issue #15: center-vs-top-left contract)', () => {
7
- it('averages the CDP content quad\'s four corners into a center point', async () => {
7
+ it("averages the CDP content quad's four corners into a center point", async () => {
8
8
  // Content quad order per CDP DOM.getBoxModel: [x1,y1 top-left, x2,y2
9
9
  // top-right, x3,y3 bottom-right, x4,y4 bottom-left]. A 372x37 box whose
10
10
  // top-left is (454, 513.5) — matching the issue's own reproduction data.
@@ -28,8 +28,12 @@ describe('deriveBoxOutput (issue #15: get box exposing both conventions)', () =>
28
28
  const center = { x: 640, y: 532, width: 372, height: 37 };
29
29
  const result = deriveBoxOutput(center);
30
30
  expect(result).toEqual({
31
- x: 454, y: 513.5, width: 372, height: 37,
32
- centerX: 640, centerY: 532,
31
+ x: 454,
32
+ y: 513.5,
33
+ width: 372,
34
+ height: 37,
35
+ centerX: 640,
36
+ centerY: 532,
33
37
  });
34
38
  });
35
39
 
@@ -8,9 +8,10 @@
8
8
  * Fixed port range (23470-23479) chosen to avoid colliding with real
9
9
  * services; each test binds/tears down its own listeners.
10
10
  */
11
- import { describe, it, expect, afterEach } from 'vitest';
12
- import { createServer as createTcpServer, type Server as TcpServer, type Socket } from 'net';
13
- import { createServer as createHttpServer, type Server as HttpServer } from 'http';
11
+
12
+ import { createServer as createHttpServer, type Server as HttpServer } from 'node:http';
13
+ import { createServer as createTcpServer, type Socket, type Server as TcpServer } from 'node:net';
14
+ import { afterEach, describe, expect, it } from 'vitest';
14
15
  import { launchBrowser } from '../browser/browser.js';
15
16
 
16
17
  const BASE = 23470;
@@ -24,16 +25,21 @@ afterEach(async () => {
24
25
  // to close on their own, which can outlast the test. Destroy explicitly.
25
26
  for (const sock of sockets) sock.destroy();
26
27
  sockets = [];
27
- await Promise.all(servers.map(s => new Promise<void>(resolve => s.close(() => resolve()))));
28
+ await Promise.all(servers.map((s) => new Promise<void>((resolve) => s.close(() => resolve()))));
28
29
  servers = [];
29
30
  }, 5000);
30
31
 
31
32
  /** Bind a bare TCP listener — accepts connections but speaks no HTTP/CDP. */
32
33
  function occupyNonChrome(port: number): Promise<void> {
33
34
  return new Promise((resolve, reject) => {
34
- const s = createTcpServer(sock => { sockets.push(sock); /* accept and do nothing — no CDP response */ });
35
+ const s = createTcpServer((sock) => {
36
+ sockets.push(sock); /* accept and do nothing — no CDP response */
37
+ });
35
38
  s.once('error', reject);
36
- s.listen(port, '127.0.0.1', () => { servers.push(s); resolve(); });
39
+ s.listen(port, '127.0.0.1', () => {
40
+ servers.push(s);
41
+ resolve();
42
+ });
37
43
  });
38
44
  }
39
45
 
@@ -45,12 +51,16 @@ function occupyChrome(port: number): Promise<void> {
45
51
  res.writeHead(200, { 'Content-Type': 'application/json' });
46
52
  res.end(JSON.stringify({ Browser: 'Chrome/999.0.0.0' }));
47
53
  } else {
48
- res.writeHead(404); res.end();
54
+ res.writeHead(404);
55
+ res.end();
49
56
  }
50
57
  });
51
- s.on('connection', sock => sockets.push(sock));
58
+ s.on('connection', (sock) => sockets.push(sock));
52
59
  s.once('error', reject);
53
- s.listen(port, '127.0.0.1', () => { servers.push(s); resolve(); });
60
+ s.listen(port, '127.0.0.1', () => {
61
+ servers.push(s);
62
+ resolve();
63
+ });
54
64
  });
55
65
  }
56
66
 
@@ -65,8 +75,9 @@ describe('launchBrowser — port scan/attach decisions', () => {
65
75
  const port = BASE + 1;
66
76
  await occupyNonChrome(port);
67
77
  await occupyChrome(port + 1); // would succeed if scanning happened — must not be reached
68
- await expect(launchBrowser({ port, strictPort: true }))
69
- .rejects.toThrow(/does not identify as Chrome/);
78
+ await expect(launchBrowser({ port, strictPort: true })).rejects.toThrow(
79
+ /does not identify as Chrome/,
80
+ );
70
81
  });
71
82
 
72
83
  it('strictPort: attaches on an occupied Chrome requested port (identical to non-strict)', async () => {
@@ -80,7 +91,7 @@ describe('launchBrowser — port scan/attach decisions', () => {
80
91
  // Occupy the full 10-port scan window with non-Chrome listeners.
81
92
  for (let i = 0; i < 10; i++) await occupyNonChrome(port + i);
82
93
  await expect(launchBrowser({ port })).rejects.toThrow(
83
- new RegExp(`Ports ${port}-${port + 9} are all occupied`)
94
+ new RegExp(`Ports ${port}-${port + 9} are all occupied`),
84
95
  );
85
96
  }, 15000); // generous margin — 10 candidates, each a fast isTcpPortOpen check
86
97
 
@@ -92,11 +103,11 @@ describe('launchBrowser — port scan/attach decisions', () => {
92
103
  // skipped, so if every candidate is occupied the call still fails even
93
104
  // though one of them is an attachable Chrome.
94
105
  const port = BASE + 4;
95
- await occupyNonChrome(port); // requested port: occupied, not Chrome
96
- await occupyChrome(port + 1); // scanned candidate: IS Chrome — must be skipped, not attached
106
+ await occupyNonChrome(port); // requested port: occupied, not Chrome
107
+ await occupyChrome(port + 1); // scanned candidate: IS Chrome — must be skipped, not attached
97
108
  for (let i = 2; i < 10; i++) await occupyNonChrome(port + i); // remaining candidates: occupied
98
109
  await expect(launchBrowser({ port })).rejects.toThrow(
99
- new RegExp(`Ports ${port}-${port + 9} are all occupied`)
110
+ new RegExp(`Ports ${port}-${port + 9} are all occupied`),
100
111
  );
101
112
  }, 15000); // generous margin — 10 candidates, each a fast isTcpPortOpen check
102
113
  });
@@ -5,7 +5,7 @@
5
5
  * `ws` module (so we can drive 'message'/'close'/'error' by hand), and
6
6
  * fetchTargets/fetchNewTarget run against a stubbed global fetch.
7
7
  */
8
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
8
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
9
 
10
10
  // --- fake `ws` -------------------------------------------------------------
11
11
  // Captures the most recently constructed socket so tests can drive its events.
@@ -140,7 +140,9 @@ describe('send', () => {
140
140
  it('refuses to exceed the 1000 in-flight command cap', async () => {
141
141
  const { client } = await connected();
142
142
  const inflight = Array.from({ length: 1000 }, () => client.send('A').catch(() => {}));
143
- await expect(client.send('A')).rejects.toThrow('CDP command queue full (>1000 in-flight commands)');
143
+ await expect(client.send('A')).rejects.toThrow(
144
+ 'CDP command queue full (>1000 in-flight commands)',
145
+ );
144
146
  client.close();
145
147
  await Promise.all(inflight);
146
148
  });
@@ -152,7 +154,9 @@ describe('send', () => {
152
154
  it('rejects a command with no response after the default 30s timeout', async () => {
153
155
  const { client } = await connected();
154
156
  const p = client.send('Runtime.evaluate');
155
- const assertion = expect(p).rejects.toThrow('CDP command "Runtime.evaluate" timed out after 30000ms');
157
+ const assertion = expect(p).rejects.toThrow(
158
+ 'CDP command "Runtime.evaluate" timed out after 30000ms',
159
+ );
156
160
  await vi.advanceTimersByTimeAsync(30_000);
157
161
  await assertion;
158
162
  });
@@ -171,7 +175,13 @@ describe('send', () => {
171
175
  await vi.advanceTimersByTimeAsync(60_000);
172
176
  // Still pending — no timeout fired.
173
177
  let settled = false;
174
- void p.then(() => { settled = true; }).catch(() => { settled = true; });
178
+ void p
179
+ .then(() => {
180
+ settled = true;
181
+ })
182
+ .catch(() => {
183
+ settled = true;
184
+ });
175
185
  await Promise.resolve();
176
186
  expect(settled).toBe(false);
177
187
  ws.deliver({ id: 1, result: {} });
@@ -331,21 +341,27 @@ describe('fetchTargets / fetchNewTarget', () => {
331
341
  globalThis.fetch = realFetch;
332
342
  });
333
343
 
334
- function stubFetch(impl: (url: string, init?: RequestInit) => Response): ReturnType<typeof vi.fn> {
344
+ function stubFetch(
345
+ impl: (url: string, init?: RequestInit) => Response,
346
+ ): ReturnType<typeof vi.fn> {
335
347
  const fn = vi.fn((url: string, init?: RequestInit) => Promise.resolve(impl(url, init)));
336
348
  globalThis.fetch = fn as unknown as typeof fetch;
337
349
  return fn;
338
350
  }
339
351
 
340
352
  it('fetchTargets hits /json/list on the given port', async () => {
341
- const fn = stubFetch(() => new Response(JSON.stringify([{ id: 'T1', type: 'page' }]), { status: 200 }));
353
+ const fn = stubFetch(
354
+ () => new Response(JSON.stringify([{ id: 'T1', type: 'page' }]), { status: 200 }),
355
+ );
342
356
  await expect(fetchTargets(9333)).resolves.toEqual([{ id: 'T1', type: 'page' }]);
343
357
  expect(fn.mock.calls[0]![0]).toBe('http://127.0.0.1:9333/json/list');
344
358
  });
345
359
 
346
360
  it('fetchTargets throws on a non-OK response', async () => {
347
361
  stubFetch(() => new Response('nope', { status: 500, statusText: 'Internal Server Error' }));
348
- await expect(fetchTargets(9222)).rejects.toThrow('Failed to fetch targets: Internal Server Error');
362
+ await expect(fetchTargets(9222)).rejects.toThrow(
363
+ 'Failed to fetch targets: Internal Server Error',
364
+ );
349
365
  });
350
366
 
351
367
  it('fetchNewTarget uses PUT and percent-encodes the URL', async () => {
@@ -354,7 +370,7 @@ describe('fetchTargets / fetchNewTarget', () => {
354
370
  const [url, init] = fn.mock.calls[0] as [string, RequestInit];
355
371
  expect(init.method).toBe('PUT');
356
372
  expect(url).toBe(
357
- 'http://127.0.0.1:9222/json/new?' + encodeURIComponent('https://x.test/a?b=1&c=2')
373
+ `http://127.0.0.1:9222/json/new?${encodeURIComponent('https://x.test/a?b=1&c=2')}`,
358
374
  );
359
375
  // The query separators of the target URL must not leak into Chrome's own query.
360
376
  expect(url.split('?').length).toBe(2);
@@ -363,7 +379,7 @@ describe('fetchTargets / fetchNewTarget', () => {
363
379
  it('fetchNewTarget throws on a non-OK response', async () => {
364
380
  stubFetch(() => new Response('', { status: 405, statusText: 'Method Not Allowed' }));
365
381
  await expect(fetchNewTarget(9222, 'about:blank')).rejects.toThrow(
366
- 'Failed to create target: Method Not Allowed'
382
+ 'Failed to create target: Method Not Allowed',
367
383
  );
368
384
  });
369
385
 
@@ -11,10 +11,12 @@
11
11
  * fake `ws` socket (same pattern as cdp-client.test.ts), and process.kill is
12
12
  * spied on rather than actually signaling anything.
13
13
  */
14
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
15
- import { mkdtemp, rm, mkdir, writeFile } from 'fs/promises';
16
- import { join } from 'path';
17
- import { tmpdir } from 'os';
14
+
15
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { setTimeout as realDelay } from 'node:timers/promises';
19
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
18
20
 
19
21
  let lastSocket: FakeWs | null = null;
20
22
 
@@ -72,7 +74,34 @@ afterEach(async () => {
72
74
  async function writePersistedPort(port: number, pid: number, savedAt: number): Promise<void> {
73
75
  const dir = join(tempDir, '.monomind', 'monobrowse');
74
76
  await mkdir(dir, { recursive: true });
75
- await writeFile(join(dir, 'active-port.json'), JSON.stringify({ port, pid, launched: true, savedAt }), 'utf-8');
77
+ await writeFile(
78
+ join(dir, 'active-port.json'),
79
+ JSON.stringify({ port, pid, launched: true, savedAt }),
80
+ 'utf-8',
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Block (in REAL time) until closeBrowser()'s process-exit poll has actually
86
+ * started, observed via the poll's own first liveness probe — `kill(pid, 0)`
87
+ * is the first thing waitForProcessExit() does.
88
+ *
89
+ * Needed because closeBrowser() awaits the persisted-port read (real
90
+ * filesystem I/O) BEFORE that poll starts, while `advanceTimersByTimeAsync`
91
+ * only moves a FAKE clock and does not wait for real I/O. Advancing a fixed
92
+ * number of times therefore raced the read: every advance can complete in
93
+ * microseconds of real time while the read is still in flight, so on a loaded
94
+ * host the budget ran out with the poll never started — and since nothing
95
+ * then drives the fake clock again, the test did not merely run slow, it
96
+ * deadlocked on an unsettled promise until the test timeout fired.
97
+ *
98
+ * Once this returns, the remaining work is pure fake timers, so a single
99
+ * bounded advance carries it deterministically.
100
+ */
101
+ async function exitPollStarted(pid: number): Promise<void> {
102
+ while (!killSpy.mock.calls.some(([p, signal]) => p === pid && signal === 0)) {
103
+ await realDelay(0);
104
+ }
76
105
  }
77
106
 
78
107
  async function connectedClient(autoAck = false) {
@@ -157,13 +186,15 @@ describe('#115 review follow-up: closeBrowser() cross-process PID-kill fallback'
157
186
 
158
187
  const client = await connectedClient(true);
159
188
  const closePromise = closeBrowser(client, port);
160
- // Still running a moment after the ack — close() must NOT have resolved.
161
- await vi.advanceTimersByTimeAsync(200);
162
189
  let settled = false;
163
190
  void closePromise.then(() => {
164
191
  settled = true;
165
192
  });
166
- await vi.advanceTimersByTimeAsync(0);
193
+
194
+ // Poll is running and the process is still alive — several poll ticks in,
195
+ // close() must NOT have resolved.
196
+ await exitPollStarted(22222);
197
+ await vi.advanceTimersByTimeAsync(200);
167
198
  expect(settled).toBe(false);
168
199
 
169
200
  alive = false; // Chrome finishes exiting
@@ -180,32 +211,24 @@ describe('#115 review follow-up: closeBrowser() cross-process PID-kill fallback'
180
211
 
181
212
  const client = await connectedClient(true);
182
213
  const closePromise = closeBrowser(client, port);
183
- // The exit poll schedules one timer per tick, each from the previous
184
- // tick's callback. How far a single advance carries through a chain like
185
- // that is a fake-timer implementation detail that differs by Node version
186
- // (runAllTimersAsync hung this test on Node 22 while passing on 26), so
187
- // step the clock until the promise settles instead of assuming.
188
- //
189
- // Each `advanceTimersByTimeAsync` call has real (non-fake) overhead of
190
- // its own — a previous fix here just raised the iteration count at a
191
- // fixed 100ms step (100ms x 600 = enough fake-time headroom, but up to
192
- // 600 real await round-trips), which still intermittently timed out on a
193
- // slow/busy CI runner because that overhead is per CALL, not per fake-ms
194
- // advanced. Stepping by a much larger 2000ms per call reaches the same
195
- // (and greater) fake-time coverage in a fraction of the calls — the exit
196
- // poll's PROCESS_EXIT_TIMEOUT_MS deadline (5000ms) is comfortably inside
197
- // even a handful of iterations — without giving up the settle-and-stop
198
- // loop that avoids the Node 22 hang a single unbounded advance hit.
199
214
  let settled = false;
200
215
  void closePromise.then(() => {
201
216
  settled = true;
202
217
  });
203
- for (let i = 0; i < 60 && !settled; i++) await vi.advanceTimersByTimeAsync(2000);
218
+
219
+ // Wait for the poll to exist before driving it (see exitPollStarted) —
220
+ // every previous fix here guessed an advance budget instead, which is
221
+ // what made this test hang rather than merely run slow. From here on the
222
+ // poll is the only thing left and it is pure fake timers, so one advance
223
+ // past its PROCESS_EXIT_TIMEOUT_MS (5000ms) deadline settles it, with no
224
+ // dependence on how fast the host happens to be.
225
+ await exitPollStarted(33333);
226
+ await vi.advanceTimersByTimeAsync(6000);
204
227
  await closePromise;
205
228
 
206
229
  expect(settled).toBe(true);
207
230
  expect(killSpy).toHaveBeenCalledWith(33333, 'SIGKILL');
208
- }, 20_000);
231
+ });
209
232
 
210
233
  it('no persisted port file at all — closeBrowser is a no-op, no kill attempted', async () => {
211
234
  vi.resetModules();
@@ -3,14 +3,14 @@
3
3
  * two per-session Maps. Nothing here touches a browser — we hand it a fake
4
4
  * client that just records handlers and lets us fire events by hand.
5
5
  */
6
- import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { beforeEach, describe, expect, it } from 'vitest';
7
7
  import type { CdpClient } from '../browser/cdp.js';
8
8
  import {
9
- setupConsoleCapture,
10
- getConsoleMessages,
11
9
  clearConsoleMessages,
12
- getPageErrors,
13
10
  clearPageErrors,
11
+ getConsoleMessages,
12
+ getPageErrors,
13
+ setupConsoleCapture,
14
14
  teardownConsoleCapture,
15
15
  } from '../browser/console-log.js';
16
16
 
@@ -91,13 +91,17 @@ describe('Runtime.consoleAPICalled capture', () => {
91
91
  client.emit(
92
92
  'Runtime.consoleAPICalled',
93
93
  { type: 'log', args: [{ value: 'count:' }, { value: 42 }, { description: 'Object {a: 1}' }] },
94
- 'S1'
94
+ 'S1',
95
95
  );
96
96
  expect(getConsoleMessages('S1')[0]!.text).toBe('count: 42 Object {a: 1}');
97
97
  });
98
98
 
99
99
  it('normalizes CDP\'s "warning" to the "warn" type used by ConsoleMessage', () => {
100
- client.emit('Runtime.consoleAPICalled', { type: 'warning', args: [{ value: 'careful' }] }, 'S1');
100
+ client.emit(
101
+ 'Runtime.consoleAPICalled',
102
+ { type: 'warning', args: [{ value: 'careful' }] },
103
+ 'S1',
104
+ );
101
105
  expect(getConsoleMessages('S1')[0]!.type).toBe('warn');
102
106
  });
103
107
 
@@ -123,8 +127,15 @@ describe('Log.entryAdded capture', () => {
123
127
  it('records text, url and lineNumber and maps "warning" to "warn"', () => {
124
128
  client.emit(
125
129
  'Log.entryAdded',
126
- { entry: { level: 'warning', text: 'deprecated API', url: 'https://x.test/a.js', lineNumber: 17 } },
127
- 'S1'
130
+ {
131
+ entry: {
132
+ level: 'warning',
133
+ text: 'deprecated API',
134
+ url: 'https://x.test/a.js',
135
+ lineNumber: 17,
136
+ },
137
+ },
138
+ 'S1',
128
139
  );
129
140
  const [msg] = getConsoleMessages('S1');
130
141
  expect(msg).toMatchObject({
@@ -156,7 +167,7 @@ describe('Runtime.exceptionThrown capture', () => {
156
167
  columnNumber: 9,
157
168
  },
158
169
  },
159
- 'S1'
170
+ 'S1',
160
171
  );
161
172
  expect(getPageErrors('S1')[0]).toMatchObject({
162
173
  text: 'TypeError: x is not a function',
@@ -167,7 +178,11 @@ describe('Runtime.exceptionThrown capture', () => {
167
178
  });
168
179
 
169
180
  it('falls back to text, then to "Unknown error"', () => {
170
- client.emit('Runtime.exceptionThrown', { exceptionDetails: { text: 'Uncaught (in promise)' } }, 'S1');
181
+ client.emit(
182
+ 'Runtime.exceptionThrown',
183
+ { exceptionDetails: { text: 'Uncaught (in promise)' } },
184
+ 'S1',
185
+ );
171
186
  client.emit('Runtime.exceptionThrown', { exceptionDetails: {} }, 'S1');
172
187
  expect(getPageErrors('S1').map((e) => e.text)).toEqual([
173
188
  'Uncaught (in promise)',
@@ -191,14 +206,22 @@ describe('multi-session isolation and clearing', () => {
191
206
  client.emit('Runtime.exceptionThrown', { exceptionDetails: { text: 'e2' } }, 'S2');
192
207
  });
193
208
 
194
- it('keeps each session\'s buffers separate', () => {
209
+ it("keeps each session's buffers separate", () => {
195
210
  expect(getConsoleMessages('S1').map((m) => m.text)).toEqual(['one']);
196
211
  expect(getConsoleMessages('S2').map((m) => m.text)).toEqual(['two']);
197
212
  });
198
213
 
199
214
  it('returns a flattened view across sessions when no session id is given', () => {
200
- expect(getConsoleMessages().map((m) => m.text).sort()).toEqual(['one', 'two']);
201
- expect(getPageErrors().map((e) => e.text).sort()).toEqual(['e1', 'e2']);
215
+ expect(
216
+ getConsoleMessages()
217
+ .map((m) => m.text)
218
+ .sort(),
219
+ ).toEqual(['one', 'two']);
220
+ expect(
221
+ getPageErrors()
222
+ .map((e) => e.text)
223
+ .sort(),
224
+ ).toEqual(['e1', 'e2']);
202
225
  });
203
226
 
204
227
  it('returns a copy, so mutating the result does not corrupt the buffer', () => {