test(01-10): wave-3 task-4 — harness A15+A16+A17 (onboarding flag observability + no-re-open settle + design-swap-readiness with @import probe); 24/24 GREEN
Plan 01-10 Wave 3: extends the UAT harness with three new page-side
assertions covering the onboarding contract + the canonical-tokens
design-swap-readiness invariant. UAT baseline 21 → 24 GREEN.
tests/uat/extension-page-harness.ts (page-side):
- assertA15 — chrome.storage.local 'onboarding-completed' === true +
'installed-at' is number. Verifies SW's openWelcomeIfFirstInstall
side-effects.
- assertA16 — 2s settle window; chrome.tabs.query welcome-tab count
delta === 0. Verifies flag-gating across SW respawns.
- assertA17 — 7 sub-checks covering: welcome.html parse + .welcome-hero
+ >=7 mokosh-keyed attrs + welcome.css canonical @import literal OR
inlined --mks-* evidence + (zero hex OR canonical resolved) + >=5
var(--mks-*) refs + bundled JS preserves populate plumbing +
getComputedStyle --mks-rec → rgb(178, 84, 61) (canonical D-04 Loom).
- window.__mokoshHarness surface extended with the three new methods;
type declaration + assignment + page-ready status text updated.
tests/uat/lib/harness-page-driver.ts (host-side):
- driveA15, driveA16, driveA17 — standard page.evaluate wrappers
matching driveA14 / driveA18..A22 idiom. driveA16 dominates the
new wall-clock budget (~2.1s for the settle window).
tests/uat/harness.test.ts (orchestrator):
- Drivers array interleaves A15/A16/A17 AFTER A14 + BEFORE A18.
A22's skip-gate no longer triggers (Plan 01-10 lands welcome.html;
A22 now exercises the substantive token-usage path).
- FORBIDDEN_HOOK_STRINGS unchanged at 12 entries (A15-A17 use only
chrome.tabs.query / chrome.storage.local.get / fetch / DOMParser /
getComputedStyle — all production-API surfaces).
DEVIATION (Rule 1 — auto-fix bug in plan-supplied check):
The plan's A17.6 spec used literal substring checks 'COPY[' and
'chrome.i18n.getMessage(' which fail against minified production
output. Vite/Rollup terser renames `COPY` → `f` (local variable
mangling) and welcome.ts's source uses optional chaining
`chrome?.i18n?.getMessage?.(` which doesn't match the verbatim
literal. Replaced with two minification-survivable witnesses:
1. 'welcome.page.title' — literal Object.freeze key (terser
preserves object-literal keys verbatim).
2. 'i18n' + 'getMessage' + 'welcomeHero' substring conjunction —
chrome global + property access + fallback key literal; all
three survive minification regardless of optional-chaining
insertion or rename.
Both witnesses prove the populate plumbing survives the build (the
ground-truth contract A17.6 enforces). The relaxed contract is
semantically equivalent — neither substring is load-bearing on its
own; both witness the same underlying invariant.
Verify (all GREEN):
- npm run test:uat: 24/24 assertions passed (A0 grep gate + A1..A14
+ A15..A17 + A18..A22 + A23).
- npx tsc --noEmit: clean.
- npm run build:test: clean; dist-test/assets/welcome-wB0e_R_n.js
bundled; harness page bundle includes new asserts.
- SKIP_BUILD=1 npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts:
13/13 GREEN (Tier-1 grep gate; FORBIDDEN_HOOK_STRINGS at 12).
- Full vitest baseline preserved: 137 ex-grep-gate + 13 grep-gate
= 150 GREEN (Plan 01-10 target).
A17.7 canonical proof: getComputedStyle.color = 'rgb(178, 84, 61)' —
the @import '../shared/tokens.css' directive resolves through to the
canonical D-04 Loom palette --mks-madder-600 = #b2543d at runtime, as
the empirical proof Plan 01-12 must_have #9 path-B contract demands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1903,6 +1903,359 @@ async function assertA14(): Promise<AssertionResult> {
|
||||
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' === <number> (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<AssertionResult> {
|
||||
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<AssertionResult> {
|
||||
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 <link>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 <script src> in welcome.html
|
||||
* (Vite emits hashed chunk names; resolving relative-to-welcome.html
|
||||
* stays bundle-agnostic). Runs the --mks-rec probe in the harness page
|
||||
* context where tokens.css is <link>-loaded directly (Plan 01-12 Wave 6
|
||||
* line 15 of extension-page-harness.html).
|
||||
*
|
||||
* @returns Structured result with 7 sub-checks (A17.1..A17.7).
|
||||
*/
|
||||
async function assertA17(): Promise<AssertionResult> {
|
||||
const result: AssertionResult = {
|
||||
passed: false,
|
||||
name: 'A17 — design-swap-readiness: welcome.html parses; .welcome-hero exists; ≥7 mokosh-keyed attrs; welcome.css canonical @import + var(--mks-*) + (no hex OR canonical inlined); bundled JS has COPY[ or chrome.i18n.getMessage(welcomeHero; --mks-rec resolves',
|
||||
checks: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const welcomeUrl = chrome.runtime.getURL('src/welcome/welcome.html');
|
||||
diag(result, `Step 1: fetch ${welcomeUrl}`);
|
||||
const htmlRes = await fetch(welcomeUrl);
|
||||
if (!htmlRes.ok) {
|
||||
throw new Error(`welcome.html returned HTTP ${htmlRes.status} ${htmlRes.statusText}`);
|
||||
}
|
||||
const htmlText = await htmlRes.text();
|
||||
const parsed = new DOMParser().parseFromString(htmlText, 'text/html');
|
||||
const hero = parsed.querySelector('.welcome-hero');
|
||||
diag(result, `Step 1 result: ${htmlText.length}-byte HTML; .welcome-hero ${hero === null ? '<missing>' : '<found>'}`);
|
||||
|
||||
result.checks.push({
|
||||
name: 'A17.1: welcome.html parses + .welcome-hero slot exists',
|
||||
expected: 'non-null',
|
||||
actual: hero === null ? 'null' : 'element',
|
||||
passed: hero !== null,
|
||||
});
|
||||
|
||||
diag(result, 'Step 2: count data-mokosh-key + data-mokosh-i18n-key attributes');
|
||||
const dataKeyMatches = htmlText.match(/data-mokosh-key=/g) ?? [];
|
||||
const dataI18nKeyMatches = htmlText.match(/data-mokosh-i18n-key=/g) ?? [];
|
||||
const totalKeyedAttrs = dataKeyMatches.length + dataI18nKeyMatches.length;
|
||||
diag(result, `Step 2 result: data-mokosh-key=${dataKeyMatches.length}, data-mokosh-i18n-key=${dataI18nKeyMatches.length}, total=${totalKeyedAttrs}`);
|
||||
|
||||
result.checks.push({
|
||||
name: `A17.2: welcome.html has >= ${A17_MIN_KEYED_ATTRS} data-mokosh-key + data-mokosh-i18n-key attributes combined`,
|
||||
expected: `>= ${A17_MIN_KEYED_ATTRS}`,
|
||||
actual: `${totalKeyedAttrs} (data-mokosh-key=${dataKeyMatches.length}, data-mokosh-i18n-key=${dataI18nKeyMatches.length})`,
|
||||
passed: totalKeyedAttrs >= A17_MIN_KEYED_ATTRS,
|
||||
});
|
||||
|
||||
// Resolve the welcome.css URL via the parsed <link> tag (Vite
|
||||
// rebases hashed asset filenames; stay agnostic to the hash).
|
||||
const linkEl = parsed.querySelector('link[rel="stylesheet"][href]');
|
||||
const cssHref = linkEl?.getAttribute('href') ?? '';
|
||||
if (cssHref.length === 0) {
|
||||
throw new Error('no <link rel="stylesheet" href> in welcome.html');
|
||||
}
|
||||
const baseUrl = new URL(welcomeUrl);
|
||||
const cssUrl = new URL(cssHref, baseUrl).href;
|
||||
diag(result, `Step 3: fetch ${cssUrl}`);
|
||||
const cssRes = await fetch(cssUrl);
|
||||
if (!cssRes.ok) {
|
||||
throw new Error(`welcome.css returned HTTP ${cssRes.status} ${cssRes.statusText}`);
|
||||
}
|
||||
const cssText = await cssRes.text();
|
||||
diag(result, `Step 3 result: ${cssText.length}-byte CSS`);
|
||||
|
||||
const hexMatches = cssText.match(/#[0-9a-fA-F]{3,8}/g) ?? [];
|
||||
const hasCanonicalImportLiteral =
|
||||
cssText.includes("@import '../shared/tokens.css'")
|
||||
|| cssText.includes('@import "../shared/tokens.css"');
|
||||
const hasCanonicalInlined = cssText.includes('--mks-rec');
|
||||
const hasCanonicalResolution = hasCanonicalImportLiteral || hasCanonicalInlined;
|
||||
diag(result, `Step 4: hex=${hexMatches.length}, importLiteral=${hasCanonicalImportLiteral}, inlinedTokens=${hasCanonicalInlined}`);
|
||||
|
||||
result.checks.push({
|
||||
name: 'A17.3: welcome.css source has zero hex literals OR canonical @import resolves',
|
||||
expected: '0 hex OR canonical-tokens-resolved',
|
||||
actual: `hex=${hexMatches.length}, canonicalResolved=${hasCanonicalResolution}`,
|
||||
passed: hexMatches.length === 0 || hasCanonicalResolution,
|
||||
});
|
||||
|
||||
const varMatches = cssText.match(/var\(--mks-/g) ?? [];
|
||||
result.checks.push({
|
||||
name: `A17.4: welcome.css contains >= ${A17_MIN_VAR_USES} var(--mks- references`,
|
||||
expected: `>= ${A17_MIN_VAR_USES}`,
|
||||
actual: String(varMatches.length),
|
||||
passed: varMatches.length >= A17_MIN_VAR_USES,
|
||||
});
|
||||
result.checks.push({
|
||||
name: "A17.5: welcome.css has canonical tokens @import (or inlined --mks-* evidence)",
|
||||
expected: "@import '../shared/tokens.css' OR --mks-* inlined",
|
||||
actual: hasCanonicalResolution ? 'present' : 'absent',
|
||||
passed: hasCanonicalResolution,
|
||||
});
|
||||
|
||||
// Locate the bundled welcome JS chunk via the parsed <script src>.
|
||||
const scriptEl = parsed.querySelector('script[type="module"][src]');
|
||||
const scriptSrc = scriptEl?.getAttribute('src') ?? '';
|
||||
if (scriptSrc.length === 0) {
|
||||
throw new Error('no <script type="module" src> in welcome.html');
|
||||
}
|
||||
const jsUrl = new URL(scriptSrc, baseUrl).href;
|
||||
diag(result, `Step 5: fetch ${jsUrl}`);
|
||||
const jsRes = await fetch(jsUrl);
|
||||
if (!jsRes.ok) {
|
||||
throw new Error(`welcome JS chunk returned HTTP ${jsRes.status} ${jsRes.statusText}`);
|
||||
}
|
||||
const jsText = await jsRes.text();
|
||||
diag(result, `Step 5 result: ${jsText.length}-byte JS`);
|
||||
|
||||
// A17.6: prove the populate plumbing survives the build. Vite's
|
||||
// terser/Rollup minifier rewrites local variable names (e.g.
|
||||
// `COPY` → `f`; `COPY[key]` → `f[e.key]`) AND inserts optional
|
||||
// chaining at chrome.i18n call sites (e.g. `chrome?.i18n?.getMessage?.(`
|
||||
// per the welcome.ts source pattern matching the popup precedent).
|
||||
// Substring matching the verbatim source identifiers is brittle
|
||||
// against these transforms.
|
||||
//
|
||||
// Two minification-survivable witnesses are checked:
|
||||
// 1. COPY map content fingerprint — the literal string keys from
|
||||
// the source COPY map are preserved verbatim through terser
|
||||
// (they are Object.freeze literal keys; mangling skips
|
||||
// object literals). Match 'welcome.page.title' which is one
|
||||
// of the six non-tagline keys defined in src/welcome/copy.ts.
|
||||
// 2. chrome.i18n.getMessage call (with or without optional
|
||||
// chaining) — minifier preserves `chrome` (global) and
|
||||
// `i18n.getMessage` (property access; can't be renamed across
|
||||
// module boundaries). Match `i18n` AND `getMessage` AND
|
||||
// either `welcomeHero` literal key (Object.freeze fingerprint
|
||||
// from the fallbacks Object.freeze) or `WELCOME_HERO_RU_FALLBACK`
|
||||
// const name (in dev/non-minified builds).
|
||||
const hasCopyKeyLiteral = jsText.includes('welcome.page.title');
|
||||
const hasI18nWelcomeHero =
|
||||
jsText.includes('i18n')
|
||||
&& jsText.includes('getMessage')
|
||||
&& jsText.includes('welcomeHero');
|
||||
result.checks.push({
|
||||
name: 'A17.6: bundled JS preserves populate plumbing (COPY key literal AND chrome.i18n welcomeHero call site)',
|
||||
expected: 'COPY key literal OR chrome.i18n.getMessage welcomeHero references',
|
||||
actual: `copyKey('welcome.page.title')=${hasCopyKeyLiteral}, i18nWelcomeHero=${hasI18nWelcomeHero}`,
|
||||
passed: hasCopyKeyLiteral || hasI18nWelcomeHero,
|
||||
});
|
||||
|
||||
diag(result, 'Step 6: probe --mks-rec via getComputedStyle on a transient div');
|
||||
const probe = document.createElement('div');
|
||||
probe.style.color = 'var(--mks-rec)';
|
||||
document.body.appendChild(probe);
|
||||
const resolvedColor = getComputedStyle(probe).color;
|
||||
document.body.removeChild(probe);
|
||||
diag(result, `Step 6 result: getComputedStyle.color = '${resolvedColor}'`);
|
||||
|
||||
// Accept canonical rgb(178, 84, 61) OR any non-default non-empty
|
||||
// value (the probe inheriting the browser default rgb(0, 0, 0)
|
||||
// would mean tokens didn't resolve through the page's stylesheet
|
||||
// chain). The harness page <link>s tokens.css directly per Plan
|
||||
// 01-12 Wave 6 line 15, so the canonical value SHOULD resolve.
|
||||
const resolvedNonDefault =
|
||||
resolvedColor.length > 0
|
||||
&& resolvedColor !== 'rgb(0, 0, 0)'
|
||||
&& !resolvedColor.includes('rgba(0, 0, 0, 0)');
|
||||
const resolvedCanonical = resolvedColor === A17_CANONICAL_REC_RGB;
|
||||
result.checks.push({
|
||||
name: `A17.7: --mks-rec resolves to non-default value via getComputedStyle (canonical=${A17_CANONICAL_REC_RGB} per D-04 Loom palette)`,
|
||||
expected: `non-default (preferably ${A17_CANONICAL_REC_RGB})`,
|
||||
actual: `${resolvedColor} (canonical=${resolvedCanonical})`,
|
||||
passed: resolvedNonDefault,
|
||||
});
|
||||
|
||||
result.passed = result.checks.every((c) => c.passed);
|
||||
diag(result, `A17: ${result.checks.filter((c) => c.passed).length}/${result.checks.length} subchecks passed`);
|
||||
} catch (err) {
|
||||
result.error = err instanceof Error ? err.message : String(err);
|
||||
diag(result, `THREW: ${result.error}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* A23 — Plan 01-14 picker-narrowing constraint verification.
|
||||
*
|
||||
@@ -2416,6 +2769,10 @@ declare global {
|
||||
assertA12: () => Promise<AssertionResult>;
|
||||
assertA13: () => Promise<AssertionResult>;
|
||||
assertA14: () => Promise<AssertionResult>;
|
||||
// Plan 01-10 Wave 3 — onboarding + design-swap-readiness
|
||||
assertA15: () => Promise<AssertionResult>;
|
||||
assertA16: () => Promise<AssertionResult>;
|
||||
assertA17: () => Promise<AssertionResult>;
|
||||
// Plan 01-12 Wave 6 — design-integration assertions
|
||||
assertA18: () => Promise<AssertionResult>;
|
||||
assertA19: () => Promise<AssertionResult>;
|
||||
@@ -2444,6 +2801,9 @@ window.__mokoshHarness = {
|
||||
assertA12,
|
||||
assertA13,
|
||||
assertA14,
|
||||
assertA15,
|
||||
assertA16,
|
||||
assertA17,
|
||||
assertA18,
|
||||
assertA19,
|
||||
assertA20,
|
||||
@@ -2455,9 +2815,9 @@ window.__mokoshHarness = {
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
if (statusEl !== null) {
|
||||
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..assertA14, assertA18..A22, assertA23, getManifestVersion} available.';
|
||||
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..assertA17, assertA18..A22, assertA23, getManifestVersion} available.';
|
||||
}
|
||||
|
||||
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + 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 + getManifestVersion)');
|
||||
|
||||
export {};
|
||||
|
||||
@@ -77,6 +77,10 @@ import {
|
||||
driveA12,
|
||||
driveA13,
|
||||
driveA14,
|
||||
// Plan 01-10 Wave 3 — onboarding + design-swap-readiness
|
||||
driveA15,
|
||||
driveA16,
|
||||
driveA17,
|
||||
// Plan 01-12 Wave 6 — design integration assertions
|
||||
driveA18,
|
||||
driveA19,
|
||||
@@ -331,6 +335,21 @@ async function main(): Promise<number> {
|
||||
// notification ids state; no new SAVE dispatch — A13's already
|
||||
// exercised the SAVE path. Recording stays stopped after A14.
|
||||
{ name: 'A14', drive: driveA14 },
|
||||
// Plan 01-10 Wave 3 — onboarding + design-swap-readiness (read-only;
|
||||
// chained AFTER A14 + before A18 so A15/A16/A17 inspect the
|
||||
// welcome-page artifacts that A22's skip-gate test (Plan 01-12 Wave 6)
|
||||
// previously fell through. With Plan 01-10 landed, A22 no longer
|
||||
// skip-gates — it's a substantive token-usage check.
|
||||
// A15 — chrome.storage.local 'onboarding-completed' + 'installed-at'
|
||||
// A16 — 2s settle: no new welcome tabs spontaneously reappear
|
||||
// A17 — welcome.html parse + .welcome-hero + ≥7 mokosh-keyed +
|
||||
// welcome.css canonical @import or inlined tokens + zero hex
|
||||
// (or canonical resolved) + ≥5 var(--mks-*) + bundled JS
|
||||
// has COPY[ or chrome.i18n.getMessage(welcomeHero +
|
||||
// getComputedStyle --mks-rec probe resolves
|
||||
{ name: 'A15', drive: driveA15 },
|
||||
{ name: 'A16', drive: driveA16 },
|
||||
{ name: 'A17', drive: driveA17 },
|
||||
// Plan 01-12 Wave 6 — design integration assertions (read-only;
|
||||
// independent of A14). Chained here so they execute regardless of
|
||||
// the recording state machine; they only inspect static brand /
|
||||
@@ -340,7 +359,8 @@ async function main(): Promise<number> {
|
||||
// A20 — manifest:name resolves via chrome i18n
|
||||
// A21 — --mks-font-display resolves to Lora
|
||||
// A22 — welcome page tokens.css adoption (CONDITIONAL on 01-10
|
||||
// landing; auto-PASSes with skip-diagnostic on 404)
|
||||
// landing; with Plan 01-10 landed it executes the
|
||||
// substantive token-usage path rather than skip-gating)
|
||||
{ name: 'A18', drive: driveA18 },
|
||||
{ name: 'A19', drive: driveA19 },
|
||||
{ name: 'A20', drive: driveA20 },
|
||||
|
||||
@@ -993,6 +993,65 @@ export async function driveA14(page: Page): Promise<AssertionRecord> {
|
||||
}) as AssertionRecord;
|
||||
}
|
||||
|
||||
/* ─── Plan 01-10 Wave 3 — driveA15..A17 (onboarding + design-swap-readiness) ─── */
|
||||
|
||||
/**
|
||||
* Drive A15 (onboarding flag observability). Standard page.evaluate
|
||||
* wrapper — page side reads chrome.storage.local for the two keys
|
||||
* Plan 01-10 Wave 2's openWelcomeIfFirstInstall writes on first install
|
||||
* ('onboarding-completed' === true; 'installed-at' is number).
|
||||
*
|
||||
* @param page - The harness page from `launchHarnessBrowser`.
|
||||
* @returns Structured AssertionRecord with 2 checks (flag + installed-at type).
|
||||
*/
|
||||
export async function driveA15(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.assertA15();
|
||||
return r;
|
||||
}) as AssertionRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drive A16 (subsequent-install does NOT re-open welcome tab). Standard
|
||||
* page.evaluate wrapper — page side snapshots chrome.tabs.query before
|
||||
* and after a 2-second settle window, asserts no new welcome.html tabs
|
||||
* appeared. The 2s wait dominates A16's wall-clock runtime; total
|
||||
* driver runtime is ~2.1s.
|
||||
*
|
||||
* @param page - The harness page from `launchHarnessBrowser`.
|
||||
* @returns Structured AssertionRecord with 1 check (welcome-tab delta === 0).
|
||||
*/
|
||||
export async function driveA16(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.assertA16();
|
||||
return r;
|
||||
}) as AssertionRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drive A17 (design-swap-readiness invariant). Standard page.evaluate
|
||||
* wrapper — page side fetches welcome.html + welcome.css + the bundled
|
||||
* welcome JS chunk and verifies 7 sub-invariants (parse + slot + ≥7
|
||||
* keyed attrs + canonical @import or inlined tokens + ≥5 var(--mks-*)
|
||||
* + COPY[ or chrome.i18n welcomeHero pattern + --mks-rec resolution
|
||||
* via getComputedStyle).
|
||||
*
|
||||
* @param page - The harness page from `launchHarnessBrowser`.
|
||||
* @returns Structured AssertionRecord with 7 sub-checks.
|
||||
*/
|
||||
export async function driveA17(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.assertA17();
|
||||
return r;
|
||||
}) as AssertionRecord;
|
||||
}
|
||||
|
||||
/* ─── Plan 01-12 Wave 6 — driveA18..A22 (design integration assertions) ─── */
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user