From 055d0999106c49a50b7daafe2e52f6bf85361732 Mon Sep 17 00:00:00 2001 From: Foscat Date: Wed, 29 Jul 2026 15:12:18 -0500 Subject: [PATCH] fix: restore demo code contrast --- demo/demo.css | 5 ++++ scripts/build-pages.mjs | 29 +++++++++++++++++++- test/demo-smoke.test.mjs | 52 ++++++++++++++++++++++++++++++++++++ test/pages-artifact.test.mjs | 19 ++++++++++--- 4 files changed, 100 insertions(+), 5 deletions(-) diff --git a/demo/demo.css b/demo/demo.css index 6e4b0cd..4170c83 100644 --- a/demo/demo.css +++ b/demo/demo.css @@ -460,6 +460,11 @@ body[data-ecosystem="all-three"] .demo-status { white-space: pre; } +/* Copy-ready snippets inherit the dedicated code surface instead of inline-code paint. */ +.demo-code-card pre code { + color: inherit; +} + .demo-copy-status[data-copy-state="error"] { color: var(--demo-danger); } diff --git a/scripts/build-pages.mjs b/scripts/build-pages.mjs index 190af5a..22bd99e 100644 --- a/scripts/build-pages.mjs +++ b/scripts/build-pages.mjs @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { join, relative } from "node:path"; import { fileURLToPath } from "node:url"; @@ -17,6 +18,23 @@ function assertInsideRoot(path) { } } +function fingerprintVersionedAsset(index, assetPath, contents) { + const assetUrl = `./${assetPath}`; + const escapedAssetUrl = assetUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const versionedAssetPattern = new RegExp(`${escapedAssetUrl}\\?v=([^"'\\s&]+)`, "g"); + const fingerprint = createHash("sha256").update(contents).digest("hex").slice(0, 12); + const fingerprintedIndex = index.replace( + versionedAssetPattern, + `${assetUrl}?v=$1-${fingerprint}` + ); + + if (fingerprintedIndex === index) { + throw new Error(`Expected a versioned Pages asset URL for ${assetPath}.`); + } + + return fingerprintedIndex; +} + assertInsideRoot(demoDir); assertInsideRoot(distDir); assertInsideRoot(pagesDir); @@ -28,11 +46,20 @@ await cp(distDir, join(pagesDir, "dist"), { recursive: true }); const indexPath = join(pagesDir, "index.html"); const index = await readFile(indexPath, "utf8"); -const pagesIndex = index.replaceAll( +let pagesIndex = index.replaceAll( "../dist/layout-style-css.css", "./dist/layout-style-css.css" ); +/* + * Pages receives immutable asset URLs derived from deployed content, so a demo + * hotfix cannot be hidden by a browser cache that still holds the same version. + */ +for (const assetPath of ["demo.css", "demo.js", "dist/layout-style-css.css"]) { + const assetContents = await readFile(join(pagesDir, ...assetPath.split("/"))); + pagesIndex = fingerprintVersionedAsset(pagesIndex, assetPath, assetContents); +} + await writeFile(indexPath, pagesIndex); await writeFile(join(pagesDir, ".nojekyll"), ""); diff --git a/test/demo-smoke.test.mjs b/test/demo-smoke.test.mjs index 66285d0..d3d9f7e 100644 --- a/test/demo-smoke.test.mjs +++ b/test/demo-smoke.test.mjs @@ -273,6 +273,57 @@ const assertTopologyReadout = async (page, expected) => { ); }; +const parseRgbColor = (value) => { + const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number); + assert.equal(channels?.length, 3, `Expected an RGB color, got "${value}".`); + return channels; +}; + +const relativeLuminance = (channels) => + channels + .map((channel) => channel / 255) + .map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4)) + .reduce( + (luminance, channel, index) => luminance + channel * [0.2126, 0.7152, 0.0722][index], + 0 + ); + +const contrastRatio = (foreground, background) => { + const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background)); + const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background)); + return (lighter + 0.05) / (darker + 0.05); +}; + +const verifyCodeBlockContrast = async (page, baseUrl) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto(`${baseUrl}?ecosystem=all-three&mode=light`, { + waitUntil: "domcontentloaded" + }); + await page.waitForFunction(() => document.body.dataset.demoReady === "true"); + + for (const mode of ["light", "dark", "contrast"]) { + await setControl(page, "modeSelect", mode); + const blocks = await page.locator(".demo-code-card pre").evaluateAll((preElements) => + preElements.map((preElement) => { + const codeElement = preElement.querySelector("code"); + return { + background: getComputedStyle(preElement).backgroundColor, + color: getComputedStyle(codeElement).color + }; + }) + ); + + assert.equal(blocks.length, 2, `${mode}: expected both copy-ready code blocks.`); + for (const [index, block] of blocks.entries()) { + const ratio = contrastRatio(parseRgbColor(block.color), parseRgbColor(block.background)); + assert( + ratio >= 4.5, + `${mode} code block ${index + 1} has ${ratio.toFixed(2)}:1 contrast; expected at least 4.5:1.` + ); + } + } +}; + const layoutSnapshot = async (page) => page.evaluate(() => { const documentElement = document.documentElement; @@ -953,6 +1004,7 @@ try { await verifyBreakoutGeometry(page, server.baseUrl); await verifyProfileAndUtilityIsolation(page, server.baseUrl); await verifyPrimitiveOverflow(page, server.baseUrl); + await verifyCodeBlockContrast(page, server.baseUrl); await verifyInteractions(page, server.baseUrl); assert.deepEqual(pageErrors, [], `Page errors:\n${pageErrors.join("\n")}`); diff --git a/test/pages-artifact.test.mjs b/test/pages-artifact.test.mjs index 46b3ba9..90fba13 100644 --- a/test/pages-artifact.test.mjs +++ b/test/pages-artifact.test.mjs @@ -3,6 +3,7 @@ import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; const root = fileURLToPath(new URL("..", import.meta.url)); const outputDir = join(root, "output", "github-pages"); @@ -63,14 +64,24 @@ assert.equal( "The canonical URL must use the repository's case-sensitive GitHub Pages slug." ); -assert( - index.includes('href="./dist/layout-style-css.css?v=3.0.0"'), - "Pages root demo should load the default v3 bundle from ./dist" -); assert( !index.includes("../dist/layout-style-css.css"), "Pages root demo should not reference parent dist paths" ); + +for (const { attribute, path } of [ + { attribute: "href", path: "demo.css" }, + { attribute: "src", path: "demo.js" }, + { attribute: "href", path: "dist/layout-style-css.css" } +]) { + const contents = readFileSync(join(outputDir, ...path.split("/"))); + const fingerprint = createHash("sha256").update(contents).digest("hex").slice(0, 12); + assert( + index.includes(`${attribute}="./${path}?v=3.0.0-${fingerprint}"`), + `Pages should fingerprint ${path} from its deployed contents.` + ); +} + assert(!index.includes("integrations/ui-style-kit.css"), "Pages must not reference the removed bridge"); assert( !pagesBuild.includes("integrations/ui-style-kit.css"),