@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,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* find.ts — locator semantics over the snapshot ref map.
|
|
3
|
+
*
|
|
4
|
+
* findByRole / findByText / findByLabel / findByPlaceholder never talk to the
|
|
5
|
+
* browser at all: they filter the in-memory ref Map. findBySelector and
|
|
6
|
+
* findByTestId do issue CDP commands, which a recording stub answers.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
9
|
+
import type { CdpClient } from '../browser/cdp.js';
|
|
10
|
+
import type { ElementRef } from '../browser/types.js';
|
|
11
|
+
import {
|
|
12
|
+
findBySelector,
|
|
13
|
+
findByRole,
|
|
14
|
+
findByText,
|
|
15
|
+
findByLabel,
|
|
16
|
+
findByPlaceholder,
|
|
17
|
+
findByTestId,
|
|
18
|
+
isEnabled,
|
|
19
|
+
isVisible,
|
|
20
|
+
isChecked,
|
|
21
|
+
scrollIntoView,
|
|
22
|
+
} from '../browser/find.js';
|
|
23
|
+
|
|
24
|
+
function ref(partial: Partial<ElementRef> & { ref: string }): ElementRef {
|
|
25
|
+
return { role: 'generic', name: '', nodeId: 1, ...partial };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Ref map mirroring a small page snapshot. */
|
|
29
|
+
function sampleRefs(): Map<string, ElementRef> {
|
|
30
|
+
const entries: ElementRef[] = [
|
|
31
|
+
ref({ ref: 'e1', role: 'button', name: 'Save changes', backendDOMNodeId: 11 }),
|
|
32
|
+
ref({ ref: 'e2', role: 'BUTTON', name: 'Save', backendDOMNodeId: 12 }),
|
|
33
|
+
ref({ ref: 'e3', role: 'link', name: 'Save as draft', backendDOMNodeId: 13 }),
|
|
34
|
+
ref({ ref: 'e4', role: 'textbox', name: 'Email', placeholder: 'you@example.com', backendDOMNodeId: 14 }),
|
|
35
|
+
ref({ ref: 'e5', role: 'textbox', name: 'Notes', placeholder: 'Optional notes', backendDOMNodeId: 15, disabled: true }),
|
|
36
|
+
];
|
|
37
|
+
return new Map(entries.map((e) => [e.ref, e]));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Answers a per-method table and records every call. */
|
|
41
|
+
function stubClient(table: Record<string, unknown> = {}): {
|
|
42
|
+
client: CdpClient;
|
|
43
|
+
calls: Array<{ method: string; params: Record<string, unknown> }>;
|
|
44
|
+
} {
|
|
45
|
+
const calls: Array<{ method: string; params: Record<string, unknown> }> = [];
|
|
46
|
+
const client = {
|
|
47
|
+
send: vi.fn(async (method: string, params: Record<string, unknown>) => {
|
|
48
|
+
calls.push({ method, params });
|
|
49
|
+
const entry = table[method];
|
|
50
|
+
if (typeof entry === 'function') return (entry as (p: unknown) => unknown)(params);
|
|
51
|
+
if (entry instanceof Error) throw entry;
|
|
52
|
+
return entry ?? {};
|
|
53
|
+
}),
|
|
54
|
+
} as unknown as CdpClient;
|
|
55
|
+
return { client, calls };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('nth validation is shared by every locator', () => {
|
|
59
|
+
const { client } = stubClient();
|
|
60
|
+
const refs = sampleRefs();
|
|
61
|
+
|
|
62
|
+
it.each([
|
|
63
|
+
['findBySelector', () => findBySelector(client, 'S1', refs, 'button', { nth: 0 })],
|
|
64
|
+
['findByRole', () => findByRole(client, 'S1', refs, 'button', { nth: 0 })],
|
|
65
|
+
['findByText', () => findByText(client, 'S1', refs, 'Save', { nth: 0 })],
|
|
66
|
+
['findByPlaceholder', () => findByPlaceholder(client, 'S1', refs, 'you', { nth: -1 })],
|
|
67
|
+
])('%s rejects nth below 1', async (_label, run) => {
|
|
68
|
+
await expect(run()).rejects.toThrow(/nth must be >= 1 \(received (0|-1)\)/);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('findByRole', () => {
|
|
73
|
+
it('matches the role case-insensitively', async () => {
|
|
74
|
+
const { client } = stubClient();
|
|
75
|
+
const found = await findByRole(client, 'S1', sampleRefs(), 'BUTTON');
|
|
76
|
+
expect(found?.ref).toBe('e1');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does substring name matching by default', async () => {
|
|
80
|
+
const { client } = stubClient();
|
|
81
|
+
const found = await findByRole(client, 'S1', sampleRefs(), 'button', { name: 'save cha' });
|
|
82
|
+
expect(found?.ref).toBe('e1');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('requires a full name match under exact', async () => {
|
|
86
|
+
const { client } = stubClient();
|
|
87
|
+
expect((await findByRole(client, 'S1', sampleRefs(), 'button', { name: 'Save', exact: true }))?.ref).toBe('e2');
|
|
88
|
+
expect(await findByRole(client, 'S1', sampleRefs(), 'button', { name: 'Sav', exact: true })).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('nth is 1-based over the filtered matches', async () => {
|
|
92
|
+
const { client } = stubClient();
|
|
93
|
+
const refs = sampleRefs();
|
|
94
|
+
expect((await findByRole(client, 'S1', refs, 'button', { nth: 1 }))?.ref).toBe('e1');
|
|
95
|
+
expect((await findByRole(client, 'S1', refs, 'button', { nth: 2 }))?.ref).toBe('e2');
|
|
96
|
+
expect(await findByRole(client, 'S1', refs, 'button', { nth: 3 })).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('last returns the final match', async () => {
|
|
100
|
+
const { client } = stubClient();
|
|
101
|
+
expect((await findByRole(client, 'S1', sampleRefs(), 'button', { last: true }))?.ref).toBe('e2');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns null when no ref carries that role', async () => {
|
|
105
|
+
const { client } = stubClient();
|
|
106
|
+
expect(await findByRole(client, 'S1', sampleRefs(), 'checkbox')).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('issues no CDP command at all', async () => {
|
|
110
|
+
const { client, calls } = stubClient();
|
|
111
|
+
await findByRole(client, 'S1', sampleRefs(), 'button');
|
|
112
|
+
expect(calls).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('findByText', () => {
|
|
117
|
+
it('matches names case-insensitively as a substring', async () => {
|
|
118
|
+
const { client } = stubClient();
|
|
119
|
+
expect((await findByText(client, 'S1', sampleRefs(), 'save as'))?.ref).toBe('e3');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('spans roles — first match wins', async () => {
|
|
123
|
+
const { client } = stubClient();
|
|
124
|
+
expect((await findByText(client, 'S1', sampleRefs(), 'save'))?.ref).toBe('e1');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('honours exact, nth and last', async () => {
|
|
128
|
+
const { client } = stubClient();
|
|
129
|
+
const refs = sampleRefs();
|
|
130
|
+
expect((await findByText(client, 'S1', refs, 'Save', { exact: true }))?.ref).toBe('e2');
|
|
131
|
+
expect((await findByText(client, 'S1', refs, 'save', { nth: 2 }))?.ref).toBe('e2');
|
|
132
|
+
expect((await findByText(client, 'S1', refs, 'save', { last: true }))?.ref).toBe('e3');
|
|
133
|
+
expect(await findByText(client, 'S1', refs, 'save', { nth: 9 })).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns null on no match', async () => {
|
|
137
|
+
const { client } = stubClient();
|
|
138
|
+
expect(await findByText(client, 'S1', sampleRefs(), 'nonexistent')).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('findByLabel delegates to the same matching', async () => {
|
|
142
|
+
const { client } = stubClient();
|
|
143
|
+
const refs = sampleRefs();
|
|
144
|
+
expect(await findByLabel(client, 'S1', refs, 'Email')).toBe(await findByText(client, 'S1', refs, 'Email'));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('findByPlaceholder', () => {
|
|
149
|
+
it('matches on the placeholder attribute, not the accessible name', async () => {
|
|
150
|
+
const { client } = stubClient();
|
|
151
|
+
expect((await findByPlaceholder(client, 'S1', sampleRefs(), 'example.com'))?.ref).toBe('e4');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('treats a missing placeholder as empty rather than crashing', async () => {
|
|
155
|
+
const { client } = stubClient();
|
|
156
|
+
// e1..e3 have no placeholder; an empty needle must not match them under exact.
|
|
157
|
+
expect((await findByPlaceholder(client, 'S1', sampleRefs(), '', { exact: true }))?.ref).toBe('e1');
|
|
158
|
+
expect(await findByPlaceholder(client, 'S1', sampleRefs(), 'nope')).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('honours exact, nth and last', async () => {
|
|
162
|
+
const { client } = stubClient();
|
|
163
|
+
const refs = sampleRefs();
|
|
164
|
+
expect((await findByPlaceholder(client, 'S1', refs, 'Optional notes', { exact: true }))?.ref).toBe('e5');
|
|
165
|
+
expect((await findByPlaceholder(client, 'S1', refs, 'o', { nth: 1 }))?.ref).toBe('e4');
|
|
166
|
+
expect((await findByPlaceholder(client, 'S1', refs, 'o', { last: true }))?.ref).toBe('e5');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('findBySelector', () => {
|
|
171
|
+
const doc = { root: { nodeId: 1 } };
|
|
172
|
+
|
|
173
|
+
it('reuses an existing snapshot ref when the backend node already maps to one', async () => {
|
|
174
|
+
const { client } = stubClient({
|
|
175
|
+
'DOM.getDocument': doc,
|
|
176
|
+
'DOM.querySelector': { nodeId: 55 },
|
|
177
|
+
'DOM.describeNode': { node: { backendNodeId: 11 } },
|
|
178
|
+
});
|
|
179
|
+
const refs = sampleRefs();
|
|
180
|
+
const found = await findBySelector(client, 'S1', refs, 'button.save');
|
|
181
|
+
expect(found?.ref).toBe('e1');
|
|
182
|
+
expect(refs.size).toBe(5);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('mints a synthetic ref for an element absent from the AX tree and registers it', async () => {
|
|
186
|
+
const { client } = stubClient({
|
|
187
|
+
'DOM.getDocument': doc,
|
|
188
|
+
'DOM.querySelector': { nodeId: 55 },
|
|
189
|
+
'DOM.describeNode': { node: { backendNodeId: 999 } },
|
|
190
|
+
});
|
|
191
|
+
const refs = sampleRefs();
|
|
192
|
+
const found = await findBySelector(client, 'S1', refs, 'div.hidden');
|
|
193
|
+
expect(found).toMatchObject({ ref: 'sel-999', role: 'generic', name: 'div.hidden', nodeId: 55 });
|
|
194
|
+
expect(refs.get('sel-999')).toBe(found);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('returns null when the selector matches nothing', async () => {
|
|
198
|
+
const { client } = stubClient({ 'DOM.getDocument': doc, 'DOM.querySelector': { nodeId: 0 } });
|
|
199
|
+
expect(await findBySelector(client, 'S1', sampleRefs(), '.missing')).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('returns null when describeNode yields no backend id', async () => {
|
|
203
|
+
const { client } = stubClient({
|
|
204
|
+
'DOM.getDocument': doc,
|
|
205
|
+
'DOM.querySelector': { nodeId: 55 },
|
|
206
|
+
'DOM.describeNode': { node: {} },
|
|
207
|
+
});
|
|
208
|
+
expect(await findBySelector(client, 'S1', sampleRefs(), 'div')).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('switches to querySelectorAll for nth and picks the 1-based entry', async () => {
|
|
212
|
+
const { client, calls } = stubClient({
|
|
213
|
+
'DOM.getDocument': doc,
|
|
214
|
+
'DOM.querySelectorAll': { nodeIds: [10, 20, 30] },
|
|
215
|
+
'DOM.describeNode': { node: { backendNodeId: 999 } },
|
|
216
|
+
});
|
|
217
|
+
await findBySelector(client, 'S1', sampleRefs(), 'li', { nth: 2 });
|
|
218
|
+
expect(calls.map((c) => c.method)).toContain('DOM.querySelectorAll');
|
|
219
|
+
expect(calls.find((c) => c.method === 'DOM.describeNode')!.params).toEqual({ nodeId: 20 });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('last picks the final nodeId', async () => {
|
|
223
|
+
const { client, calls } = stubClient({
|
|
224
|
+
'DOM.getDocument': doc,
|
|
225
|
+
'DOM.querySelectorAll': { nodeIds: [10, 20, 30] },
|
|
226
|
+
'DOM.describeNode': { node: { backendNodeId: 999 } },
|
|
227
|
+
});
|
|
228
|
+
await findBySelector(client, 'S1', sampleRefs(), 'li', { last: true });
|
|
229
|
+
expect(calls.find((c) => c.method === 'DOM.describeNode')!.params).toEqual({ nodeId: 30 });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('returns null when querySelectorAll finds nothing', async () => {
|
|
233
|
+
const { client } = stubClient({
|
|
234
|
+
'DOM.getDocument': doc,
|
|
235
|
+
'DOM.querySelectorAll': { nodeIds: [] },
|
|
236
|
+
});
|
|
237
|
+
expect(await findBySelector(client, 'S1', sampleRefs(), 'li', { nth: 1 })).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('rethrows a malformed-selector error instead of masking it as "not found"', async () => {
|
|
241
|
+
const { client } = stubClient({
|
|
242
|
+
'DOM.getDocument': doc,
|
|
243
|
+
'DOM.querySelector': new Error("SyntaxError: '((' is not a valid selector"),
|
|
244
|
+
});
|
|
245
|
+
await expect(findBySelector(client, 'S1', sampleRefs(), '((')).rejects.toThrow(/not a valid selector/);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('swallows transient CDP errors and reports not-found', async () => {
|
|
249
|
+
const { client } = stubClient({
|
|
250
|
+
'DOM.getDocument': doc,
|
|
251
|
+
'DOM.querySelector': new Error('Node with given id does not belong to the document'),
|
|
252
|
+
});
|
|
253
|
+
expect(await findBySelector(client, 'S1', sampleRefs(), 'div')).toBeNull();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('findByTestId', () => {
|
|
258
|
+
it('tries data-testid, then data-test-id, then data-test', async () => {
|
|
259
|
+
const seen: string[] = [];
|
|
260
|
+
const client = {
|
|
261
|
+
send: vi.fn(async (_m: string, params: { expression: string }) => {
|
|
262
|
+
seen.push(params.expression);
|
|
263
|
+
// Only the third form exists on this page.
|
|
264
|
+
return { result: { value: seen.length === 3 } };
|
|
265
|
+
}),
|
|
266
|
+
} as unknown as CdpClient;
|
|
267
|
+
await expect(findByTestId(client, 'S1', 'submit')).resolves.toBe('[data-test="submit"]');
|
|
268
|
+
expect(seen).toHaveLength(3);
|
|
269
|
+
expect(seen.map((e) => JSON.parse(e.slice('!!document.querySelector('.length, -1)))).toEqual([
|
|
270
|
+
'[data-testid="submit"]',
|
|
271
|
+
'[data-test-id="submit"]',
|
|
272
|
+
'[data-test="submit"]',
|
|
273
|
+
]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('returns the first attribute form that matches', async () => {
|
|
277
|
+
const client = {
|
|
278
|
+
send: vi.fn(async () => ({ result: { value: true } })),
|
|
279
|
+
} as unknown as CdpClient;
|
|
280
|
+
await expect(findByTestId(client, 'S1', 'submit')).resolves.toBe('[data-testid="submit"]');
|
|
281
|
+
expect(client.send).toHaveBeenCalledTimes(1);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('returns null when no attribute form matches', async () => {
|
|
285
|
+
const client = { send: vi.fn(async () => ({ result: { value: false } })) } as unknown as CdpClient;
|
|
286
|
+
await expect(findByTestId(client, 'S1', 'nope')).resolves.toBeNull();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('escapes quotes and backslashes so the testId cannot break out of the attribute selector', async () => {
|
|
290
|
+
const seen: string[] = [];
|
|
291
|
+
const client = {
|
|
292
|
+
send: vi.fn(async (_m: string, params: { expression: string }) => {
|
|
293
|
+
seen.push(params.expression);
|
|
294
|
+
return { result: { value: false } };
|
|
295
|
+
}),
|
|
296
|
+
} as unknown as CdpClient;
|
|
297
|
+
await findByTestId(client, 'S1', 'a"]:has(script)\\b');
|
|
298
|
+
// The selector is JSON-embedded, so parsing it back must recover an
|
|
299
|
+
// attribute selector whose value is the raw testId with escapes intact.
|
|
300
|
+
const literal = seen[0]!.slice('!!document.querySelector('.length, -1);
|
|
301
|
+
expect(JSON.parse(literal)).toBe('[data-testid="a\\"]:has(script)\\\\b"]');
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('element state helpers', () => {
|
|
306
|
+
it('isEnabled reads the snapshot flag without a round trip', async () => {
|
|
307
|
+
const { client, calls } = stubClient();
|
|
308
|
+
await expect(isEnabled(client, 'S1', ref({ ref: 'e1' }))).resolves.toBe(true);
|
|
309
|
+
await expect(isEnabled(client, 'S1', ref({ ref: 'e5', disabled: true }))).resolves.toBe(false);
|
|
310
|
+
expect(calls).toEqual([]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('isVisible / isChecked report false for a ref with no backend node', async () => {
|
|
314
|
+
const { client, calls } = stubClient();
|
|
315
|
+
const stale = ref({ ref: 'gone' });
|
|
316
|
+
await expect(isVisible(client, 'S1', stale)).resolves.toBe(false);
|
|
317
|
+
await expect(isChecked(client, 'S1', stale)).resolves.toBe(false);
|
|
318
|
+
expect(calls).toEqual([]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('isVisible / isChecked report false when the node no longer resolves', async () => {
|
|
322
|
+
const { client } = stubClient({ 'DOM.resolveNode': { object: {} } });
|
|
323
|
+
const r = ref({ ref: 'e1', backendDOMNodeId: 11 });
|
|
324
|
+
await expect(isVisible(client, 'S1', r)).resolves.toBe(false);
|
|
325
|
+
await expect(isChecked(client, 'S1', r)).resolves.toBe(false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('isVisible returns the page-side boolean via callFunctionOn', async () => {
|
|
329
|
+
const { client, calls } = stubClient({
|
|
330
|
+
'DOM.resolveNode': { object: { objectId: 'OBJ1' } },
|
|
331
|
+
'Runtime.callFunctionOn': { result: { value: true } },
|
|
332
|
+
});
|
|
333
|
+
await expect(isVisible(client, 'S1', ref({ ref: 'e1', backendDOMNodeId: 11 }))).resolves.toBe(true);
|
|
334
|
+
expect(calls.find((c) => c.method === 'Runtime.callFunctionOn')!.params).toMatchObject({
|
|
335
|
+
objectId: 'OBJ1',
|
|
336
|
+
returnByValue: true,
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('isChecked coerces a missing value to false', async () => {
|
|
341
|
+
const { client } = stubClient({
|
|
342
|
+
'DOM.resolveNode': { object: { objectId: 'OBJ1' } },
|
|
343
|
+
'Runtime.callFunctionOn': { result: {} },
|
|
344
|
+
});
|
|
345
|
+
await expect(isChecked(client, 'S1', ref({ ref: 'e1', backendDOMNodeId: 11 }))).resolves.toBe(false);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('scrollIntoView throws a ref-named error when the node is gone', async () => {
|
|
349
|
+
const { client } = stubClient({ 'DOM.resolveNode': { object: {} } });
|
|
350
|
+
await expect(
|
|
351
|
+
scrollIntoView(client, 'S1', ref({ ref: 'e1', backendDOMNodeId: 11 }))
|
|
352
|
+
).rejects.toThrow('Cannot scroll: ref @e1 not found in DOM');
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -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');
|