Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions demo/demo.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
29 changes: 28 additions & 1 deletion scripts/build-pages.mjs
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Expand All @@ -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"), "");

Expand Down
52 changes: 52 additions & 0 deletions test/demo-smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")}`);
Expand Down
19 changes: 15 additions & 4 deletions test/pages-artifact.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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"),
Expand Down
Loading