// 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();
});
}