@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.
- package/README.md +1 -1
- package/dist/src/__tests__/adapters.test.js +5 -2
- package/dist/src/__tests__/adapters.test.js.map +1 -1
- package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
- package/dist/src/__tests__/box-model.test.js +9 -5
- package/dist/src/__tests__/box-model.test.js.map +1 -1
- package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
- package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
- package/dist/src/__tests__/browser-launch.test.js +17 -10
- package/dist/src/__tests__/browser-launch.test.js.map +1 -1
- package/dist/src/__tests__/cdp-client.test.js +9 -3
- package/dist/src/__tests__/cdp-client.test.js.map +1 -1
- package/dist/src/__tests__/close-browser.test.d.ts +13 -0
- package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
- package/dist/src/__tests__/close-browser.test.js +40 -26
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/__tests__/console-log.test.js +17 -6
- package/dist/src/__tests__/console-log.test.js.map +1 -1
- package/dist/src/__tests__/find.test.js +26 -6
- package/dist/src/__tests__/find.test.js.map +1 -1
- package/dist/src/__tests__/frame-command.test.js +9 -3
- package/dist/src/__tests__/frame-command.test.js.map +1 -1
- package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
- package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
- package/dist/src/__tests__/output.test.js +1 -1
- package/dist/src/__tests__/output.test.js.map +1 -1
- package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
- package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
- package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
- package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
- package/dist/src/__tests__/ref-cache.test.js +18 -10
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/session-state.test.d.ts +8 -0
- package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
- package/dist/src/__tests__/session-state.test.js +10 -10
- package/dist/src/__tests__/session-state.test.js.map +1 -1
- package/dist/src/__tests__/snapshot.test.js +45 -15
- package/dist/src/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/__tests__/storage-eval.test.js +14 -6
- package/dist/src/__tests__/storage-eval.test.js.map +1 -1
- package/dist/src/__tests__/tabs.test.js +25 -9
- package/dist/src/__tests__/tabs.test.js.map +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
- package/dist/src/browser/action-builder/analyzer.js +25 -9
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/action-builder/types.d.ts.map +1 -1
- package/dist/src/browser/actions.d.ts +1 -1
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +16 -6
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
- package/dist/src/browser/adapters/gemini.js.map +1 -1
- package/dist/src/browser/adapters/google.d.ts.map +1 -1
- package/dist/src/browser/adapters/google.js.map +1 -1
- package/dist/src/browser/adapters/index.d.ts.map +1 -1
- package/dist/src/browser/adapters/index.js +3 -3
- package/dist/src/browser/adapters/index.js.map +1 -1
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
- package/dist/src/browser/adapters/instagram.js.map +1 -1
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
- package/dist/src/browser/adapters/linkedin.js.map +1 -1
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
- package/dist/src/browser/adapters/microsoft.js.map +1 -1
- package/dist/src/browser/adapters/x.d.ts.map +1 -1
- package/dist/src/browser/adapters/x.js.map +1 -1
- package/dist/src/browser/browser.d.ts +30 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +122 -15
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +21 -8
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts.map +1 -1
- package/dist/src/browser/console-log.js +2 -2
- package/dist/src/browser/console-log.js.map +1 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +29 -19
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +153 -165
- package/dist/src/browser/dialog.d.ts.map +1 -1
- package/dist/src/browser/dialog.js +3 -1
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/emulation.d.ts.map +1 -1
- package/dist/src/browser/emulation.js +21 -6
- package/dist/src/browser/emulation.js.map +1 -1
- package/dist/src/browser/find.d.ts +4 -4
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +14 -6
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts.map +1 -1
- package/dist/src/browser/har.js +14 -7
- package/dist/src/browser/har.js.map +1 -1
- package/dist/src/browser/index.d.ts +16 -16
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +16 -16
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +14 -4
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/pdf.d.ts.map +1 -1
- package/dist/src/browser/pdf.js +3 -3
- package/dist/src/browser/pdf.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +10 -5
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/record.d.ts +1 -1
- package/dist/src/browser/record.d.ts.map +1 -1
- package/dist/src/browser/record.js +18 -7
- package/dist/src/browser/record.js.map +1 -1
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +9 -4
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +11 -7
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/session.d.ts +1 -1
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +27 -8
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.d.ts +1 -1
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +8 -4
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts.map +1 -1
- package/dist/src/browser/storage.js.map +1 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +9 -3
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts.map +1 -1
- package/dist/src/browser/trace.js +3 -3
- package/dist/src/browser/trace.js.map +1 -1
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +23 -4
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts.map +1 -1
- package/dist/src/browser/vitals.js +1 -1
- package/dist/src/browser/vitals.js.map +1 -1
- package/dist/src/browser/wait.d.ts.map +1 -1
- package/dist/src/browser/wait.js +11 -5
- package/dist/src/browser/wait.js.map +1 -1
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +14 -11
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +453 -159
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.js +5 -5
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/cli/platform.d.ts.map +1 -1
- package/dist/src/cli/platform.js +15 -11
- package/dist/src/cli/platform.js.map +1 -1
- package/dist/src/cli/types.d.ts +1 -1
- package/dist/src/cli/types.d.ts.map +1 -1
- package/dist/src/cli.js +21 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapters.test.ts +15 -8
- package/src/__tests__/batch-eval-parsing.test.ts +4 -2
- package/src/__tests__/box-model.test.ts +9 -5
- package/src/__tests__/browser-launch.test.ts +26 -15
- package/src/__tests__/cdp-client.test.ts +25 -9
- package/src/__tests__/close-browser.test.ts +49 -26
- package/src/__tests__/console-log.test.ts +36 -13
- package/src/__tests__/find.test.ts +58 -20
- package/src/__tests__/frame-command.test.ts +12 -6
- package/src/__tests__/open-url-navigate-error.test.ts +2 -4
- package/src/__tests__/output.test.ts +1 -1
- package/src/__tests__/reap-idle-browser.test.ts +276 -0
- package/src/__tests__/ref-cache.test.ts +25 -12
- package/src/__tests__/session-state.test.ts +40 -28
- package/src/__tests__/snapshot.test.ts +61 -22
- package/src/__tests__/storage-eval.test.ts +30 -16
- package/src/__tests__/tabs.test.ts +26 -10
- package/src/browser/action-builder/analyzer.ts +39 -15
- package/src/browser/action-builder/types.ts +45 -8
- package/src/browser/actions.ts +288 -123
- package/src/browser/adapters/gemini.ts +7 -3
- package/src/browser/adapters/google.ts +5 -3
- package/src/browser/adapters/index.ts +5 -4
- package/src/browser/adapters/instagram.ts +4 -2
- package/src/browser/adapters/linkedin.ts +4 -2
- package/src/browser/adapters/microsoft.ts +9 -5
- package/src/browser/adapters/x.ts +7 -3
- package/src/browser/browser.ts +188 -42
- package/src/browser/cdp.ts +43 -11
- package/src/browser/console-log.ts +24 -7
- package/src/browser/dashboard/server.ts +44 -25
- package/src/browser/dashboard/ui.html +153 -165
- package/src/browser/dialog.ts +25 -8
- package/src/browser/emulation.ts +86 -36
- package/src/browser/find.ts +110 -47
- package/src/browser/har.ts +58 -26
- package/src/browser/index.ts +16 -16
- package/src/browser/network.ts +147 -46
- package/src/browser/pdf.ts +4 -4
- package/src/browser/profiler.ts +23 -10
- package/src/browser/record.ts +38 -19
- package/src/browser/ref-cache.ts +38 -17
- package/src/browser/screenshot.ts +58 -32
- package/src/browser/session.ts +51 -18
- package/src/browser/snapshot.ts +34 -20
- package/src/browser/storage.ts +73 -17
- package/src/browser/tabs.ts +29 -17
- package/src/browser/trace.ts +14 -10
- package/src/browser/types.ts +23 -4
- package/src/browser/vitals.ts +33 -16
- package/src/browser/wait.ts +47 -21
- package/src/cli/action.ts +52 -21
- package/src/cli/commands.ts +1093 -385
- package/src/cli/output.ts +5 -5
- package/src/cli/platform.ts +44 -23
- package/src/cli/types.ts +1 -1
- package/src/cli.ts +34 -18
- 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.
|
|
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": ">=
|
|
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,
|
|
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(
|
|
76
|
-
|
|
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({
|
|
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(
|
|
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,
|
|
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([
|
|
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,
|
|
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(
|
|
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,
|
|
32
|
-
|
|
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
|
-
|
|
12
|
-
import { createServer as
|
|
13
|
-
import { createServer as
|
|
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 => {
|
|
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', () => {
|
|
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);
|
|
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', () => {
|
|
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
|
-
|
|
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);
|
|
96
|
-
await occupyChrome(port + 1);
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
import { mkdtemp, rm,
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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 {
|
|
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(
|
|
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
|
-
{
|
|
127
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
201
|
-
|
|
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', () => {
|