|
|
|
|
@@ -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 {};
|
|
|
|
|
|