@monoes/monobrowse 1.0.6 → 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 +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 +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 +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 +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__/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/tabs.ts +12 -4
- package/src/browser/types.ts +4 -0
- package/src/cli/commands.ts +90 -4
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frame command (#97) — switching to an iframe must retain the OOPIF
|
|
3
|
+
* sessionId returned by switchToFrame so subsequent commands act on the
|
|
4
|
+
* frame, and `frame main` must detach the frame session and restore the
|
|
5
|
+
* page session. The browser engine is mocked; no real Chrome.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
8
|
+
import type { Command, CommandContext } from '../cli/types.js';
|
|
9
|
+
|
|
10
|
+
const mocks = vi.hoisted(() => {
|
|
11
|
+
const send = vi.fn(async (_method: string, _params?: unknown, _sid?: string) => ({}));
|
|
12
|
+
const client = { send, isConnected: () => true, close: vi.fn() };
|
|
13
|
+
return {
|
|
14
|
+
send,
|
|
15
|
+
client,
|
|
16
|
+
browser: {
|
|
17
|
+
launchBrowser: vi.fn(async () => 9222),
|
|
18
|
+
connectToTarget: vi.fn(async () => ({
|
|
19
|
+
client,
|
|
20
|
+
sessionId: 'S-PAGE',
|
|
21
|
+
target: { id: 'T-PAGE', url: 'https://x.test' },
|
|
22
|
+
})),
|
|
23
|
+
loadActivePortInfo: vi.fn(async () => null),
|
|
24
|
+
loadRefCache: vi.fn(async () => null),
|
|
25
|
+
switchToFrame: vi.fn(async (): Promise<{ url: string | null; sessionId: string | null }> => ({ url: 'https://f.test/frame', sessionId: 'S-FRAME' })),
|
|
26
|
+
enableSessionDomains: vi.fn(async () => {}),
|
|
27
|
+
teardownRouteInterception: vi.fn(),
|
|
28
|
+
stopRequestCapture: vi.fn(),
|
|
29
|
+
teardownDialogHandling: vi.fn(),
|
|
30
|
+
teardownConsoleCapture: vi.fn(),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock('../index.js', () => mocks.browser);
|
|
36
|
+
|
|
37
|
+
async function loadFrameCommand(): Promise<Command> {
|
|
38
|
+
const mod = (await import('../cli/commands.js')).default;
|
|
39
|
+
const cmd = mod.subcommands!.find((c) => c.name === 'frame');
|
|
40
|
+
if (!cmd?.action) throw new Error('frame command not found');
|
|
41
|
+
return cmd;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ctx = (args: string[]): CommandContext => ({
|
|
45
|
+
args,
|
|
46
|
+
flags: { _: [] },
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
interactive: false,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
mocks.browser.loadActivePortInfo.mockResolvedValue(null);
|
|
54
|
+
mocks.browser.loadRefCache.mockResolvedValue(null);
|
|
55
|
+
mocks.browser.switchToFrame.mockResolvedValue({ url: 'https://f.test/frame', sessionId: 'S-FRAME' });
|
|
56
|
+
mocks.browser.connectToTarget.mockResolvedValue({
|
|
57
|
+
client: mocks.client,
|
|
58
|
+
sessionId: 'S-PAGE',
|
|
59
|
+
target: { id: 'T-PAGE', url: 'https://x.test' },
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('browse frame command (#97)', () => {
|
|
64
|
+
it('stores the OOPIF sessionId, enables its domains, and `frame main` detaches it and restores the page session', async () => {
|
|
65
|
+
vi.resetModules();
|
|
66
|
+
const frame = await loadFrameCommand();
|
|
67
|
+
|
|
68
|
+
// Enter the frame — engine reports the OOPIF session
|
|
69
|
+
await frame.action!(ctx(['iframe']));
|
|
70
|
+
expect(mocks.browser.switchToFrame).toHaveBeenCalledWith(mocks.client, 'S-PAGE', 'iframe');
|
|
71
|
+
expect(mocks.browser.enableSessionDomains).toHaveBeenCalledWith(mocks.client, 'S-FRAME');
|
|
72
|
+
|
|
73
|
+
// Back to main — must detach the FRAME session (proves _sessionId was updated)
|
|
74
|
+
await frame.action!(ctx(['main']));
|
|
75
|
+
const detach = mocks.send.mock.calls.find(
|
|
76
|
+
([m]) => m === 'Target.detachFromTarget'
|
|
77
|
+
);
|
|
78
|
+
expect(detach).toBeDefined();
|
|
79
|
+
expect(detach![1]).toEqual({ sessionId: 'S-FRAME' });
|
|
80
|
+
|
|
81
|
+
// Entering a frame again must start from the restored page session
|
|
82
|
+
await frame.action!(ctx(['iframe']));
|
|
83
|
+
expect(mocks.browser.switchToFrame).toHaveBeenLastCalledWith(mocks.client, 'S-PAGE', 'iframe');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('keeps the page session when the frame has no OOPIF target', async () => {
|
|
87
|
+
vi.resetModules();
|
|
88
|
+
mocks.browser.switchToFrame.mockResolvedValue({ url: 'https://f.test/frame', sessionId: null });
|
|
89
|
+
const frame = await loadFrameCommand();
|
|
90
|
+
|
|
91
|
+
await frame.action!(ctx(['iframe']));
|
|
92
|
+
expect(mocks.browser.enableSessionDomains).not.toHaveBeenCalled();
|
|
93
|
+
|
|
94
|
+
// `frame main` with no stored parent session must not detach anything
|
|
95
|
+
await frame.action!(ctx(['main']));
|
|
96
|
+
expect(mocks.send.mock.calls.some(([m]) => m === 'Target.detachFromTarget')).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -138,13 +138,13 @@ describe('active-port persistence', () => {
|
|
|
138
138
|
const mod = await importRefCache();
|
|
139
139
|
await mod.saveActivePort(9333);
|
|
140
140
|
expect(await mod.loadActivePort()).toBe(9333);
|
|
141
|
-
expect(await mod.loadActivePortInfo()).toEqual({ port: 9333, launched: true });
|
|
141
|
+
expect(await mod.loadActivePortInfo()).toEqual({ port: 9333, launched: true, savedAt: expect.any(Number) });
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
it('connect provenance: launched:false survives the round-trip', async () => {
|
|
145
145
|
const mod = await importRefCache();
|
|
146
146
|
await mod.saveActivePort(9229, { launched: false });
|
|
147
|
-
expect(await mod.loadActivePortInfo()).toEqual({ port: 9229, launched: false });
|
|
147
|
+
expect(await mod.loadActivePortInfo()).toEqual({ port: 9229, launched: false, savedAt: expect.any(Number) });
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
it('clear removes the file; load returns null afterwards', async () => {
|
|
@@ -161,6 +161,51 @@ describe('active-port persistence', () => {
|
|
|
161
161
|
await expect(mod.clearActivePort()).resolves.toBeUndefined();
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
it('#115: pid and userDataDir persist and round-trip so a later process can kill an orphaned launch', async () => {
|
|
165
|
+
const mod = await importRefCache();
|
|
166
|
+
await mod.saveActivePort(9333, { pid: 54321, userDataDir: '/tmp/monomind-browser-9333' });
|
|
167
|
+
expect(await mod.loadActivePortInfo()).toEqual({
|
|
168
|
+
port: 9333,
|
|
169
|
+
launched: true,
|
|
170
|
+
pid: 54321,
|
|
171
|
+
userDataDir: '/tmp/monomind-browser-9333',
|
|
172
|
+
savedAt: expect.any(Number),
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('#115: an attached (launched:false) session with no pid argument persists none', async () => {
|
|
177
|
+
const mod = await importRefCache();
|
|
178
|
+
await mod.saveActivePort(9229, { launched: false });
|
|
179
|
+
const info = await mod.loadActivePortInfo();
|
|
180
|
+
expect(info!.launched).toBe(false);
|
|
181
|
+
expect(info!.pid).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('#124-review: saveActivePort does NOT strip pid when launched:false — callers must not pass one for an attach', async () => {
|
|
185
|
+
// The property above (attach sessions have no pid) is caller discipline,
|
|
186
|
+
// not something this function enforces — the one production caller
|
|
187
|
+
// (commands.ts's `connect` action) simply never passes a pid alongside
|
|
188
|
+
// launched:false. This test documents that explicitly so it can't be
|
|
189
|
+
// mistaken for an invariant the code itself guarantees.
|
|
190
|
+
const mod = await importRefCache();
|
|
191
|
+
await mod.saveActivePort(9229, { launched: false, pid: 4242 });
|
|
192
|
+
const info = await mod.loadActivePortInfo();
|
|
193
|
+
expect(info!.launched).toBe(false);
|
|
194
|
+
expect(info!.pid).toBe(4242);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('#115: a non-numeric or non-positive persisted pid is dropped, not trusted', async () => {
|
|
198
|
+
const mod = await importRefCache();
|
|
199
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
200
|
+
const dir = join(process.cwd(), '.monomind', 'monobrowse');
|
|
201
|
+
await mkdir(dir, { recursive: true });
|
|
202
|
+
for (const badPid of ['54321', -1, 0, 1.5, null]) {
|
|
203
|
+
await writeFile(join(dir, 'active-port.json'), JSON.stringify({ port: 9333, launched: true, pid: badPid }));
|
|
204
|
+
const info = await mod.loadActivePortInfo();
|
|
205
|
+
expect(info!.pid).toBeUndefined();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
164
209
|
it('rejects out-of-range or non-integer persisted ports', async () => {
|
|
165
210
|
const mod = await importRefCache();
|
|
166
211
|
const { writeFile, mkdir } = await import('fs/promises');
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session.ts — the path-safety guards and the state-file round trip.
|
|
3
|
+
*
|
|
4
|
+
* Session files hold cookies and localStorage, so the traversal guards and the
|
|
5
|
+
* 0600/0700 permission bits are security-relevant. All of it is filesystem +
|
|
6
|
+
* validation logic: the CdpClient is a stub that answers the handful of CDP
|
|
7
|
+
* commands the round trip issues.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
10
|
+
import { mkdtemp, rm, readFile, stat, mkdir } from 'fs/promises';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { tmpdir } from 'os';
|
|
13
|
+
import type { CdpClient } from '../browser/cdp.js';
|
|
14
|
+
import {
|
|
15
|
+
saveStateFile,
|
|
16
|
+
loadStateFile,
|
|
17
|
+
saveSession,
|
|
18
|
+
loadSession,
|
|
19
|
+
listSessions,
|
|
20
|
+
} from '../browser/session.js';
|
|
21
|
+
|
|
22
|
+
const COOKIES = [{ name: 'sid', value: 'abc', domain: 'x.test', path: '/' }];
|
|
23
|
+
|
|
24
|
+
/** Records every CDP command and answers the ones session.ts depends on. */
|
|
25
|
+
function stubClient(): { client: CdpClient; calls: Array<{ method: string; params: unknown; sid?: string }> } {
|
|
26
|
+
const calls: Array<{ method: string; params: unknown; sid?: string }> = [];
|
|
27
|
+
const client = {
|
|
28
|
+
send: vi.fn(async (method: string, params: unknown, sid?: string) => {
|
|
29
|
+
calls.push({ method, params, sid });
|
|
30
|
+
if (method === 'Network.getCookies') return { cookies: COOKIES };
|
|
31
|
+
if (method === 'Runtime.evaluate') {
|
|
32
|
+
const expr = (params as { expression: string }).expression;
|
|
33
|
+
if (expr.includes('Object.entries(localStorage)')) return { result: { value: '{"k":"v"}' } };
|
|
34
|
+
if (expr.includes('Object.entries(sessionStorage)')) return { result: { value: '{"s":"1"}' } };
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}),
|
|
38
|
+
} as unknown as CdpClient;
|
|
39
|
+
return { client, calls };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let dir: string;
|
|
43
|
+
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
dir = await mkdtemp(join(tmpdir(), 'monobrowse-session-'));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
vi.restoreAllMocks();
|
|
50
|
+
await rm(dir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('validateSessionName (via saveSession/loadSession)', () => {
|
|
54
|
+
const { client } = stubClient();
|
|
55
|
+
|
|
56
|
+
// Rejection happens before any filesystem or CDP work, so these never touch
|
|
57
|
+
// the real ~/.monomind directory.
|
|
58
|
+
const bad: Array<[string, string]> = [
|
|
59
|
+
['empty', ''],
|
|
60
|
+
['forward slash', 'a/b'],
|
|
61
|
+
['backslash', 'a\\b'],
|
|
62
|
+
['NUL byte', 'a\x00b'],
|
|
63
|
+
['dot', '.'],
|
|
64
|
+
['dot-dot', '..'],
|
|
65
|
+
['dot-dot prefix', '..evil'],
|
|
66
|
+
['traversal', '../../etc/passwd'],
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const [label, name] of bad) {
|
|
70
|
+
it(`rejects ${label}`, async () => {
|
|
71
|
+
await expect(saveSession(client, 'S1', 'T1', name, 'https://x.test', 'T')).rejects.toThrow(
|
|
72
|
+
/Invalid session name/
|
|
73
|
+
);
|
|
74
|
+
await expect(loadSession(client, 'S1', name)).rejects.toThrow(/Invalid session name/);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
it('accepts an ordinary name (fails later, on the missing file, not on validation)', async () => {
|
|
79
|
+
await expect(loadSession(client, 'S1', 'no-such-session-xyz')).rejects.toThrow(
|
|
80
|
+
'Session not found: no-such-session-xyz'
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('does not issue a single CDP command for a rejected name', async () => {
|
|
85
|
+
const { client: c, calls } = stubClient();
|
|
86
|
+
await expect(saveSession(c, 'S1', 'T1', '../x', 'https://x.test', 'T')).rejects.toThrow();
|
|
87
|
+
expect(calls).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('validateFilePath (via saveStateFile/loadStateFile)', () => {
|
|
92
|
+
const { client } = stubClient();
|
|
93
|
+
|
|
94
|
+
const bad: Array<[string, string]> = [
|
|
95
|
+
['empty', ''],
|
|
96
|
+
['NUL byte', '/tmp/a\x00b.json'],
|
|
97
|
+
['embedded ..', '/tmp/a/../../etc/passwd'],
|
|
98
|
+
['leading ../', '../secret.json'],
|
|
99
|
+
['trailing /..', '/tmp/state/..'],
|
|
100
|
+
['backslash', 'C:\\Windows\\state.json'],
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const [label, filePath] of bad) {
|
|
104
|
+
it(`rejects ${label}`, async () => {
|
|
105
|
+
await expect(
|
|
106
|
+
saveStateFile(client, 'S1', 'T1', filePath, 'https://x.test', 'T')
|
|
107
|
+
).rejects.toThrow(/Invalid file path/);
|
|
108
|
+
await expect(loadStateFile(client, 'S1', filePath)).rejects.toThrow(/Invalid file path/);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
it('allows a plain absolute path', async () => {
|
|
113
|
+
const p = join(dir, 'state.json');
|
|
114
|
+
await expect(saveStateFile(client, 'S1', 'T1', p, 'https://x.test', 'T')).resolves.toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('state file round trip', () => {
|
|
119
|
+
it('captures cookies plus local/session storage and writes owner-only (0600)', async () => {
|
|
120
|
+
const { client, calls } = stubClient();
|
|
121
|
+
const p = join(dir, 'nested', 'state.json');
|
|
122
|
+
await saveStateFile(client, 'S1', 'T1', p, 'https://x.test', 'Page Title');
|
|
123
|
+
|
|
124
|
+
const state = JSON.parse(await readFile(p, 'utf8'));
|
|
125
|
+
expect(state).toEqual({
|
|
126
|
+
targetId: 'T1',
|
|
127
|
+
sessionId: 'S1',
|
|
128
|
+
url: 'https://x.test',
|
|
129
|
+
title: 'Page Title',
|
|
130
|
+
cookies: COOKIES,
|
|
131
|
+
localStorage: { k: 'v' },
|
|
132
|
+
sessionStorage: { s: '1' },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect((await stat(p)).mode & 0o777).toBe(0o600);
|
|
136
|
+
// Parent directory created owner-only too.
|
|
137
|
+
expect((await stat(join(dir, 'nested'))).mode & 0o777).toBe(0o700);
|
|
138
|
+
|
|
139
|
+
// Cookies were scoped to the page URL, not fetched globally.
|
|
140
|
+
expect(calls.find((c) => c.method === 'Network.getCookies')?.params).toEqual({
|
|
141
|
+
urls: ['https://x.test'],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('replays cookies and both storages back into the page on load', async () => {
|
|
146
|
+
const saver = stubClient();
|
|
147
|
+
const p = join(dir, 'state.json');
|
|
148
|
+
await saveStateFile(saver.client, 'S1', 'T1', p, 'https://x.test', 'T');
|
|
149
|
+
|
|
150
|
+
const loader = stubClient();
|
|
151
|
+
const state = await loadStateFile(loader.client, 'S2', p);
|
|
152
|
+
expect(state.cookies).toEqual(COOKIES);
|
|
153
|
+
|
|
154
|
+
const setCookies = loader.calls.find((c) => c.method === 'Network.setCookies');
|
|
155
|
+
expect(setCookies).toMatchObject({ params: { cookies: COOKIES }, sid: 'S2' });
|
|
156
|
+
|
|
157
|
+
const scripts = loader.calls
|
|
158
|
+
.filter((c) => c.method === 'Runtime.evaluate')
|
|
159
|
+
.map((c) => (c.params as { expression: string }).expression);
|
|
160
|
+
expect(scripts).toContain('localStorage.setItem("k", "v")');
|
|
161
|
+
expect(scripts).toContain('sessionStorage.setItem("s", "1")');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('rejects a state file whose cookies field is not an array', async () => {
|
|
165
|
+
const { client } = stubClient();
|
|
166
|
+
const p = join(dir, 'bad.json');
|
|
167
|
+
const { writeFile } = await import('fs/promises');
|
|
168
|
+
await writeFile(p, JSON.stringify({ cookies: { name: 'sid' } }));
|
|
169
|
+
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
170
|
+
'Invalid state file: cookies is not an array'
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('rejects a state file whose localStorage is an array rather than an object', async () => {
|
|
175
|
+
const { client } = stubClient();
|
|
176
|
+
const p = join(dir, 'bad2.json');
|
|
177
|
+
const { writeFile } = await import('fs/promises');
|
|
178
|
+
await writeFile(p, JSON.stringify({ cookies: [], localStorage: ['nope'] }));
|
|
179
|
+
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
180
|
+
'Invalid state file: localStorage is not a plain object'
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('rejects a state file whose sessionStorage is an array', async () => {
|
|
185
|
+
const { client } = stubClient();
|
|
186
|
+
const p = join(dir, 'bad3.json');
|
|
187
|
+
const { writeFile } = await import('fs/promises');
|
|
188
|
+
await writeFile(p, JSON.stringify({ cookies: [], sessionStorage: ['nope'] }));
|
|
189
|
+
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
190
|
+
'Invalid state file: sessionStorage is not a plain object'
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('accepts a state file with neither storage map present', async () => {
|
|
195
|
+
const { client, calls } = stubClient();
|
|
196
|
+
const p = join(dir, 'minimal.json');
|
|
197
|
+
const { writeFile } = await import('fs/promises');
|
|
198
|
+
await writeFile(p, JSON.stringify({ targetId: 'T', sessionId: 'S', url: '', title: '', cookies: [] }));
|
|
199
|
+
await expect(loadStateFile(client, 'S1', p)).resolves.toMatchObject({ cookies: [] });
|
|
200
|
+
expect(calls.filter((c) => c.method === 'Runtime.evaluate')).toEqual([]);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('listSessions', () => {
|
|
205
|
+
// SESSION_DIR is computed from homedir() at module load, so each case
|
|
206
|
+
// re-imports session.js with 'os' mocked to point at a temp home.
|
|
207
|
+
async function withHome(home: string): Promise<typeof import('../browser/session.js')> {
|
|
208
|
+
vi.resetModules();
|
|
209
|
+
const actualOs = await vi.importActual<typeof import('os')>('os');
|
|
210
|
+
vi.doMock('os', () => ({ ...actualOs, default: actualOs, homedir: () => home }));
|
|
211
|
+
return import('../browser/session.js');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
afterEach(() => {
|
|
215
|
+
vi.doUnmock('os');
|
|
216
|
+
vi.resetModules();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('returns [] when the session directory does not exist', async () => {
|
|
220
|
+
const mod = await withHome(join(dir, 'no-such-home'));
|
|
221
|
+
await expect(mod.listSessions()).resolves.toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('lists only .json files, with the extension stripped', async () => {
|
|
225
|
+
const sessionDir = join(dir, '.monomind', 'browser-sessions');
|
|
226
|
+
await mkdir(sessionDir, { recursive: true });
|
|
227
|
+
const { writeFile } = await import('fs/promises');
|
|
228
|
+
await writeFile(join(sessionDir, 'alpha.json'), '{}');
|
|
229
|
+
await writeFile(join(sessionDir, 'beta.json'), '{}');
|
|
230
|
+
await writeFile(join(sessionDir, 'notes.txt'), 'ignore me');
|
|
231
|
+
|
|
232
|
+
const mod = await withHome(dir);
|
|
233
|
+
await expect(mod.listSessions()).resolves.toEqual(['alpha', 'beta']);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('saveSession writes into the session dir and loadSession reads it back', async () => {
|
|
237
|
+
const mod = await withHome(dir);
|
|
238
|
+
const saver = stubClient();
|
|
239
|
+
const filePath = await mod.saveSession(saver.client, 'S1', 'T1', 'my-session', 'https://x.test', 'T');
|
|
240
|
+
expect(filePath).toBe(join(dir, '.monomind', 'browser-sessions', 'my-session.json'));
|
|
241
|
+
expect((await stat(filePath)).mode & 0o777).toBe(0o600);
|
|
242
|
+
expect((await stat(join(dir, '.monomind', 'browser-sessions'))).mode & 0o777).toBe(0o700);
|
|
243
|
+
|
|
244
|
+
const loader = stubClient();
|
|
245
|
+
await expect(mod.loadSession(loader.client, 'S2', 'my-session')).resolves.toMatchObject({
|
|
246
|
+
cookies: COOKIES,
|
|
247
|
+
url: 'https://x.test',
|
|
248
|
+
});
|
|
249
|
+
await expect(mod.listSessions()).resolves.toEqual(['my-session']);
|
|
250
|
+
});
|
|
251
|
+
});
|