diff --git a/tests/uat/extension-page-harness.ts b/tests/uat/extension-page-harness.ts index f56b8fa..1d71906 100644 --- a/tests/uat/extension-page-harness.ts +++ b/tests/uat/extension-page-harness.ts @@ -3418,6 +3418,137 @@ async function assertA29(): Promise { return result; } +/* ─── Plan 03-02 — A30 (event-log verification; SPEC §10 #5) ──────── + * + * A30 — REQ-user-event-log empirical: the production listeners at + * src/content/index.ts (setupClickLogging at line 61, + * setupInputLogging at line 77, setupNavigationLogging at line + * 99, setupErrorLogging at line 133, setupNetworkLogging at + * line 164) all fire on synthetic browser events dispatched + * on the harness page, producing UserEvent entries with each + * of the 5 type-values (click / input / navigation / + * js_error / network_error) in logs/events.json. + * + * Trigger strategy (all on the harness page; no new tabs opened): + * - click: programmatic .click() on #probe-submit + * - input: set #probe-email.value + dispatch Event('input', bubbles:true) + * - navigation: history.pushState (intercepted at src/content/index.ts:121) + * - js_error: window.dispatchEvent(new ErrorEvent('error', ...)) + * - network_error: fetch(404-probe-url).catch(noop) — production + * fetch interception at src/content/index.ts:167 logs response.ok===false + * + * Page-side dispatches all 5 triggers + settles + SAVE. Host-side + * driveA30 JSZip-parses logs/events.json and asserts each of the 5 + * UserEvent.type literal values is present. + * + * FORBIDDEN_HOOK_STRINGS impact: NONE. A30 rides production listeners + * + existing helpers. Tier-1 inventory stays at 12. + */ + +/** SAVE_ARCHIVE dispatch timeout for A30 — matches A24/A25/A27/A29. */ +const A30_SAVE_ARCHIVE_TIMEOUT_MS = 15_000; +/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */ +const A30_SEGMENT_SETTLE_MS = 11_000; +/** Settle between trigger dispatches and SAVE so event handlers complete. */ +const A30_TRIGGER_SETTLE_MS = 500; +/** 404 probe URL — chrome.tabs perm grant is irrelevant; fetch happens + * from the harness page realm. example.com is RFC 2606 reserved + + * serves a 404 reliably for unknown paths under headless Chrome. */ +const A30_404_PROBE_URL = 'https://example.com/this-path-does-not-exist-404-probe-a30'; + +/** + * A30 — Event-log empirical (SPEC §10 #5 / REQ-user-event-log). + * + * Dispatches 5 synthetic browser events that exercise each of the + * production listeners; runs setupFreshRecording so event-log + * cleanup hasn't dropped anything; settles a segment; SAVEs. Host-side + * driveA30 inspects logs/events.json from the produced zip and asserts + * each of the 5 UserEvent.type literal values appears at least once. + * + * @returns AssertionResult with 1 page-side check (SAVE ack); host-side + * driveA30 appends 5 UserEvent.type presence checks. + */ +async function assertA30(): Promise { + const result: AssertionResult = { + passed: false, + name: 'A30 — event log captures 5 UserEvent types (SPEC §10 #5 / REQ-user-event-log)', + checks: [], + diagnostics: [], + }; + + try { + diag(result, 'Step 1: setupFreshRecording (A30 owns its recording — clean event-log window)'); + const setupResp = await setupFreshRecording(); + if (!setupResp.ok) { + throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`); + } + diag(result, 'Step 1 OK — REC state established'); + + diag(result, `Step 2: settle ${A30_SEGMENT_SETTLE_MS}ms for first segment rotation`); + await new Promise((r) => setTimeout(r, A30_SEGMENT_SETTLE_MS)); + + diag(result, 'Step 3: click trigger — programmatic .click() on #probe-submit'); + const submitBtn = document.querySelector('#probe-submit'); + if (submitBtn !== null) { + submitBtn.click(); + } else { + diag(result, 'Step 3 WARN — #probe-submit missing; click trigger skipped'); + } + + diag(result, 'Step 4: input trigger — set #probe-email.value + dispatch input event'); + const emailInput = document.querySelector('#probe-email'); + if (emailInput !== null) { + emailInput.value = 'a30@probe.local'; + emailInput.dispatchEvent(new Event('input', { bubbles: true })); + } else { + diag(result, 'Step 4 WARN — #probe-email missing; input trigger skipped'); + } + + diag(result, 'Step 5: navigation trigger — history.pushState (production wrapper at src/content/index.ts:121 intercepts)'); + history.pushState({}, '', window.location.pathname + '#a30-probe'); + + diag(result, 'Step 6: js_error trigger — window.dispatchEvent(ErrorEvent("error"))'); + window.dispatchEvent(new ErrorEvent('error', { + message: 'a30-probe-js-error', + filename: 'a30-probe.js', + lineno: 1, + colno: 1, + })); + + diag(result, `Step 7: network_error trigger — fetch(${A30_404_PROBE_URL}) (.catch noop)`); + try { + await fetch(A30_404_PROBE_URL); + } catch (fetchErr) { + diag(result, `Step 7 fetch threw (acceptable for network_error path): ${String(fetchErr)}`); + } + + diag(result, `Step 8: settle ${A30_TRIGGER_SETTLE_MS}ms so async handlers (fetch.then) complete`); + await new Promise((r) => setTimeout(r, A30_TRIGGER_SETTLE_MS)); + + diag(result, 'Step 9: dispatch SAVE_ARCHIVE'); + const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>( + { type: 'SAVE_ARCHIVE' }, + A30_SAVE_ARCHIVE_TIMEOUT_MS, + 'SAVE_ARCHIVE (A30)', + ); + diag(result, `Step 9 result: ${JSON.stringify(ack)}`); + + result.checks.push({ + name: 'A30.1: SAVE_ARCHIVE ack received with success=true', + expected: true, + actual: ack.success, + passed: ack.success === true, + }); + + result.passed = result.checks.every((c) => c.passed); + } catch (err) { + result.error = err instanceof Error ? err.message : String(err); + diag(result, `THREW: ${result.error}`); + } + + return result; +} + /** * Read `chrome.runtime.getManifest().version`. Used by the host-side * orchestrator at startup to capture the expected version for A13's @@ -3471,6 +3602,8 @@ declare global { assertA28: () => Promise; // Plan 03-01 — rrweb DOM verification (SPEC §10 #4) assertA29: () => Promise; + // Plan 03-02 — event-log verification (SPEC §10 #5) + assertA30: () => Promise; getManifestVersion: () => Promise; }; } @@ -3506,14 +3639,15 @@ window.__mokoshHarness = { assertA27, assertA28, assertA29, + assertA30, getManifestVersion, }; const statusEl = document.getElementById('status'); if (statusEl !== null) { - statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A29, getManifestVersion} available.'; + statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A30, getManifestVersion} available.'; } -console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + getManifestVersion)'); +console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + Plan 03-02: A30 + getManifestVersion)'); export {};