@ai-ecoverse/slicc-shared-web 1.5.0 → 1.5.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/README.md +2 -2
- package/harness/cdn.mjs +15 -3
- package/harness/cdp.mjs +14 -3
- package/harness/page.mjs +21 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,9 +62,9 @@ test('boots', async (t) => {
|
|
|
62
62
|
| `intercept` | Remote URL prefixes answered from a disk cache in `node_modules/.cache/` | none |
|
|
63
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 |
|
|
64
64
|
| `args` | Extra Chromium flags, such as `--host-resolver-rules` or `--ignore-certificate-errors` | none |
|
|
65
|
-
| `timeout` | How long `page.until` polls, in ms | `30000` |
|
|
65
|
+
| `timeout` | How long `page.until` polls and `page.evaluate` waits, in ms | `30000` |
|
|
66
66
|
|
|
67
|
-
`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()`, `errors`, `responses` and `dir` (the test's artifact directory). `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. `chrome.requests` lists the paths the server saw in this test, `chrome.overrides` is a `Map` of paths to bodies served instead of files, and `chrome.cdn` takes `status` (answer every intercepted request with it), `corrupt` (flip the last byte) and lists the intercepted `requests`. All of them reset with each test. `serve(options)` starts the same server on its own, for an `npm start`.
|
|
67
|
+
`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()`, `errors`, `responses` and `dir` (the test's artifact directory). `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. Downloads of intercepted URLs time out after 60 s and are tried up to three times; a `4xx` is not retried. `chrome.requests` lists the paths the server saw in this test, `chrome.overrides` is a `Map` of paths to bodies served instead of files, and `chrome.cdn` takes `status` (answer every intercepted request with it), `corrupt` (flip the last byte) and lists the intercepted `requests`. All of them reset with each test. `serve(options)` starts the same server on its own, for an `npm start`.
|
|
68
68
|
|
|
69
69
|
Why it works the way it does:
|
|
70
70
|
|
package/harness/cdn.mjs
CHANGED
|
@@ -7,15 +7,27 @@ import { cache, ignore } from './util.mjs';
|
|
|
7
7
|
const cors = [{ name: 'access-control-allow-origin', value: '*' }];
|
|
8
8
|
const store = new URL('cdn/', cache);
|
|
9
9
|
|
|
10
|
+
export const DOWNLOAD_TIMEOUT = 60000;
|
|
11
|
+
|
|
12
|
+
export async function fetched(url, { attempts = 3, ms = DOWNLOAD_TIMEOUT, fetcher = fetch } = {}) {
|
|
13
|
+
for (let attempt = 1; ; attempt++) {
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetcher(url, { signal: AbortSignal.timeout(ms) });
|
|
16
|
+
if (!response.ok) throw new Error(`${response.status} ${url}`);
|
|
17
|
+
return Buffer.from(await response.arrayBuffer());
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (attempt >= attempts || /^4\d\d /.test(error.message)) throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
export async function download(url) {
|
|
11
25
|
const { host, pathname, search } = new URL(url);
|
|
12
26
|
const query = search ? encodeURIComponent(search) : '';
|
|
13
27
|
const file = new URL(`./${host}${pathname}${query}`, store);
|
|
14
28
|
const hit = await readFile(file).catch(ignore);
|
|
15
29
|
if (hit) return hit;
|
|
16
|
-
const
|
|
17
|
-
if (!response.ok) throw new Error(`${response.status} ${url}`);
|
|
18
|
-
const body = Buffer.from(await response.arrayBuffer());
|
|
30
|
+
const body = await fetched(url);
|
|
19
31
|
await mkdir(dirname(fileURLToPath(file)), { recursive: true });
|
|
20
32
|
const partial = new URL(`${file.href}.${process.pid}`);
|
|
21
33
|
await writeFile(partial, body);
|
package/harness/cdp.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export const SEND_TIMEOUT = 60000;
|
|
2
|
+
|
|
1
3
|
export async function connect(url) {
|
|
2
4
|
const socket = new WebSocket(url);
|
|
3
5
|
await new Promise((resolve, reject) => {
|
|
@@ -29,10 +31,19 @@ export async function connect(url) {
|
|
|
29
31
|
for (const call of pending.values()) call.reject(new Error(`${call.method}: socket closed`));
|
|
30
32
|
};
|
|
31
33
|
return {
|
|
32
|
-
send: (method, params = {}, sessionId) =>
|
|
34
|
+
send: (method, params = {}, sessionId, ms = SEND_TIMEOUT) =>
|
|
33
35
|
new Promise((resolve, reject) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const key = ++id;
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
pending.delete(key);
|
|
39
|
+
reject(new Error(`${method}: no answer in ${ms} ms`));
|
|
40
|
+
}, ms);
|
|
41
|
+
const settle = (fn) => (value) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
fn(value);
|
|
44
|
+
};
|
|
45
|
+
pending.set(key, { method, sessionId, resolve: settle(resolve), reject: settle(reject) });
|
|
46
|
+
socket.send(JSON.stringify({ id: key, method, params, sessionId }));
|
|
36
47
|
}),
|
|
37
48
|
on: (listener) => {
|
|
38
49
|
listeners.add(listener);
|
package/harness/page.mjs
CHANGED
|
@@ -85,7 +85,7 @@ export function decoded({ value, unserializableValue }) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function page(cdp, { sessionId, targetId }, server, timeout = 30000) {
|
|
88
|
-
const send = (method, params) => cdp.send(method, params, sessionId);
|
|
88
|
+
const send = (method, params, ms) => cdp.send(method, params, sessionId, ms);
|
|
89
89
|
const errors = [];
|
|
90
90
|
const responses = [];
|
|
91
91
|
const bindings = new Map();
|
|
@@ -99,23 +99,27 @@ export function page(cdp, { sessionId, targetId }, server, timeout = 30000) {
|
|
|
99
99
|
if (method === 'Network.responseReceived') responses.push(params.response);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
async function
|
|
102
|
+
async function run(fn, args, ms) {
|
|
103
103
|
const expression = `(${fn})(...${JSON.stringify(args)})`;
|
|
104
|
-
const { result, exceptionDetails } = await send(
|
|
105
|
-
|
|
106
|
-
awaitPromise: true,
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
const { result, exceptionDetails } = await send(
|
|
105
|
+
'Runtime.evaluate',
|
|
106
|
+
{ expression, awaitPromise: true, returnByValue: true },
|
|
107
|
+
ms
|
|
108
|
+
);
|
|
109
109
|
if (exceptionDetails) throw new Error(exceptionDetails.exception?.description);
|
|
110
110
|
return decoded(result);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
function evaluate(fn, ...args) {
|
|
114
|
+
return run(fn, args, timeout);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function within(ms, fn, ...args) {
|
|
118
|
+
const deadline = Date.now() + ms;
|
|
115
119
|
let last;
|
|
116
|
-
|
|
120
|
+
for (let left = ms; left > 0; left = deadline - Date.now()) {
|
|
117
121
|
try {
|
|
118
|
-
last = await
|
|
122
|
+
last = await run(fn, args, left);
|
|
119
123
|
if (last) return last;
|
|
120
124
|
} catch (error) {
|
|
121
125
|
last = error.message;
|
|
@@ -123,10 +127,14 @@ export function page(cdp, { sessionId, targetId }, server, timeout = 30000) {
|
|
|
123
127
|
await sleep(25);
|
|
124
128
|
}
|
|
125
129
|
throw new Error(
|
|
126
|
-
`Timed out waiting for a truthy result from ${fn}\nlast result: ${JSON.stringify(last)}`
|
|
130
|
+
`Timed out after ${ms} ms waiting for a truthy result from ${fn}\nlast result: ${JSON.stringify(last)}`
|
|
127
131
|
);
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
function until(fn, ...args) {
|
|
135
|
+
return within(timeout, fn, ...args);
|
|
136
|
+
}
|
|
137
|
+
|
|
130
138
|
async function press(key, ...modifiers) {
|
|
131
139
|
const event = keystroke(key, modifiers);
|
|
132
140
|
await send('Input.dispatchKeyEvent', { ...event, type: event.text ? 'keyDown' : 'rawKeyDown' });
|
|
@@ -152,6 +160,7 @@ export function page(cdp, { sessionId, targetId }, server, timeout = 30000) {
|
|
|
152
160
|
dispose,
|
|
153
161
|
evaluate,
|
|
154
162
|
until,
|
|
163
|
+
within,
|
|
155
164
|
press,
|
|
156
165
|
goto: (path) => navigate(`location.assign(${JSON.stringify(new URL(path, server.url).href)})`),
|
|
157
166
|
reload: () => navigate('location.reload()'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-shared-web",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
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",
|