@ultimat3/testing 22.7.1 → 22.8.1
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/package.json +14 -14
- package/src/cdp-browser.ts +9 -0
- package/src/cdp-e2e-page.ts +39 -0
- package/src/cdp-launch.ts +27 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/testing",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.8.1",
|
|
4
4
|
"description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
"test": "bun test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ultimat3/action": "22.
|
|
39
|
-
"@ultimat3/cache": "22.
|
|
40
|
-
"@ultimat3/core": "22.
|
|
41
|
-
"@ultimat3/db": "22.
|
|
42
|
-
"@ultimat3/entity": "22.
|
|
43
|
-
"@ultimat3/i18n": "22.
|
|
44
|
-
"@ultimat3/jobs": "22.
|
|
45
|
-
"@ultimat3/mail": "22.
|
|
46
|
-
"@ultimat3/policy": "22.
|
|
47
|
-
"@ultimat3/query": "22.
|
|
48
|
-
"@ultimat3/realtime": "22.
|
|
49
|
-
"@ultimat3/render": "22.
|
|
50
|
-
"@ultimat3/time": "22.
|
|
38
|
+
"@ultimat3/action": "22.8.1",
|
|
39
|
+
"@ultimat3/cache": "22.8.1",
|
|
40
|
+
"@ultimat3/core": "22.8.1",
|
|
41
|
+
"@ultimat3/db": "22.8.1",
|
|
42
|
+
"@ultimat3/entity": "22.8.1",
|
|
43
|
+
"@ultimat3/i18n": "22.8.1",
|
|
44
|
+
"@ultimat3/jobs": "22.8.1",
|
|
45
|
+
"@ultimat3/mail": "22.8.1",
|
|
46
|
+
"@ultimat3/policy": "22.8.1",
|
|
47
|
+
"@ultimat3/query": "22.8.1",
|
|
48
|
+
"@ultimat3/realtime": "22.8.1",
|
|
49
|
+
"@ultimat3/render": "22.8.1",
|
|
50
|
+
"@ultimat3/time": "22.8.1"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/src/cdp-browser.ts
CHANGED
|
@@ -29,6 +29,11 @@ export interface E2eBrowser {
|
|
|
29
29
|
readonly session: E2eSession;
|
|
30
30
|
/** Idempotent, and it closes both halves: the CDP socket, then the process and its profile. */
|
|
31
31
|
close(): void;
|
|
32
|
+
/**
|
|
33
|
+
* `close()`, resolving once the process has exited (bounded — `CLOSE_GRACE_MS`, then SIGKILL).
|
|
34
|
+
* Optional so a test double need not model a process; the launched browser always has it.
|
|
35
|
+
*/
|
|
36
|
+
closed?(): Promise<void>;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export interface OpenE2eBrowserOptions {
|
|
@@ -55,6 +60,10 @@ const compose = (launched: LaunchedBrowser, session: E2eSession, page: E2eTab):
|
|
|
55
60
|
// the process out from under an open connection makes every in-flight call report "the browser
|
|
56
61
|
// closed the CDP connection", which is true and useless.
|
|
57
62
|
close: () => launched.close(),
|
|
63
|
+
closed: async () => {
|
|
64
|
+
if (launched.closed === undefined) launched.close();
|
|
65
|
+
else await launched.closed();
|
|
66
|
+
},
|
|
58
67
|
});
|
|
59
68
|
|
|
60
69
|
/**
|
package/src/cdp-e2e-page.ts
CHANGED
|
@@ -35,6 +35,22 @@ export interface E2eTab extends E2eBrowserPage {
|
|
|
35
35
|
readonly targetId: string;
|
|
36
36
|
/** Browser-wide, like the switch it models: every tab AND every worker goes with it. */
|
|
37
37
|
offline(enabled: boolean): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* THIS tab's scripting, off or back on (`Emulation.setScriptExecutionDisabled`): the page's own
|
|
40
|
+
* scripts stop. What proves a page works with no JS — a link that only a script could follow is
|
|
41
|
+
* a page that fails for every visitor whose JS did not load. Takes effect from the next document.
|
|
42
|
+
* Off, `evaluate` still answers a SYNCHRONOUS expression and `click` still clicks, but no page
|
|
43
|
+
* callback runs — so `goto`, which waits on the page's `load` listener, never settles: navigate
|
|
44
|
+
* with `evaluate('location.href = …')` and poll with `waitFor`.
|
|
45
|
+
*/
|
|
46
|
+
scripting(enabled: boolean): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* A REAL click: the pointer moved to the element's centre, pressed and released through
|
|
49
|
+
* `Input.dispatchMouseEvent`, so the browser hit-tests it as it would a person's — and anything
|
|
50
|
+
* painted over the element (a view transition's overlay, a modal) receives it instead. `click()`
|
|
51
|
+
* dispatches straight at the element and can never see that.
|
|
52
|
+
*/
|
|
53
|
+
pointerClick(selector: string): Promise<void>;
|
|
38
54
|
/** Reload and wait for the load, at the url the tab already had. */
|
|
39
55
|
reload(): Promise<void>;
|
|
40
56
|
/** Poll `expression` in the page until it is truthy, or refuse naming `what`. */
|
|
@@ -157,7 +173,30 @@ export function cdpE2eTab(options: CdpE2eTabOptions): E2eTab {
|
|
|
157
173
|
});
|
|
158
174
|
}
|
|
159
175
|
},
|
|
176
|
+
async pointerClick(selector: string): Promise<void> {
|
|
177
|
+
const at = await evaluate(
|
|
178
|
+
`(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return null; const r = el.getBoundingClientRect(); return [r.left + r.width / 2, r.top + r.height / 2]; })()`,
|
|
179
|
+
);
|
|
180
|
+
if (!Array.isArray(at) || typeof at[0] !== 'number' || typeof at[1] !== 'number') {
|
|
181
|
+
throw new CdpCallFailedError({
|
|
182
|
+
method: `pointerClick(${selector})`,
|
|
183
|
+
detail: 'no element in the page matches that selector',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const [x, y] = at;
|
|
187
|
+
for (const type of ['mouseMoved', 'mousePressed', 'mouseReleased'] as const) {
|
|
188
|
+
const pressed = type !== 'mouseMoved';
|
|
189
|
+
await send(
|
|
190
|
+
'Input.dispatchMouseEvent',
|
|
191
|
+
{ type, x, y, button: pressed ? 'left' : 'none', clickCount: pressed ? 1 : 0 },
|
|
192
|
+
sessionId,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
160
196
|
offline: (enabled: boolean) => options.offline(enabled),
|
|
197
|
+
async scripting(enabled: boolean): Promise<void> {
|
|
198
|
+
await send('Emulation.setScriptExecutionDisabled', { value: !enabled }, sessionId);
|
|
199
|
+
},
|
|
161
200
|
async reload(): Promise<void> {
|
|
162
201
|
const at = current;
|
|
163
202
|
// Reload is a navigation to the url the tab already has — the one wait the port proves.
|
package/src/cdp-launch.ts
CHANGED
|
@@ -86,6 +86,14 @@ export interface LaunchedBrowser {
|
|
|
86
86
|
readonly connection: CdpConnection;
|
|
87
87
|
/** Idempotent: closes the connection, kills the process, deletes the profile. */
|
|
88
88
|
close(): void;
|
|
89
|
+
/**
|
|
90
|
+
* `close()`, then resolves once the process has EXITED — SIGTERM, then SIGKILL after
|
|
91
|
+
* `CLOSE_GRACE_MS`, never waiting longer than twice that. What a suite's `afterAll` awaits: a
|
|
92
|
+
* Chrome told to die but still shutting down competes with the next suite's launch on a 4-CPU
|
|
93
|
+
* runner, and one still running keeps the test process from exiting at all. Optional so a test
|
|
94
|
+
* double of a launch need not model a process; `launchChrome` always provides it.
|
|
95
|
+
*/
|
|
96
|
+
closed?(): Promise<void>;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
export interface LaunchOptions {
|
|
@@ -125,6 +133,9 @@ function stderrTail(stream: ReadableStream<Uint8Array>): {
|
|
|
125
133
|
*/
|
|
126
134
|
const FAILURE_DRAIN_MS = 1_000;
|
|
127
135
|
|
|
136
|
+
/** How long a closed Chrome gets to exit on SIGTERM before it is killed outright. */
|
|
137
|
+
export const CLOSE_GRACE_MS = 5_000;
|
|
138
|
+
|
|
128
139
|
const within = (ms: number, work: Promise<unknown>): Promise<unknown> =>
|
|
129
140
|
Promise.race([work, Bun.sleep(ms)]);
|
|
130
141
|
|
|
@@ -163,9 +174,24 @@ export async function launchChrome(options: LaunchOptions): Promise<LaunchedBrow
|
|
|
163
174
|
child.kill();
|
|
164
175
|
rmSync(profileDir, { recursive: true, force: true });
|
|
165
176
|
};
|
|
177
|
+
let exiting: Promise<void> | undefined;
|
|
178
|
+
const closeAndWait = (): Promise<void> => {
|
|
179
|
+
close();
|
|
180
|
+
exiting ??= (async () => {
|
|
181
|
+
const exited =
|
|
182
|
+
(await within(
|
|
183
|
+
CLOSE_GRACE_MS,
|
|
184
|
+
child.exited.then(() => true),
|
|
185
|
+
)) === true;
|
|
186
|
+
if (exited) return;
|
|
187
|
+
child.kill('SIGKILL');
|
|
188
|
+
await within(CLOSE_GRACE_MS, child.exited);
|
|
189
|
+
})();
|
|
190
|
+
return exiting;
|
|
191
|
+
};
|
|
166
192
|
try {
|
|
167
193
|
await connection.send('Browser.getVersion');
|
|
168
|
-
return { connection, close };
|
|
194
|
+
return { connection, close, closed: closeAndWait };
|
|
169
195
|
} catch {
|
|
170
196
|
await within(FAILURE_DRAIN_MS, child.exited);
|
|
171
197
|
close();
|