@monoes/monobrowse 1.0.8 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/__tests__/close-browser.test.js +38 -10
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/__tests__/open-url-navigate-error.test.d.ts +2 -0
- package/dist/src/__tests__/open-url-navigate-error.test.d.ts.map +1 -0
- package/dist/src/__tests__/open-url-navigate-error.test.js +48 -0
- package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -0
- package/dist/src/__tests__/snapshot.test.js +2 -2
- package/dist/src/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +7 -1
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/__tests__/close-browser.test.ts +42 -9
- package/src/__tests__/open-url-navigate-error.test.ts +58 -0
- package/src/__tests__/snapshot.test.ts +2 -2
- package/src/browser/browser.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monobrowse",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Native browser automation via Chrome DevTools Protocol — the engine powering monomind browse",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
34
|
+
"node": ">=22.12.0"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"browser-automation",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
15
15
|
import { mkdtemp, rm, mkdir, writeFile } from 'fs/promises';
|
|
16
|
+
import { setTimeout as realDelay } from 'timers/promises';
|
|
16
17
|
import { join } from 'path';
|
|
17
18
|
import { tmpdir } from 'os';
|
|
18
19
|
|
|
@@ -52,6 +53,10 @@ let tempDir: string;
|
|
|
52
53
|
let killSpy: ReturnType<typeof vi.spyOn>;
|
|
53
54
|
|
|
54
55
|
beforeEach(async () => {
|
|
56
|
+
// Clean env vars that can interfere with timer behavior in test environment
|
|
57
|
+
delete process.env.MONOMIND_SDK_AGENT;
|
|
58
|
+
delete process.env.MONOMIND_HOOK_QUIET;
|
|
59
|
+
|
|
55
60
|
vi.useFakeTimers();
|
|
56
61
|
lastSocket = null;
|
|
57
62
|
tempDir = await mkdtemp(join(tmpdir(), 'monobrowse-close-test-'));
|
|
@@ -71,6 +76,29 @@ async function writePersistedPort(port: number, pid: number, savedAt: number): P
|
|
|
71
76
|
await writeFile(join(dir, 'active-port.json'), JSON.stringify({ port, pid, launched: true, savedAt }), 'utf-8');
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Block (in REAL time) until closeBrowser()'s process-exit poll has actually
|
|
81
|
+
* started, observed via the poll's own first liveness probe — `kill(pid, 0)`
|
|
82
|
+
* is the first thing waitForProcessExit() does.
|
|
83
|
+
*
|
|
84
|
+
* Needed because closeBrowser() awaits the persisted-port read (real
|
|
85
|
+
* filesystem I/O) BEFORE that poll starts, while `advanceTimersByTimeAsync`
|
|
86
|
+
* only moves a FAKE clock and does not wait for real I/O. Advancing a fixed
|
|
87
|
+
* number of times therefore raced the read: every advance can complete in
|
|
88
|
+
* microseconds of real time while the read is still in flight, so on a loaded
|
|
89
|
+
* host the budget ran out with the poll never started — and since nothing
|
|
90
|
+
* then drives the fake clock again, the test did not merely run slow, it
|
|
91
|
+
* deadlocked on an unsettled promise until the test timeout fired.
|
|
92
|
+
*
|
|
93
|
+
* Once this returns, the remaining work is pure fake timers, so a single
|
|
94
|
+
* bounded advance carries it deterministically.
|
|
95
|
+
*/
|
|
96
|
+
async function exitPollStarted(pid: number): Promise<void> {
|
|
97
|
+
while (!killSpy.mock.calls.some(([p, signal]) => p === pid && signal === 0)) {
|
|
98
|
+
await realDelay(0);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
async function connectedClient(autoAck = false) {
|
|
75
103
|
vi.resetModules();
|
|
76
104
|
const { CdpClient } = await import('../browser/cdp.js');
|
|
@@ -153,13 +181,15 @@ describe('#115 review follow-up: closeBrowser() cross-process PID-kill fallback'
|
|
|
153
181
|
|
|
154
182
|
const client = await connectedClient(true);
|
|
155
183
|
const closePromise = closeBrowser(client, port);
|
|
156
|
-
// Still running a moment after the ack — close() must NOT have resolved.
|
|
157
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
158
184
|
let settled = false;
|
|
159
185
|
void closePromise.then(() => {
|
|
160
186
|
settled = true;
|
|
161
187
|
});
|
|
162
|
-
|
|
188
|
+
|
|
189
|
+
// Poll is running and the process is still alive — several poll ticks in,
|
|
190
|
+
// close() must NOT have resolved.
|
|
191
|
+
await exitPollStarted(22222);
|
|
192
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
163
193
|
expect(settled).toBe(false);
|
|
164
194
|
|
|
165
195
|
alive = false; // Chrome finishes exiting
|
|
@@ -176,16 +206,19 @@ describe('#115 review follow-up: closeBrowser() cross-process PID-kill fallback'
|
|
|
176
206
|
|
|
177
207
|
const client = await connectedClient(true);
|
|
178
208
|
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
209
|
let settled = false;
|
|
185
210
|
void closePromise.then(() => {
|
|
186
211
|
settled = true;
|
|
187
212
|
});
|
|
188
|
-
|
|
213
|
+
|
|
214
|
+
// Wait for the poll to exist before driving it (see exitPollStarted) —
|
|
215
|
+
// every previous fix here guessed an advance budget instead, which is
|
|
216
|
+
// what made this test hang rather than merely run slow. From here on the
|
|
217
|
+
// poll is the only thing left and it is pure fake timers, so one advance
|
|
218
|
+
// past its PROCESS_EXIT_TIMEOUT_MS (5000ms) deadline settles it, with no
|
|
219
|
+
// dependence on how fast the host happens to be.
|
|
220
|
+
await exitPollStarted(33333);
|
|
221
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
189
222
|
await closePromise;
|
|
190
223
|
|
|
191
224
|
expect(settled).toBe(true);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openUrl() (browser.ts) previously ignored Page.navigate's own response and
|
|
3
|
+
* always fell through to waitForNetworkIdle, so a protocol-level navigation
|
|
4
|
+
* failure (e.g. a refused connection) settled on the chrome-error://
|
|
5
|
+
* chromewebdata/ page and `browse open` reported success with exit 0. CDP's
|
|
6
|
+
* Page.navigate response carries errorText when navigation itself failed —
|
|
7
|
+
* this checks that response before doing anything else.
|
|
8
|
+
*/
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import type { CdpClient } from '../browser/cdp.js';
|
|
11
|
+
import { openUrl } from '../browser/browser.js';
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** Client stub that records calls and answers from a per-method table. */
|
|
18
|
+
function stubClient(table: Record<string, unknown> = {}): {
|
|
19
|
+
client: CdpClient;
|
|
20
|
+
calls: Array<{ method: string; params: unknown }>;
|
|
21
|
+
} {
|
|
22
|
+
const calls: Array<{ method: string; params: unknown }> = [];
|
|
23
|
+
const client = {
|
|
24
|
+
send: vi.fn(async (method: string, params: unknown) => {
|
|
25
|
+
calls.push({ method, params });
|
|
26
|
+
const entry = table[method];
|
|
27
|
+
if (entry instanceof Error) throw entry;
|
|
28
|
+
return entry ?? {};
|
|
29
|
+
}),
|
|
30
|
+
on: vi.fn(() => {
|
|
31
|
+
throw new Error('waitForNetworkIdle should not run once Page.navigate reports errorText');
|
|
32
|
+
}),
|
|
33
|
+
} as unknown as CdpClient;
|
|
34
|
+
return { client, calls };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('openUrl navigation-failure detection', () => {
|
|
38
|
+
it('throws when Page.navigate reports errorText, without waiting for network idle', async () => {
|
|
39
|
+
const { client, calls } = stubClient({
|
|
40
|
+
'Page.navigate': { frameId: 'f1', errorText: 'net::ERR_CONNECTION_REFUSED' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await expect(openUrl(client, 'sess-1', 'http://127.0.0.1:1/')).rejects.toThrow(
|
|
44
|
+
/net::ERR_CONNECTION_REFUSED/,
|
|
45
|
+
);
|
|
46
|
+
expect(calls).toEqual([
|
|
47
|
+
{ method: 'Page.navigate', params: { url: 'http://127.0.0.1:1/' } },
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rejects the 2 MB URL guard before ever calling Page.navigate', async () => {
|
|
52
|
+
const { client, calls } = stubClient();
|
|
53
|
+
const hugeUrl = `http://x/${'a'.repeat(2_097_152)}`;
|
|
54
|
+
|
|
55
|
+
await expect(openUrl(client, 'sess-1', hugeUrl)).rejects.toThrow(/2 MB/);
|
|
56
|
+
expect(calls).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -276,11 +276,11 @@ describe('captureSnapshot — property extraction and rendering', () => {
|
|
|
276
276
|
});
|
|
277
277
|
|
|
278
278
|
describe('resolveRef', () => {
|
|
279
|
-
it('returns the ref when present', () => {
|
|
279
|
+
it('returns the ref when present', async () => {
|
|
280
280
|
const refs = new Map<string, ElementRef>([
|
|
281
281
|
['e1', { ref: 'e1', role: 'button', name: 'Go', nodeId: 1 }],
|
|
282
282
|
]);
|
|
283
|
-
expect(resolveRef({} as CdpClient, 'S1', refs, 'e1')).resolves.toMatchObject({ ref: 'e1' });
|
|
283
|
+
await expect(resolveRef({} as CdpClient, 'S1', refs, 'e1')).resolves.toMatchObject({ ref: 'e1' });
|
|
284
284
|
});
|
|
285
285
|
|
|
286
286
|
it('throws a snapshot-first hint when the ref is unknown', async () => {
|
package/src/browser/browser.ts
CHANGED
|
@@ -387,7 +387,12 @@ async function waitForProcessExit(pid: number): Promise<boolean> {
|
|
|
387
387
|
export async function openUrl(client: CdpClient, sessionId: string, url: string): Promise<void> {
|
|
388
388
|
// Cap to 2 MB to prevent OOM in CDP message serializer (e.g. data: URI attacks)
|
|
389
389
|
if (url.length > 2_097_152) throw new Error('URL exceeds 2 MB limit');
|
|
390
|
-
|
|
390
|
+
// Page.navigate reports a protocol-level failure (e.g. a refused connection)
|
|
391
|
+
// via errorText in its own response, before Chrome ever settles on the
|
|
392
|
+
// chrome-error://chromewebdata/ page — without this check a refused
|
|
393
|
+
// connection previously reported success against that error page.
|
|
394
|
+
const nav = await client.send<{ errorText?: string }>('Page.navigate', { url }, sessionId);
|
|
395
|
+
if (nav.errorText) throw new Error(`Navigation to ${url} failed: ${nav.errorText}`);
|
|
391
396
|
await waitForNetworkIdle(client, sessionId, 500, 30_000);
|
|
392
397
|
}
|
|
393
398
|
|