@monoes/monobrowse 1.0.6 → 1.0.8
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/LICENSE +191 -0
- 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 +0 -0
- package/dist/src/__tests__/box-model.test.d.ts.map +0 -0
- package/dist/src/__tests__/box-model.test.js +0 -0
- package/dist/src/__tests__/box-model.test.js.map +0 -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 +188 -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 +13 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +99 -16
- 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/dashboard/ui.html +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 +0 -0
- package/dist/src/browser/snapshot.d.ts.map +0 -0
- package/dist/src/browser/snapshot.js +0 -0
- package/dist/src/browser/snapshot.js.map +0 -0
- 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 +0 -0
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +96 -4
- 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.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 +11 -8
- package/src/__tests__/adapters.test.ts +125 -0
- package/src/__tests__/batch-eval-parsing.test.ts +0 -0
- package/src/__tests__/box-model.test.ts +0 -0
- package/src/__tests__/browser-launch.test.ts +0 -0
- package/src/__tests__/cdp-client.test.ts +375 -0
- package/src/__tests__/close-browser.test.ts +205 -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__/output.test.ts +0 -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/action-builder/analyzer.ts +0 -0
- package/src/browser/action-builder/types.ts +0 -0
- package/src/browser/actions.ts +0 -0
- package/src/browser/adapters/gemini.ts +0 -0
- package/src/browser/adapters/google.ts +0 -0
- package/src/browser/adapters/index.ts +0 -0
- package/src/browser/adapters/instagram.ts +0 -0
- package/src/browser/adapters/linkedin.ts +0 -0
- package/src/browser/adapters/microsoft.ts +0 -0
- package/src/browser/adapters/x.ts +0 -0
- package/src/browser/batch.ts +0 -0
- package/src/browser/browser.ts +101 -16
- package/src/browser/cdp.ts +52 -15
- package/src/browser/console-log.ts +0 -0
- package/src/browser/dashboard/api-types.ts +0 -0
- package/src/browser/dashboard/server.ts +0 -0
- package/src/browser/dashboard/ui.html +0 -0
- package/src/browser/dialog.ts +0 -0
- package/src/browser/emulation.ts +0 -0
- package/src/browser/find.ts +0 -0
- package/src/browser/har.ts +0 -0
- package/src/browser/index.ts +0 -0
- package/src/browser/network.ts +0 -0
- package/src/browser/pdf.ts +0 -0
- package/src/browser/profiler.ts +0 -0
- package/src/browser/record.ts +0 -0
- package/src/browser/ref-cache.ts +29 -6
- package/src/browser/screenshot.ts +0 -0
- package/src/browser/session.ts +0 -0
- package/src/browser/snapshot.ts +0 -0
- package/src/browser/storage.ts +0 -0
- package/src/browser/tabs.ts +12 -4
- package/src/browser/trace.ts +0 -0
- package/src/browser/types.ts +4 -0
- package/src/browser/vitals.ts +0 -0
- package/src/browser/wait.ts +0 -0
- package/src/cli/action.ts +0 -0
- package/src/cli/commands.ts +90 -4
- package/src/cli/output.ts +0 -0
- package/src/cli/platform.ts +0 -0
- package/src/cli/types.ts +0 -0
- package/src/cli.ts +0 -0
- package/src/index.ts +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
/** When true, send() acknowledges the CDP command by delivering a matching
|
|
34
|
+
* empty result — the GRACEFUL path. Default false leaves it hanging so
|
|
35
|
+
* closeBrowser's own BROWSER_CLOSE_TIMEOUT_MS race settles it (ungraceful). */
|
|
36
|
+
autoAck = false;
|
|
37
|
+
send(data: string, cb?: (err?: Error) => void): void {
|
|
38
|
+
void cb;
|
|
39
|
+
if (!this.autoAck) return;
|
|
40
|
+
const { id } = JSON.parse(data) as { id: number };
|
|
41
|
+
queueMicrotask(() => this.deliver({ id, result: {} }));
|
|
42
|
+
}
|
|
43
|
+
close(): void {}
|
|
44
|
+
deliver(msg: unknown): void {
|
|
45
|
+
this.emit('message', Buffer.from(JSON.stringify(msg)));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
vi.mock('ws', () => ({ WebSocket: FakeWs }));
|
|
50
|
+
|
|
51
|
+
let tempDir: string;
|
|
52
|
+
let killSpy: ReturnType<typeof vi.spyOn>;
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
vi.useFakeTimers();
|
|
56
|
+
lastSocket = null;
|
|
57
|
+
tempDir = await mkdtemp(join(tmpdir(), 'monobrowse-close-test-'));
|
|
58
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
59
|
+
killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true as never);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(async () => {
|
|
63
|
+
vi.useRealTimers();
|
|
64
|
+
vi.restoreAllMocks();
|
|
65
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
async function writePersistedPort(port: number, pid: number, savedAt: number): Promise<void> {
|
|
69
|
+
const dir = join(tempDir, '.monomind', 'monobrowse');
|
|
70
|
+
await mkdir(dir, { recursive: true });
|
|
71
|
+
await writeFile(join(dir, 'active-port.json'), JSON.stringify({ port, pid, launched: true, savedAt }), 'utf-8');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function connectedClient(autoAck = false) {
|
|
75
|
+
vi.resetModules();
|
|
76
|
+
const { CdpClient } = await import('../browser/cdp.js');
|
|
77
|
+
const client = new CdpClient();
|
|
78
|
+
const p = client.connect('ws://127.0.0.1:9333/devtools/page/ABC');
|
|
79
|
+
lastSocket!.autoAck = autoAck;
|
|
80
|
+
lastSocket!.emit('open');
|
|
81
|
+
await p;
|
|
82
|
+
return client;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('#115 review follow-up: closeBrowser() cross-process PID-kill fallback', () => {
|
|
86
|
+
it('uses a fresh persisted PID (in-process map empty) and SIGKILLs it on an ungraceful close', async () => {
|
|
87
|
+
vi.resetModules();
|
|
88
|
+
const { closeBrowser, getLaunchedPid } = await import('../browser/browser.js');
|
|
89
|
+
const port = 9333;
|
|
90
|
+
await writePersistedPort(port, 54321, Date.now());
|
|
91
|
+
// Confirm the in-process map really is empty for this port (fresh module state).
|
|
92
|
+
expect(getLaunchedPid(port)).toBeUndefined();
|
|
93
|
+
|
|
94
|
+
const client = await connectedClient();
|
|
95
|
+
const closePromise = closeBrowser(client, port);
|
|
96
|
+
await vi.advanceTimersByTimeAsync(3000); // BROWSER_CLOSE_TIMEOUT_MS
|
|
97
|
+
await closePromise;
|
|
98
|
+
|
|
99
|
+
expect(killSpy).toHaveBeenCalledWith(54321, 0);
|
|
100
|
+
expect(killSpy).toHaveBeenCalledWith(54321, 'SIGKILL');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('#124-review: does not trust a persisted PID older than the staleness bound', async () => {
|
|
104
|
+
vi.resetModules();
|
|
105
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
106
|
+
const port = 9334;
|
|
107
|
+
const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000;
|
|
108
|
+
await writePersistedPort(port, 99999, twoDaysAgo);
|
|
109
|
+
|
|
110
|
+
const client = await connectedClient();
|
|
111
|
+
const closePromise = closeBrowser(client, port);
|
|
112
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
113
|
+
await closePromise;
|
|
114
|
+
|
|
115
|
+
expect(killSpy).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('#124-review: a persisted PID that is no longer alive is not force-killed (liveness check via kill(pid,0))', async () => {
|
|
119
|
+
vi.resetModules();
|
|
120
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
121
|
+
const port = 9335;
|
|
122
|
+
await writePersistedPort(port, 11111, Date.now());
|
|
123
|
+
killSpy.mockImplementation((_pid: unknown, signal?: unknown) => {
|
|
124
|
+
if (signal === 0) throw Object.assign(new Error('ESRCH'), { code: 'ESRCH' });
|
|
125
|
+
return true as never;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const client = await connectedClient();
|
|
129
|
+
const closePromise = closeBrowser(client, port);
|
|
130
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
131
|
+
await closePromise;
|
|
132
|
+
|
|
133
|
+
expect(killSpy).toHaveBeenCalledWith(11111, 0);
|
|
134
|
+
expect(killSpy).not.toHaveBeenCalledWith(11111, 'SIGKILL');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// The graceful path is what the monodesign detection driver actually takes,
|
|
138
|
+
// and it had no coverage: closeBrowser used to return on the Browser.close
|
|
139
|
+
// ack while Chrome was still exiting, holding its CDP port and its profile's
|
|
140
|
+
// singleton lock, with an unref'd 1s timer as the only force-kill (which
|
|
141
|
+
// never fired at all if the caller's process exited first).
|
|
142
|
+
it('graceful close waits for the process to actually exit, without killing it', async () => {
|
|
143
|
+
vi.resetModules();
|
|
144
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
145
|
+
const port = 9337;
|
|
146
|
+
await writePersistedPort(port, 22222, Date.now());
|
|
147
|
+
|
|
148
|
+
let alive = true;
|
|
149
|
+
killSpy.mockImplementation((_pid: unknown, signal?: unknown) => {
|
|
150
|
+
if (signal === 0 && !alive) throw Object.assign(new Error('ESRCH'), { code: 'ESRCH' });
|
|
151
|
+
return true as never;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const client = await connectedClient(true);
|
|
155
|
+
const closePromise = closeBrowser(client, port);
|
|
156
|
+
// Still running a moment after the ack — close() must NOT have resolved.
|
|
157
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
158
|
+
let settled = false;
|
|
159
|
+
void closePromise.then(() => {
|
|
160
|
+
settled = true;
|
|
161
|
+
});
|
|
162
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
163
|
+
expect(settled).toBe(false);
|
|
164
|
+
|
|
165
|
+
alive = false; // Chrome finishes exiting
|
|
166
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
167
|
+
await closePromise;
|
|
168
|
+
expect(killSpy).not.toHaveBeenCalledWith(22222, 'SIGKILL');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('graceful close force-kills a process that never exits', async () => {
|
|
172
|
+
vi.resetModules();
|
|
173
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
174
|
+
const port = 9338;
|
|
175
|
+
await writePersistedPort(port, 33333, Date.now());
|
|
176
|
+
|
|
177
|
+
const client = await connectedClient(true);
|
|
178
|
+
const closePromise = closeBrowser(client, port);
|
|
179
|
+
// The exit poll schedules one timer per tick, each from the previous
|
|
180
|
+
// tick's callback. How far a single advance carries through a chain like
|
|
181
|
+
// that is a fake-timer implementation detail that differs by Node version
|
|
182
|
+
// (runAllTimersAsync hung this test on Node 22 while passing on 26), so
|
|
183
|
+
// step the clock until the promise settles instead of assuming.
|
|
184
|
+
let settled = false;
|
|
185
|
+
void closePromise.then(() => {
|
|
186
|
+
settled = true;
|
|
187
|
+
});
|
|
188
|
+
for (let i = 0; i < 200 && !settled; i++) await vi.advanceTimersByTimeAsync(100);
|
|
189
|
+
await closePromise;
|
|
190
|
+
|
|
191
|
+
expect(settled).toBe(true);
|
|
192
|
+
expect(killSpy).toHaveBeenCalledWith(33333, 'SIGKILL');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('no persisted port file at all — closeBrowser is a no-op, no kill attempted', async () => {
|
|
196
|
+
vi.resetModules();
|
|
197
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
198
|
+
const client = await connectedClient();
|
|
199
|
+
const closePromise = closeBrowser(client, 9336);
|
|
200
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
201
|
+
await closePromise;
|
|
202
|
+
|
|
203
|
+
expect(killSpy).not.toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -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
|
+
});
|