openvibers/openvibe.shared Commits
CI: Shared's suite on Windows and macOS too (#140)
Changes
@@@ -31,6 +31,22 @@ jobs:mkdir "$RUNNER_TEMP/consumer" && cd "$RUNNER_TEMP/consumer" && npm init -y >/dev/nullnpm install --no-audit --no-fund "$RUNNER_TEMP/openvibe-shared.tgz"node -e "require('openvibe-shared'); require('openvibe-shared/navbar'); require('openvibe-shared/legal'); require('openvibe-shared/metrics'); require('openvibe-shared/ready'); require('openvibe-shared/release'); require('openvibe-shared/release-compat'); require('openvibe-shared/analytics'); require('openvibe-shared/analytics/pg'); require('openvibe-shared/analytics/event.v1.json'); const f = require('openvibe-shared/files'); for (const n of f.BROWSER) require('fs').accessSync(f.path(n)); console.log('ok', require('openvibe-shared/package.json').version)"++ # Sites built on these files are developed on every OS (owner, 2026-10-27: "the shared SDK and API … work well across++ # multi-platform"): the whole suite on Windows and macOS too.++ other-os:++ name: test (${{ matrix.os }})++ strategy:++ fail-fast: false++ matrix:++ os: [windows-latest, macos-latest]++ runs-on: ${{ matrix.os }}++ steps:++ - uses: actions/checkout@v4++ - uses: actions/setup-node@v4++ with:++ node-version: 22.22.1++ - run: npm ci --no-audit --no-fund++ - run: npm test# Secret scan (gitleaks) and dependency audit, the workflow every OpenVibers repo calls (Track Q).security:uses: ./.github/workflows/security.yml@@@ -4,6 +4,15 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking change to any exported module, browser global or served file name is a new major.A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).++## Unreleased++++- **The browser harness on macOS** (`browser-harness.js`): Chrome starts with `--disable-backgrounding-occluded-windows`,++ `--disable-renderer-backgrounding` and `--disable-background-timer-throttling`, so every page draws and runs its timers++ as if in front. On macOS a new page counted as an occluded window and painted nothing (the Core Web Vitals test saw no++ paint and no shift). CI now runs the suite on Windows and macOS too.++- **`perf-budget` vitals wait for the first paint before the click** (`measure({ browser })`): Chrome stops recording++ LCP at the first input, and a slower machine clicked before anything was painted (no LCP at all on the macOS runner).++## 4.17.0 — 2026-10-27- **`wallet: true`: OpenCoins and Vibes in the bar on any site** (`navbar.js`; owner, 2026-10-27: "make that as a@@@ -138,6 +138,9 @@ async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, hea...(headless ? ['--headless=new'] : []), '--remote-debugging-port=0', `--user-data-dir=${profile}`,...(sandbox ? [] : ['--no-sandbox']), '--no-first-run', '--no-default-browser-check', '--disable-extensions','--disable-background-networking', '--disable-component-update', '--disable-sync', '--disable-default-apps',++ // Every page draws and runs timers as if in front: on macOS a new page counted as an occluded window, painted++ // nothing, and the Core Web Vitals test saw no paint and no shift (2026-10-27, the first macOS CI run).++ '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-background-timer-throttling',// No back/forward cache: kept pages would count in the navigation check's heap, node and document samples.'--mute-audio', '--disk-cache-size=16777216', '--window-size=1280,900', '--disable-features=BackForwardCache', ...args, 'about:blank',], { stdio: 'ignore', detached: true }); // its own process group: close() stops Chrome and its helpers together@@@ -97,6 +97,9 @@ async function vitals(url, browser) {page = await harness.openPage(b, context, VIEWPORT);await page.send('Page.addScriptToEvaluateOnNewDocument', { source: CWV_PROBE });await page.goto(url);++ // The click waits for the first paint: Chrome stops recording LCP at the first input, and a slower machine (the++ // macOS CI runner, 2026-10-27) could click before anything was painted, so no LCP at all.++ await page.evaluate(`new Promise((ok) => { const t0 = Date.now(); (function poll() { if (performance.getEntriesByType('paint').some((e) => e.name === 'first-contentful-paint') || Date.now() - t0 > 3000) return ok(true); requestAnimationFrame(() => setTimeout(poll, 16)); })(); })`);const at = { x: VIEWPORT.width / 2, y: VIEWPORT.height / 2, button: 'left', clickCount: 1 };await page.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...at });await page.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...at });@@@ -52,6 +52,11 @@ const server = http.createServer((req, res) => {ac.abort(); return [added - before, __ovProbe().documentListeners - before]; })()`);assert.deepStrictEqual(probe, [1, 0], 'the probe counts a signal listener, then forgets it on abort');++ // The bar's one-time extras load on their own a few seconds in (the release watch at 4 s, the launcher's icons++ // when idle): let them settle first, so the laps count only what each re-render adds (a slower macOS runner++ // caught the release watch's window listeners mid-lap, 2026-10-27).++ await p.evaluate(`new Promise((r) => { const t0 = Date.now(); const t = setInterval(() => { if ((window.OVRelease && window.OpenVibeIcons) || Date.now() - t0 > 9000) { clearInterval(t); setTimeout(r, 300); } }, 100); })`);++const move = (id) => p.evaluate(`new Promise((done) => { document.addEventListener('ov:boost:load', () => done(location.pathname), { once: true }); document.getElementById('${id}').click(); })`);const sample = async () => {await p.send('HeapProfiler.collectGarbage'); await wait(150); await p.send('HeapProfiler.collectGarbage');
@@ -31,6 +31,22 @@ jobs:
@@ -31,6 +31,22 @@ jobs:
mkdir "$RUNNER_TEMP/consumer" && cd "$RUNNER_TEMP/consumer" && npm init -y >/dev/null
mkdir "$RUNNER_TEMP/consumer" && cd "$RUNNER_TEMP/consumer" && npm init -y >/dev/null
npm install --no-audit --no-fund "$RUNNER_TEMP/openvibe-shared.tgz"
npm install --no-audit --no-fund "$RUNNER_TEMP/openvibe-shared.tgz"
node -e "require('openvibe-shared'); require('openvibe-shared/navbar'); require('openvibe-shared/legal'); require('openvibe-shared/metrics'); require('openvibe-shared/ready'); require('openvibe-shared/release'); require('openvibe-shared/release-compat'); require('openvibe-shared/analytics'); require('openvibe-shared/analytics/pg'); require('openvibe-shared/analytics/event.v1.json'); const f = require('openvibe-shared/files'); for (const n of f.BROWSER) require('fs').accessSync(f.path(n)); console.log('ok', require('openvibe-shared/package.json').version)"
node -e "require('openvibe-shared'); require('openvibe-shared/navbar'); require('openvibe-shared/legal'); require('openvibe-shared/metrics'); require('openvibe-shared/ready'); require('openvibe-shared/release'); require('openvibe-shared/release-compat'); require('openvibe-shared/analytics'); require('openvibe-shared/analytics/pg'); require('openvibe-shared/analytics/event.v1.json'); const f = require('openvibe-shared/files'); for (const n of f.BROWSER) require('fs').accessSync(f.path(n)); console.log('ok', require('openvibe-shared/package.json').version)"
+ # Sites built on these files are developed on every OS (owner, 2026-10-27: "the shared SDK and API … work well across
+ # multi-platform"): the whole suite on Windows and macOS too.
+ other-os:
+ name: test (${{ matrix.os }})
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22.22.1
+ - run: npm ci --no-audit --no-fund
+ - run: npm test
# Secret scan (gitleaks) and dependency audit, the workflow every OpenVibers repo calls (Track Q).
# Secret scan (gitleaks) and dependency audit, the workflow every OpenVibers repo calls (Track Q).
security:
security:
uses: ./.github/workflows/security.yml
uses: ./.github/workflows/security.yml
@@ -4,6 +4,15 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,15 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
+## Unreleased
+
+- **The browser harness on macOS** (`browser-harness.js`): Chrome starts with `--disable-backgrounding-occluded-windows`,
+ `--disable-renderer-backgrounding` and `--disable-background-timer-throttling`, so every page draws and runs its timers
+ as if in front. On macOS a new page counted as an occluded window and painted nothing (the Core Web Vitals test saw no
+ paint and no shift). CI now runs the suite on Windows and macOS too.
+- **`perf-budget` vitals wait for the first paint before the click** (`measure({ browser })`): Chrome stops recording
+ LCP at the first input, and a slower machine clicked before anything was painted (no LCP at all on the macOS runner).
+
## 4.17.0 — 2026-10-27
## 4.17.0 — 2026-10-27
- **`wallet: true`: OpenCoins and Vibes in the bar on any site** (`navbar.js`; owner, 2026-10-27: "make that as a
- **`wallet: true`: OpenCoins and Vibes in the bar on any site** (`navbar.js`; owner, 2026-10-27: "make that as a
@@ -138,6 +138,9 @@ async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, hea
@@ -138,6 +138,9 @@ async function launch({ bin, browserURL, tmpDir, args = [], sandbox = false, hea
...(headless ? ['--headless=new'] : []), '--remote-debugging-port=0', `--user-data-dir=${profile}`,
...(headless ? ['--headless=new'] : []), '--remote-debugging-port=0', `--user-data-dir=${profile}`,
...(sandbox ? [] : ['--no-sandbox']), '--no-first-run', '--no-default-browser-check', '--disable-extensions',
...(sandbox ? [] : ['--no-sandbox']), '--no-first-run', '--no-default-browser-check', '--disable-extensions',
'--disable-background-networking', '--disable-component-update', '--disable-sync', '--disable-default-apps',
'--disable-background-networking', '--disable-component-update', '--disable-sync', '--disable-default-apps',
+ // Every page draws and runs timers as if in front: on macOS a new page counted as an occluded window, painted
+ // nothing, and the Core Web Vitals test saw no paint and no shift (2026-10-27, the first macOS CI run).
+ '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-background-timer-throttling',
// No back/forward cache: kept pages would count in the navigation check's heap, node and document samples.
// No back/forward cache: kept pages would count in the navigation check's heap, node and document samples.
'--mute-audio', '--disk-cache-size=16777216', '--window-size=1280,900', '--disable-features=BackForwardCache', ...args, 'about:blank',
'--mute-audio', '--disk-cache-size=16777216', '--window-size=1280,900', '--disable-features=BackForwardCache', ...args, 'about:blank',
], { stdio: 'ignore', detached: true }); // its own process group: close() stops Chrome and its helpers together
], { stdio: 'ignore', detached: true }); // its own process group: close() stops Chrome and its helpers together
@@ -97,6 +97,9 @@ async function vitals(url, browser) {
@@ -97,6 +97,9 @@ async function vitals(url, browser) {
page = await harness.openPage(b, context, VIEWPORT);
page = await harness.openPage(b, context, VIEWPORT);
await page.send('Page.addScriptToEvaluateOnNewDocument', { source: CWV_PROBE });
await page.send('Page.addScriptToEvaluateOnNewDocument', { source: CWV_PROBE });
await page.goto(url);
await page.goto(url);
+ // The click waits for the first paint: Chrome stops recording LCP at the first input, and a slower machine (the
+ // macOS CI runner, 2026-10-27) could click before anything was painted, so no LCP at all.
+ await page.evaluate(`new Promise((ok) => { const t0 = Date.now(); (function poll() { if (performance.getEntriesByType('paint').some((e) => e.name === 'first-contentful-paint') || Date.now() - t0 > 3000) return ok(true); requestAnimationFrame(() => setTimeout(poll, 16)); })(); })`);
const at = { x: VIEWPORT.width / 2, y: VIEWPORT.height / 2, button: 'left', clickCount: 1 };
const at = { x: VIEWPORT.width / 2, y: VIEWPORT.height / 2, button: 'left', clickCount: 1 };
await page.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...at });
await page.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...at });
await page.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...at });
await page.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...at });
@@ -52,6 +52,11 @@ const server = http.createServer((req, res) => {
@@ -52,6 +52,11 @@ const server = http.createServer((req, res) => {
ac.abort(); return [added - before, __ovProbe().documentListeners - before]; })()`);
ac.abort(); return [added - before, __ovProbe().documentListeners - before]; })()`);
assert.deepStrictEqual(probe, [1, 0], 'the probe counts a signal listener, then forgets it on abort');
assert.deepStrictEqual(probe, [1, 0], 'the probe counts a signal listener, then forgets it on abort');
+ // The bar's one-time extras load on their own a few seconds in (the release watch at 4 s, the launcher's icons
+ // when idle): let them settle first, so the laps count only what each re-render adds (a slower macOS runner
+ // caught the release watch's window listeners mid-lap, 2026-10-27).
+ await p.evaluate(`new Promise((r) => { const t0 = Date.now(); const t = setInterval(() => { if ((window.OVRelease && window.OpenVibeIcons) || Date.now() - t0 > 9000) { clearInterval(t); setTimeout(r, 300); } }, 100); })`);
+
const move = (id) => p.evaluate(`new Promise((done) => { document.addEventListener('ov:boost:load', () => done(location.pathname), { once: true }); document.getElementById('${id}').click(); })`);
const move = (id) => p.evaluate(`new Promise((done) => { document.addEventListener('ov:boost:load', () => done(location.pathname), { once: true }); document.getElementById('${id}').click(); })`);
const sample = async () => {
const sample = async () => {
await p.send('HeapProfiler.collectGarbage'); await wait(150); await p.send('HeapProfiler.collectGarbage');
await p.send('HeapProfiler.collectGarbage'); await wait(150); await p.send('HeapProfiler.collectGarbage');