@ai-ecoverse/slicc-shared-web 1.10.0 → 1.10.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/README.md +10 -3
- package/harness/cdp.mjs +43 -11
- package/harness/chrome.mjs +112 -17
- package/harness/extensions.mjs +7 -0
- package/harness/recorder.mjs +4 -3
- package/harness/server.mjs +5 -1
- package/harness/util.mjs +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ The hooks expect two scripts in each repo: `test:unit`, which writes `coverage/u
|
|
|
31
31
|
|
|
32
32
|
## Integration-test harness
|
|
33
33
|
|
|
34
|
-
`harness/` drives playwright-core's Chromium over raw CDP
|
|
34
|
+
`harness/` drives playwright-core's Chromium over raw CDP on `--remote-debugging-pipe`, as its only client, from `node --test`. Every page, service worker, shared worker and dedicated worker, nested ones included, starts paused, gets coverage and the CPU profiler switched on in the same tick, and only then runs. Each test gets its own browser context and leaves `artifacts/<suite>/<test>/` behind: one CPU profile per target and snapshot, a screenshot per tab and a `console.log` of every target. Lines logged before the first test, such as an extension service worker's at startup, go into the first test's `console.log`. The global teardown merges the raw coverage into `coverage/` (console table, lcov, V8 HTML) and writes `artifacts/hotspots.md`, the 15 frames with the most self time in the repo's own scripts.
|
|
35
35
|
|
|
36
36
|
`playwright-core` and `monocart-coverage-reports` are peer dependencies; pin both in the repo's `devDependencies`. A repo describes what to serve and launches the browser once per test file:
|
|
37
37
|
|
|
@@ -60,12 +60,12 @@ test('boots', async (t) => {
|
|
|
60
60
|
| `coverage` | URL prefixes whose `.js`/`.mjs` scripts count for coverage. `node_modules/` never does. A `.map` next to a file is used | `['/']` |
|
|
61
61
|
| `exits` | `{ urlPath: [regex, ...] }`: a breakpoint on the first line matching each pattern in that worker script, where the harness takes the worker's coverage and profile before it closes itself | none |
|
|
62
62
|
| `intercept` | Remote URL prefixes answered from a disk cache in `node_modules/.cache/`. Registry documents (anything on a `registry.*` host but a `/-/….tgz` tarball: packuments, abbreviated or full, and dist-tags) are fetched fresh once per test process and only served from the disk cache when the registry can't be reached (a `4xx` is passed on) | none |
|
|
63
|
-
| `extensions` | Unpacked extension directories to load. Their scripts are covered and profiled like the repo's own, and tests then run in the default browser context, closing their tabs when they end, because Chrome does not run extensions in the per-test contexts | none |
|
|
63
|
+
| `extensions` | Unpacked extension directories to load. They are loaded with `Extensions.loadUnpacked` (behind `--enable-unsafe-extension-debugging`) once the harness is attached, so their service workers start paused like every other target. Their scripts are covered and profiled like the repo's own, and tests then run in the default browser context, closing their tabs when they end, because Chrome does not run extensions in the per-test contexts | none |
|
|
64
64
|
| `args` | Extra Chromium flags, such as `--host-resolver-rules` or `--ignore-certificate-errors` | none |
|
|
65
65
|
| `timeout` | How long `page.until` polls and `page.evaluate` waits, in ms | `30000` |
|
|
66
66
|
| `stallAfter` | How long one evaluation may run before the harness probes the test's targets, in ms | `30000` |
|
|
67
67
|
|
|
68
|
-
`chrome.page(t)` opens a tab and returns the page API: `goto(path)`, `reload()`, `evaluate(fn, ...args)`, `until(fn, ...args)`, `press(key, ...modifiers)` (a key name like `Enter` or `F5`, or a single character typed with its US-layout key code), `type(text)`, `insert(text)`, `enter()`, `init(fn)` (runs in every new document), `expose(name, handler)` (a binding the page calls with a JSON string), `screenshot(file)`, `tab()` (another tab in the same context), `close()`, `send(method, params, ms)` (a raw CDP call in the tab's session), `errors`, `responses` and `dir` (the test's artifact directory). `chrome.pid` is the browser's process id. `goto` and `reload` wait until the new document is complete and no longer `about:blank`, and retry the navigation once if that never happens. `until` polls until the function returns a truthy value (not only strict `true`) and resolves with that value; the timeout error names the last result. `within(ms, fn, ...args)` is `until` with its own deadline, for a stage that should take a known time. Both stop at the deadline even if one evaluation never answers. Every CDP call has a deadline: `connect(url).send(method, params, sessionId, ms)` rejects with `<method>: no answer in <ms> ms` (60 s unless given), so a stuck page fails its test instead of hanging the run. A crash fails it at once instead:
|
|
68
|
+
`chrome.page(t)` opens a tab and returns the page API: `goto(path)`, `reload()`, `evaluate(fn, ...args)`, `until(fn, ...args)`, `press(key, ...modifiers)` (a key name like `Enter` or `F5`, or a single character typed with its US-layout key code), `type(text)`, `insert(text)`, `enter()`, `init(fn)` (runs in every new document), `expose(name, handler)` (a binding the page calls with a JSON string), `screenshot(file)`, `tab()` (another tab in the same context), `close()`, `send(method, params, ms)` (a raw CDP call in the tab's session), `errors`, `responses` and `dir` (the test's artifact directory). `chrome.pid` is the browser's process id and `chrome.profile` its `slicc-harness-*` profile directory in `$TMPDIR`. `goto` and `reload` wait until the new document is complete and no longer `about:blank`, and retry the navigation once if that never happens. `until` polls until the function returns a truthy value (not only strict `true`) and resolves with that value; the timeout error names the last result. `within(ms, fn, ...args)` is `until` with its own deadline, for a stage that should take a known time. Both stop at the deadline even if one evaluation never answers. Every CDP call has a deadline: `connect(url).send(method, params, sessionId, ms)` rejects with `<method>: no answer in <ms> ms` (60 s unless given), so a stuck page fails its test instead of hanging the run. A crash fails it at once instead:
|
|
69
69
|
- when the browser dies, every pending and later call rejects with `<method>: browser exited with <signal>` (or `with code <n>`);
|
|
70
70
|
- when a renderer crashes, every pending and later call to that target, and to the dedicated workers it started, rejects with `<method>: page renderer crashed (<status>, code <n>) at <url>` (or `worker`, `shared worker`, `service worker`), and `console.log` gets the same line;
|
|
71
71
|
- `until`, `within`, `goto` and `reload` stop retrying and rethrow it;
|
|
@@ -96,6 +96,13 @@ Why it works the way it does:
|
|
|
96
96
|
- A worker blocked in `Atomics.wait`, or one that is already gone, may never answer; snapshots give up after 3 s, and closing a context or the browser after 10 s. A call to a target that detaches or crashes fails instead of waiting forever, and a new tab that never attaches fails after 10 s.
|
|
97
97
|
- A WebSocket that has closed takes new messages without an error, and a crashed target never answers, so after a crash every call used to wait out its own deadline: a browser that died 4 s in showed up as `no answer` 15 minutes later. The harness now remembers the crash or the exit and fails each call with it.
|
|
98
98
|
|
|
99
|
+
## Cleanup
|
|
100
|
+
|
|
101
|
+
`close()` stops Chrome and removes its profile. It asks Chrome to close, waits up to 10 s for the exit, sends `SIGKILL`, waits up to 5 s more and then moves on with a warning, so a browser that ignores `Browser.close` or a missed exit never hangs teardown. A test process that ends without `close()` still leaves nothing running:
|
|
102
|
+
- on `exit` and on `SIGINT`, `SIGTERM` or `SIGHUP`, the harness kills every browser it started in that process with `SIGKILL` and removes its profile and crash dump directory, then re-raises the signal so the process ends the way it would have;
|
|
103
|
+
- when the test process dies of `SIGKILL`, no handler runs. A watchdog, a `sh` that reads a pipe from the test process, then gets end of file and kills Chrome with `SIGKILL` at once. Chrome also shuts itself down when the CDP pipe closes, but that can take seconds. Only the profile stays behind;
|
|
104
|
+
- the first `launch()` in a process removes every `slicc-harness-*` directory in `$TMPDIR` that is older than a day, or older than a minute and no running process uses as its `--user-data-dir`. It never kills a process.
|
|
105
|
+
|
|
99
106
|
## Crash dumps
|
|
100
107
|
|
|
101
108
|
Chrome for Testing runs crashpad by default and ignores `--crash-dumps-dir` and `--enable-crash-reporter`: on Linux it writes every dump to `~/.config/google-chrome-for-testing/Crash Reports/pending/`. `launch` points it at a fresh directory per browser through `BREAKPAD_DUMP_LOCATION`, waits up to 10 s after a crash for crashpad to finish writing, copies the new dumps into the test's artifact directory and deletes the directory on `close()`. Nothing is uploaded.
|
package/harness/cdp.mjs
CHANGED
|
@@ -17,12 +17,42 @@ function grace(ms) {
|
|
|
17
17
|
return new Promise((resolve) => setTimeout(resolve, ms, 'socket closed').unref());
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
async function websocket(url) {
|
|
21
21
|
const socket = new WebSocket(url);
|
|
22
22
|
await new Promise((resolve, reject) => {
|
|
23
23
|
socket.onopen = resolve;
|
|
24
24
|
socket.onerror = reject;
|
|
25
25
|
});
|
|
26
|
+
return {
|
|
27
|
+
send: (text) => socket.send(text),
|
|
28
|
+
close: () => socket.close(),
|
|
29
|
+
listen(message, closed) {
|
|
30
|
+
socket.onmessage = ({ data }) => message(data);
|
|
31
|
+
socket.onclose = closed;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function pipe(input, output) {
|
|
37
|
+
input.on('error', () => null);
|
|
38
|
+
return {
|
|
39
|
+
send: (text) => input.write(`${text}\0`),
|
|
40
|
+
close: () => input.end(),
|
|
41
|
+
listen(message, closed) {
|
|
42
|
+
let partial = '';
|
|
43
|
+
output.setEncoding('utf8');
|
|
44
|
+
output.on('data', (chunk) => {
|
|
45
|
+
const parts = `${partial}${chunk}`.split('\0');
|
|
46
|
+
partial = parts.pop();
|
|
47
|
+
for (const part of parts) message(part);
|
|
48
|
+
});
|
|
49
|
+
output.once('close', closed);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function connect(url, { exited, tail = () => [] } = {}) {
|
|
55
|
+
const socket = typeof url?.listen === 'function' ? url : await websocket(url);
|
|
26
56
|
const pending = new Map();
|
|
27
57
|
const listeners = new Set();
|
|
28
58
|
const targets = new Map();
|
|
@@ -79,16 +109,18 @@ export async function connect(url, { exited, tail = () => [] } = {}) {
|
|
|
79
109
|
gone = reason;
|
|
80
110
|
drop(() => true, told(reason), fatal);
|
|
81
111
|
};
|
|
82
|
-
socket.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
112
|
+
socket.listen(
|
|
113
|
+
(data) => {
|
|
114
|
+
const message = JSON.parse(data);
|
|
115
|
+
track(message);
|
|
116
|
+
const call = pending.get(message.id);
|
|
117
|
+
pending.delete(message.id);
|
|
118
|
+
if (!call) notify(message);
|
|
119
|
+
else if (message.error) call.reject(new Error(`${call.method}: ${message.error.message}`));
|
|
120
|
+
else call.resolve(message.result);
|
|
121
|
+
},
|
|
122
|
+
async () => die(await Promise.race([exited ?? 'socket closed', grace(2000)]))
|
|
123
|
+
);
|
|
92
124
|
void exited?.then(die);
|
|
93
125
|
return {
|
|
94
126
|
send: (method, params = {}, sessionId, ms = SEND_TIMEOUT) =>
|
package/harness/chrome.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { rmSync } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { basename, join } from 'node:path';
|
|
5
6
|
import { env as inherited } from 'node:process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
6
8
|
import { chromium } from 'playwright-core';
|
|
7
9
|
import { cdn } from './cdn.mjs';
|
|
8
|
-
import { CRASHED, connect } from './cdp.mjs';
|
|
10
|
+
import { CRASHED, connect, pipe } from './cdp.mjs';
|
|
9
11
|
import { crashpad } from './dumps.mjs';
|
|
10
|
-
import { extensionSource, unpacked } from './extensions.mjs';
|
|
12
|
+
import { extensionSource, install, unpacked } from './extensions.mjs';
|
|
11
13
|
import { page } from './page.mjs';
|
|
12
14
|
import { breakpoints, recorder } from './recorder.mjs';
|
|
13
15
|
import { serve } from './server.mjs';
|
|
@@ -16,6 +18,7 @@ import { artifacts, bounded, ignore, sleep, slug, TIMED_OUT } from './util.mjs';
|
|
|
16
18
|
export const flags = [
|
|
17
19
|
'--headless',
|
|
18
20
|
'--remote-debugging-port=0',
|
|
21
|
+
'--remote-debugging-pipe',
|
|
19
22
|
'--no-first-run',
|
|
20
23
|
'--no-default-browser-check',
|
|
21
24
|
'--no-sandbox',
|
|
@@ -32,9 +35,7 @@ export const flags = [
|
|
|
32
35
|
export function commandLine(profile, extensions = [], extra = []) {
|
|
33
36
|
const loaded = extensions.length > 0;
|
|
34
37
|
const base = loaded ? flags.filter((flag) => flag !== '--disable-extensions') : flags;
|
|
35
|
-
const load = loaded
|
|
36
|
-
? [`--disable-extensions-except=${extensions}`, `--load-extension=${extensions}`]
|
|
37
|
-
: [];
|
|
38
|
+
const load = loaded ? ['--enable-unsafe-extension-debugging'] : [];
|
|
38
39
|
return [...base, ...load, ...extra, `--user-data-dir=${profile}`, 'about:blank'];
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -71,7 +72,10 @@ export async function start(
|
|
|
71
72
|
executable = chromium.executablePath(),
|
|
72
73
|
env = inherited
|
|
73
74
|
) {
|
|
74
|
-
const child = spawn(executable, args, {
|
|
75
|
+
const child = spawn(executable, args, {
|
|
76
|
+
env,
|
|
77
|
+
stdio: ['ignore', 'ignore', 'pipe', 'pipe', 'pipe'],
|
|
78
|
+
});
|
|
75
79
|
const stderr = lines(child.stderr);
|
|
76
80
|
const drained = new Promise((resolve) => child.stderr.once('close', resolve));
|
|
77
81
|
const exited = new Promise((resolve) => {
|
|
@@ -99,17 +103,102 @@ export function died(child) {
|
|
|
99
103
|
return (child.exitCode ?? 0) !== 0;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
export
|
|
106
|
+
export const WAITS = { close: 10000, exit: 10000, kill: 5000 };
|
|
107
|
+
|
|
108
|
+
export async function stop(cdp, child, waits = WAITS) {
|
|
109
|
+
const gone = () => child.exitCode !== null || child.signalCode !== null;
|
|
103
110
|
const exited = new Promise((resolve) => child.once('exit', resolve));
|
|
104
|
-
await bounded(cdp.send('Browser.close').catch(ignore), 'Browser.close');
|
|
111
|
+
await bounded(cdp.send('Browser.close').catch(ignore), 'Browser.close', waits.close);
|
|
105
112
|
cdp.close();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
if (gone() || (await bounded(exited, 'exit', waits.exit)) !== TIMED_OUT || gone()) return;
|
|
114
|
+
child.kill('SIGKILL');
|
|
115
|
+
if (gone() || (await bounded(exited, 'exit after SIGKILL', waits.kill)) !== TIMED_OUT) return;
|
|
116
|
+
if (!gone()) console.warn(`chrome: pid ${child.pid} still runs after SIGKILL, moving on`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const PREFIX = 'slicc-harness-';
|
|
120
|
+
export const DAY = 24 * 60 * 60 * 1000;
|
|
121
|
+
export const GRACE = 60 * 1000;
|
|
122
|
+
|
|
123
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
124
|
+
const running = new Set();
|
|
125
|
+
|
|
126
|
+
function kill(child) {
|
|
127
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function reap() {
|
|
131
|
+
for (const { children, paths } of running) {
|
|
132
|
+
for (const child of children) kill(child);
|
|
133
|
+
for (const path of paths) rmSync(path, { recursive: true, force: true });
|
|
110
134
|
}
|
|
135
|
+
running.clear();
|
|
136
|
+
unhook();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function reraise(signal) {
|
|
140
|
+
reap();
|
|
141
|
+
process.kill(process.pid, signal);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hook() {
|
|
145
|
+
process.on('exit', reap);
|
|
146
|
+
for (const signal of signals) process.on(signal, reraise);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function unhook() {
|
|
150
|
+
process.off('exit', reap);
|
|
151
|
+
for (const signal of signals) process.off(signal, reraise);
|
|
111
152
|
}
|
|
112
153
|
|
|
154
|
+
export function watchdog(pid) {
|
|
155
|
+
const dog = spawn('sh', ['-c', 'read -r _; kill -9 "$0" 2>/dev/null', String(pid)], {
|
|
156
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
157
|
+
});
|
|
158
|
+
dog.on('error', ignore);
|
|
159
|
+
dog.stdin.on('error', ignore);
|
|
160
|
+
return dog;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function track(child, paths) {
|
|
164
|
+
const dog = watchdog(child.pid);
|
|
165
|
+
const entry = { children: [child, dog], paths };
|
|
166
|
+
if (running.size === 0) hook();
|
|
167
|
+
running.add(entry);
|
|
168
|
+
return () => {
|
|
169
|
+
running.delete(entry);
|
|
170
|
+
if (running.size === 0) unhook();
|
|
171
|
+
kill(dog);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function commandLines() {
|
|
176
|
+
const { stdout } = await promisify(execFile)('ps', ['axww', '-o', 'args='], {
|
|
177
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
178
|
+
});
|
|
179
|
+
return stdout.split('\n').map((line) => `${line} `);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function prune(dir = tmpdir(), grace = GRACE) {
|
|
183
|
+
const lines = await commandLines().catch(ignore);
|
|
184
|
+
const used = (path) => lines.some((line) => line.includes(`--user-data-dir=${path} `));
|
|
185
|
+
const names = await readdir(dir).catch(() => []);
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
const removing = names
|
|
188
|
+
.filter((name) => name.startsWith(PREFIX))
|
|
189
|
+
.map(async (name) => {
|
|
190
|
+
const path = join(dir, name);
|
|
191
|
+
const info = await lstat(path).catch(ignore);
|
|
192
|
+
if (!info?.isDirectory()) return;
|
|
193
|
+
const age = now - info.mtimeMs;
|
|
194
|
+
const orphaned = lines !== null && age > grace && !used(path);
|
|
195
|
+
if (age > DAY || orphaned) await rm(path, { recursive: true, force: true }).catch(ignore);
|
|
196
|
+
});
|
|
197
|
+
await Promise.all(removing);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let pruning = null;
|
|
201
|
+
|
|
113
202
|
export async function launch({
|
|
114
203
|
roots,
|
|
115
204
|
aliases,
|
|
@@ -122,21 +211,24 @@ export async function launch({
|
|
|
122
211
|
timeout,
|
|
123
212
|
stallAfter,
|
|
124
213
|
} = {}) {
|
|
214
|
+
pruning ??= prune();
|
|
215
|
+
await pruning;
|
|
125
216
|
const server = await serve({ roots, aliases, isolated });
|
|
126
217
|
const found = await unpacked(extensions);
|
|
127
218
|
const fromExtension = extensionSource(found);
|
|
128
219
|
const files = { ...server, source: (href) => server.source(href) ?? fromExtension(href) };
|
|
129
220
|
const shared = extensions.length > 0;
|
|
130
|
-
const profile = await mkdtemp(join(tmpdir(),
|
|
221
|
+
const profile = await mkdtemp(join(tmpdir(), PREFIX));
|
|
131
222
|
const loaded = [...found.values()].map((root) => root.slice(0, -1));
|
|
132
223
|
const dumps = await crashpad();
|
|
133
|
-
const { child,
|
|
224
|
+
const { child, exited, stderr } = await start(
|
|
134
225
|
profile,
|
|
135
226
|
commandLine(profile, loaded, args),
|
|
136
227
|
chromium.executablePath(),
|
|
137
228
|
dumps.env
|
|
138
229
|
);
|
|
139
|
-
const
|
|
230
|
+
const untrack = track(child, [profile, dumps.dir]);
|
|
231
|
+
const cdp = await connect(pipe(child.stdio[3], child.stdio[4]), {
|
|
140
232
|
exited,
|
|
141
233
|
tail: () => stderr.tail().map((line) => `stderr: ${line}`),
|
|
142
234
|
});
|
|
@@ -154,6 +246,7 @@ export async function launch({
|
|
|
154
246
|
waitForDebuggerOnStart: true,
|
|
155
247
|
flatten: true,
|
|
156
248
|
});
|
|
249
|
+
await install(cdp, found);
|
|
157
250
|
|
|
158
251
|
async function tabs() {
|
|
159
252
|
const { targetInfos } = await cdp.send('Target.getTargets');
|
|
@@ -189,6 +282,7 @@ export async function launch({
|
|
|
189
282
|
return {
|
|
190
283
|
url: server.url,
|
|
191
284
|
pid: child.pid,
|
|
285
|
+
profile,
|
|
192
286
|
cdn: remote.state,
|
|
193
287
|
requests: server.requests,
|
|
194
288
|
overrides: server.overrides,
|
|
@@ -223,6 +317,7 @@ export async function launch({
|
|
|
223
317
|
async close() {
|
|
224
318
|
await stop(cdp, child);
|
|
225
319
|
await rm(profile, { recursive: true, force: true });
|
|
320
|
+
untrack();
|
|
226
321
|
closed = true;
|
|
227
322
|
if (open === 0) await dumps.remove();
|
|
228
323
|
await server.close();
|
package/harness/extensions.mjs
CHANGED
|
@@ -26,3 +26,10 @@ export function extensionSource(found) {
|
|
|
26
26
|
return root ? join(root, normalize(decodeURIComponent(pathname))) : null;
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
export async function install(cdp, found) {
|
|
31
|
+
for (const [expected, root] of [...found]) {
|
|
32
|
+
const { id } = await cdp.send('Extensions.loadUnpacked', { path: root.slice(0, -1) });
|
|
33
|
+
if (id !== expected) found.set(id, root);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/harness/recorder.mjs
CHANGED
|
@@ -125,6 +125,7 @@ export function recorder(cdp, server, { coverage = ['/'], exits = new Map() } =
|
|
|
125
125
|
const pauses = new Map();
|
|
126
126
|
const parents = new Map();
|
|
127
127
|
const tabs = new Map();
|
|
128
|
+
const early = [];
|
|
128
129
|
const ours = covered(server, coverage);
|
|
129
130
|
let run = null;
|
|
130
131
|
|
|
@@ -204,8 +205,8 @@ export function recorder(cdp, server, { coverage = ['/'], exits = new Map() } =
|
|
|
204
205
|
|
|
205
206
|
function log(sessionId, line) {
|
|
206
207
|
const target = sessions.get(sessionId);
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
const into = run ? target?.browserContextId === run.context && run.console : target && early;
|
|
209
|
+
if (into) into.push(`${label(target)}: ${line}`);
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
async function capture(sessionId, target) {
|
|
@@ -274,7 +275,7 @@ export function recorder(cdp, server, { coverage = ['/'], exits = new Map() } =
|
|
|
274
275
|
}
|
|
275
276
|
|
|
276
277
|
function begin(context, dir, name) {
|
|
277
|
-
run = { context, dir, name, scripts: [], console:
|
|
278
|
+
run = { context, dir, name, scripts: [], console: early.splice(0), profiles: 0, self: {} };
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
async function end() {
|
package/harness/server.mjs
CHANGED
|
@@ -78,6 +78,10 @@ export async function serve({
|
|
|
78
78
|
locate,
|
|
79
79
|
source: (href) =>
|
|
80
80
|
href.startsWith(url) ? lookup(decodeURIComponent(new URL(href).pathname)) : null,
|
|
81
|
-
close: () =>
|
|
81
|
+
close: () =>
|
|
82
|
+
new Promise((done) => {
|
|
83
|
+
server.close(done);
|
|
84
|
+
server.closeAllConnections();
|
|
85
|
+
}),
|
|
82
86
|
};
|
|
83
87
|
}
|
package/harness/util.mjs
CHANGED
|
@@ -19,7 +19,11 @@ export function sleep(ms) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export async function bounded(promise, label, ms = 10000) {
|
|
22
|
-
|
|
22
|
+
let timer;
|
|
23
|
+
const expired = new Promise((resolve) => {
|
|
24
|
+
timer = setTimeout(resolve, ms, TIMED_OUT);
|
|
25
|
+
});
|
|
26
|
+
const result = await Promise.race([promise, expired]).finally(() => clearTimeout(timer));
|
|
23
27
|
if (result === TIMED_OUT) console.warn(`chrome: ${label} took over ${ms} ms, moving on`);
|
|
24
28
|
return result;
|
|
25
29
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-shared-web",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.2",
|
|
4
4
|
"description": "Shared Renovate, Biome, TypeScript, lefthook and GitHub Actions configuration, plus the CDP integration-test harness, for SLICC's web repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|