@monoes/monobrowse 1.0.5 → 1.0.7
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 +68 -0
- package/dist/src/__tests__/adapters.test.d.ts +2 -0
- package/dist/src/__tests__/adapters.test.d.ts.map +1 -0
- package/dist/src/__tests__/adapters.test.js +104 -0
- package/dist/src/__tests__/adapters.test.js.map +1 -0
- package/dist/src/__tests__/batch-eval-parsing.test.d.ts +0 -0
- package/dist/src/__tests__/batch-eval-parsing.test.d.ts.map +0 -0
- package/dist/src/__tests__/batch-eval-parsing.test.js +0 -0
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +0 -0
- package/dist/src/__tests__/box-model.test.d.ts +2 -0
- package/dist/src/__tests__/box-model.test.d.ts.map +1 -0
- package/dist/src/__tests__/box-model.test.js +36 -0
- package/dist/src/__tests__/box-model.test.js.map +1 -0
- package/dist/src/__tests__/browser-launch.test.d.ts +0 -0
- package/dist/src/__tests__/browser-launch.test.d.ts.map +0 -0
- package/dist/src/__tests__/browser-launch.test.js +0 -0
- package/dist/src/__tests__/browser-launch.test.js.map +0 -0
- package/dist/src/__tests__/cdp-client.test.d.ts +2 -0
- package/dist/src/__tests__/cdp-client.test.d.ts.map +1 -0
- package/dist/src/__tests__/cdp-client.test.js +326 -0
- package/dist/src/__tests__/cdp-client.test.js.map +1 -0
- package/dist/src/__tests__/close-browser.test.d.ts +2 -0
- package/dist/src/__tests__/close-browser.test.d.ts.map +1 -0
- package/dist/src/__tests__/close-browser.test.js +128 -0
- package/dist/src/__tests__/close-browser.test.js.map +1 -0
- package/dist/src/__tests__/console-log.test.d.ts +2 -0
- package/dist/src/__tests__/console-log.test.d.ts.map +1 -0
- package/dist/src/__tests__/console-log.test.js +195 -0
- package/dist/src/__tests__/console-log.test.js.map +1 -0
- package/dist/src/__tests__/find.test.d.ts +2 -0
- package/dist/src/__tests__/find.test.d.ts.map +1 -0
- package/dist/src/__tests__/find.test.js +299 -0
- package/dist/src/__tests__/find.test.js.map +1 -0
- package/dist/src/__tests__/frame-command.test.d.ts +2 -0
- package/dist/src/__tests__/frame-command.test.d.ts.map +1 -0
- package/dist/src/__tests__/frame-command.test.js +85 -0
- package/dist/src/__tests__/frame-command.test.js.map +1 -0
- package/dist/src/__tests__/output.test.d.ts +0 -0
- package/dist/src/__tests__/output.test.d.ts.map +0 -0
- package/dist/src/__tests__/output.test.js +0 -0
- package/dist/src/__tests__/output.test.js.map +0 -0
- package/dist/src/__tests__/ref-cache.test.d.ts +0 -0
- package/dist/src/__tests__/ref-cache.test.d.ts.map +0 -0
- package/dist/src/__tests__/ref-cache.test.js +43 -2
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/session-state.test.d.ts +2 -0
- package/dist/src/__tests__/session-state.test.d.ts.map +1 -0
- package/dist/src/__tests__/session-state.test.js +203 -0
- package/dist/src/__tests__/session-state.test.js.map +1 -0
- package/dist/src/__tests__/snapshot.test.d.ts +2 -0
- package/dist/src/__tests__/snapshot.test.d.ts.map +1 -0
- package/dist/src/__tests__/snapshot.test.js +262 -0
- package/dist/src/__tests__/snapshot.test.js.map +1 -0
- package/dist/src/__tests__/storage-eval.test.d.ts +2 -0
- package/dist/src/__tests__/storage-eval.test.d.ts.map +1 -0
- package/dist/src/__tests__/storage-eval.test.js +133 -0
- package/dist/src/__tests__/storage-eval.test.js.map +1 -0
- package/dist/src/__tests__/tabs.test.d.ts +2 -0
- package/dist/src/__tests__/tabs.test.d.ts.map +1 -0
- package/dist/src/__tests__/tabs.test.js +161 -0
- package/dist/src/__tests__/tabs.test.js.map +1 -0
- package/dist/src/browser/action-builder/analyzer.d.ts +0 -0
- package/dist/src/browser/action-builder/analyzer.d.ts.map +0 -0
- package/dist/src/browser/action-builder/analyzer.js +0 -0
- package/dist/src/browser/action-builder/analyzer.js.map +0 -0
- package/dist/src/browser/action-builder/types.d.ts +0 -0
- package/dist/src/browser/action-builder/types.d.ts.map +0 -0
- package/dist/src/browser/action-builder/types.js +0 -0
- package/dist/src/browser/action-builder/types.js.map +0 -0
- package/dist/src/browser/actions.d.ts +0 -0
- package/dist/src/browser/actions.d.ts.map +0 -0
- package/dist/src/browser/actions.js +0 -0
- package/dist/src/browser/actions.js.map +0 -0
- package/dist/src/browser/adapters/gemini.d.ts +0 -0
- package/dist/src/browser/adapters/gemini.d.ts.map +0 -0
- package/dist/src/browser/adapters/gemini.js +0 -0
- package/dist/src/browser/adapters/gemini.js.map +0 -0
- package/dist/src/browser/adapters/google.d.ts +0 -0
- package/dist/src/browser/adapters/google.d.ts.map +0 -0
- package/dist/src/browser/adapters/google.js +0 -0
- package/dist/src/browser/adapters/google.js.map +0 -0
- package/dist/src/browser/adapters/index.d.ts +0 -0
- package/dist/src/browser/adapters/index.d.ts.map +0 -0
- package/dist/src/browser/adapters/index.js +0 -0
- package/dist/src/browser/adapters/index.js.map +0 -0
- package/dist/src/browser/adapters/instagram.d.ts +0 -0
- package/dist/src/browser/adapters/instagram.d.ts.map +0 -0
- package/dist/src/browser/adapters/instagram.js +0 -0
- package/dist/src/browser/adapters/instagram.js.map +0 -0
- package/dist/src/browser/adapters/linkedin.d.ts +0 -0
- package/dist/src/browser/adapters/linkedin.d.ts.map +0 -0
- package/dist/src/browser/adapters/linkedin.js +0 -0
- package/dist/src/browser/adapters/linkedin.js.map +0 -0
- package/dist/src/browser/adapters/microsoft.d.ts +0 -0
- package/dist/src/browser/adapters/microsoft.d.ts.map +0 -0
- package/dist/src/browser/adapters/microsoft.js +0 -0
- package/dist/src/browser/adapters/microsoft.js.map +0 -0
- package/dist/src/browser/adapters/x.d.ts +0 -0
- package/dist/src/browser/adapters/x.d.ts.map +0 -0
- package/dist/src/browser/adapters/x.js +0 -0
- package/dist/src/browser/adapters/x.js.map +0 -0
- package/dist/src/browser/batch.d.ts +0 -0
- package/dist/src/browser/batch.d.ts.map +0 -0
- package/dist/src/browser/batch.js +0 -0
- package/dist/src/browser/batch.js.map +0 -0
- package/dist/src/browser/browser.d.ts +4 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +55 -7
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts +4 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +45 -16
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts +0 -0
- package/dist/src/browser/console-log.d.ts.map +0 -0
- package/dist/src/browser/console-log.js +0 -0
- package/dist/src/browser/console-log.js.map +0 -0
- package/dist/src/browser/dashboard/api-types.d.ts +0 -0
- package/dist/src/browser/dashboard/api-types.d.ts.map +0 -0
- package/dist/src/browser/dashboard/api-types.js +0 -0
- package/dist/src/browser/dashboard/api-types.js.map +0 -0
- package/dist/src/browser/dashboard/server.d.ts +0 -0
- package/dist/src/browser/dashboard/server.d.ts.map +0 -0
- package/dist/src/browser/dashboard/server.js +0 -0
- package/dist/src/browser/dashboard/server.js.map +0 -0
- package/dist/src/browser/dialog.d.ts +0 -0
- package/dist/src/browser/dialog.d.ts.map +0 -0
- package/dist/src/browser/dialog.js +0 -0
- package/dist/src/browser/dialog.js.map +0 -0
- package/dist/src/browser/emulation.d.ts +0 -0
- package/dist/src/browser/emulation.d.ts.map +0 -0
- package/dist/src/browser/emulation.js +0 -0
- package/dist/src/browser/emulation.js.map +0 -0
- package/dist/src/browser/find.d.ts +0 -0
- package/dist/src/browser/find.d.ts.map +0 -0
- package/dist/src/browser/find.js +0 -0
- package/dist/src/browser/find.js.map +0 -0
- package/dist/src/browser/har.d.ts +0 -0
- package/dist/src/browser/har.d.ts.map +0 -0
- package/dist/src/browser/har.js +0 -0
- package/dist/src/browser/har.js.map +0 -0
- package/dist/src/browser/index.d.ts +0 -0
- package/dist/src/browser/index.d.ts.map +0 -0
- package/dist/src/browser/index.js +0 -0
- package/dist/src/browser/index.js.map +0 -0
- package/dist/src/browser/network.d.ts +0 -0
- package/dist/src/browser/network.d.ts.map +0 -0
- package/dist/src/browser/network.js +0 -0
- package/dist/src/browser/network.js.map +0 -0
- package/dist/src/browser/pdf.d.ts +0 -0
- package/dist/src/browser/pdf.d.ts.map +0 -0
- package/dist/src/browser/pdf.js +0 -0
- package/dist/src/browser/pdf.js.map +0 -0
- package/dist/src/browser/profiler.d.ts +0 -0
- package/dist/src/browser/profiler.d.ts.map +0 -0
- package/dist/src/browser/profiler.js +0 -0
- package/dist/src/browser/profiler.js.map +0 -0
- package/dist/src/browser/record.d.ts +0 -0
- package/dist/src/browser/record.d.ts.map +0 -0
- package/dist/src/browser/record.js +0 -0
- package/dist/src/browser/record.js.map +0 -0
- package/dist/src/browser/ref-cache.d.ts +6 -1
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +21 -3
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts +0 -0
- package/dist/src/browser/screenshot.d.ts.map +0 -0
- package/dist/src/browser/screenshot.js +0 -0
- package/dist/src/browser/screenshot.js.map +0 -0
- package/dist/src/browser/session.d.ts +0 -0
- package/dist/src/browser/session.d.ts.map +0 -0
- package/dist/src/browser/session.js +0 -0
- package/dist/src/browser/session.js.map +0 -0
- package/dist/src/browser/snapshot.d.ts +11 -0
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +11 -0
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts +0 -0
- package/dist/src/browser/storage.d.ts.map +0 -0
- package/dist/src/browser/storage.js +0 -0
- package/dist/src/browser/storage.js.map +0 -0
- package/dist/src/browser/tabs.d.ts +5 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +4 -3
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts +0 -0
- package/dist/src/browser/trace.d.ts.map +0 -0
- package/dist/src/browser/trace.js +0 -0
- package/dist/src/browser/trace.js.map +0 -0
- package/dist/src/browser/types.d.ts +4 -0
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +0 -0
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts +0 -0
- package/dist/src/browser/vitals.d.ts.map +0 -0
- package/dist/src/browser/vitals.js +0 -0
- package/dist/src/browser/vitals.js.map +0 -0
- package/dist/src/browser/wait.d.ts +0 -0
- package/dist/src/browser/wait.d.ts.map +0 -0
- package/dist/src/browser/wait.js +0 -0
- package/dist/src/browser/wait.js.map +0 -0
- package/dist/src/cli/action.d.ts +0 -0
- package/dist/src/cli/action.d.ts.map +0 -0
- package/dist/src/cli/action.js +0 -0
- package/dist/src/cli/action.js.map +0 -0
- package/dist/src/cli/commands.d.ts +21 -0
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +123 -7
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.d.ts +0 -0
- package/dist/src/cli/output.d.ts.map +0 -0
- package/dist/src/cli/output.js +0 -0
- package/dist/src/cli/output.js.map +0 -0
- package/dist/src/cli/platform.d.ts +0 -0
- package/dist/src/cli/platform.d.ts.map +0 -0
- package/dist/src/cli/platform.js +0 -0
- package/dist/src/cli/platform.js.map +0 -0
- package/dist/src/cli/types.d.ts +0 -0
- package/dist/src/cli/types.d.ts.map +0 -0
- package/dist/src/cli/types.js +0 -0
- package/dist/src/cli/types.js.map +0 -0
- package/dist/src/cli.d.ts +0 -0
- package/dist/src/cli.d.ts.map +0 -0
- package/dist/src/cli.js +0 -0
- package/dist/src/cli.js.map +0 -0
- package/dist/src/index.d.ts +0 -0
- package/dist/src/index.d.ts.map +0 -0
- package/dist/src/index.js +0 -0
- package/dist/src/index.js.map +0 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -5
- package/src/__tests__/adapters.test.ts +125 -0
- package/src/__tests__/box-model.test.ts +39 -0
- package/src/__tests__/cdp-client.test.ts +375 -0
- package/src/__tests__/close-browser.test.ts +141 -0
- package/src/__tests__/console-log.test.ts +250 -0
- package/src/__tests__/find.test.ts +354 -0
- package/src/__tests__/frame-command.test.ts +98 -0
- package/src/__tests__/ref-cache.test.ts +47 -2
- package/src/__tests__/session-state.test.ts +251 -0
- package/src/__tests__/snapshot.test.ts +331 -0
- package/src/__tests__/storage-eval.test.ts +171 -0
- package/src/__tests__/tabs.test.ts +183 -0
- package/src/browser/browser.ts +59 -7
- package/src/browser/cdp.ts +52 -15
- package/src/browser/ref-cache.ts +29 -6
- package/src/browser/snapshot.ts +11 -0
- package/src/browser/tabs.ts +12 -4
- package/src/browser/types.ts +4 -0
- package/src/cli/commands.ts +119 -7
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* closeBrowser()'s cross-process PID-kill fallback (browser.ts) — previously
|
|
3
|
+
* had zero test coverage anywhere in this package (found in a review pass
|
|
4
|
+
* after commit c4847e22). Covers:
|
|
5
|
+
* - using a persisted PID when the in-process launchedPids map is empty
|
|
6
|
+
* (the always-true case for a fresh CLI invocation)
|
|
7
|
+
* - rejecting a persisted PID that is too old to trust (staleness bound)
|
|
8
|
+
* - a liveness check (process.kill(pid, 0)) gating the SIGKILL itself
|
|
9
|
+
*
|
|
10
|
+
* No real Chrome or child process is spawned: Browser.close is driven via a
|
|
11
|
+
* fake `ws` socket (same pattern as cdp-client.test.ts), and process.kill is
|
|
12
|
+
* spied on rather than actually signaling anything.
|
|
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';
|
|
18
|
+
|
|
19
|
+
let lastSocket: FakeWs | null = null;
|
|
20
|
+
|
|
21
|
+
class FakeWs {
|
|
22
|
+
handlers = new Map<string, Array<(...a: unknown[]) => void>>();
|
|
23
|
+
constructor(public url: string) {
|
|
24
|
+
lastSocket = this;
|
|
25
|
+
}
|
|
26
|
+
on(event: string, fn: (...a: unknown[]) => void): void {
|
|
27
|
+
if (!this.handlers.has(event)) this.handlers.set(event, []);
|
|
28
|
+
this.handlers.get(event)!.push(fn);
|
|
29
|
+
}
|
|
30
|
+
emit(event: string, ...args: unknown[]): void {
|
|
31
|
+
for (const fn of [...(this.handlers.get(event) ?? [])]) fn(...args);
|
|
32
|
+
}
|
|
33
|
+
send(_data: string, cb?: (err?: Error) => void): void {
|
|
34
|
+
// Never call cb — simulates a hung Browser.close so closeBrowser's own
|
|
35
|
+
// BROWSER_CLOSE_TIMEOUT_MS race is what settles it (ungraceful path).
|
|
36
|
+
void cb;
|
|
37
|
+
}
|
|
38
|
+
close(): void {}
|
|
39
|
+
deliver(msg: unknown): void {
|
|
40
|
+
this.emit('message', Buffer.from(JSON.stringify(msg)));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
vi.mock('ws', () => ({ WebSocket: FakeWs }));
|
|
45
|
+
|
|
46
|
+
let tempDir: string;
|
|
47
|
+
let killSpy: ReturnType<typeof vi.spyOn>;
|
|
48
|
+
|
|
49
|
+
beforeEach(async () => {
|
|
50
|
+
vi.useFakeTimers();
|
|
51
|
+
lastSocket = null;
|
|
52
|
+
tempDir = await mkdtemp(join(tmpdir(), 'monobrowse-close-test-'));
|
|
53
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
54
|
+
killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true as never);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
vi.useRealTimers();
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
async function writePersistedPort(port: number, pid: number, savedAt: number): Promise<void> {
|
|
64
|
+
const dir = join(tempDir, '.monomind', 'monobrowse');
|
|
65
|
+
await mkdir(dir, { recursive: true });
|
|
66
|
+
await writeFile(join(dir, 'active-port.json'), JSON.stringify({ port, pid, launched: true, savedAt }), 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function connectedClient() {
|
|
70
|
+
vi.resetModules();
|
|
71
|
+
const { CdpClient } = await import('../browser/cdp.js');
|
|
72
|
+
const client = new CdpClient();
|
|
73
|
+
const p = client.connect('ws://127.0.0.1:9333/devtools/page/ABC');
|
|
74
|
+
lastSocket!.emit('open');
|
|
75
|
+
await p;
|
|
76
|
+
return client;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('#115 review follow-up: closeBrowser() cross-process PID-kill fallback', () => {
|
|
80
|
+
it('uses a fresh persisted PID (in-process map empty) and SIGKILLs it on an ungraceful close', async () => {
|
|
81
|
+
vi.resetModules();
|
|
82
|
+
const { closeBrowser, getLaunchedPid } = await import('../browser/browser.js');
|
|
83
|
+
const port = 9333;
|
|
84
|
+
await writePersistedPort(port, 54321, Date.now());
|
|
85
|
+
// Confirm the in-process map really is empty for this port (fresh module state).
|
|
86
|
+
expect(getLaunchedPid(port)).toBeUndefined();
|
|
87
|
+
|
|
88
|
+
const client = await connectedClient();
|
|
89
|
+
const closePromise = closeBrowser(client, port);
|
|
90
|
+
await vi.advanceTimersByTimeAsync(3000); // BROWSER_CLOSE_TIMEOUT_MS
|
|
91
|
+
await closePromise;
|
|
92
|
+
|
|
93
|
+
expect(killSpy).toHaveBeenCalledWith(54321, 0);
|
|
94
|
+
expect(killSpy).toHaveBeenCalledWith(54321, 'SIGKILL');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('#124-review: does not trust a persisted PID older than the staleness bound', async () => {
|
|
98
|
+
vi.resetModules();
|
|
99
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
100
|
+
const port = 9334;
|
|
101
|
+
const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000;
|
|
102
|
+
await writePersistedPort(port, 99999, twoDaysAgo);
|
|
103
|
+
|
|
104
|
+
const client = await connectedClient();
|
|
105
|
+
const closePromise = closeBrowser(client, port);
|
|
106
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
107
|
+
await closePromise;
|
|
108
|
+
|
|
109
|
+
expect(killSpy).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('#124-review: a persisted PID that is no longer alive is not force-killed (liveness check via kill(pid,0))', async () => {
|
|
113
|
+
vi.resetModules();
|
|
114
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
115
|
+
const port = 9335;
|
|
116
|
+
await writePersistedPort(port, 11111, Date.now());
|
|
117
|
+
killSpy.mockImplementation((_pid: unknown, signal?: unknown) => {
|
|
118
|
+
if (signal === 0) throw Object.assign(new Error('ESRCH'), { code: 'ESRCH' });
|
|
119
|
+
return true as never;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const client = await connectedClient();
|
|
123
|
+
const closePromise = closeBrowser(client, port);
|
|
124
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
125
|
+
await closePromise;
|
|
126
|
+
|
|
127
|
+
expect(killSpy).toHaveBeenCalledWith(11111, 0);
|
|
128
|
+
expect(killSpy).not.toHaveBeenCalledWith(11111, 'SIGKILL');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('no persisted port file at all — closeBrowser is a no-op, no kill attempted', async () => {
|
|
132
|
+
vi.resetModules();
|
|
133
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
134
|
+
const client = await connectedClient();
|
|
135
|
+
const closePromise = closeBrowser(client, 9336);
|
|
136
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
137
|
+
await closePromise;
|
|
138
|
+
|
|
139
|
+
expect(killSpy).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* console-log.ts is pure in-process state: three CDP event listeners feeding
|
|
3
|
+
* two per-session Maps. Nothing here touches a browser — we hand it a fake
|
|
4
|
+
* client that just records handlers and lets us fire events by hand.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
7
|
+
import type { CdpClient } from '../browser/cdp.js';
|
|
8
|
+
import {
|
|
9
|
+
setupConsoleCapture,
|
|
10
|
+
getConsoleMessages,
|
|
11
|
+
clearConsoleMessages,
|
|
12
|
+
getPageErrors,
|
|
13
|
+
clearPageErrors,
|
|
14
|
+
teardownConsoleCapture,
|
|
15
|
+
} from '../browser/console-log.js';
|
|
16
|
+
|
|
17
|
+
type Handler = (params: Record<string, unknown>, sessionId?: string) => void;
|
|
18
|
+
|
|
19
|
+
class FakeClient {
|
|
20
|
+
listeners = new Map<string, Set<Handler>>();
|
|
21
|
+
offCalls = 0;
|
|
22
|
+
|
|
23
|
+
on(event: string, fn: Handler): () => void {
|
|
24
|
+
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
|
|
25
|
+
this.listeners.get(event)!.add(fn);
|
|
26
|
+
return () => {
|
|
27
|
+
this.offCalls++;
|
|
28
|
+
this.listeners.get(event)!.delete(fn);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
emit(event: string, params: Record<string, unknown>, sessionId?: string): void {
|
|
33
|
+
for (const fn of [...(this.listeners.get(event) ?? [])]) fn(params, sessionId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get handlerCount(): number {
|
|
37
|
+
return [...this.listeners.values()].reduce((n, s) => n + s.size, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
asCdp(): CdpClient {
|
|
41
|
+
return this as unknown as CdpClient;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let client: FakeClient;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
client = new FakeClient();
|
|
49
|
+
// Wipe cross-test global state in the module's module-level Maps.
|
|
50
|
+
clearConsoleMessages();
|
|
51
|
+
clearPageErrors();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('setupConsoleCapture', () => {
|
|
55
|
+
it('registers exactly the three CDP listeners it needs', () => {
|
|
56
|
+
setupConsoleCapture(client.asCdp(), 'S1');
|
|
57
|
+
expect([...client.listeners.keys()].sort()).toEqual([
|
|
58
|
+
'Log.entryAdded',
|
|
59
|
+
'Runtime.consoleAPICalled',
|
|
60
|
+
'Runtime.exceptionThrown',
|
|
61
|
+
]);
|
|
62
|
+
expect(client.handlerCount).toBe(3);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('detaches the previous generation of listeners when re-run for the same session', () => {
|
|
66
|
+
setupConsoleCapture(client.asCdp(), 'S1');
|
|
67
|
+
setupConsoleCapture(client.asCdp(), 'S1');
|
|
68
|
+
// Three old handlers unsubscribed, three new ones live — not six live.
|
|
69
|
+
expect(client.offCalls).toBe(3);
|
|
70
|
+
expect(client.handlerCount).toBe(3);
|
|
71
|
+
|
|
72
|
+
// And a single emitted event must be recorded once, not twice.
|
|
73
|
+
client.emit('Runtime.consoleAPICalled', { type: 'log', args: [{ value: 'hi' }] }, 'S1');
|
|
74
|
+
expect(getConsoleMessages('S1')).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('resets buffered messages for the session it re-attaches to', () => {
|
|
78
|
+
setupConsoleCapture(client.asCdp(), 'S1');
|
|
79
|
+
client.emit('Runtime.consoleAPICalled', { type: 'log', args: [{ value: 'old' }] }, 'S1');
|
|
80
|
+
expect(getConsoleMessages('S1')).toHaveLength(1);
|
|
81
|
+
|
|
82
|
+
setupConsoleCapture(client.asCdp(), 'S1');
|
|
83
|
+
expect(getConsoleMessages('S1')).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('Runtime.consoleAPICalled capture', () => {
|
|
88
|
+
beforeEach(() => setupConsoleCapture(client.asCdp(), 'S1'));
|
|
89
|
+
|
|
90
|
+
it('joins multiple args with a space, preferring description over value', () => {
|
|
91
|
+
client.emit(
|
|
92
|
+
'Runtime.consoleAPICalled',
|
|
93
|
+
{ type: 'log', args: [{ value: 'count:' }, { value: 42 }, { description: 'Object {a: 1}' }] },
|
|
94
|
+
'S1'
|
|
95
|
+
);
|
|
96
|
+
expect(getConsoleMessages('S1')[0]!.text).toBe('count: 42 Object {a: 1}');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('normalizes CDP\'s "warning" to the "warn" type used by ConsoleMessage', () => {
|
|
100
|
+
client.emit('Runtime.consoleAPICalled', { type: 'warning', args: [{ value: 'careful' }] }, 'S1');
|
|
101
|
+
expect(getConsoleMessages('S1')[0]!.type).toBe('warn');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('passes other CDP types through unchanged', () => {
|
|
105
|
+
client.emit('Runtime.consoleAPICalled', { type: 'error', args: [{ value: 'boom' }] }, 'S1');
|
|
106
|
+
expect(getConsoleMessages('S1')[0]!.type).toBe('error');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('tolerates a missing args array', () => {
|
|
110
|
+
client.emit('Runtime.consoleAPICalled', { type: 'log' }, 'S1');
|
|
111
|
+
expect(getConsoleMessages('S1')[0]!.text).toBe('');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('ignores events addressed to a different session', () => {
|
|
115
|
+
client.emit('Runtime.consoleAPICalled', { type: 'log', args: [{ value: 'other' }] }, 'S2');
|
|
116
|
+
expect(getConsoleMessages('S1')).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('Log.entryAdded capture', () => {
|
|
121
|
+
beforeEach(() => setupConsoleCapture(client.asCdp(), 'S1'));
|
|
122
|
+
|
|
123
|
+
it('records text, url and lineNumber and maps "warning" to "warn"', () => {
|
|
124
|
+
client.emit(
|
|
125
|
+
'Log.entryAdded',
|
|
126
|
+
{ entry: { level: 'warning', text: 'deprecated API', url: 'https://x.test/a.js', lineNumber: 17 } },
|
|
127
|
+
'S1'
|
|
128
|
+
);
|
|
129
|
+
const [msg] = getConsoleMessages('S1');
|
|
130
|
+
expect(msg).toMatchObject({
|
|
131
|
+
type: 'warn',
|
|
132
|
+
text: 'deprecated API',
|
|
133
|
+
url: 'https://x.test/a.js',
|
|
134
|
+
lineNumber: 17,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('defaults text to empty string when the entry omits it', () => {
|
|
139
|
+
client.emit('Log.entryAdded', { entry: { level: 'info' } }, 'S1');
|
|
140
|
+
expect(getConsoleMessages('S1')[0]).toMatchObject({ type: 'info', text: '' });
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('Runtime.exceptionThrown capture', () => {
|
|
145
|
+
beforeEach(() => setupConsoleCapture(client.asCdp(), 'S1'));
|
|
146
|
+
|
|
147
|
+
it('prefers exception.description over the generic text field', () => {
|
|
148
|
+
client.emit(
|
|
149
|
+
'Runtime.exceptionThrown',
|
|
150
|
+
{
|
|
151
|
+
exceptionDetails: {
|
|
152
|
+
text: 'Uncaught',
|
|
153
|
+
exception: { description: 'TypeError: x is not a function' },
|
|
154
|
+
url: 'https://x.test/a.js',
|
|
155
|
+
lineNumber: 3,
|
|
156
|
+
columnNumber: 9,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
'S1'
|
|
160
|
+
);
|
|
161
|
+
expect(getPageErrors('S1')[0]).toMatchObject({
|
|
162
|
+
text: 'TypeError: x is not a function',
|
|
163
|
+
url: 'https://x.test/a.js',
|
|
164
|
+
lineNumber: 3,
|
|
165
|
+
columnNumber: 9,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('falls back to text, then to "Unknown error"', () => {
|
|
170
|
+
client.emit('Runtime.exceptionThrown', { exceptionDetails: { text: 'Uncaught (in promise)' } }, 'S1');
|
|
171
|
+
client.emit('Runtime.exceptionThrown', { exceptionDetails: {} }, 'S1');
|
|
172
|
+
expect(getPageErrors('S1').map((e) => e.text)).toEqual([
|
|
173
|
+
'Uncaught (in promise)',
|
|
174
|
+
'Unknown error',
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('keeps page errors out of the console message buffer', () => {
|
|
179
|
+
client.emit('Runtime.exceptionThrown', { exceptionDetails: { text: 'nope' } }, 'S1');
|
|
180
|
+
expect(getConsoleMessages('S1')).toEqual([]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('multi-session isolation and clearing', () => {
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
setupConsoleCapture(client.asCdp(), 'S1');
|
|
187
|
+
setupConsoleCapture(client.asCdp(), 'S2');
|
|
188
|
+
client.emit('Runtime.consoleAPICalled', { type: 'log', args: [{ value: 'one' }] }, 'S1');
|
|
189
|
+
client.emit('Runtime.consoleAPICalled', { type: 'log', args: [{ value: 'two' }] }, 'S2');
|
|
190
|
+
client.emit('Runtime.exceptionThrown', { exceptionDetails: { text: 'e1' } }, 'S1');
|
|
191
|
+
client.emit('Runtime.exceptionThrown', { exceptionDetails: { text: 'e2' } }, 'S2');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('keeps each session\'s buffers separate', () => {
|
|
195
|
+
expect(getConsoleMessages('S1').map((m) => m.text)).toEqual(['one']);
|
|
196
|
+
expect(getConsoleMessages('S2').map((m) => m.text)).toEqual(['two']);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
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']);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('returns a copy, so mutating the result does not corrupt the buffer', () => {
|
|
205
|
+
const snapshot = getConsoleMessages('S1');
|
|
206
|
+
snapshot.push({ type: 'log', text: 'injected', timestamp: 0 });
|
|
207
|
+
expect(getConsoleMessages('S1')).toHaveLength(1);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('clears only the named session', () => {
|
|
211
|
+
clearConsoleMessages('S1');
|
|
212
|
+
clearPageErrors('S1');
|
|
213
|
+
expect(getConsoleMessages('S1')).toEqual([]);
|
|
214
|
+
expect(getPageErrors('S1')).toEqual([]);
|
|
215
|
+
expect(getConsoleMessages('S2')).toHaveLength(1);
|
|
216
|
+
expect(getPageErrors('S2')).toHaveLength(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('clears every session when called with no id', () => {
|
|
220
|
+
clearConsoleMessages();
|
|
221
|
+
clearPageErrors();
|
|
222
|
+
expect(getConsoleMessages()).toEqual([]);
|
|
223
|
+
expect(getPageErrors()).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('a cleared session keeps capturing new events', () => {
|
|
227
|
+
clearConsoleMessages('S1');
|
|
228
|
+
client.emit('Runtime.consoleAPICalled', { type: 'log', args: [{ value: 'again' }] }, 'S1');
|
|
229
|
+
expect(getConsoleMessages('S1').map((m) => m.text)).toEqual(['again']);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('teardownConsoleCapture', () => {
|
|
234
|
+
it('unsubscribes the listeners and drops both buffers for that session only', () => {
|
|
235
|
+
setupConsoleCapture(client.asCdp(), 'S1');
|
|
236
|
+
setupConsoleCapture(client.asCdp(), 'S2');
|
|
237
|
+
client.emit('Runtime.consoleAPICalled', { type: 'log', args: [{ value: 'two' }] }, 'S2');
|
|
238
|
+
|
|
239
|
+
teardownConsoleCapture('S1');
|
|
240
|
+
|
|
241
|
+
expect(client.offCalls).toBe(3);
|
|
242
|
+
client.emit('Runtime.consoleAPICalled', { type: 'log', args: [{ value: 'late' }] }, 'S1');
|
|
243
|
+
expect(getConsoleMessages('S1')).toEqual([]);
|
|
244
|
+
expect(getConsoleMessages('S2')).toHaveLength(1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('is a no-op for an unknown session', () => {
|
|
248
|
+
expect(() => teardownConsoleCapture('never-registered')).not.toThrow();
|
|
249
|
+
});
|
|
250
|
+
});
|