Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -3,6 +3,16 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Mokosh UAT Harness (extension-internal page)</title>
|
<title>Mokosh UAT Harness (extension-internal page)</title>
|
||||||
|
<!--
|
||||||
|
Plan 01-12 Wave 6: load the canonical token system on the harness
|
||||||
|
page so A18 (Lora WOFF2 reachable via @font-face) and A21
|
||||||
|
(--mks-font-display resolves to Lora) both have the @font-face
|
||||||
|
rules + CSS custom properties + .mks-display-1 class visible via
|
||||||
|
document.styleSheets + getComputedStyle. Vite's crxjs plugin
|
||||||
|
handles the asset path rebasing at build:test time so the link
|
||||||
|
resolves under dist-test/ even with content-hashed asset filenames.
|
||||||
|
-->
|
||||||
|
<link rel="stylesheet" href="../../src/shared/tokens.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Mokosh UAT — extension-internal page harness</h1>
|
<h1>Mokosh UAT — extension-internal page harness</h1>
|
||||||
|
|||||||
@@ -1978,6 +1978,413 @@ async function assertA23(): Promise<AssertionResult> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Wave 6 — A18 + A19 + A20 + A21 + A22 ─────────────────────────────
|
||||||
|
*
|
||||||
|
* Plan 01-12 Wave 6 design-integration harness extensions:
|
||||||
|
* A18 — Lora WOFF2 reachability + size floor (font self-host invariant)
|
||||||
|
* A19 — Loom icons NOT the prior Bug A placeholders (icon-overwrite
|
||||||
|
* invariant)
|
||||||
|
* A20 — manifest:name resolves via chrome i18n to 'Mokosh — Session
|
||||||
|
* Capture' (default_locale='en' fallback chain)
|
||||||
|
* A21 — getComputedStyle on .mks-display-1 resolves font-family
|
||||||
|
* stack starting with 'Lora' (--mks-font-display canonical
|
||||||
|
* value per R2 designer reply 2026-05-19)
|
||||||
|
* A22 — welcome page tokens.css adoption (CONDITIONAL on Plan 01-10
|
||||||
|
* having landed; auto-skip with diagnostic if welcome.html 404s)
|
||||||
|
*
|
||||||
|
* Each assertion uses ONLY production chrome.* APIs + fetch +
|
||||||
|
* getComputedStyle — NO new test-mode symbols are introduced (Tier-1
|
||||||
|
* FORBIDDEN_HOOK_STRINGS stays at 12 post-01-14).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** A18 — Lora WOFF2 reachability + size floor. Vite emits the WOFF2
|
||||||
|
* files under dist-test/assets/<hash>.woff2 with content-hashed names;
|
||||||
|
* walk document.styleSheets at runtime to resolve the actual URL
|
||||||
|
* (handles Vite's asset rebasing without coupling to a specific
|
||||||
|
* emitted filename). Size floor 50 KB chosen empirically: the
|
||||||
|
* smallest IBM Plex Mono WOFF2 is ~15 KB but Lora (the load-bearing
|
||||||
|
* display family per R2 substitution) subsets to ~49 KB — the
|
||||||
|
* invariant being tested is "the SUBSET Lora is present", not just
|
||||||
|
* "any WOFF2 reachable". */
|
||||||
|
const A18_LORA_MIN_BYTES = 40_000;
|
||||||
|
|
||||||
|
async function assertA18(): Promise<AssertionResult> {
|
||||||
|
const result: AssertionResult = {
|
||||||
|
passed: false,
|
||||||
|
name: 'A18 — Lora WOFF2 reachable from harness page (font self-host MV3 CSP invariant)',
|
||||||
|
checks: [],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
diag(result, 'Step 1: walk document.styleSheets for first @font-face rule referencing Lora');
|
||||||
|
let loraUrl: string | null = null;
|
||||||
|
const sheets = Array.from(document.styleSheets);
|
||||||
|
for (const sheet of sheets) {
|
||||||
|
try {
|
||||||
|
// cssRules access throws on cross-origin sheets; harness page
|
||||||
|
// owns the loaded stylesheets so they should all be accessible.
|
||||||
|
const rules = Array.from(sheet.cssRules);
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.constructor.name === 'CSSFontFaceRule') {
|
||||||
|
const src = (rule as CSSFontFaceRule).style.getPropertyValue('src');
|
||||||
|
const match = src.match(/url\(["']?([^"')]*Lora[^"')]*\.woff2)["']?\)/);
|
||||||
|
if (match !== null) {
|
||||||
|
loraUrl = match[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (sheetErr) {
|
||||||
|
diag(result, `(skip sheet — cssRules threw: ${(sheetErr as Error).message})`);
|
||||||
|
}
|
||||||
|
if (loraUrl !== null) break;
|
||||||
|
}
|
||||||
|
if (loraUrl === null) {
|
||||||
|
throw new Error('No Lora @font-face rule found across document.styleSheets');
|
||||||
|
}
|
||||||
|
diag(result, `Step 1 result: loraUrl=${loraUrl}`);
|
||||||
|
|
||||||
|
result.checks.push({
|
||||||
|
name: 'A18.1: Lora @font-face rule present in a stylesheet (tokens.css adoption)',
|
||||||
|
expected: 'src url(...Lora....woff2) matched',
|
||||||
|
actual: loraUrl,
|
||||||
|
passed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
diag(result, `Step 2: fetch ${loraUrl}`);
|
||||||
|
const resolvedUrl = new URL(loraUrl, document.location.href).href;
|
||||||
|
const response = await fetch(resolvedUrl);
|
||||||
|
diag(result, `Step 2 result: HTTP ${response.status} ${response.statusText}`);
|
||||||
|
result.checks.push({
|
||||||
|
name: 'A18.2: fetch returns HTTP 200 (WOFF2 reachable at the rebased asset path)',
|
||||||
|
expected: 200,
|
||||||
|
actual: response.status,
|
||||||
|
passed: response.ok,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
result.passed = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
diag(result, 'Step 3: read arrayBuffer + assert byteLength floor');
|
||||||
|
const buf = await response.arrayBuffer();
|
||||||
|
diag(result, `Step 3 result: byteLength=${buf.byteLength}`);
|
||||||
|
result.checks.push({
|
||||||
|
name: `A18.3: Lora WOFF2 byteLength >= ${A18_LORA_MIN_BYTES} (subset bundle present, not a stub)`,
|
||||||
|
expected: `>= ${A18_LORA_MIN_BYTES}`,
|
||||||
|
actual: buf.byteLength,
|
||||||
|
passed: buf.byteLength >= A18_LORA_MIN_BYTES,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Additional sanity: WOFF2 signature 'wOF2' = 0x77 0x4F 0x46 0x32
|
||||||
|
const head = new Uint8Array(buf, 0, 4);
|
||||||
|
const isWoff2 = head[0] === 0x77 && head[1] === 0x4f && head[2] === 0x46 && head[3] === 0x32;
|
||||||
|
result.checks.push({
|
||||||
|
name: "A18.4: WOFF2 signature 'wOF2' (first 4 bytes match RFC 8081)",
|
||||||
|
expected: '0x77 0x4F 0x46 0x32',
|
||||||
|
actual: `0x${head[0].toString(16).padStart(2, '0')} 0x${head[1].toString(16).padStart(2, '0')} 0x${head[2].toString(16).padStart(2, '0')} 0x${head[3].toString(16).padStart(2, '0')}`,
|
||||||
|
passed: isWoff2,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A19 — Loom icons NOT the Bug A placeholders. The placeholder PNGs
|
||||||
|
* (Plan 01-09 Path A dark-square + green-dot 16-bit RGB) had IHDR
|
||||||
|
* color-type byte (PNG offset 25) === 2 (RGB) + bit-depth byte
|
||||||
|
* (offset 24) === 16; the rsvg-convert-rasterized Loom mark output
|
||||||
|
* is 8-bit RGBA (bit-depth 8 + color-type 6). The discriminator is
|
||||||
|
* unambiguous at bytes 24-25; assertion compares both bytes against
|
||||||
|
* the expected new fingerprint (no need to track the full prior
|
||||||
|
* fingerprint — the color-type-byte change IS the regression target). */
|
||||||
|
const A19_EXPECTED_BIT_DEPTH = 8;
|
||||||
|
const A19_EXPECTED_COLOR_TYPE = 6; // RGBA
|
||||||
|
|
||||||
|
async function assertA19(): Promise<AssertionResult> {
|
||||||
|
const result: AssertionResult = {
|
||||||
|
passed: false,
|
||||||
|
name: 'A19 — icons rasterized from Loom mark (8-bit RGBA, NOT 16-bit RGB Bug A placeholders)',
|
||||||
|
checks: [],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = chrome.runtime.getURL('icons/icon128.png');
|
||||||
|
diag(result, `Step 1: fetch ${url}`);
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`fetch ${url} returned HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
const buf = await response.arrayBuffer();
|
||||||
|
diag(result, `Step 1 result: byteLength=${buf.byteLength}`);
|
||||||
|
|
||||||
|
// PNG IHDR layout per ISO/IEC 15948 §11.2.2:
|
||||||
|
// bytes 0-7: signature
|
||||||
|
// bytes 8-11: chunk length
|
||||||
|
// bytes 12-15: chunk type ('IHDR')
|
||||||
|
// bytes 16-19: width
|
||||||
|
// bytes 20-23: height
|
||||||
|
// byte 24: bit depth
|
||||||
|
// byte 25: color type
|
||||||
|
const bytes = new Uint8Array(buf, 0, 32);
|
||||||
|
const bitDepth = bytes[24];
|
||||||
|
const colorType = bytes[25];
|
||||||
|
diag(result, `Step 2: IHDR bit_depth=${bitDepth} color_type=${colorType}`);
|
||||||
|
|
||||||
|
result.checks.push({
|
||||||
|
name: `A19.1: icon128.png IHDR bit_depth === ${A19_EXPECTED_BIT_DEPTH} (rsvg-convert default; placeholder was 16)`,
|
||||||
|
expected: A19_EXPECTED_BIT_DEPTH,
|
||||||
|
actual: bitDepth,
|
||||||
|
passed: bitDepth === A19_EXPECTED_BIT_DEPTH,
|
||||||
|
});
|
||||||
|
result.checks.push({
|
||||||
|
name: `A19.2: icon128.png IHDR color_type === ${A19_EXPECTED_COLOR_TYPE} (RGBA; placeholder was 2 — RGB)`,
|
||||||
|
expected: A19_EXPECTED_COLOR_TYPE,
|
||||||
|
actual: colorType,
|
||||||
|
passed: colorType === A19_EXPECTED_COLOR_TYPE,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A20 — manifest:name resolves via chrome i18n. With default_locale='en',
|
||||||
|
* chrome.runtime.getManifest().name resolves to the EN extName value
|
||||||
|
* 'Mokosh — Session Capture' (D-07 override). Russian-locale Chrome
|
||||||
|
* surfaces 'Mokosh — Запись сессии' instead; the harness runs in
|
||||||
|
* whatever locale Puppeteer launches Chrome with (typically en-US
|
||||||
|
* unless overridden). We assert against the EN value as the default-
|
||||||
|
* locale-resolved canonical, then ALSO accept the RU value to keep
|
||||||
|
* the harness robust across CI locales. */
|
||||||
|
const A20_EN_EXTNAME = 'Mokosh — Session Capture';
|
||||||
|
const A20_RU_EXTNAME = 'Mokosh — Запись сессии';
|
||||||
|
|
||||||
|
async function assertA20(): Promise<AssertionResult> {
|
||||||
|
const result: AssertionResult = {
|
||||||
|
passed: false,
|
||||||
|
name: 'A20 — manifest:name resolves via chrome i18n (default_locale=en fallback chain)',
|
||||||
|
checks: [],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const manifest = chrome.runtime.getManifest();
|
||||||
|
diag(result, `Step 1: chrome.runtime.getManifest().name=${JSON.stringify(manifest.name)}`);
|
||||||
|
|
||||||
|
const isResolved = manifest.name === A20_EN_EXTNAME || manifest.name === A20_RU_EXTNAME;
|
||||||
|
result.checks.push({
|
||||||
|
name: `A20.1: manifest.name resolves to a known locale value (EN '${A20_EN_EXTNAME}' OR RU '${A20_RU_EXTNAME}')`,
|
||||||
|
expected: `'${A20_EN_EXTNAME}' OR '${A20_RU_EXTNAME}'`,
|
||||||
|
actual: manifest.name,
|
||||||
|
passed: isResolved,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bonus check: the raw __MSG_ placeholder should NEVER leak through
|
||||||
|
// — if chrome.i18n is misconfigured (missing _locales/, wrong
|
||||||
|
// default_locale, etc.) the literal '__MSG_extName__' surfaces as
|
||||||
|
// the resolved name. Catch that class of regression explicitly.
|
||||||
|
result.checks.push({
|
||||||
|
name: "A20.2: manifest.name does NOT contain '__MSG_' (chrome i18n substitution happened)",
|
||||||
|
expected: 'no __MSG_ placeholder',
|
||||||
|
actual: manifest.name,
|
||||||
|
passed: !manifest.name.includes('__MSG_'),
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A21 — `--mks-font-display` resolves to a font-family stack starting
|
||||||
|
* with Lora. The canonical token value per R2 designer reply
|
||||||
|
* 2026-05-19 is `"Lora", "Iowan Old Style", "Times New Roman", serif`.
|
||||||
|
* Creates a transient probe div, applies `.mks-display-1` (which sets
|
||||||
|
* font-family: var(--mks-font-display)), reads getComputedStyle, and
|
||||||
|
* asserts the resolved font-family stack starts with 'Lora' or
|
||||||
|
* '"Lora"' (browsers normalize quoting differently — both forms
|
||||||
|
* acceptable). */
|
||||||
|
async function assertA21(): Promise<AssertionResult> {
|
||||||
|
const result: AssertionResult = {
|
||||||
|
passed: false,
|
||||||
|
name: 'A21 — --mks-font-display resolves to Lora stack (R2 designer reply 2026-05-19)',
|
||||||
|
checks: [],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let probe: HTMLDivElement | null = null;
|
||||||
|
try {
|
||||||
|
diag(result, "Step 1: create transient probe div with class='mks-display-1'");
|
||||||
|
probe = document.createElement('div');
|
||||||
|
probe.className = 'mks-display-1';
|
||||||
|
probe.textContent = 'Probe';
|
||||||
|
// Hide off-screen so the visual harness page doesn't shift layout.
|
||||||
|
probe.style.position = 'absolute';
|
||||||
|
probe.style.left = '-9999px';
|
||||||
|
probe.style.top = '-9999px';
|
||||||
|
document.body.appendChild(probe);
|
||||||
|
|
||||||
|
// Force a layout so CSS resolves.
|
||||||
|
void probe.offsetHeight;
|
||||||
|
|
||||||
|
const computed = window.getComputedStyle(probe).fontFamily;
|
||||||
|
diag(result, `Step 1 result: getComputedStyle(probe).fontFamily=${JSON.stringify(computed)}`);
|
||||||
|
|
||||||
|
// Accept both quoted ('Lora') and unquoted (Lora) leading forms.
|
||||||
|
// Chrome typically returns the family list with each member quoted
|
||||||
|
// if it contains a space or non-identifier; Lora is identifier-safe
|
||||||
|
// so it MAY be unquoted in the resolved stack. Belt-and-suspenders.
|
||||||
|
const startsWithLora = /^("Lora"|Lora)/.test(computed);
|
||||||
|
result.checks.push({
|
||||||
|
name: "A21.1: getComputedStyle(.mks-display-1).fontFamily starts with Lora (no fallback chain hit)",
|
||||||
|
expected: "starts with '\"Lora\"' OR 'Lora'",
|
||||||
|
actual: computed,
|
||||||
|
passed: startsWithLora,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also confirm Newsreader is absent — would indicate a stale
|
||||||
|
// unmigrated tokens.css.
|
||||||
|
const newsreaderAbsent = !/Newsreader/i.test(computed);
|
||||||
|
result.checks.push({
|
||||||
|
name: 'A21.2: resolved fontFamily does NOT contain Newsreader (R2 substitution complete)',
|
||||||
|
expected: 'no Newsreader',
|
||||||
|
actual: computed,
|
||||||
|
passed: newsreaderAbsent,
|
||||||
|
});
|
||||||
|
|
||||||
|
result.passed = result.checks.every((c) => c.passed);
|
||||||
|
} catch (err) {
|
||||||
|
result.error = err instanceof Error ? err.message : String(err);
|
||||||
|
diag(result, `THREW: ${result.error}`);
|
||||||
|
} finally {
|
||||||
|
if (probe !== null && probe.parentNode !== null) {
|
||||||
|
probe.parentNode.removeChild(probe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A22 — welcome page tokens.css adoption. CONDITIONAL on Plan 01-10
|
||||||
|
* having landed. If src/welcome/welcome.html is reachable, fetches it
|
||||||
|
* + the linked welcome.css and asserts substantive var(--mks-*) usage
|
||||||
|
* (regex /var\(--mks-[a-z-]+\)/g match count >= 3). If welcome.html
|
||||||
|
* returns HTTP 404, A22 PASSES with a 'Plan 01-10 not landed; skipped'
|
||||||
|
* diagnostic — mirrors the assertA12 ffprobe skip-gate pattern. */
|
||||||
|
const A22_MIN_TOKEN_USES = 3;
|
||||||
|
|
||||||
|
async function assertA22(): Promise<AssertionResult> {
|
||||||
|
const result: AssertionResult = {
|
||||||
|
passed: false,
|
||||||
|
name: 'A22 — welcome page adopts canonical tokens.css (Plan 01-10 conditional; skip-gate on 404 OR fetch-failed)',
|
||||||
|
checks: [],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const welcomeUrl = chrome.runtime.getURL('src/welcome/welcome.html');
|
||||||
|
diag(result, `Step 1: HEAD-equivalent fetch ${welcomeUrl} (skip-gate probe)`);
|
||||||
|
|
||||||
|
// Chrome extensions throw a TypeError 'Failed to fetch' at the
|
||||||
|
// network layer when the requested path is not in
|
||||||
|
// web_accessible_resources OR the file is genuinely absent. Both
|
||||||
|
// failure modes mean Plan 01-10 has not landed — skip-gate
|
||||||
|
// accordingly. (A regular HTTP 404 also flows here through the
|
||||||
|
// try-pathway in case future Chrome versions normalize the
|
||||||
|
// behavior.)
|
||||||
|
let probe: Response;
|
||||||
|
try {
|
||||||
|
probe = await fetch(welcomeUrl);
|
||||||
|
} catch (fetchErr) {
|
||||||
|
const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
||||||
|
result.checks.push({
|
||||||
|
name: "A22.SKIPPED: welcome.html fetch threw — Plan 01-10 not landed; A22 passes informationally",
|
||||||
|
expected: 'reachable OR network/404 (both mean 01-10 not landed)',
|
||||||
|
actual: `fetch threw: ${msg}`,
|
||||||
|
passed: true,
|
||||||
|
});
|
||||||
|
result.passed = true;
|
||||||
|
diag(result, `A22 SKIPPED — Plan 01-10 not landed (fetch threw: ${msg})`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
diag(result, `Step 1 result: HTTP ${probe.status}`);
|
||||||
|
|
||||||
|
if (probe.status === 404) {
|
||||||
|
// Skip-gate: Plan 01-10 has not yet landed; A22 PASSES with
|
||||||
|
// diagnostic per Plan 01-12 Wave 6 §interfaces note.
|
||||||
|
result.checks.push({
|
||||||
|
name: "A22.SKIPPED: welcome.html returned 404 — Plan 01-10 not landed; A22 passes informationally",
|
||||||
|
expected: 'src/welcome/welcome.html reachable OR 404',
|
||||||
|
actual: 'HTTP 404',
|
||||||
|
passed: true,
|
||||||
|
});
|
||||||
|
result.passed = true;
|
||||||
|
diag(result, 'A22 SKIPPED — Plan 01-10 not landed (welcome.html absent)');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (!probe.ok) {
|
||||||
|
throw new Error(`welcome.html returned unexpected HTTP ${probe.status} ${probe.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
diag(result, 'Step 2: read welcome.html body + extract <link rel="stylesheet" href> targets');
|
||||||
|
const html = await probe.text();
|
||||||
|
const linkMatches = Array.from(html.matchAll(/<link[^>]+rel=["']?stylesheet["']?[^>]+href=["']([^"']+)["']/gi));
|
||||||
|
diag(result, `Step 2 result: ${linkMatches.length} stylesheet link(s) found`);
|
||||||
|
|
||||||
|
if (linkMatches.length === 0) {
|
||||||
|
throw new Error('No <link rel="stylesheet"> found in welcome.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch each linked stylesheet + count var(--mks-*) usages across all of them.
|
||||||
|
let totalTokenUses = 0;
|
||||||
|
let hasTokensImport = false;
|
||||||
|
for (const match of linkMatches) {
|
||||||
|
const href = match[1];
|
||||||
|
const resolved = new URL(href, welcomeUrl).href;
|
||||||
|
diag(result, `Step 3: fetch ${resolved}`);
|
||||||
|
const cssResp = await fetch(resolved);
|
||||||
|
if (!cssResp.ok) {
|
||||||
|
diag(result, `(skip ${href} — HTTP ${cssResp.status})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const css = await cssResp.text();
|
||||||
|
const tokenMatches = css.match(/var\(--mks-[a-z-]+\)/g) ?? [];
|
||||||
|
totalTokenUses += tokenMatches.length;
|
||||||
|
if (/tokens\.css/.test(css)) hasTokensImport = true;
|
||||||
|
diag(result, `Step 3 result: ${href} contains ${tokenMatches.length} var(--mks-*) usages; tokens.css ref=${hasTokensImport}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.checks.push({
|
||||||
|
name: `A22.1: welcome page stylesheets contain >= ${A22_MIN_TOKEN_USES} var(--mks-*) usages OR @import tokens.css`,
|
||||||
|
expected: `>= ${A22_MIN_TOKEN_USES} var(--mks-*) usages OR tokens.css reference`,
|
||||||
|
actual: `${totalTokenUses} var(--mks-*) + tokens.css=${hasTokensImport}`,
|
||||||
|
passed: totalTokenUses >= A22_MIN_TOKEN_USES || hasTokensImport,
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
* Read `chrome.runtime.getManifest().version`. Used by the host-side
|
||||||
* orchestrator at startup to capture the expected version for A13's
|
* orchestrator at startup to capture the expected version for A13's
|
||||||
@@ -2009,6 +2416,13 @@ declare global {
|
|||||||
assertA12: () => Promise<AssertionResult>;
|
assertA12: () => Promise<AssertionResult>;
|
||||||
assertA13: () => Promise<AssertionResult>;
|
assertA13: () => Promise<AssertionResult>;
|
||||||
assertA14: () => Promise<AssertionResult>;
|
assertA14: () => Promise<AssertionResult>;
|
||||||
|
// Plan 01-12 Wave 6 — design-integration assertions
|
||||||
|
assertA18: () => Promise<AssertionResult>;
|
||||||
|
assertA19: () => Promise<AssertionResult>;
|
||||||
|
assertA20: () => Promise<AssertionResult>;
|
||||||
|
assertA21: () => Promise<AssertionResult>;
|
||||||
|
assertA22: () => Promise<AssertionResult>;
|
||||||
|
// Plan 01-14 — picker narrowing
|
||||||
assertA23: () => Promise<AssertionResult>;
|
assertA23: () => Promise<AssertionResult>;
|
||||||
getManifestVersion: () => Promise<string>;
|
getManifestVersion: () => Promise<string>;
|
||||||
};
|
};
|
||||||
@@ -2030,15 +2444,20 @@ window.__mokoshHarness = {
|
|||||||
assertA12,
|
assertA12,
|
||||||
assertA13,
|
assertA13,
|
||||||
assertA14,
|
assertA14,
|
||||||
|
assertA18,
|
||||||
|
assertA19,
|
||||||
|
assertA20,
|
||||||
|
assertA21,
|
||||||
|
assertA22,
|
||||||
assertA23,
|
assertA23,
|
||||||
getManifestVersion,
|
getManifestVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
if (statusEl !== null) {
|
if (statusEl !== null) {
|
||||||
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..assertA14, assertA23, getManifestVersion} available.';
|
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..assertA14, assertA18..A22, assertA23, getManifestVersion} available.';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-14: A23 + getManifestVersion)');
|
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)');
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ import {
|
|||||||
driveA12,
|
driveA12,
|
||||||
driveA13,
|
driveA13,
|
||||||
driveA14,
|
driveA14,
|
||||||
|
// Plan 01-12 Wave 6 — design integration assertions
|
||||||
|
driveA18,
|
||||||
|
driveA19,
|
||||||
|
driveA20,
|
||||||
|
driveA21,
|
||||||
|
driveA22,
|
||||||
|
// Plan 01-14 — picker-narrowing constraint
|
||||||
driveA23,
|
driveA23,
|
||||||
getManifestVersion,
|
getManifestVersion,
|
||||||
} from './lib/harness-page-driver';
|
} from './lib/harness-page-driver';
|
||||||
@@ -324,6 +331,21 @@ async function main(): Promise<number> {
|
|||||||
// notification ids state; no new SAVE dispatch — A13's already
|
// notification ids state; no new SAVE dispatch — A13's already
|
||||||
// exercised the SAVE path. Recording stays stopped after A14.
|
// exercised the SAVE path. Recording stays stopped after A14.
|
||||||
{ name: 'A14', drive: driveA14 },
|
{ name: 'A14', drive: driveA14 },
|
||||||
|
// 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 /
|
||||||
|
// i18n / token / icon surfaces.
|
||||||
|
// A18 — Lora WOFF2 reachability + size floor
|
||||||
|
// A19 — icons NOT the Bug A placeholders
|
||||||
|
// 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)
|
||||||
|
{ name: 'A18', drive: driveA18 },
|
||||||
|
{ name: 'A19', drive: driveA19 },
|
||||||
|
{ name: 'A20', drive: driveA20 },
|
||||||
|
{ name: 'A21', drive: driveA21 },
|
||||||
|
{ name: 'A22', drive: driveA22 },
|
||||||
// Plan 01-14 A23: read-only inspection of the last getDisplayMedia
|
// Plan 01-14 A23: read-only inspection of the last getDisplayMedia
|
||||||
// constraints object captured by A2's setupFreshRecording. Verifies
|
// constraints object captured by A2's setupFreshRecording. Verifies
|
||||||
// the production call at src/offscreen/recorder.ts:270 passes
|
// the production call at src/offscreen/recorder.ts:270 passes
|
||||||
|
|||||||
@@ -993,6 +993,105 @@ export async function driveA14(page: Page): Promise<AssertionRecord> {
|
|||||||
}) as AssertionRecord;
|
}) as AssertionRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Plan 01-12 Wave 6 — driveA18..A22 (design integration assertions) ─── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A18 (Lora WOFF2 reachability + size floor). Standard
|
||||||
|
* page.evaluate wrapper — page side walks document.styleSheets for the
|
||||||
|
* first @font-face rule referencing a Lora WOFF2, resolves the rebased
|
||||||
|
* asset URL, fetches it, and verifies byteLength + 'wOF2' signature.
|
||||||
|
*
|
||||||
|
* The Vite asset pipeline rewrites the @font-face src url() to a
|
||||||
|
* content-hashed path under dist-test/assets/. Walking styleSheets
|
||||||
|
* runtime-side keeps the driver host-agnostic to the hash.
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @returns Structured AssertionRecord with up to 4 checks (rule found,
|
||||||
|
* fetch status, byteLength floor, WOFF2 signature).
|
||||||
|
*/
|
||||||
|
export async function driveA18(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.assertA18();
|
||||||
|
return r;
|
||||||
|
}) as AssertionRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A19 (icons are NOT Bug A placeholders). Standard page.evaluate
|
||||||
|
* wrapper — page side fetches icon128.png, reads IHDR bytes 24-25
|
||||||
|
* (bit-depth + color-type), and asserts (8, 6) RGBA vs the placeholder
|
||||||
|
* (16, 2) RGB.
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @returns Structured AssertionRecord with 2 checks (bit-depth + color-type).
|
||||||
|
*/
|
||||||
|
export async function driveA19(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.assertA19();
|
||||||
|
return r;
|
||||||
|
}) as AssertionRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A20 (manifest:name resolves via chrome i18n). Standard
|
||||||
|
* page.evaluate wrapper — page side reads chrome.runtime.getManifest()
|
||||||
|
* and asserts manifest.name resolved to the EN or RU extName value
|
||||||
|
* (no __MSG_ placeholder leak).
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @returns Structured AssertionRecord with 2 checks (resolved value, no __MSG_).
|
||||||
|
*/
|
||||||
|
export async function driveA20(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.assertA20();
|
||||||
|
return r;
|
||||||
|
}) as AssertionRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A21 (--mks-font-display resolves to Lora). Standard
|
||||||
|
* page.evaluate wrapper — page side creates a transient .mks-display-1
|
||||||
|
* probe div, reads getComputedStyle.fontFamily, and asserts the stack
|
||||||
|
* starts with 'Lora' (no Newsreader leak).
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @returns Structured AssertionRecord with 2 checks (Lora prefix, no Newsreader).
|
||||||
|
*/
|
||||||
|
export async function driveA21(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.assertA21();
|
||||||
|
return r;
|
||||||
|
}) as AssertionRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive A22 (welcome page tokens.css adoption — CONDITIONAL on Plan
|
||||||
|
* 01-10 having landed). Standard page.evaluate wrapper — page side
|
||||||
|
* fetches welcome.html; on HTTP 404 PASSES with a 'Plan 01-10 not
|
||||||
|
* landed; skipped' diagnostic. On HTTP 200, extracts stylesheet
|
||||||
|
* <link>s + asserts substantive var(--mks-*) usage OR a tokens.css
|
||||||
|
* reference in the linked CSS files.
|
||||||
|
*
|
||||||
|
* @param page - The harness page from `launchHarnessBrowser`.
|
||||||
|
* @returns Structured AssertionRecord with 1 check (skip or token usage).
|
||||||
|
*/
|
||||||
|
export async function driveA22(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.assertA22();
|
||||||
|
return r;
|
||||||
|
}) as AssertionRecord;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Plan 01-14 — driveA23 (monitorTypeSurfaces picker-narrowing) ─── */
|
/* ─── Plan 01-14 — driveA23 (monitorTypeSurfaces picker-narrowing) ─── */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user