@ai-ecoverse/slicc-shared-web 1.4.0 → 1.5.0

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 CHANGED
@@ -11,6 +11,7 @@ Configuration shared by SLICC's web repos ([slicc-bios](https://github.com/ai-ec
11
11
  | `lefthook.yml` | Pre-commit hooks: Biome, typecheck, no comments, no unit tests in git, AGENTS.md, diff coverage | `lefthook.yml`: `extends: [node_modules/@ai-ecoverse/slicc-shared-web/lefthook.yml]` |
12
12
  | `mcr.config.mjs` | Unit-test coverage for JS and TS sources in `src/` (TS types are stripped first), written to `coverage/unit/lcov.info` | `test:unit`: `mcr -c node_modules/@ai-ecoverse/slicc-shared-web/mcr.config.mjs node --test 'test/unit/**/*.test.mjs'` |
13
13
  | `bin/` | `slicc-lint` runs every check below plus Biome and, if the repo has one, `typecheck`. `slicc-lint-comments` runs the [slicc no-comment checker](https://github.com/ai-ecoverse/slicc/tree/main/packages/dev-tools/no-comment) at a pinned commit, allowing AGENTS.md. `slicc-agents-md`, `slicc-no-unit-tests` and `slicc-diff-cover` complete the set | `lint`: `slicc-lint`, and the hooks above |
14
+ | `pnpmfile.mjs` | pnpm's counterpart to the Renovate preset: an `updateConfig` hook that adds `@ai-ecoverse/*` to `minimumReleaseAgeExclude`, so pnpm 12 doesn't hold back our own fresh releases (`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`) while third-party packages keep its age check | in a repo with a pnpm lockfile, as a [config dependency](https://pnpm.io/config-dependencies) in `pnpm-workspace.yaml`: `configDependencies: { '@ai-ecoverse/slicc-shared-web': X.Y.Z }` plus `pnpmfile: node_modules/.pnpm-config/@ai-ecoverse/slicc-shared-web/pnpmfile.mjs` |
14
15
  | `harness/` | The CDP integration-test harness, described [below](#integration-test-harness) | `import { launch } from '@ai-ecoverse/slicc-shared-web/harness'`, and `--test-global-setup=node_modules/@ai-ecoverse/slicc-shared-web/harness/global.mjs` |
15
16
 
16
17
  Install it from npm, and pin the reusable workflow to the matching `vX.Y.Z` tag. Renovate keeps both current without the usual waiting period:
@@ -63,12 +64,13 @@ test('boots', async (t) => {
63
64
  | `args` | Extra Chromium flags, such as `--host-resolver-rules` or `--ignore-certificate-errors` | none |
64
65
  | `timeout` | How long `page.until` polls, in ms | `30000` |
65
66
 
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). `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. `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
68
 
68
69
  Why it works the way it does:
69
70
 
70
71
  - 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.
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`.
72
+ - `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.
73
+ - `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.
72
74
  - `takePreciseCoverage` resets the counters, so every snapshot is a delta, and all of them are kept and summed.
73
75
  - monocart's `add()` from parallel test processes loses data, so each test writes raw JSON and the teardown merges it once.
74
76
  - 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.
package/harness/page.mjs CHANGED
@@ -73,7 +73,15 @@ export function mark() {
73
73
  }
74
74
 
75
75
  export function fresh() {
76
- return !window.stale;
76
+ return !window.stale && document.readyState === 'complete' && location.href !== 'about:blank';
77
+ }
78
+
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];
77
85
  }
78
86
 
79
87
  export function page(cdp, { sessionId, targetId }, server, timeout = 30000) {
@@ -99,18 +107,24 @@ export function page(cdp, { sessionId, targetId }, server, timeout = 30000) {
99
107
  returnByValue: true,
100
108
  });
101
109
  if (exceptionDetails) throw new Error(exceptionDetails.exception?.description);
102
- return result.value;
110
+ return decoded(result);
103
111
  }
104
112
 
105
113
  async function until(fn, ...args) {
106
114
  const deadline = Date.now() + timeout;
107
115
  let last;
108
116
  while (Date.now() < deadline) {
109
- last = await evaluate(fn, ...args).catch((error) => error.message);
110
- if (last === true) return;
117
+ try {
118
+ last = await evaluate(fn, ...args);
119
+ if (last) return last;
120
+ } catch (error) {
121
+ last = error.message;
122
+ }
111
123
  await sleep(25);
112
124
  }
113
- throw new Error(`Timed out waiting for ${fn}\nlast result: ${JSON.stringify(last)}`);
125
+ throw new Error(
126
+ `Timed out waiting for a truthy result from ${fn}\nlast result: ${JSON.stringify(last)}`
127
+ );
114
128
  }
115
129
 
116
130
  async function press(key, ...modifiers) {
@@ -120,9 +134,16 @@ export function page(cdp, { sessionId, targetId }, server, timeout = 30000) {
120
134
  }
121
135
 
122
136
  async function navigate(expression) {
123
- await evaluate(mark);
124
- await send('Runtime.evaluate', { expression });
125
- await until(fresh);
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
+ }
126
147
  }
127
148
 
128
149
  return {
@@ -154,7 +154,12 @@ export function recorder(cdp, server, { coverage = ['/'], exits = new Map() } =
154
154
  async function paused(sessionId) {
155
155
  const target = sessions.get(sessionId);
156
156
  if (target?.type === 'worker') await dump(sessionId, target, false);
157
- else await checkpoint();
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
+ }
158
163
  await cdp.send('Debugger.resume', {}, sessionId).catch(ignore);
159
164
  }
160
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-shared-web",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
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",
@@ -28,7 +28,8 @@
28
28
  "./package.json": "./package.json",
29
29
  "./mcr.config.mjs": "./mcr.config.mjs",
30
30
  "./harness": "./harness/index.mjs",
31
- "./harness/global": "./harness/global.mjs"
31
+ "./harness/global": "./harness/global.mjs",
32
+ "./pnpmfile.mjs": "./pnpmfile.mjs"
32
33
  },
33
34
  "bin": {
34
35
  "slicc-agents-md": "bin/slicc-agents-md.sh",
@@ -44,7 +45,8 @@
44
45
  "default.json",
45
46
  "lefthook.yml",
46
47
  "mcr.config.mjs",
47
- "tsconfig.json"
48
+ "tsconfig.json",
49
+ "pnpmfile.mjs"
48
50
  ],
49
51
  "scripts": {
50
52
  "lint": "bin/slicc-lint.sh && shellcheck bin/*.sh",
@@ -59,6 +61,7 @@
59
61
  "@biomejs/biome": "2.5.15",
60
62
  "monocart-coverage-reports": "2.13.0",
61
63
  "playwright-core": "1.63.0",
64
+ "pnpm": "12.9.1",
62
65
  "semantic-release": "25.0.9"
63
66
  },
64
67
  "peerDependencies": {
package/pnpmfile.mjs ADDED
@@ -0,0 +1,6 @@
1
+ export const hooks = {
2
+ updateConfig(config) {
3
+ const exclude = new Set([...(config.minimumReleaseAgeExclude ?? []), '@ai-ecoverse/*']);
4
+ return Object.assign(config, { minimumReleaseAgeExclude: [...exclude] });
5
+ },
6
+ };