@monoes/monobrowse 1.0.11 → 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/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 +5 -5
- 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 +44 -14
- 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 +1 -1
- 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 +11 -6
- 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 +60 -21
- 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
|
@@ -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,11 +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 { join } from 'path';
|
|
18
|
-
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';
|
|
19
20
|
|
|
20
21
|
let lastSocket: FakeWs | null = null;
|
|
21
22
|
|
|
@@ -73,7 +74,11 @@ afterEach(async () => {
|
|
|
73
74
|
async function writePersistedPort(port: number, pid: number, savedAt: number): Promise<void> {
|
|
74
75
|
const dir = join(tempDir, '.monomind', 'monobrowse');
|
|
75
76
|
await mkdir(dir, { recursive: true });
|
|
76
|
-
await writeFile(
|
|
77
|
+
await writeFile(
|
|
78
|
+
join(dir, 'active-port.json'),
|
|
79
|
+
JSON.stringify({ port, pid, launched: true, savedAt }),
|
|
80
|
+
'utf-8',
|
|
81
|
+
);
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
/**
|
|
@@ -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', () => {
|
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
* browser at all: they filter the in-memory ref Map. findBySelector and
|
|
6
6
|
* findByTestId do issue CDP commands, which a recording stub answers.
|
|
7
7
|
*/
|
|
8
|
-
import { describe,
|
|
8
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
9
9
|
import type { CdpClient } from '../browser/cdp.js';
|
|
10
|
-
import type { ElementRef } from '../browser/types.js';
|
|
11
10
|
import {
|
|
12
|
-
findBySelector,
|
|
13
|
-
findByRole,
|
|
14
|
-
findByText,
|
|
15
11
|
findByLabel,
|
|
16
12
|
findByPlaceholder,
|
|
13
|
+
findByRole,
|
|
14
|
+
findBySelector,
|
|
17
15
|
findByTestId,
|
|
16
|
+
findByText,
|
|
17
|
+
isChecked,
|
|
18
18
|
isEnabled,
|
|
19
19
|
isVisible,
|
|
20
|
-
isChecked,
|
|
21
20
|
scrollIntoView,
|
|
22
21
|
} from '../browser/find.js';
|
|
22
|
+
import type { ElementRef } from '../browser/types.js';
|
|
23
23
|
|
|
24
24
|
function ref(partial: Partial<ElementRef> & { ref: string }): ElementRef {
|
|
25
25
|
return { role: 'generic', name: '', nodeId: 1, ...partial };
|
|
@@ -31,8 +31,21 @@ function sampleRefs(): Map<string, ElementRef> {
|
|
|
31
31
|
ref({ ref: 'e1', role: 'button', name: 'Save changes', backendDOMNodeId: 11 }),
|
|
32
32
|
ref({ ref: 'e2', role: 'BUTTON', name: 'Save', backendDOMNodeId: 12 }),
|
|
33
33
|
ref({ ref: 'e3', role: 'link', name: 'Save as draft', backendDOMNodeId: 13 }),
|
|
34
|
-
ref({
|
|
35
|
-
|
|
34
|
+
ref({
|
|
35
|
+
ref: 'e4',
|
|
36
|
+
role: 'textbox',
|
|
37
|
+
name: 'Email',
|
|
38
|
+
placeholder: 'you@example.com',
|
|
39
|
+
backendDOMNodeId: 14,
|
|
40
|
+
}),
|
|
41
|
+
ref({
|
|
42
|
+
ref: 'e5',
|
|
43
|
+
role: 'textbox',
|
|
44
|
+
name: 'Notes',
|
|
45
|
+
placeholder: 'Optional notes',
|
|
46
|
+
backendDOMNodeId: 15,
|
|
47
|
+
disabled: true,
|
|
48
|
+
}),
|
|
36
49
|
];
|
|
37
50
|
return new Map(entries.map((e) => [e.ref, e]));
|
|
38
51
|
}
|
|
@@ -84,8 +97,12 @@ describe('findByRole', () => {
|
|
|
84
97
|
|
|
85
98
|
it('requires a full name match under exact', async () => {
|
|
86
99
|
const { client } = stubClient();
|
|
87
|
-
expect(
|
|
88
|
-
|
|
100
|
+
expect(
|
|
101
|
+
(await findByRole(client, 'S1', sampleRefs(), 'button', { name: 'Save', exact: true }))?.ref,
|
|
102
|
+
).toBe('e2');
|
|
103
|
+
expect(
|
|
104
|
+
await findByRole(client, 'S1', sampleRefs(), 'button', { name: 'Sav', exact: true }),
|
|
105
|
+
).toBeNull();
|
|
89
106
|
});
|
|
90
107
|
|
|
91
108
|
it('nth is 1-based over the filtered matches', async () => {
|
|
@@ -98,7 +115,9 @@ describe('findByRole', () => {
|
|
|
98
115
|
|
|
99
116
|
it('last returns the final match', async () => {
|
|
100
117
|
const { client } = stubClient();
|
|
101
|
-
expect((await findByRole(client, 'S1', sampleRefs(), 'button', { last: true }))?.ref).toBe(
|
|
118
|
+
expect((await findByRole(client, 'S1', sampleRefs(), 'button', { last: true }))?.ref).toBe(
|
|
119
|
+
'e2',
|
|
120
|
+
);
|
|
102
121
|
});
|
|
103
122
|
|
|
104
123
|
it('returns null when no ref carries that role', async () => {
|
|
@@ -141,7 +160,9 @@ describe('findByText', () => {
|
|
|
141
160
|
it('findByLabel delegates to the same matching', async () => {
|
|
142
161
|
const { client } = stubClient();
|
|
143
162
|
const refs = sampleRefs();
|
|
144
|
-
expect(await findByLabel(client, 'S1', refs, 'Email')).toBe(
|
|
163
|
+
expect(await findByLabel(client, 'S1', refs, 'Email')).toBe(
|
|
164
|
+
await findByText(client, 'S1', refs, 'Email'),
|
|
165
|
+
);
|
|
145
166
|
});
|
|
146
167
|
});
|
|
147
168
|
|
|
@@ -154,14 +175,18 @@ describe('findByPlaceholder', () => {
|
|
|
154
175
|
it('treats a missing placeholder as empty rather than crashing', async () => {
|
|
155
176
|
const { client } = stubClient();
|
|
156
177
|
// e1..e3 have no placeholder; an empty needle must not match them under exact.
|
|
157
|
-
expect((await findByPlaceholder(client, 'S1', sampleRefs(), '', { exact: true }))?.ref).toBe(
|
|
178
|
+
expect((await findByPlaceholder(client, 'S1', sampleRefs(), '', { exact: true }))?.ref).toBe(
|
|
179
|
+
'e1',
|
|
180
|
+
);
|
|
158
181
|
expect(await findByPlaceholder(client, 'S1', sampleRefs(), 'nope')).toBeNull();
|
|
159
182
|
});
|
|
160
183
|
|
|
161
184
|
it('honours exact, nth and last', async () => {
|
|
162
185
|
const { client } = stubClient();
|
|
163
186
|
const refs = sampleRefs();
|
|
164
|
-
expect(
|
|
187
|
+
expect(
|
|
188
|
+
(await findByPlaceholder(client, 'S1', refs, 'Optional notes', { exact: true }))?.ref,
|
|
189
|
+
).toBe('e5');
|
|
165
190
|
expect((await findByPlaceholder(client, 'S1', refs, 'o', { nth: 1 }))?.ref).toBe('e4');
|
|
166
191
|
expect((await findByPlaceholder(client, 'S1', refs, 'o', { last: true }))?.ref).toBe('e5');
|
|
167
192
|
});
|
|
@@ -190,7 +215,12 @@ describe('findBySelector', () => {
|
|
|
190
215
|
});
|
|
191
216
|
const refs = sampleRefs();
|
|
192
217
|
const found = await findBySelector(client, 'S1', refs, 'div.hidden');
|
|
193
|
-
expect(found).toMatchObject({
|
|
218
|
+
expect(found).toMatchObject({
|
|
219
|
+
ref: 'sel-999',
|
|
220
|
+
role: 'generic',
|
|
221
|
+
name: 'div.hidden',
|
|
222
|
+
nodeId: 55,
|
|
223
|
+
});
|
|
194
224
|
expect(refs.get('sel-999')).toBe(found);
|
|
195
225
|
});
|
|
196
226
|
|
|
@@ -242,7 +272,9 @@ describe('findBySelector', () => {
|
|
|
242
272
|
'DOM.getDocument': doc,
|
|
243
273
|
'DOM.querySelector': new Error("SyntaxError: '((' is not a valid selector"),
|
|
244
274
|
});
|
|
245
|
-
await expect(findBySelector(client, 'S1', sampleRefs(), '((')).rejects.toThrow(
|
|
275
|
+
await expect(findBySelector(client, 'S1', sampleRefs(), '((')).rejects.toThrow(
|
|
276
|
+
/not a valid selector/,
|
|
277
|
+
);
|
|
246
278
|
});
|
|
247
279
|
|
|
248
280
|
it('swallows transient CDP errors and reports not-found', async () => {
|
|
@@ -282,7 +314,9 @@ describe('findByTestId', () => {
|
|
|
282
314
|
});
|
|
283
315
|
|
|
284
316
|
it('returns null when no attribute form matches', async () => {
|
|
285
|
-
const client = {
|
|
317
|
+
const client = {
|
|
318
|
+
send: vi.fn(async () => ({ result: { value: false } })),
|
|
319
|
+
} as unknown as CdpClient;
|
|
286
320
|
await expect(findByTestId(client, 'S1', 'nope')).resolves.toBeNull();
|
|
287
321
|
});
|
|
288
322
|
|
|
@@ -330,7 +364,9 @@ describe('element state helpers', () => {
|
|
|
330
364
|
'DOM.resolveNode': { object: { objectId: 'OBJ1' } },
|
|
331
365
|
'Runtime.callFunctionOn': { result: { value: true } },
|
|
332
366
|
});
|
|
333
|
-
await expect(isVisible(client, 'S1', ref({ ref: 'e1', backendDOMNodeId: 11 }))).resolves.toBe(
|
|
367
|
+
await expect(isVisible(client, 'S1', ref({ ref: 'e1', backendDOMNodeId: 11 }))).resolves.toBe(
|
|
368
|
+
true,
|
|
369
|
+
);
|
|
334
370
|
expect(calls.find((c) => c.method === 'Runtime.callFunctionOn')!.params).toMatchObject({
|
|
335
371
|
objectId: 'OBJ1',
|
|
336
372
|
returnByValue: true,
|
|
@@ -342,13 +378,15 @@ describe('element state helpers', () => {
|
|
|
342
378
|
'DOM.resolveNode': { object: { objectId: 'OBJ1' } },
|
|
343
379
|
'Runtime.callFunctionOn': { result: {} },
|
|
344
380
|
});
|
|
345
|
-
await expect(isChecked(client, 'S1', ref({ ref: 'e1', backendDOMNodeId: 11 }))).resolves.toBe(
|
|
381
|
+
await expect(isChecked(client, 'S1', ref({ ref: 'e1', backendDOMNodeId: 11 }))).resolves.toBe(
|
|
382
|
+
false,
|
|
383
|
+
);
|
|
346
384
|
});
|
|
347
385
|
|
|
348
386
|
it('scrollIntoView throws a ref-named error when the node is gone', async () => {
|
|
349
387
|
const { client } = stubClient({ 'DOM.resolveNode': { object: {} } });
|
|
350
388
|
await expect(
|
|
351
|
-
scrollIntoView(client, 'S1', ref({ ref: 'e1', backendDOMNodeId: 11 }))
|
|
389
|
+
scrollIntoView(client, 'S1', ref({ ref: 'e1', backendDOMNodeId: 11 })),
|
|
352
390
|
).rejects.toThrow('Cannot scroll: ref @e1 not found in DOM');
|
|
353
391
|
});
|
|
354
392
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* frame, and `frame main` must detach the frame session and restore the
|
|
5
5
|
* page session. The browser engine is mocked; no real Chrome.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
8
|
import type { Command, CommandContext } from '../cli/types.js';
|
|
9
9
|
|
|
10
10
|
const mocks = vi.hoisted(() => {
|
|
@@ -22,7 +22,12 @@ const mocks = vi.hoisted(() => {
|
|
|
22
22
|
})),
|
|
23
23
|
loadActivePortInfo: vi.fn(async () => null),
|
|
24
24
|
loadRefCache: vi.fn(async () => null),
|
|
25
|
-
switchToFrame: vi.fn(
|
|
25
|
+
switchToFrame: vi.fn(
|
|
26
|
+
async (): Promise<{ url: string | null; sessionId: string | null }> => ({
|
|
27
|
+
url: 'https://f.test/frame',
|
|
28
|
+
sessionId: 'S-FRAME',
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
26
31
|
enableSessionDomains: vi.fn(async () => {}),
|
|
27
32
|
teardownRouteInterception: vi.fn(),
|
|
28
33
|
stopRequestCapture: vi.fn(),
|
|
@@ -52,7 +57,10 @@ beforeEach(() => {
|
|
|
52
57
|
vi.clearAllMocks();
|
|
53
58
|
mocks.browser.loadActivePortInfo.mockResolvedValue(null);
|
|
54
59
|
mocks.browser.loadRefCache.mockResolvedValue(null);
|
|
55
|
-
mocks.browser.switchToFrame.mockResolvedValue({
|
|
60
|
+
mocks.browser.switchToFrame.mockResolvedValue({
|
|
61
|
+
url: 'https://f.test/frame',
|
|
62
|
+
sessionId: 'S-FRAME',
|
|
63
|
+
});
|
|
56
64
|
mocks.browser.connectToTarget.mockResolvedValue({
|
|
57
65
|
client: mocks.client,
|
|
58
66
|
sessionId: 'S-PAGE',
|
|
@@ -72,9 +80,7 @@ describe('browse frame command (#97)', () => {
|
|
|
72
80
|
|
|
73
81
|
// Back to main — must detach the FRAME session (proves _sessionId was updated)
|
|
74
82
|
await frame.action!(ctx(['main']));
|
|
75
|
-
const detach = mocks.send.mock.calls.find(
|
|
76
|
-
([m]) => m === 'Target.detachFromTarget'
|
|
77
|
-
);
|
|
83
|
+
const detach = mocks.send.mock.calls.find(([m]) => m === 'Target.detachFromTarget');
|
|
78
84
|
expect(detach).toBeDefined();
|
|
79
85
|
expect(detach![1]).toEqual({ sessionId: 'S-FRAME' });
|
|
80
86
|
|