// tests/uat/lib/harness-page-driver.ts — Plan 01-13 Wave 2. // // Driver wrappers — one per assertion (A1..A13). Each wraps a single // `page.evaluate(() => window.__mokoshHarness.assertXX())` call, // returning the structured AssertionRecord (or the extended shape with // `bytesBase64` for A5/A12/A13 which return host-side-required payloads // like the downloaded zip bytes or the recorded webm bytes). // // Centralizing the page.evaluate call here means adding or renaming an // assertion requires a two-file edit: // 1. extension-page-harness.ts — page-side impl + window.__mokoshHarness wire // 2. this file — host-side driver wrapper // instead of touching every test-file that calls the assertion. // // Wave 2 ONLY wires `driveA6` (the proven assertion from the c647f61 // prototype). The 12 Wave-3 assertions are stubbed as `throw new // Error('NOT YET IMPLEMENTED — Wave 3 wires this')` so the // orchestrator's `for (const drive of drivers)` loop fails cleanly on // the first unimplemented one (bail-on-first-failure semantics in // `harness.test.ts` lands in Wave 3A). // // Wave 3A wires driveA1/A2/A3/A4 (page-side surface in // `extension-page-harness.ts` from the same wave). // Wave 3B wires driveA5 (page-side ack + HOST-side fs polling for the // dropped `session_report_*.zip` in `handles.downloadsDir`) + driveA7 // (standard page.evaluate wrapper). The driveA5 signature requires a // second `downloadsDir` argument; the orchestrator at `harness.test.ts` // threads `handles.downloadsDir` through. // // References: // - puppeteer Page.evaluate: // https://pptr.dev/api/puppeteer.page.evaluate // - Node fs.readdirSync / statSync: // https://nodejs.org/api/fs.html import { spawnSync } from 'node:child_process'; import { existsSync, mkdtempSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve as resolvePath } from 'node:path'; import JSZip from 'jszip'; import { EventType, IncrementalSource } from '@rrweb/types'; import type { Page } from 'puppeteer'; import type { AssertionRecord, CheckRecord } from './assertions'; import { assertArchiveShape, extractEntryToFile } from './zip'; import type { UserEvent } from '../../../src/shared/types'; /** * Extended assertion-record shape for A5/A12/A13 which return * host-side-required binary payloads: * - A5 (SAVE_ARCHIVE): `bytesBase64` is the downloaded zip bytes * (read by host-side from `handles.downloadsDir`); page side only * returns the trigger ack. * - A12 (ffprobe): `bytesBase64` is the recorded webm bytes — * extracted from the zip by the host so ffprobe (host-side binary) * can analyze it. * - A13 (zip shape): `bytesBase64` is the zip bytes; `expectedVersion` * is the manifest version the harness was built against. * * All Wave-3 assertions; not used in Wave 2. */ export interface AssertionWithBytes { readonly passed: boolean; readonly name: string; readonly checks: ReadonlyArray; readonly diagnostics: ReadonlyArray; readonly error?: string; readonly bytesBase64?: string; readonly expectedVersion?: string; } // Note (Wave 3D — all 13 drivers wired): the WAVE3_STUB_PREFIX marker // that gated unimplemented drivers across Waves 3A-3C has been retired // — there are no more stubs. Future assertions (A14+) would follow // the same wired-driver pattern below; no stub-marker is reintroduced // unless multi-wave incremental rollout is needed again. /** * Drive the A6 (Bug B canonical) assertion. The proven, prototype- * inherited driver. Page side does all orchestration (ensureOffscreen + * start + wait + dispatch + assert); host side just triggers + reads * the result. * * @param page - The harness page (from `launchHarnessBrowser`). * @returns Structured AssertionRecord with 5 checks (SETUP + A6.1..A6.4). */ export async function driveA6(page: Page): Promise { return await page.evaluate(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose. const harness = (window as any).__mokoshHarness; const r: AssertionRecord = await harness.assertA6(); return r; }) as AssertionRecord; } /* ─── Wave 3A — WIRED ─────────────────────────────────────────────── */ /** * Drive A1 (SW bootstrap state). Asserts the post-load idle-mode state: * badge='', popup='', isRecording=false. MUST run BEFORE A2 in any * orchestrated sequence — A2 manually sets badge='REC' which invalidates * the A1 contract until the SW is reset. * * @param page - The harness page from `launchHarnessBrowser`. * @returns Structured AssertionRecord with 3 checks (badge + popup + isRecording). */ export async function driveA1(page: Page): Promise { return await page.evaluate(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose. const harness = (window as any).__mokoshHarness; const r: AssertionRecord = await harness.assertA1(); return r; }) as AssertionRecord; } /** * Drive A2 (toolbar onClicked → REC). Uses the direct-offscreen workaround * for the missing `tabs` manifest permission (per 01-11-SUMMARY). Leaves * the offscreen recording active — A3 + A4 chain off A2's REC state. * * @param page - The harness page from `launchHarnessBrowser`. * @returns Structured AssertionRecord with 2 checks (badge + popup). */ export async function driveA2(page: Page): Promise { return await page.evaluate(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose. const harness = (window as any).__mokoshHarness; const r: AssertionRecord = await harness.assertA2(); return r; }) as AssertionRecord; } /** * Drive A3 (displaySurface === 'monitor'). Assumes A2 left recording * active. Queries the offscreen `get-display-surface` bridge op. * * @param page - The harness page from `launchHarnessBrowser`. * @returns Structured AssertionRecord with 1 check (displaySurface). */ export async function driveA3(page: Page): Promise { return await page.evaluate(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose. const harness = (window as any).__mokoshHarness; const r: AssertionRecord = await harness.assertA3(); return r; }) as AssertionRecord; } /** * Drive A4 (popup pinned + single offscreen during recording). Assumes * A2 left recording active. Verifies getPopup unchanged + hasDocument * true (no duplicate offscreen spawned). * * @param page - The harness page from `launchHarnessBrowser`. * @returns Structured AssertionRecord with 2 checks (popup + hasDocument). */ export async function driveA4(page: Page): Promise { return await page.evaluate(async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose. const harness = (window as any).__mokoshHarness; const r: AssertionRecord = await harness.assertA4(); return r; }) as AssertionRecord; } /* ─── Wave 3B — WIRED ─────────────────────────────────────────────── */ /** Maximum wait for the SAVE_ARCHIVE zip to appear in `downloadsDir`. */ const A5_DOWNLOAD_POLL_TIMEOUT_MS = 15_000; /** Polling cadence while waiting for the zip. */ const A5_DOWNLOAD_POLL_INTERVAL_MS = 200; /** Filename suffix for the dropped archive. Production code in * `src/background/index.ts:downloadArchive` requests * `session_report__