diff --git a/tests/uat/extension-page-harness.ts b/tests/uat/extension-page-harness.ts index 14c42cf..15566c8 100644 --- a/tests/uat/extension-page-harness.ts +++ b/tests/uat/extension-page-harness.ts @@ -1903,6 +1903,359 @@ async function assertA14(): Promise { return result; } +/* ─── Plan 01-10 Wave 3 — A15 + A16 + A17 (onboarding + design-swap-readiness) ─── + * + * Plan 01-10 D-17-onboarding harness extensions: + * A15 — onboarding flag observability: chrome.storage.local + * 'onboarding-completed' === true AND 'installed-at' is a + * number (SW's openWelcomeIfFirstInstall side-effects observed + * from extension-internal context with full chrome.* privilege). + * A16 — subsequent-install does NOT re-open welcome tab: snapshot + * chrome.tabs.query before a 2-second settle window; assert no + * new welcome.html tabs appear (flag-gating contract: the SW's + * onInstalled handler running again across SW respawns must + * NOT spawn additional welcome tabs once the flag is set). + * A17 — design-swap-readiness invariant (EXTENDED per Plan 01-12 + * path-B revision): welcome.html parses + .welcome-hero slot + * exists + ≥7 mokosh-keyed attrs + welcome.css carries the + * canonical @import directive (or inlined evidence) + zero + * #hex literals in welcome.css source-file region + ≥5 + * var(--mks-*) references + bundled welcome JS contains COPY[ + * OR chrome.i18n.getMessage('welcomeHero pattern + A17.7's + * getComputedStyle probe resolves --mks-rec to a non-default + * value (canonical rgb(178, 84, 61) = #b2543d). + * + * Each assertion uses ONLY production chrome.* APIs + fetch + DOMParser + * + getComputedStyle — NO new test-mode symbols are introduced (Tier-1 + * FORBIDDEN_HOOK_STRINGS stays at 12 post-01-14). + */ + +/** + * A15 — onboarding flag observability. Read-only inspection of + * chrome.storage.local for the two keys Plan 01-10 Wave 2's + * openWelcomeIfFirstInstall sets on first install: + * - 'onboarding-completed' === true + * - 'installed-at' === (Date.now() at install) + * + * The flag-gating contract this verifies is: the SW's onInstalled + * handler ran with reason='install' AT LEAST ONCE this session AND + * the helper completed both chrome.storage.local.set + chrome.tabs.create + * before A15 fires. The harness page is opened as a normal tab during + * `launchHarnessBrowser` AFTER Chrome has loaded the extension, so by + * the time A15 fires the chrome.runtime.onInstalled.addListener has + * already fired with reason='install' (first-install path) and the + * helper has had ample time to complete its three awaited calls. + * + * Mirrors `assertA22` style: bridge-free read of public chrome.storage.local. + * + * @returns Structured result with 2 checks (flag === true + installed-at number). + */ +async function assertA15(): Promise { + const result: AssertionResult = { + passed: false, + name: "A15 — onboarding flag set on first install (chrome.storage.local 'onboarding-completed' === true; 'installed-at' is number)", + checks: [], + diagnostics: [], + }; + + try { + diag(result, "Step 1: chrome.storage.local.get(['onboarding-completed', 'installed-at'])"); + const stored = await chrome.storage.local.get([ + 'onboarding-completed', + 'installed-at', + ]); + diag(result, `Step 1 result: ${JSON.stringify(stored)}`); + + result.checks.push({ + name: "A15.1: chrome.storage.local 'onboarding-completed' === true (set by openWelcomeIfFirstInstall on first install)", + expected: true, + actual: stored['onboarding-completed'], + passed: stored['onboarding-completed'] === true, + }); + result.checks.push({ + name: "A15.2: chrome.storage.local 'installed-at' is a number (Date.now() recorded at install)", + expected: 'number', + actual: typeof stored['installed-at'], + passed: typeof stored['installed-at'] === 'number', + }); + + 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; +} + +/** A16 — subsequent-install settle window (welcome tab does NOT spontaneously + * reappear). Snapshot before/after a 2-second window; the delta MUST be 0 + * because the openWelcomeIfFirstInstall helper's flag-gating prevents + * re-fire even if the SW spawns multiple onInstalled events across + * respawn cycles. 2 s is a generous over-budget; the SW's storage.get + * + storage.set + tabs.create round-trip is sub-100ms. */ +const A16_SETTLE_MS = 2_000; +const A16_WELCOME_URL_SUFFIX = 'src/welcome/welcome.html'; + +/** + * A16 — subsequent-install does NOT re-open welcome tab. Snapshot + * chrome.tabs.query({}) for welcome.html tab URLs before a settle + * window, then again after, and assert no NEW welcome tabs appeared. + * + * Implementation note: chrome.tabs.query without filters returns all + * tabs across all windows. We filter to URLs ending with the + * '/src/welcome/welcome.html' suffix (extension-id-agnostic). + * + * @returns Structured result with 1 check (delta === 0). + */ +async function assertA16(): Promise { + const result: AssertionResult = { + passed: false, + name: 'A16 — subsequent install does NOT re-open welcome tab (2s settle window: no new welcome.html tabs appear)', + checks: [], + diagnostics: [], + }; + + try { + diag(result, 'Step 1: snapshot chrome.tabs.query({}) — count welcome.html tabs BEFORE settle'); + const tabsBefore = await chrome.tabs.query({}); + const beforeCount = tabsBefore.filter( + (t) => typeof t.url === 'string' && t.url.endsWith(A16_WELCOME_URL_SUFFIX), + ).length; + diag(result, `Step 1 result: ${beforeCount} welcome tab(s) before settle`); + + diag(result, `Step 2: settle ${A16_SETTLE_MS}ms (sufficient for any spurious onInstalled re-fire to flush)`); + await new Promise((r) => setTimeout(r, A16_SETTLE_MS)); + + diag(result, 'Step 3: re-snapshot chrome.tabs.query({}) — count welcome.html tabs AFTER settle'); + const tabsAfter = await chrome.tabs.query({}); + const afterCount = tabsAfter.filter( + (t) => typeof t.url === 'string' && t.url.endsWith(A16_WELCOME_URL_SUFFIX), + ).length; + diag(result, `Step 3 result: ${afterCount} welcome tab(s) after settle (delta=${afterCount - beforeCount})`); + + const delta = afterCount - beforeCount; + result.checks.push({ + name: 'A16.1: welcome-tab count delta over 2s settle === 0 (onInstalled flag-gating works)', + expected: 0, + actual: delta, + passed: delta === 0, + }); + + 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; +} + +/** A17 invariants — design-swap-readiness contract from Plan 01-10 + * must_have #9. Each constant captures one numeric threshold used in + * the A17 sub-checks. + * + * A17.3: zero hex literals in welcome.css source-file region — relaxed + * per post-01-12 revision: if Vite inlines @import contents, hex + * literals from the canonical token values may surface in the BUILT + * artifact. Pass criteria becomes (no hex OR canonical-tokens-resolved). + * A17.4: ≥ 5 var(--mks-*) references. + * A17.5: @import '../shared/tokens.css' literal in welcome.css OR + * inlined evidence via '--mks-rec' literal in the compiled bundle. + * A17.6: bundled JS contains 'COPY[' OR 'chrome.i18n.getMessage(' + * with welcomeHero substring (either pattern proves populate plumbing). + * A17.7: getComputedStyle probe — --mks-rec resolves to canonical + * rgb(178, 84, 61). The harness page s tokens.css directly (Plan + * 01-12 Wave 6); the probe creates a transient div, applies the var, + * reads computed color. + */ +const A17_MIN_KEYED_ATTRS = 7; +const A17_MIN_VAR_USES = 5; +const A17_CANONICAL_REC_RGB = 'rgb(178, 84, 61)'; + +/** + * A17 — design-swap-readiness invariant. Multi-step assertion verifying + * welcome.html + welcome.css + the bundled welcome JS chunk all hold + * the contracts that Plan 01-10's path-B (canonical tokens import) + + * chrome.i18n adoption depend on. + * + * Reaches the bundled JS via parsing the