@ai-ecoverse/slicc-shared-web 1.3.0 → 1.4.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 +6 -3
- package/harness/cdp.mjs +11 -1
- package/harness/chrome.mjs +43 -8
- package/harness/extensions.mjs +28 -0
- package/harness/page.mjs +74 -16
- package/harness/recorder.mjs +15 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,17 +59,20 @@ test('boots', async (t) => {
|
|
|
59
59
|
| `coverage` | URL prefixes whose `.js`/`.mjs` scripts count for coverage. `node_modules/` never does. A `.map` next to a file is used | `['/']` |
|
|
60
60
|
| `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 |
|
|
61
61
|
| `intercept` | Remote URL prefixes answered from a disk cache in `node_modules/.cache/` | none |
|
|
62
|
+
| `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
|
+
| `args` | Extra Chromium flags, such as `--host-resolver-rules` or `--ignore-certificate-errors` | none |
|
|
62
64
|
| `timeout` | How long `page.until` polls, in ms | `30000` |
|
|
63
65
|
|
|
64
|
-
`chrome.page(t)` opens a tab and returns the page API: `goto(path)`, `reload()`, `evaluate(fn, ...args)`, `until(fn, ...args)`, `press(key, ...modifiers)`, `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), `errors`, `responses` and `dir` (the test's artifact directory). `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`.
|
|
66
|
+
`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`.
|
|
65
67
|
|
|
66
68
|
Why it works the way it does:
|
|
67
69
|
|
|
68
70
|
- Playwright detaches from shared workers, which resumes them early, so their coverage started late about half the time. Raw CDP keeps every worker paused until the profiler runs.
|
|
69
|
-
- `debugger;` pauses in `beforeunload` but not in `pagehide` or `pageswap`, so a `beforeunload` hook, installed after `Page.enable`, takes the coverage of a page before it navigates. The pause only answers CDP when the page itself starts the navigation, so `goto` and `reload` go through `location`, never `Page.navigate` or `Page.reload`.
|
|
71
|
+
- `debugger;` pauses in `beforeunload` but not in `pagehide` or `pageswap`, so a `beforeunload` hook, installed after `Page.enable`, takes the coverage of a page before it navigates. The pause only answers CDP when the page itself starts the navigation, so `goto` and `reload` go through `location`, never `Page.navigate` or `Page.reload`. Navigating away from `about:blank` skips the coverage dump so the first real load is not held in `beforeunload` for a useless snapshot.
|
|
72
|
+
- `goto` used to return as soon as the document swapped (`window.stale` cleared), which is before `readyState` is `complete` and before module scripts run. About 1 load in 20 then looked blank to the next assertion. It now waits for a complete, non-blank document.
|
|
70
73
|
- `takePreciseCoverage` resets the counters, so every snapshot is a delta, and all of them are kept and summed.
|
|
71
74
|
- monocart's `add()` from parallel test processes loses data, so each test writes raw JSON and the teardown merges it once.
|
|
72
75
|
- Coverage of a script served from `chrome.overrides` would be mapped onto the file on disk, so a path overridden during a test is left out of that test's coverage.
|
|
73
76
|
- `--disable-extensions --disable-component-extensions-with-background-pages` keeps a Hangouts service worker out of every run.
|
|
74
77
|
- The attach event can arrive before `Target.createTarget` returns, so new tabs wait on a slot keyed by target id.
|
|
75
|
-
- 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.
|
|
78
|
+
- 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.
|
package/harness/cdp.mjs
CHANGED
|
@@ -7,8 +7,18 @@ export async function connect(url) {
|
|
|
7
7
|
const pending = new Map();
|
|
8
8
|
const listeners = new Set();
|
|
9
9
|
let id = 0;
|
|
10
|
+
const drop = (sessionId, reason) => {
|
|
11
|
+
for (const [key, call] of pending) {
|
|
12
|
+
if (call.sessionId !== sessionId) continue;
|
|
13
|
+
pending.delete(key);
|
|
14
|
+
call.reject(new Error(`${call.method}: ${reason}`));
|
|
15
|
+
}
|
|
16
|
+
};
|
|
10
17
|
socket.onmessage = ({ data }) => {
|
|
11
18
|
const message = JSON.parse(data);
|
|
19
|
+
if (message.method === 'Target.detachedFromTarget')
|
|
20
|
+
drop(message.params.sessionId, 'target detached');
|
|
21
|
+
if (message.method === 'Inspector.targetCrashed') drop(message.sessionId, 'target crashed');
|
|
12
22
|
const call = pending.get(message.id);
|
|
13
23
|
pending.delete(message.id);
|
|
14
24
|
if (!call) for (const listener of listeners) listener(message);
|
|
@@ -21,7 +31,7 @@ export async function connect(url) {
|
|
|
21
31
|
return {
|
|
22
32
|
send: (method, params = {}, sessionId) =>
|
|
23
33
|
new Promise((resolve, reject) => {
|
|
24
|
-
pending.set(++id, { method, resolve, reject });
|
|
34
|
+
pending.set(++id, { method, sessionId, resolve, reject });
|
|
25
35
|
socket.send(JSON.stringify({ id, method, params, sessionId }));
|
|
26
36
|
}),
|
|
27
37
|
on: (listener) => {
|
package/harness/chrome.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { basename, join } from 'node:path';
|
|
|
5
5
|
import { chromium } from 'playwright-core';
|
|
6
6
|
import { cdn } from './cdn.mjs';
|
|
7
7
|
import { connect } from './cdp.mjs';
|
|
8
|
+
import { extensionSource, unpacked } from './extensions.mjs';
|
|
8
9
|
import { page } from './page.mjs';
|
|
9
10
|
import { breakpoints, recorder } from './recorder.mjs';
|
|
10
11
|
import { serve } from './server.mjs';
|
|
@@ -26,8 +27,20 @@ export const flags = [
|
|
|
26
27
|
'--window-size=1280,800',
|
|
27
28
|
];
|
|
28
29
|
|
|
29
|
-
export
|
|
30
|
-
const
|
|
30
|
+
export function commandLine(profile, extensions = [], extra = []) {
|
|
31
|
+
const loaded = extensions.length > 0;
|
|
32
|
+
const base = loaded ? flags.filter((flag) => flag !== '--disable-extensions') : flags;
|
|
33
|
+
const load = loaded
|
|
34
|
+
? [`--disable-extensions-except=${extensions}`, `--load-extension=${extensions}`]
|
|
35
|
+
: [];
|
|
36
|
+
return [...base, ...load, ...extra, `--user-data-dir=${profile}`, 'about:blank'];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function start(
|
|
40
|
+
profile,
|
|
41
|
+
args = commandLine(profile),
|
|
42
|
+
executable = chromium.executablePath()
|
|
43
|
+
) {
|
|
31
44
|
const child = spawn(executable, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
32
45
|
let log = '';
|
|
33
46
|
const url = await new Promise((resolve, reject) => {
|
|
@@ -59,13 +72,20 @@ export async function launch({
|
|
|
59
72
|
coverage,
|
|
60
73
|
exits = {},
|
|
61
74
|
intercept = [],
|
|
75
|
+
extensions = [],
|
|
76
|
+
args = [],
|
|
62
77
|
timeout,
|
|
63
78
|
} = {}) {
|
|
64
79
|
const server = await serve({ roots, aliases, isolated });
|
|
80
|
+
const found = await unpacked(extensions);
|
|
81
|
+
const fromExtension = extensionSource(found);
|
|
82
|
+
const files = { ...server, source: (href) => server.source(href) ?? fromExtension(href) };
|
|
83
|
+
const shared = extensions.length > 0;
|
|
65
84
|
const profile = await mkdtemp(join(tmpdir(), 'slicc-harness-'));
|
|
66
|
-
const
|
|
85
|
+
const loaded = [...found.values()].map((root) => root.slice(0, -1));
|
|
86
|
+
const { child, url } = await start(profile, commandLine(profile, loaded, args));
|
|
67
87
|
const cdp = await connect(url);
|
|
68
|
-
const record = recorder(cdp,
|
|
88
|
+
const record = recorder(cdp, files, { coverage, exits: await breakpoints(server, exits) });
|
|
69
89
|
const remote = await cdn(cdp, intercept);
|
|
70
90
|
await cdp.send('Target.setAutoAttach', {
|
|
71
91
|
autoAttach: true,
|
|
@@ -73,7 +93,18 @@ export async function launch({
|
|
|
73
93
|
flatten: true,
|
|
74
94
|
});
|
|
75
95
|
|
|
76
|
-
async function
|
|
96
|
+
async function tabs() {
|
|
97
|
+
const { targetInfos } = await cdp.send('Target.getTargets');
|
|
98
|
+
return targetInfos.filter(({ type }) => type === 'page').map(({ targetId }) => targetId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function sweep(before) {
|
|
102
|
+
const added = (await tabs()).filter((targetId) => !before.has(targetId));
|
|
103
|
+
const closing = added.map((targetId) => cdp.send('Target.closeTarget', { targetId }));
|
|
104
|
+
await Promise.all(closing.map((sent) => sent.catch(ignore)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function finish(pages, browserContextId, dir, before) {
|
|
77
108
|
const shots = pages.map((opened, i) =>
|
|
78
109
|
opened.screenshot(new URL(`tab-${i + 1}.png`, dir)).catch(ignore)
|
|
79
110
|
);
|
|
@@ -81,7 +112,10 @@ export async function launch({
|
|
|
81
112
|
await record.end();
|
|
82
113
|
for (const opened of pages) opened.dispose();
|
|
83
114
|
remote.reset();
|
|
84
|
-
|
|
115
|
+
const closing = shared
|
|
116
|
+
? sweep(before)
|
|
117
|
+
: cdp.send('Target.disposeBrowserContext', { browserContextId });
|
|
118
|
+
await bounded(closing, 'dispose');
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
return {
|
|
@@ -93,13 +127,14 @@ export async function launch({
|
|
|
93
127
|
const suite = slug(basename(t.filePath ?? 'test', '.test.mjs'));
|
|
94
128
|
const dir = new URL(`${suite}/${slug(t.name)}/`, artifacts);
|
|
95
129
|
await mkdir(dir, { recursive: true });
|
|
96
|
-
const { browserContextId } = await cdp.send('Target.createBrowserContext');
|
|
130
|
+
const { browserContextId } = shared ? {} : await cdp.send('Target.createBrowserContext');
|
|
131
|
+
const before = new Set(shared ? await tabs() : []);
|
|
97
132
|
record.begin(browserContextId, dir, `${suite}-${slug(t.name)}`);
|
|
98
133
|
server.requests.length = 0;
|
|
99
134
|
server.overrides.clear();
|
|
100
135
|
server.overridden.clear();
|
|
101
136
|
const pages = [];
|
|
102
|
-
t.after(() => finish(pages, browserContextId, dir));
|
|
137
|
+
t.after(() => finish(pages, browserContextId, dir, before));
|
|
103
138
|
const tab = async () => {
|
|
104
139
|
const opened = page(cdp, await record.open(browserContextId), server, timeout);
|
|
105
140
|
pages.push(opened);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import { join, normalize, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export function extensionId(input) {
|
|
6
|
+
const hex = createHash('sha256').update(input).digest('hex').slice(0, 32);
|
|
7
|
+
return [...hex].map((digit) => String.fromCharCode(97 + Number.parseInt(digit, 16))).join('');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function unpacked(dirs) {
|
|
11
|
+
const found = new Map();
|
|
12
|
+
for (const dir of dirs) {
|
|
13
|
+
const root = await realpath(dir);
|
|
14
|
+
const manifest = JSON.parse(await readFile(join(root, 'manifest.json'), 'utf8'));
|
|
15
|
+
const id = extensionId(manifest.key ? Buffer.from(manifest.key, 'base64') : root);
|
|
16
|
+
found.set(id, `${root}${sep}`);
|
|
17
|
+
}
|
|
18
|
+
return found;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function extensionSource(found) {
|
|
22
|
+
return (href) => {
|
|
23
|
+
if (!href.startsWith('chrome-extension://')) return null;
|
|
24
|
+
const { host, pathname } = new URL(href);
|
|
25
|
+
const root = found.get(host);
|
|
26
|
+
return root ? join(root, normalize(decodeURIComponent(pathname))) : null;
|
|
27
|
+
};
|
|
28
|
+
}
|
package/harness/page.mjs
CHANGED
|
@@ -7,21 +7,57 @@ const keys = new Map([
|
|
|
7
7
|
['Tab', { code: 'Tab', keyCode: 9, text: '\t' }],
|
|
8
8
|
['Backspace', { code: 'Backspace', keyCode: 8 }],
|
|
9
9
|
['Escape', { code: 'Escape', keyCode: 27 }],
|
|
10
|
+
['Delete', { code: 'Delete', keyCode: 46 }],
|
|
11
|
+
['Insert', { code: 'Insert', keyCode: 45 }],
|
|
12
|
+
['Home', { code: 'Home', keyCode: 36 }],
|
|
13
|
+
['End', { code: 'End', keyCode: 35 }],
|
|
14
|
+
['PageUp', { code: 'PageUp', keyCode: 33 }],
|
|
15
|
+
['PageDown', { code: 'PageDown', keyCode: 34 }],
|
|
10
16
|
['ArrowUp', { code: 'ArrowUp', keyCode: 38 }],
|
|
11
17
|
['ArrowDown', { code: 'ArrowDown', keyCode: 40 }],
|
|
12
18
|
['ArrowLeft', { code: 'ArrowLeft', keyCode: 37 }],
|
|
13
19
|
['ArrowRight', { code: 'ArrowRight', keyCode: 39 }],
|
|
14
|
-
[' ', { code: 'Space', keyCode: 32, text: ' ' }],
|
|
15
20
|
]);
|
|
21
|
+
const punctuation = new Map([
|
|
22
|
+
[' ', ['Space', 32]],
|
|
23
|
+
[';', ['Semicolon', 186]],
|
|
24
|
+
['=', ['Equal', 187]],
|
|
25
|
+
[',', ['Comma', 188]],
|
|
26
|
+
['-', ['Minus', 189]],
|
|
27
|
+
['.', ['Period', 190]],
|
|
28
|
+
['/', ['Slash', 191]],
|
|
29
|
+
['`', ['Backquote', 192]],
|
|
30
|
+
['[', ['BracketLeft', 219]],
|
|
31
|
+
['\\', ['Backslash', 220]],
|
|
32
|
+
[']', ['BracketRight', 221]],
|
|
33
|
+
["'", ['Quote', 222]],
|
|
34
|
+
]);
|
|
35
|
+
const shifted = new Map(
|
|
36
|
+
[...'~!@#$%^&*()_+{}|:"<>?'].map((char, i) => [char, "`1234567890-=[]\\;',./"[i]])
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function named(key) {
|
|
40
|
+
const known = keys.get(key);
|
|
41
|
+
if (known) return known;
|
|
42
|
+
const fn = /^F(\d{1,2})$/.exec(key);
|
|
43
|
+
return { code: key, keyCode: fn ? 111 + Number(fn[1]) : 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function character(char) {
|
|
47
|
+
const base = shifted.get(char) ?? char.toLowerCase();
|
|
48
|
+
const upper = base.toUpperCase();
|
|
49
|
+
if (/^[a-z]$/.test(base))
|
|
50
|
+
return { code: `Key${upper}`, keyCode: upper.charCodeAt(0), text: char };
|
|
51
|
+
if (/^\d$/.test(base)) return { code: `Digit${base}`, keyCode: base.charCodeAt(0), text: char };
|
|
52
|
+
const [code, keyCode] = punctuation.get(base) ?? ['', 0];
|
|
53
|
+
return { code, keyCode, text: char };
|
|
54
|
+
}
|
|
16
55
|
|
|
17
56
|
export function keystroke(key, modifiers) {
|
|
18
57
|
const bits = modifiers.reduce((sum, name) => sum | masks[name], 0);
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const plain = named ? named.text : key;
|
|
23
|
-
const text = bits & ~masks.shift ? undefined : plain;
|
|
24
|
-
return { key, code, windowsVirtualKeyCode: keyCode, modifiers: bits, text };
|
|
58
|
+
const { code, keyCode, text } = [...key].length === 1 ? character(key) : named(key);
|
|
59
|
+
const typed = bits & ~masks.shift ? undefined : text;
|
|
60
|
+
return { key, code, windowsVirtualKeyCode: keyCode, modifiers: bits, text: typed };
|
|
25
61
|
}
|
|
26
62
|
|
|
27
63
|
export function settle(ms = 1000) {
|
|
@@ -37,10 +73,18 @@ export function mark() {
|
|
|
37
73
|
}
|
|
38
74
|
|
|
39
75
|
export function fresh() {
|
|
40
|
-
return !window.stale;
|
|
76
|
+
return !window.stale && document.readyState === 'complete' && location.href !== 'about:blank';
|
|
41
77
|
}
|
|
42
78
|
|
|
43
|
-
|
|
79
|
+
const specials = { Infinity, '-Infinity': -Infinity, NaN, '-0': -0 };
|
|
80
|
+
|
|
81
|
+
export function decoded({ value, unserializableValue }) {
|
|
82
|
+
if (unserializableValue === undefined) return value;
|
|
83
|
+
if (unserializableValue.endsWith('n')) return BigInt(unserializableValue.slice(0, -1));
|
|
84
|
+
return specials[unserializableValue];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function page(cdp, { sessionId, targetId }, server, timeout = 30000) {
|
|
44
88
|
const send = (method, params) => cdp.send(method, params, sessionId);
|
|
45
89
|
const errors = [];
|
|
46
90
|
const responses = [];
|
|
@@ -63,18 +107,24 @@ export function page(cdp, sessionId, server, timeout = 30000) {
|
|
|
63
107
|
returnByValue: true,
|
|
64
108
|
});
|
|
65
109
|
if (exceptionDetails) throw new Error(exceptionDetails.exception?.description);
|
|
66
|
-
return result
|
|
110
|
+
return decoded(result);
|
|
67
111
|
}
|
|
68
112
|
|
|
69
113
|
async function until(fn, ...args) {
|
|
70
114
|
const deadline = Date.now() + timeout;
|
|
71
115
|
let last;
|
|
72
116
|
while (Date.now() < deadline) {
|
|
73
|
-
|
|
74
|
-
|
|
117
|
+
try {
|
|
118
|
+
last = await evaluate(fn, ...args);
|
|
119
|
+
if (last) return last;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
last = error.message;
|
|
122
|
+
}
|
|
75
123
|
await sleep(25);
|
|
76
124
|
}
|
|
77
|
-
throw new Error(
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Timed out waiting for a truthy result from ${fn}\nlast result: ${JSON.stringify(last)}`
|
|
127
|
+
);
|
|
78
128
|
}
|
|
79
129
|
|
|
80
130
|
async function press(key, ...modifiers) {
|
|
@@ -84,9 +134,16 @@ export function page(cdp, sessionId, server, timeout = 30000) {
|
|
|
84
134
|
}
|
|
85
135
|
|
|
86
136
|
async function navigate(expression) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
137
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
138
|
+
await evaluate(mark);
|
|
139
|
+
await send('Runtime.evaluate', { expression });
|
|
140
|
+
try {
|
|
141
|
+
await until(fresh);
|
|
142
|
+
return;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (attempt === 1) throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
90
147
|
}
|
|
91
148
|
|
|
92
149
|
return {
|
|
@@ -99,6 +156,7 @@ export function page(cdp, sessionId, server, timeout = 30000) {
|
|
|
99
156
|
goto: (path) => navigate(`location.assign(${JSON.stringify(new URL(path, server.url).href)})`),
|
|
100
157
|
reload: () => navigate('location.reload()'),
|
|
101
158
|
insert: (text) => send('Input.insertText', { text }),
|
|
159
|
+
close: () => cdp.send('Target.closeTarget', { targetId }),
|
|
102
160
|
enter: () => press('Enter'),
|
|
103
161
|
async type(text) {
|
|
104
162
|
for (const key of text) await press(key);
|
package/harness/recorder.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { basename, relative } from 'node:path';
|
|
3
3
|
import { cwd } from 'node:process';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import { ignore, raw, samples, sleep } from './util.mjs';
|
|
5
|
+
import { bounded, ignore, raw, samples, sleep, TIMED_OUT } from './util.mjs';
|
|
6
6
|
|
|
7
7
|
const profiled = new Set(['page', 'worker', 'service_worker', 'shared_worker']);
|
|
8
8
|
const nested = { autoAttach: true, waitForDebuggerOnStart: true, flatten: true };
|
|
@@ -32,13 +32,10 @@ export async function breakpoints(server, exits) {
|
|
|
32
32
|
|
|
33
33
|
export function covered(server, prefixes) {
|
|
34
34
|
return ({ url }) => {
|
|
35
|
-
if (!
|
|
36
|
-
const { pathname } = new URL(url);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
!pathname.includes('/node_modules/') &&
|
|
40
|
-
prefixes.some((prefix) => pathname.startsWith(prefix))
|
|
41
|
-
);
|
|
35
|
+
if (!server.source(url)) return false;
|
|
36
|
+
const { pathname, protocol } = new URL(url);
|
|
37
|
+
const ours = protocol === 'chrome-extension:' || prefixes.some((p) => pathname.startsWith(p));
|
|
38
|
+
return ours && /\.m?js$/.test(pathname) && !pathname.includes('/node_modules/');
|
|
42
39
|
};
|
|
43
40
|
}
|
|
44
41
|
|
|
@@ -157,7 +154,12 @@ export function recorder(cdp, server, { coverage = ['/'], exits = new Map() } =
|
|
|
157
154
|
async function paused(sessionId) {
|
|
158
155
|
const target = sessions.get(sessionId);
|
|
159
156
|
if (target?.type === 'worker') await dump(sessionId, target, false);
|
|
160
|
-
else
|
|
157
|
+
else {
|
|
158
|
+
const { result } = await cdp
|
|
159
|
+
.send('Runtime.evaluate', { expression: 'location.href', returnByValue: true }, sessionId)
|
|
160
|
+
.catch(() => ({ result: {} }));
|
|
161
|
+
if (result?.value && result.value !== 'about:blank') await checkpoint();
|
|
162
|
+
}
|
|
161
163
|
await cdp.send('Debugger.resume', {}, sessionId).catch(ignore);
|
|
162
164
|
}
|
|
163
165
|
|
|
@@ -180,9 +182,11 @@ export function recorder(cdp, server, { coverage = ['/'], exits = new Map() } =
|
|
|
180
182
|
url: 'about:blank',
|
|
181
183
|
browserContextId,
|
|
182
184
|
});
|
|
183
|
-
const sessionId = await tab(targetId).session;
|
|
185
|
+
const sessionId = await bounded(tab(targetId).session, 'attach');
|
|
184
186
|
tabs.delete(targetId);
|
|
185
|
-
|
|
187
|
+
if (sessionId === TIMED_OUT) throw new Error(`Target.createTarget: ${targetId} never attached`);
|
|
188
|
+
if (run && !run.context) run.context = sessions.get(sessionId)?.browserContextId;
|
|
189
|
+
return { sessionId, targetId };
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
function begin(context, dir, name) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-shared-web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|