@ultimat3/testing 22.8.0 → 22.8.2
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 +27 -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.8.
|
|
3
|
+
"version": "22.8.2",
|
|
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.8.
|
|
39
|
-
"@ultimat3/cache": "22.8.
|
|
40
|
-
"@ultimat3/core": "22.8.
|
|
41
|
-
"@ultimat3/db": "22.8.
|
|
42
|
-
"@ultimat3/entity": "22.8.
|
|
43
|
-
"@ultimat3/i18n": "22.8.
|
|
44
|
-
"@ultimat3/jobs": "22.8.
|
|
45
|
-
"@ultimat3/mail": "22.8.
|
|
46
|
-
"@ultimat3/policy": "22.8.
|
|
47
|
-
"@ultimat3/query": "22.8.
|
|
48
|
-
"@ultimat3/realtime": "22.8.
|
|
49
|
-
"@ultimat3/render": "22.8.
|
|
50
|
-
"@ultimat3/time": "22.8.
|
|
38
|
+
"@ultimat3/action": "22.8.2",
|
|
39
|
+
"@ultimat3/cache": "22.8.2",
|
|
40
|
+
"@ultimat3/core": "22.8.2",
|
|
41
|
+
"@ultimat3/db": "22.8.2",
|
|
42
|
+
"@ultimat3/entity": "22.8.2",
|
|
43
|
+
"@ultimat3/i18n": "22.8.2",
|
|
44
|
+
"@ultimat3/jobs": "22.8.2",
|
|
45
|
+
"@ultimat3/mail": "22.8.2",
|
|
46
|
+
"@ultimat3/policy": "22.8.2",
|
|
47
|
+
"@ultimat3/query": "22.8.2",
|
|
48
|
+
"@ultimat3/realtime": "22.8.2",
|
|
49
|
+
"@ultimat3/render": "22.8.2",
|
|
50
|
+
"@ultimat3/time": "22.8.2"
|
|
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
|
@@ -44,6 +44,13 @@ export interface E2eTab extends E2eBrowserPage {
|
|
|
44
44
|
* with `evaluate('location.href = …')` and poll with `waitFor`.
|
|
45
45
|
*/
|
|
46
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>;
|
|
47
54
|
/** Reload and wait for the load, at the url the tab already had. */
|
|
48
55
|
reload(): Promise<void>;
|
|
49
56
|
/** Poll `expression` in the page until it is truthy, or refuse naming `what`. */
|
|
@@ -166,6 +173,26 @@ export function cdpE2eTab(options: CdpE2eTabOptions): E2eTab {
|
|
|
166
173
|
});
|
|
167
174
|
}
|
|
168
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
|
+
},
|
|
169
196
|
offline: (enabled: boolean) => options.offline(enabled),
|
|
170
197
|
async scripting(enabled: boolean): Promise<void> {
|
|
171
198
|
await send('Emulation.setScriptExecutionDisabled', { value: !enabled }, sessionId);
|
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();
|