// tests/uat/lib/offscreen.ts — Plan 01-11 harness offscreen-context helpers. // // Each helper is a thin wrapper over `offPage.evaluate(() => ...)`. // The Bug B BLOCKER (RESEARCH §7) lives in simulateUserStop — // DO NOT REFACTOR to track.stop(). // // References: // - MediaStreamTrack 'ended' event: // https://developer.mozilla.org/docs/Web/API/MediaStreamTrack/ended_event // - MediaStreamTrack.stop spec note (stop does NOT fire 'ended' on the same track): // https://www.w3.org/TR/mediacapture-streams/#dom-mediastreamtrack-stop import type { Page } from 'puppeteer'; /// /** * Read the displaySurface from the active MediaStream's video track. * Used by assertion 3 to verify monitor-only enforcement (the * post-grant validation in src/offscreen/recorder.ts). * * Returns null when there is no active recording (the harness MUST * start a recording before calling this). * * @param offPage - Offscreen Page handle. * @returns 'monitor' on success, other strings on regression, null when no stream. */ export async function getDisplaySurface(offPage: Page): Promise { return await offPage.evaluate(() => { const hook = globalThis.__mokoshTest; if (hook === undefined || hook.getCurrentStream === undefined) { return null; } const stream = hook.getCurrentStream(); if (stream === null) { return null; } const track = stream.getVideoTracks()[0]; if (track === undefined) { return null; } const ds = track.getSettings().displaySurface; return typeof ds === 'string' ? ds : null; }); } /** * Simulate the operator clicking Chrome's "Stop sharing" overlay. * * **BLOCKER (RESEARCH §7) — DO NOT REFACTOR to `track.stop()`.** * * `track.stop()` releases the capture but does NOT fire the 'ended' * event on the same track per the W3C Screen Capture spec. The * production `onUserStoppedSharing` handler (src/offscreen/recorder.ts: * 451) is wired to 'ended' — using `track.stop()` would silently bypass * the entire Bug B fix path that this assertion exists to verify. * * `track.dispatchEvent(new Event('ended'))` IS the only path that * triggers our handler. After dispatch, the production handler calls * `stream.getTracks().forEach(t => t.stop())` which DOES release the * capture (just doesn't refire 'ended' on the same track — spec-correct). * * @param offPage - Offscreen Page handle. * @throws If no active MediaStream OR no video track in the stream. */ export async function simulateUserStop(offPage: Page): Promise { await offPage.evaluate(() => { const hook = globalThis.__mokoshTest; if (hook === undefined || hook.getCurrentStream === undefined) { throw new Error('simulateUserStop: __mokoshTest.getCurrentStream missing'); } const stream = hook.getCurrentStream(); if (stream === null) { throw new Error( 'simulateUserStop: no current MediaStream — recording must be active', ); } const track = stream.getVideoTracks()[0]; if (track === undefined) { throw new Error('simulateUserStop: no video track in stream'); } // CRITICAL: dispatchEvent, NOT track.stop(). See preamble for the // BLOCKER analysis (RESEARCH §7). track.dispatchEvent(new Event('ended')); }); } /** * Read the current segment count from the offscreen recorder's ring * buffer. Used by assertion 11 to verify the 30s window per D-13 * (3 × 10s segments expected after 35s of recording). * * Returns -1 when the hook is not installed (defensive — should * never happen against a dist-test/ bundle). * * @param offPage - Offscreen Page handle. * @returns Current segment count. */ export async function getSegmentCount(offPage: Page): Promise { return await offPage.evaluate(() => { const hook = globalThis.__mokoshTest; if (hook === undefined || hook.getSegmentCount === undefined) { return -1; } return hook.getSegmentCount(); }); }