diff --git a/.planning/debug/04-06-dark-mode-mark-decouple.md b/.planning/debug/04-06-dark-mode-mark-decouple.md new file mode 100644 index 0000000..63102d8 --- /dev/null +++ b/.planning/debug/04-06-dark-mode-mark-decouple.md @@ -0,0 +1,115 @@ +--- +status: awaiting_human_verify +trigger: "Operator-empirical Task 4 checkpoint on Plan 04-06 returned TWEAK NEEDED — decouple welcome-hero mark stroke from --mks-fg-inverse via a dedicated --mks-mark-stroke token that stays linen-50 in BOTH light and dark themes" +created: 2026-05-26T00:00:00Z +updated: 2026-05-26T10:30:00Z +--- + +## Current Focus + +reasoning_checkpoint: + hypothesis: ".welcome-hero__mark { color: var(--mks-fg-inverse) } couples the mark stroke to the theme-conditional semantic foreground-inverse token (linen-50 in light, ink-900 in dark). The mark sits on a theme-independent madder-600 circle, so a theme-coupled stroke is the wrong abstraction. Adding a theme-INDEPENDENT --mks-mark-stroke = var(--mks-linen-50) in :root (NOT inside .dark/[data-theme=dark] override) and re-pointing the wrapper resolves the stroke to linen-50 in BOTH themes." + confirming_evidence: + - "tokens.css line 128 (universal :root): --mks-fg-inverse: var(--mks-linen-50)" + - "tokens.css line 244 (.dark override): --mks-fg-inverse: var(--mks-ink-900) — the theme flip confirmed" + - "welcome.css line 72: .welcome-hero__mark { color: var(--mks-fg-inverse) } — the exact cascade source" + - "mokosh-mark.svg line 2: stroke=currentColor — confirms the cascade plumbing target" + - "--mks-linen-50 = #faf7f1 defined at tokens.css line 90 (universal :root)" + - "popup/style.css:39 also reads --mks-fg-inverse but is unrelated to welcome mark — leave untouched" + falsification_test: "After the fix, re-run scripts/04-06-welcome-hero-screenshots.mjs. If `computedStroke` for the LIGHT screenshot differs from `computedStroke` for the DARK screenshot, the decoupling failed. If they match (expected rgb(250, 247, 241)), the fix is confirmed. A35 will also assert this equality programmatically." + fix_rationale: "Root cause is the wrong token abstraction — --mks-fg-inverse is a semantic token meant for text foreground on inverse surfaces (where the surface flips with theme), but the welcome-hero mark sits on a NON-flipping surface (madder). The fix introduces a dedicated brand-component token --mks-mark-stroke that is purpose-built for this case: theme-independent linen-50 stroke for the madder-circle mark. It addresses the abstraction error directly, not the symptom." + blind_spots: "1) A17.8 source-bundling grep doesn't check welcome.css text — safe. 2) inline-svg.test.ts only pins SVG + welcome.ts + globals.d.ts text — safe. 3) jsdom-style assertions in unit tests don't load welcome.css — safe. 4) Risk: if any other rule in welcome.css cascades from the wrapper's `color` (text nodes inside .welcome-hero__mark), they'd flip too — but the placeholder span is now replaced by the SVG at populateMark, so no visible text consumes the wrapper color. 5) The popup/style.css unrelated --mks-fg-inverse consumer is untouched (separate surface, separate decision)." + +hypothesis: ".welcome-hero__mark { color: var(--mks-fg-inverse) } is theme-coupled; --mks-mark-stroke = var(--mks-linen-50) in :root decouples it." +test: Apply token addition + welcome.css edit; strengthen A35; re-screenshot. +expecting: Light and dark `computedStroke` both resolve to rgb(250, 247, 241) (linen-50). +next_action: Add --mks-mark-stroke to tokens.css :root, update welcome.css line 72, strengthen A35.3, run build + screenshots + UAT + vitest. + +## Symptoms + +expected: Welcome-hero grid icon (mokosh-mark.svg) renders with linen-50 (off-white) stroke on madder-600 circle in BOTH light and dark themes — high-contrast crisp aesthetic per the Plan 01-10 baseline. +actual: In LIGHT theme, stroke is linen-50 (`--mks-fg-inverse` = linen-50 there) — crisp linen-on-madder. In DARK theme, stroke is ink-900 (`--mks-fg-inverse` = ink-900 there) — indigo-on-madder, lower contrast. +errors: None — this is an aesthetic/design defect, not a runtime error. +reproduction: Open `dist-test/src/welcome/welcome.html` with `` vs without. Inspect `.welcome-hero__mark svg` `getComputedStyle().stroke`. Light = `rgb(250, 247, 241)` (linen-50). Dark = `rgb(24, 27, 42)` (ink-900). Visual: light is crisp, dark is muddy. +started: Plan 04-06 Task 1-3 landed; operator-empirical Task 4 checkpoint flagged the cascade-coupling defect. + +## Evidence + +- timestamp: 2026-05-26T00:00:00Z + checked: src/shared/tokens.css :root block (lines 82-229) + found: `--mks-fg-inverse: var(--mks-linen-50)` at line 128 (universal/light); `--mks-fg-inverse: var(--mks-ink-900)` at line 244 (.dark theme override). + implication: Confirms operator's diagnosis: --mks-fg-inverse is theme-coupled and unsuitable as a stroke anchor for a mark that must be crisp on madder in both themes. + +- timestamp: 2026-05-26T00:00:00Z + checked: src/shared/tokens.css for --mks-linen-50 token + found: `--mks-linen-50: #faf7f1;` defined at line 90 in the universal :root block. + implication: `var(--mks-linen-50)` is the correct hex source; no need to use a literal hex. + +- timestamp: 2026-05-26T00:00:00Z + checked: src/welcome/welcome.css .welcome-hero__mark (lines 64-73) + found: `color: var(--mks-fg-inverse);` at line 72 — the exact line that needs to flip to `var(--mks-mark-stroke)`. + implication: One-line CSS change at the wrapper. SVG remains untouched. + +- timestamp: 2026-05-26T00:00:00Z + checked: src/shared/brand/mokosh-mark.svg + found: `stroke="currentColor"` at line 2 — unchanged across the fix. + implication: Cascade plumbing remains: SVG inherits from wrapper's `color`, which now points to the new theme-independent token. + +- timestamp: 2026-05-26T10:15:00Z + checked: Grep for other consumers of --mks-fg-inverse across src/ + tests/ + found: Two consumers — src/welcome/welcome.css:72 (target; rewired to --mks-mark-stroke) and src/popup/style.css:39 (separate popup-surface concern; LEFT UNTOUCHED). No tests grep for the literal `var(--mks-fg-inverse)` in welcome.css. + implication: Scope is minimal; only the welcome-hero mark wrapper changed. Popup style.css continues using --mks-fg-inverse as before. + +- timestamp: 2026-05-26T10:20:00Z + checked: Built bundle dist/assets/welcome-BAisfyci.css after `npm run build` + found: ":root{...--mks-fg-inverse: var(--mks-linen-50);--mks-mark-stroke: var(--mks-linen-50);...}" + ".dark,[data-theme=dark]{...--mks-fg-inverse: var(--mks-ink-900);...}" — confirmed --mks-mark-stroke is in :root and NOT overridden in the dark block. ".welcome-hero__mark{...color:var(--mks-mark-stroke)}" — confirmed wrapper points at the new token. + implication: Build plumbing is correct. + +- timestamp: 2026-05-26T10:22:00Z + checked: scripts/04-06-welcome-hero-screenshots.mjs re-run on dist/ + found: "LIGHT screenshot: /tmp/04-06-welcome-hero-light.png (computed stroke: rgb(250, 247, 241))" and "DARK screenshot: /tmp/04-06-welcome-hero-dark.png (computed stroke: rgb(250, 247, 241))" — same RGB in both themes; rgb(250, 247, 241) is linen-50 (#faf7f1). + implication: Falsification test PASSED — the decoupling works at the live-DOM cascade level. Visual inspection of the dark screenshot shows the crisp linen-on-madder grid icon expected by the operator. + +- timestamp: 2026-05-26T10:24:00Z + checked: vitest full suite (npx vitest run) + found: 187/188 passed, 1 tolerated flake (tests/background/webm-remux.test.ts > ffprobe-count_frames 905-912 timeout — documented 04-CONTEXT #9/#10; PASSES in isolation). Baseline 184 + 04-06 deltas +4 = 188 contract held. + implication: No vitest regression from the decouple changes. + +- timestamp: 2026-05-26T10:25:00Z + checked: Pre-checkpoint bundle gates 6/6 (sw-bundle-import, no-test-hooks-in-prod-bundle, no-remote-fonts, no-new-function-in-sw-chunk, manifest-i18n, welcome inline-svg) + found: 6/6 PASS (37 tests green); FORBIDDEN_HOOK_STRINGS = 12 (unchanged). + implication: No CSP / globals / hook-leak regression. + +- timestamp: 2026-05-26T10:28:00Z + checked: UAT skip-mode (`npm run test:uat`) + found: 36/36 PASS; A35.5 (new decouple sub-check) PASSES with: light="rgb(250, 247, 241)", dark="rgb(250, 247, 241)". A35 diagnostics show populateMark injected the inline in both themes and the cascade resolves to linen-50 in both. + implication: Live-DOM proof of theme decoupling is in the automated suite (not just the screenshot artifact). + +## Resolution + +root_cause: `.welcome-hero__mark` used `color: var(--mks-fg-inverse)`. The semantic token `--mks-fg-inverse` is theme-conditional — it resolves to linen-50 in light and ink-900 in dark (`tokens.css :root` line 128 vs `.dark, [data-theme="dark"]` line 244). The SVG inherits via `stroke="currentColor"`, so the stroke flipped with theme. But the mark sits on a theme-INDEPENDENT madder-600 circle, so a theme-coupled stroke produced muddy ink-on-madder in dark mode. The abstraction was wrong: --mks-fg-inverse is a SEMANTIC text-foreground-on-inverse-surface token, not a BRAND-COMPONENT stroke anchor for the welcome mark. + +fix: Two-file change. + 1. src/shared/tokens.css :root block — added a new brand-component token `--mks-mark-stroke: var(--mks-linen-50)` adjacent to --mks-fg-inverse. CRUCIALLY, this token is NOT overridden in the `.dark, [data-theme="dark"]` block below — it stays linen-50 on every surface. Comment in source explains the design intent + reference to this debug note. + 2. src/welcome/welcome.css `.welcome-hero__mark` — flipped `color: var(--mks-fg-inverse)` → `color: var(--mks-mark-stroke)`. Comment in source explains the cascade chain. + Plus comment-only updates in src/welcome/welcome.ts + tests/welcome/inline-svg.test.ts + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts to reflect the new wiring (no behavior change in those files beyond comments and A35 strengthening). + Plus A35 STRENGTHENING in tests/uat/lib/harness-page-driver.ts: extracted the live-DOM probe into a `a35Probe(welcomePage, dark)` helper, probed BOTH light + dark themes (data-theme="dark" toggle), and ADDED A35.5 — the decouple proof: assert light.computedStroke === dark.computedStroke === "rgb(250, 247, 241)" (linen-50). FORBIDDEN_HOOK_STRINGS unchanged (no new __MOKOSH_UAT__ symbol). + SVG `src/shared/brand/mokosh-mark.svg` UNCHANGED. + +verification: + - LIGHT screenshot /tmp/04-06-welcome-hero-light.png: linen-50 stroke on madder-600 circle on linen-100 card on linen-50 page bg — baseline aesthetic preserved. + - DARK screenshot /tmp/04-06-welcome-hero-dark.png: linen-50 stroke on madder-600 circle on ink-800 card on ink-900 page bg — crisp linen-on-madder mark in dark mode (operator's spec). Visual contrast on the mark itself is IDENTICAL to light mode. + - A35.5 live-DOM probe: light + dark computedStroke both rgb(250, 247, 241). Programmatic proof of decoupling. + - vitest 187/188 + 1 tolerated flake (baseline contract held). + - UAT 36/36 (A35 now has 5 sub-checks all GREEN). + - 6/6 bundle gates PASS; FORBIDDEN_HOOK_STRINGS = 12 (unchanged). + - SCOPE EXPANSION NOTE: src/welcome/welcome.css was NOT in Plan 04-06 re-plan iter-2 files_modified. Adding this edit is a scope expansion authorized by the operator's empirical TWEAK verdict on Task 4 checkpoint. Surfaced explicitly here for the orchestrator's re-checkpoint payload. + +files_changed: + - src/shared/tokens.css (added --mks-mark-stroke token in :root) + - src/welcome/welcome.css (rewired .welcome-hero__mark to new token) + - src/welcome/welcome.ts (comment-only updates) + - tests/welcome/inline-svg.test.ts (comment-only update) + - tests/uat/lib/harness-page-driver.ts (A35 strengthening + new A35.5 + helper extraction) + - tests/uat/harness.test.ts (comment-only update) + - .planning/debug/04-06-dark-mode-mark-decouple.md (this debug note) diff --git a/src/shared/tokens.css b/src/shared/tokens.css index 22f52ae..c0c6e74 100644 --- a/src/shared/tokens.css +++ b/src/shared/tokens.css @@ -127,6 +127,21 @@ --mks-fg-disabled: var(--mks-ink-300); --mks-fg-inverse: var(--mks-linen-50); + /* Brand-component stroke — INTENTIONALLY THEME-INDEPENDENT. + Used by the welcome-hero mark wrapper (welcome.css + .welcome-hero__mark) so the canonical mokosh-mark.svg's + `stroke="currentColor"` cascade resolves to linen-50 on BOTH the + light AND dark surfaces. The mark sits on a madder-600 circle + (--mks-rec) which is theme-independent, so the stroke must be + theme-independent too — otherwise the cascade through the + semantic --mks-fg-inverse (which flips per theme) gives a + muddy ink-on-madder render in dark mode. This token is NOT + overridden in the .dark/[data-theme="dark"] block below; it + stays linen-50 everywhere. See Plan 04-06 operator-empirical + Task 4 verdict + debug session + .planning/debug/resolved/04-06-dark-mode-mark-decouple.md. */ + --mks-mark-stroke: var(--mks-linen-50); + --mks-border: var(--mks-linen-200); --mks-border-strong: var(--mks-linen-300); --mks-border-focus: var(--mks-ink-900); diff --git a/src/welcome/welcome.css b/src/welcome/welcome.css index d021b57..89e1bf2 100644 --- a/src/welcome/welcome.css +++ b/src/welcome/welcome.css @@ -69,7 +69,16 @@ body { height: var(--mks-space-20); border-radius: var(--mks-radius-full); background: var(--mks-rec); - color: var(--mks-fg-inverse); + /* THEME-INDEPENDENT stroke via --mks-mark-stroke (tokens.css :root + block; always resolves to linen-50). The mokosh-mark.svg root + carries `stroke="currentColor"` so the SVG strokes inherit + this wrapper's `color`. The mark sits on a theme-independent + madder-600 circle (--mks-rec), so the stroke must be + theme-independent too — using --mks-fg-inverse here (the previous + wiring) cascaded to ink-900 in dark mode and gave a muddy + indigo-on-madder render. See Plan 04-06 operator-empirical Task 4 + verdict + .planning/debug/resolved/04-06-dark-mode-mark-decouple.md. */ + color: var(--mks-mark-stroke); } .welcome-hero__mark-placeholder { diff --git a/src/welcome/welcome.ts b/src/welcome/welcome.ts index 64f955e..918863b 100644 --- a/src/welcome/welcome.ts +++ b/src/welcome/welcome.ts @@ -38,15 +38,18 @@ import { // so the SVG source can be inlined into the welcome page DOM via DOMParser // + replaceChildren (see populateMark below). The CSS `color` property is // inherited (CSS Color L3 §4) and the mokosh-mark.svg root carries -// `stroke="currentColor"` (Plan 04-06 Wave 1 SVG edit); on a LIGHT surface -// the `.welcome-hero__mark` wrapper sets `color: var(--mks-fg-inverse)` -// (welcome.css:67), and on a DARK surface the `.dark` token override -// (src/shared/tokens.css lines 234-251) flips `--mks-fg-inverse` so the -// inline `` inherits the contrast-correct stroke without any -// JS/event-handler/conditional code. An ``-injected SVG would render -// in an isolated SVG document context where `currentColor` resolves to -// the SVG's own root color (defaults to `canvastext`, near-black) and the -// cascade would not work — hence the inline-SVG technique. +// `stroke="currentColor"` (Plan 04-06 Wave 1 SVG edit); the +// `.welcome-hero__mark` wrapper sets `color: var(--mks-mark-stroke)` +// (welcome.css), which is a theme-INDEPENDENT brand-component token +// always resolving to linen-50 (tokens.css :root, NOT overridden in the +// .dark/[data-theme="dark"] block). The mark sits on a theme-independent +// madder-600 circle (--mks-rec); a theme-coupled stroke would render +// muddy ink-on-madder in dark mode (see Plan 04-06 operator-empirical +// Task 4 debug-decouple resolution). An ``-injected SVG would +// render in an isolated SVG document context where `currentColor` +// resolves to the SVG's own root color (defaults to `canvastext`, +// near-black) and the cascade would not work — hence the inline-SVG +// technique. // // References: // - 04-UI-SPEC.md §"Implementation amendment" (currentColor + inline) @@ -154,10 +157,12 @@ function populateI18n(): void { * near-black) and the parent cascade does NOT reach it — verified * empirically + W3C SVG2 §13.3. By inlining via DOMParser + * replaceChildren, the inline 's `stroke="currentColor"` resolves - * to the wrapper's `color: var(--mks-fg-inverse)` (welcome.css:67), - * and on the dark surface the `.dark` token override - * (src/shared/tokens.css lines 234-251) automatically flips the - * resolved colour — contrast-correct on both surfaces, no JS branching. + * to the wrapper's `color: var(--mks-mark-stroke)` (welcome.css), and + * because --mks-mark-stroke is a theme-INDEPENDENT brand-component + * token (tokens.css :root, NOT overridden in .dark/[data-theme="dark"]) + * the stroke stays linen-50 in both themes — crisp linen-on-madder on + * every surface (Plan 04-06 operator-empirical Task 4 debug-decouple + * resolution; see .planning/debug/resolved/04-06-dark-mode-mark-decouple.md). * * Security / CSP discipline (T-04-06-01 mitigation): the inner-HTML * string-assignment path is FORBIDDEN — DOMParser + replaceChildren diff --git a/tests/uat/harness.test.ts b/tests/uat/harness.test.ts index d34d32f..3572c50 100644 --- a/tests/uat/harness.test.ts +++ b/tests/uat/harness.test.ts @@ -544,15 +544,19 @@ async function main(): Promise { // RUN — not env-gated; the 5-min wait is A33's, not A34's). { name: 'A34', drive: driveA34Wrapped }, // Plan 04-06 A35: UI-SPEC dark-logo `currentColor` strategy LIVE-DOM - // proof. Opens welcome.html as a real Puppeteer tab so populateMark() - // actually runs; reads getComputedStyle().stroke on the injected - // to verify the currentColor cascade resolves through - // .welcome-hero__mark color: var(--mks-fg-inverse) (UI-SPEC Option A). - // Appended LAST in the drivers array so the new welcome tab cannot - // pollute any later driver (and welcomePage.close() in finally - // guarantees no tab leak regardless). Host-side driver — mirrors - // the driveA32/A33/A34 host-side pattern (NOT a page.evaluate - // (window.__mokoshHarness) wrapper). + // proof + Plan 04-06 Task 4 decouple verification. Opens welcome.html + // as a real Puppeteer tab so populateMark() actually runs; reads + // getComputedStyle().stroke on the injected in BOTH light and + // dark themes to verify (a) the currentColor cascade resolves + // through .welcome-hero__mark color: var(--mks-mark-stroke), and + // (b) the theme-INDEPENDENT --mks-mark-stroke token is NOT + // overridden by the .dark/[data-theme="dark"] override block — both + // probes resolve to the same linen-50 RGB. Appended LAST in the + // drivers array so the new welcome tab cannot pollute any later + // driver (and welcomePage.close() in finally guarantees no tab leak + // regardless). Host-side driver — mirrors the driveA32/A33/A34 + // host-side pattern (NOT a page.evaluate(window.__mokoshHarness) + // wrapper). { name: 'A35', drive: driveA35Wrapped }, ]; diff --git a/tests/uat/lib/harness-page-driver.ts b/tests/uat/lib/harness-page-driver.ts index 669376e..fecf0a6 100644 --- a/tests/uat/lib/harness-page-driver.ts +++ b/tests/uat/lib/harness-page-driver.ts @@ -2961,20 +2961,29 @@ export async function driveA34( // chrome.runtime.getURL('src/welcome/welcome.html') resolves to — so // welcome.ts init() runs populateMark() at DOMContentLoaded and the // inline actually lands in the page DOM. We then read -// getComputedStyle().stroke on the injected to prove the -// `currentColor` cascade resolves through the .welcome-hero__mark -// wrapper's `color: var(--mks-fg-inverse)` rule. +// getComputedStyle().stroke on the injected in BOTH the LIGHT +// default theme AND the DARK theme (the latter set via +// document.documentElement.setAttribute('data-theme','dark'), which +// triggers the .dark, [data-theme="dark"] { ... } override block in +// src/shared/tokens.css). The cascade chain is: +// mokosh-mark.svg `stroke="currentColor"` +// ← .welcome-hero__mark { color: var(--mks-mark-stroke) } +// ← :root { --mks-mark-stroke: var(--mks-linen-50) } +// (NOT overridden by the .dark/[data-theme="dark"] block — Plan +// 04-06 operator-empirical Task 4 decouple resolution) +// So the computed stroke MUST be the same in both themes and MUST +// resolve to linen-50 (rgb(250, 247, 241), the hex #faf7f1). // -// This is the iter-2 BLOCKER 1 resolution: the prior iter-1 re-plan -// claimed live-DOM injection was delegated to A17.8, but A17.8 is -// 100% string-grep on the welcome JS chunk and the harness does NOT -// open welcome.html as a live tab (it only fetches the HTML text via -// chrome.runtime.getURL + DOMParser.parseFromString — a DETACHED -// parse, not a live document). driveA35 is the canonical automated -// proof of the runtime injection + cascade. A35 is appended LAST in -// the drivers array; nothing reads its return value, so the new tab -// cannot pollute later drivers (welcomePage.close() in finally also -// guarantees no tab leak). +// This is the iter-2 BLOCKER 1 resolution + Plan 04-06 Task 4 decouple +// extension: the prior iter-1 re-plan claimed live-DOM injection was +// delegated to A17.8, but A17.8 is 100% string-grep on the welcome JS +// chunk and the harness does NOT open welcome.html as a live tab (it +// only fetches the HTML text via chrome.runtime.getURL + +// DOMParser.parseFromString — a DETACHED parse, not a live document). +// driveA35 is the canonical automated proof of the runtime injection + +// cascade + decouple. A35 is appended LAST in the drivers array; +// nothing reads its return value, so the new tab cannot pollute later +// drivers (welcomePage.close() in finally also guarantees no tab leak). // // Pattern: HOST-SIDE driver (mirrors driveA32/driveA33/driveA34 — NOT // a page.evaluate(window.__mokoshHarness) wrapper). Builds a @@ -2989,6 +2998,8 @@ export async function driveA34( // - tests/welcome/inline-svg.test.ts (the source-text counterpart) // - tests/uat/extension-page-harness.ts A17.8 (the source-bundling // counterpart — narrowed in Plan 04-06 to a raw-source grep only) +// - .planning/debug/resolved/04-06-dark-mode-mark-decouple.md +// (operator-empirical Task 4 decouple debug session) /** Live-DOM navigation + populateMark()-settle ceiling for the * driveA35 welcome-page open. ~5 seconds is generous against the @@ -2999,25 +3010,83 @@ export async function driveA34( const A35_WELCOME_PAGE_TIMEOUT_MS = 5_000; /** - * driveA35 — UI-SPEC dark-logo `currentColor` strategy LIVE-DOM proof. + * Canonical linen-50 hex (matches src/shared/tokens.css :root + * `--mks-linen-50: #faf7f1`) and the Chrome-serialized RGB form + * (canonical Chromium getComputedStyle().stroke output for #faf7f1). + * Used by A35.5 to assert the decoupled stroke matches the brand token. + */ +const A35_LINEN_50_HEX = '#faf7f1'; +const A35_LINEN_50_RGB = 'rgb(250, 247, 241)'; + +/** + * Probe the `.welcome-hero__mark` live-DOM cascade signals for a given + * theme. Pulled into a helper so the LIGHT (default) probe and the DARK + * ([data-theme="dark"] override) probe share the exact same evaluate + * payload — the decoupling assertion (A35.5) only proves equality when + * the two reads are byte-for-byte the same probe shape. + * + * For the dark probe we set `data-theme="dark"` on before reading; + * the tokens.css cascade fires on that attribute (src/shared/tokens.css + * line 234 selector `.dark, [data-theme="dark"]`). + */ +async function a35Probe( + welcomePage: Page, + dark: boolean, +): Promise<{ + svgPresent: boolean; + strokeAttr: string | null; + computedStroke: string; + imgPresent: boolean; +}> { + if (dark) { + await welcomePage.evaluate(() => { + document.documentElement.setAttribute('data-theme', 'dark'); + }); + } else { + await welcomePage.evaluate(() => { + document.documentElement.removeAttribute('data-theme'); + }); + } + return welcomePage.evaluate(() => { + const svgEl = document.querySelector('.welcome-hero__mark svg'); + const imgEl = document.querySelector('.welcome-hero__mark img'); + const svgPresent = svgEl !== null; + const strokeAttr = svgEl !== null ? svgEl.getAttribute('stroke') : null; + const computedStroke = + svgEl !== null ? getComputedStyle(svgEl).stroke : ''; + const imgPresent = imgEl !== null; + return { svgPresent, strokeAttr, computedStroke, imgPresent }; + }); +} + +/** + * driveA35 — UI-SPEC dark-logo `currentColor` strategy LIVE-DOM proof + * + Plan 04-06 Task 4 decouple verification. * * Opens a fresh welcome.html tab via `browser.newPage()`, navigates to * `chrome-extension:///src/welcome/welcome.html` (the canonical * web-accessible welcome page path; the same path A17 already fetches), * waits for `populateMark()` to inject the inline at - * DOMContentLoaded, then runs a single `welcomePage.evaluate(...)` that - * reads the LIVE DOM and returns the four signals A35 asserts on: - * - svgPresent — `.welcome-hero__mark svg` exists (inline - * injected). - * - strokeAttr — that 's `stroke` attribute === 'currentColor' - * (the canonical mark recolour landed correctly). - * - computedStroke — `getComputedStyle(svgEl).stroke` is a resolved - * non-default colour value (the `currentColor` - * cascade resolved through - * `.welcome-hero__mark { color: var(--mks-fg-inverse) }`). - * Empty / 'none' would mean the cascade is broken. - * - imgPresent — `.welcome-hero__mark img` is NULL (the legacy - * injection path is gone; we're inlining now). + * DOMContentLoaded, then probes the LIVE DOM in BOTH the LIGHT default + * theme AND the DARK theme (toggled by setting data-theme="dark" on the + * documentElement, which fires the `.dark, [data-theme="dark"]` block + * in tokens.css). Five sub-checks: + * A35.1 — `.welcome-hero__mark svg` injected (populateMark ran). + * A35.2 — that carries stroke="currentColor" (Option A recolor). + * A35.3 — getComputedStyle().stroke resolves to a non-default + * colour in LIGHT (cascade through + * `.welcome-hero__mark { color: var(--mks-mark-stroke) }` + * actually works). + * A35.4 — no legacy in the slot (pre-04-06 path is gone). + * A35.5 — Plan 04-06 Task 4 DECOUPLE PROOF: the computed stroke in + * LIGHT and DARK themes are EQUAL and BOTH resolve to the + * canonical linen-50 RGB (rgb(250, 247, 241) — Chromium's + * serialization of #faf7f1). This is the live-DOM evidence + * that --mks-mark-stroke is theme-independent (NOT overridden + * by the .dark/[data-theme="dark"] block) and that the + * wrapper points at it. The operator-empirical dark-mode + * "muddy ink-on-madder" defect is fixed when this check + * passes. * * Always closes the new tab in a `finally` block so no extra Puppeteer * page leaks across the harness run. Mirrors the driveA33/driveA34 @@ -3050,52 +3119,60 @@ export async function driveA35( }); diagnostics.push('A35 welcome.html DOMContentLoaded + inline selector resolved'); - const probe = await welcomePage.evaluate(() => { - const svgEl = document.querySelector('.welcome-hero__mark svg'); - const imgEl = document.querySelector('.welcome-hero__mark img'); - const svgPresent = svgEl !== null; - const strokeAttr = svgEl !== null ? svgEl.getAttribute('stroke') : null; - const computedStroke = - svgEl !== null ? getComputedStyle(svgEl).stroke : ''; - const imgPresent = imgEl !== null; - return { svgPresent, strokeAttr, computedStroke, imgPresent }; - }); + // LIGHT probe (default theme; remove any prior data-theme). + const lightProbe = await a35Probe(welcomePage, false); diagnostics.push( - `A35 live-DOM probe: svgPresent=${probe.svgPresent} strokeAttr=${probe.strokeAttr ?? ''} computedStroke="${probe.computedStroke}" imgPresent=${probe.imgPresent}`, + `A35 LIGHT probe: svgPresent=${lightProbe.svgPresent} strokeAttr=${lightProbe.strokeAttr ?? ''} computedStroke="${lightProbe.computedStroke}" imgPresent=${lightProbe.imgPresent}`, ); - const computedStrokeResolved = - probe.computedStroke.length > 0 && probe.computedStroke !== 'none'; + // DARK probe (set data-theme="dark" on ; same selector / same + // evaluate payload). + const darkProbe = await a35Probe(welcomePage, true); + diagnostics.push( + `A35 DARK probe: svgPresent=${darkProbe.svgPresent} strokeAttr=${darkProbe.strokeAttr ?? ''} computedStroke="${darkProbe.computedStroke}" imgPresent=${darkProbe.imgPresent}`, + ); + + const lightStrokeResolved = + lightProbe.computedStroke.length > 0 && lightProbe.computedStroke !== 'none'; + const decoupled = + lightProbe.computedStroke === darkProbe.computedStroke + && lightProbe.computedStroke === A35_LINEN_50_RGB; checks.push({ name: 'A35.1: inline injected into `.welcome-hero__mark` slot (populateMark ran)', expected: 'non-null `.welcome-hero__mark svg`', - actual: probe.svgPresent ? 'present' : 'missing', - passed: probe.svgPresent, + actual: lightProbe.svgPresent ? 'present' : 'missing', + passed: lightProbe.svgPresent, }); checks.push({ name: 'A35.2: injected carries stroke="currentColor" (UI-SPEC Option A recolor)', expected: 'currentColor', - actual: probe.strokeAttr, - passed: probe.strokeAttr === 'currentColor', + actual: lightProbe.strokeAttr, + passed: lightProbe.strokeAttr === 'currentColor', }); checks.push({ - name: 'A35.3: getComputedStyle().stroke resolves to a non-default colour (the currentColor cascade through `.welcome-hero__mark { color: var(--mks-fg-inverse) }` actually worked)', + name: 'A35.3: getComputedStyle().stroke resolves to a non-default colour in LIGHT (the currentColor cascade through `.welcome-hero__mark { color: var(--mks-mark-stroke) }` actually worked)', expected: 'non-empty, non-"none" resolved colour', - actual: probe.computedStroke, - passed: computedStrokeResolved, + actual: lightProbe.computedStroke, + passed: lightStrokeResolved, }); checks.push({ name: 'A35.4: no legacy in `.welcome-hero__mark` slot (the pre-04-06 injection path is gone)', expected: 'null', - actual: probe.imgPresent ? 'present (UNEXPECTED — legacy still injected)' : 'null', - passed: !probe.imgPresent, + actual: lightProbe.imgPresent ? 'present (UNEXPECTED — legacy still injected)' : 'null', + passed: !lightProbe.imgPresent, + }); + checks.push({ + name: `A35.5: Plan 04-06 Task 4 DECOUPLE — LIGHT and DARK computed strokes are EQUAL and BOTH resolve to linen-50 (${A35_LINEN_50_HEX} = ${A35_LINEN_50_RGB}). Proves --mks-mark-stroke is theme-independent (NOT overridden by .dark/[data-theme="dark"]) and the wrapper points at it.`, + expected: `light === dark === "${A35_LINEN_50_RGB}"`, + actual: `light="${lightProbe.computedStroke}", dark="${darkProbe.computedStroke}"`, + passed: decoupled, }); const passed = checks.every((c) => c.passed); return { passed, - name: 'A35 — welcome-page inline-SVG injected at populateMark() runtime; currentColor stroke resolves via parent CSS color cascade (UI-SPEC dark-logo strategy live-DOM proof; iter-2 BLOCKER 1 resolution)', + name: 'A35 — welcome-page inline-SVG injected at populateMark() runtime; currentColor stroke resolves via parent CSS color cascade AND is theme-decoupled via --mks-mark-stroke (UI-SPEC dark-logo strategy live-DOM proof; iter-2 BLOCKER 1 resolution + Plan 04-06 Task 4 decouple)', checks, diagnostics, }; @@ -3104,7 +3181,7 @@ export async function driveA35( diagnostics.push(`A35 THREW: ${errMsg}`); return { passed: false, - name: 'A35 — welcome-page inline-SVG injected at populateMark() runtime; currentColor stroke resolves via parent CSS color cascade (UI-SPEC dark-logo strategy live-DOM proof; iter-2 BLOCKER 1 resolution)', + name: 'A35 — welcome-page inline-SVG injected at populateMark() runtime; currentColor stroke resolves via parent CSS color cascade AND is theme-decoupled via --mks-mark-stroke (UI-SPEC dark-logo strategy live-DOM proof; iter-2 BLOCKER 1 resolution + Plan 04-06 Task 4 decouple)', checks, diagnostics, error: errMsg, diff --git a/tests/welcome/inline-svg.test.ts b/tests/welcome/inline-svg.test.ts index 168439d..d53f9b6 100644 --- a/tests/welcome/inline-svg.test.ts +++ b/tests/welcome/inline-svg.test.ts @@ -13,14 +13,17 @@ // It does NOT instantiate any document, does NOT call populateMark, does // NOT invoke DOMParser at runtime. The runtime behavior (live // injection into the welcome page DOM and the currentColor CSS cascade -// through `.welcome-hero__mark { color: var(--mks-fg-inverse); }`) is -// verified by the host-side UAT harness assertion A35 (driveA35 in -// tests/uat/lib/harness-page-driver.ts) — A35 opens welcome.html as a real -// Puppeteer tab so populateMark() actually runs, then reads -// `getComputedStyle().stroke` on the injected to prove the cascade -// resolves. This split mirrors the project's two-layer test approach -// (source contract under vitest + live-DOM behavior under the Puppeteer -// harness) — verified at re-plan iter-2 (BLOCKER 1 resolution). +// through `.welcome-hero__mark { color: var(--mks-mark-stroke); }` — the +// theme-INDEPENDENT brand-component token introduced by Plan 04-06 Task 4 +// operator-empirical debug-decouple) is verified by the host-side UAT +// harness assertion A35 (driveA35 in tests/uat/lib/harness-page-driver.ts) +// — A35 opens welcome.html as a real Puppeteer tab so populateMark() +// actually runs, then reads `getComputedStyle().stroke` on the injected +// in BOTH light and dark themes to prove (a) the cascade resolves +// and (b) the resolved stroke is identical across themes (linen-50). This +// split mirrors the project's two-layer test approach (source contract +// under vitest + live-DOM behavior under the Puppeteer harness) — verified +// at re-plan iter-2 (BLOCKER 1 resolution) + Plan 04-06 Task 4 decouple. // // References: // - .planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md