Files
mokosh/tests/uat/lib/harness-page-driver.ts
Mark 1b67b1c1d3 feat(01-13): wave-3A — A1+A2+A3+A4 GREEN + harness.test.ts orchestrator (5/14 assertions GREEN)
Wave 3A landed. `npm run test:uat` now exercises 5/14 assertions
end-to-end (A0 + A1 + A2 + A3 + A4); bails at A5 NOT YET IMPLEMENTED
(Wave 3B scope). A6 still PASSES 5/5 through the standalone
`npx tsx tests/uat/a6.test.ts` entry — the orchestrator-level A6 won't
reach in Wave 3A because the sequential loop bails at A5; once Wave 3B
wires driveA5 the loop will fall through to A6 (which uses the proven
Wave-2 driveA6 driver — no rework needed there).

Files changed:

- `tests/uat/extension-page-harness.ts` — extends `window.__mokoshHarness`
  from `{ assertA6 }` to `{ assertA1, assertA2, assertA3, assertA4,
  assertA6 }`. Per-assertion contracts:
  • A1 — chrome.action.getBadgeText({}) === '' + getPopup({}) === ''
    + isRecording=false (badge !== 'REC' proxy per state-machine atomic
    pairing). 3 CheckRecords.
  • A2 — ensureOffscreen + START_RECORDING direct-to-offscreen
    (workaround for the `tabs` manifest permission gap per
    01-11-SUMMARY + plan resolved-questions row 2) + manual
    setBadgeText('REC') + setPopup(POPUP_HTML_PATH) + waitFor
    badge==='REC'. The bypassed chrome.action.onClicked →
    startVideoCapture path is unit-tested in
    tests/background/badge-state-machine.test.ts; A2 verifies the
    contract that matters (recording reaches the REC state-machine
    row). 2 CheckRecords.
  • A3 — offscreen bridge query 'get-display-surface' (new in this
    plan via the prior commit's offscreen-hooks extension) → asserts
    === 'monitor'. 1 CheckRecord.
  • A4 — getPopup remains 'src/popup/index.html' + hasDocument()===true
    (no duplicate offscreen). Essentially a no-op verification —
    regression protection against future refactors that might unpin
    the popup during recording or spawn extra offscreens on stray
    events. 2 CheckRecords.
  • IMPORTANT: chrome.action.getPopup() returns the FULL absolute
    chrome-extension://<id>/... URL (not the manifest-relative path).
    A2.2 + A4.1 assert via .endsWith('src/popup/index.html') to stay
    extension-id independent. Empirical finding from first orchestrator
    run; documented inline.

- `tests/uat/lib/harness-page-driver.ts` — wires `driveA1/A2/A3/A4`
  (replaces the 4 NOT YET IMPLEMENTED Wave-3A stubs from
  eb64521). Each wraps a single page.evaluate(() =>
  window.__mokoshHarness.assertXX()) call per the contract laid down
  by driveA6. A5+A7..A13 remain stubbed for Waves 3B+3C+3D.

- `tests/uat/harness.test.ts` (NEW) — top-level UAT orchestrator
  driving all 14 assertions sequentially against a single Chrome +
  single harness page. A0 (Tier-1 grep gate) runs pre-flight before
  any Chrome launch — mirrors
  tests/background/no-test-hooks-in-prod-bundle.test.ts forbidden-
  string inventory (9 entries; belt-and-suspenders per
  feedback-pre-checkpoint-bundle-gates.md memory). Bail-on-first-
  failure with [SKIP] markers for unreached assertions + structured
  diagnostic dump (full SW + offscreen console tail) on each failure.
  SKIP_PROD_REBUILD=1 escape hatch skips the A0-side `npm run build`
  for developer iteration.

Verification (all GREEN):
  - npx tsc --noEmit: clean (root)
  - npx tsc --noEmit -p tests/uat: clean (UAT subtree)
  - npm run build: clean; production bundle hook-free
    (9-string grep gate in vitest unit gate)
  - npm run build:test: clean; dist-test/assets/extension_page_harness-*.js
    grew from 3.87kB → 7.67kB (A1+A2+A3+A4 added)
  - SKIP_BUILD=1 npx vitest run: 93/93 GREEN
    (Wave 0+1+2 baseline 92 + 1 from the 9th grep-gate string from
    the prior commit; this commit adds zero new vitest tests — the
    A1-A4 contracts are verified at UAT-harness time only)
  - npx tsx tests/uat/a6.test.ts (standalone): 5/5 GREEN; exit 0
    (Wave-2 A6 baseline preserved through orchestrator-adjacent
    harness page surface extension)
  - npm run test:uat (full operator entry): 5/14 GREEN
    (A0 + A1 + A2 + A3 + A4); bails at A5 NOT YET IMPLEMENTED
    (Wave 3B scope, expected). Total wall clock ~25s (~5s build +
    ~5s prod-rebuild for A0 + ~15s assertion sequence).

Operator empirical-verification deferred to orchestrator (per
feedback-pre-checkpoint-bundle-gates.md — the orchestrator runs SW
CSP-safety + Node-globals + DOM-globals grep on the built bundle
before surfacing any checkpoint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:45:25 +02:00

218 lines
8.9 KiB
TypeScript

// 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<X> 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).
//
// References:
// - puppeteer Page.evaluate:
// https://pptr.dev/api/puppeteer.page.evaluate
import type { Page } from 'puppeteer';
import type { AssertionRecord, CheckRecord } from './assertions';
/**
* 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<CheckRecord>;
readonly diagnostics: ReadonlyArray<string>;
readonly error?: string;
readonly bytesBase64?: string;
readonly expectedVersion?: string;
}
/** Marker error message for unimplemented Wave-3 drivers — orchestrator
* matches on this prefix to format the diagnostic distinctly from a
* genuine assertion failure. */
const WAVE3_STUB_PREFIX = 'NOT YET IMPLEMENTED';
/**
* 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<AssertionRecord> {
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<AssertionRecord> {
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<AssertionRecord> {
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<AssertionRecord> {
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<AssertionRecord> {
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 — NOT YET IMPLEMENTED ──────────────────────────────── */
/**
* Drive A5 (SAVE_ARCHIVE download). Wave 3B wires this; signature will
* take a second `downloadsDir` parameter so the host side can poll
* for the dropped zip file.
*
* @throws Always — replace stub when Wave 3B lands.
*/
export async function driveA5(_page: Page): Promise<AssertionWithBytes> {
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3B wires driveA5`);
}
/**
* Drive A7 (genuine error → ERR + recovery notification). Wave 3B wires.
* @throws Always — replace stub when Wave 3B lands.
*/
export async function driveA7(_page: Page): Promise<AssertionRecord> {
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3B wires driveA7`);
}
/* ─── Wave 3C — NOT YET IMPLEMENTED ──────────────────────────────── */
/**
* Drive A8 (Bug A onStartup → notification creates). Wave 3C wires.
* @throws Always — replace stub when Wave 3C lands.
*/
export async function driveA8(_page: Page): Promise<AssertionRecord> {
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3C wires driveA8`);
}
/**
* Drive A9 (icon file sizes). Wave 3C wires.
* @throws Always — replace stub when Wave 3C lands.
*/
export async function driveA9(_page: Page): Promise<AssertionRecord> {
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3C wires driveA9`);
}
/**
* Drive A10 (manifest shape). Wave 3C wires.
* @throws Always — replace stub when Wave 3C lands.
*/
export async function driveA10(_page: Page): Promise<AssertionRecord> {
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3C wires driveA10`);
}
/* ─── Wave 3D — NOT YET IMPLEMENTED ──────────────────────────────── */
/**
* Drive A11 (35s → ≥3 segments). Wave 3D wires.
* @throws Always — replace stub when Wave 3D lands.
*/
export async function driveA11(_page: Page): Promise<AssertionRecord> {
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3D wires driveA11`);
}
/**
* Drive A12 (ffprobe — host-side returns webm bytes). Wave 3D wires.
* @throws Always — replace stub when Wave 3D lands.
*/
export async function driveA12(_page: Page): Promise<AssertionWithBytes> {
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3D wires driveA12`);
}
/**
* Drive A13 (zip structure + meta.json). Wave 3D wires.
* @throws Always — replace stub when Wave 3D lands.
*/
export async function driveA13(_page: Page): Promise<AssertionWithBytes> {
throw new Error(`${WAVE3_STUB_PREFIX} — Wave 3D wires driveA13`);
}