Files
Jyotisha/frontend/tests/serif-headings-contract.test.ts
T
Jesse_ChenandClaude Opus 5.5 8112f62b51 perf(fonts): heading serif slices served from public/ under content-hashed names, cached for good (BUG-1128)
Bundled media carry Next's per-deploy ?dpl= and were downloaded again after
every release. Bytes unchanged; /fonts/serif-sc/* is immutable. Overturns the
serif-headings contract line "bundled by Next, not served from public/"
per TASK-home-first-load-20260930 decision 2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-01 00:40:11 +08:00

114 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { cssDeclarations } from "./css-contract-test-support.ts";
/**
* TASK-serif-headings-20260928: headings (every `var(--font-display)` rule)
* take the self-hosted "Jyotisha Serif SC"; body, buttons, tables, inputs,
* labels and admin stay sans. A few headings are sans on purpose. This file
* pins those exceptions so a later "make every heading serif" sweep has to
* change a test — and say why — instead of flipping them silently.
*/
const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
const css = read("../src/app/globals.css");
function exactRule(selector: string): string {
// cssDeclarations matches by suffix; these exceptions need the exact selector
// so the generic `.message-markdown h2` rule cannot satisfy them.
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, " ");
const bodies: string[] = [];
for (const [, selectorList, body] of withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
const targets = selectorList.split(",").map((value) => value.trim().replace(/\s+/g, " "));
if (targets.includes(selector)) bodies.push(body.trim());
}
assert.notEqual(bodies.length, 0, `missing CSS selector: ${selector}`);
return bodies.join(" ");
}
test("rectification narrative headings stay on the body face", () => {
for (const selector of [
".conversational-narrative .message-markdown h2",
".conversational-narrative .message-markdown h3",
]) {
const body = exactRule(selector);
assert.match(body, /font-family:\s*var\(--font-body\)/, `${selector} is a deliberate sans heading`);
assert.match(body, /font-weight:\s*600/);
assert.doesNotMatch(body, /--font-display/);
}
});
test("report chart-card titles stay on the body face", () => {
const body = exactRule(".personal-report-chart-card h3");
assert.match(body, /font-family:\s*var\(--font-body\)/);
assert.doesNotMatch(body, /--font-display/);
});
test("the loading screen title stays on the body face (BUG-1129)", () => {
// It paints before the bundle runs; a serif title would pull 3-4 font slices
// onto the critical path next to the first-screen JS.
const body = exactRule(".app-loading-content strong");
assert.match(body, /font-family:\s*var\(--font-body\)/);
assert.doesNotMatch(body, /--font-display/);
});
test("assistant answer headings and report cover / section titles take the heading face", () => {
assert.match(exactRule(".message-markdown h2"), /font-family:\s*var\(--font-display\)/);
assert.match(cssDeclarations(".personal-report-cover h1"), /font-family:\s*var\(--font-display\)/);
assert.match(cssDeclarations(".personal-report-section-heading h2"), /font-family:\s*var\(--font-display\)/);
assert.match(exactRule(".personal-report-md-article h1"), /font-family:\s*var\(--font-display\)/);
assert.match(exactRule(".personal-report-md-article h2"), /font-family:\s*var\(--font-display\)/);
});
test("body text keeps the sans stack", () => {
assert.match(cssDeclarations("body"), /font-family:\s*var\(--font-body\)/);
const bodyStack = css.match(/^\s*--font-body:\s*([^;]+);/m)?.[1] ?? "";
assert.doesNotMatch(bodyStack, /Jyotisha Serif SC|serif"|Songti|SimSun/);
});
test("admin never loads the heading face", () => {
const adminCss = read("../src/app/admin/admin.css");
const adminApp = read("../src/components/admin/admin-app.tsx");
const adminLayout = read("../src/app/admin/layout.tsx");
for (const source of [adminCss, adminApp, adminLayout]) {
assert.doesNotMatch(source, /Jyotisha Serif SC|serif-sc|--font-display/);
}
// The @font-face list rides with globals.css (site-styles.ts), never with the
// root layout that admin also sits under.
assert.match(read("../src/app/site-styles.ts"), /import "\.\/fonts\/serif-sc\/serif-sc\.css"/);
assert.doesNotMatch(read("../src/app/layout.tsx"), /serif-sc/);
});
test("root boundary pages keep their inline sans stack", () => {
for (const file of ["error.tsx", "not-found.tsx", "forbidden.tsx", "global-error.tsx"]) {
const source = read(`../src/app/${file}`);
assert.doesNotMatch(source, /Jyotisha Serif SC|serif-sc|Songti|SimSun/, `${file} is a documented sans exception`);
}
});
test("the heading face swaps in without preload", () => {
const faces = read("../src/app/fonts/serif-sc/serif-sc.css");
const blocks = [...faces.matchAll(/@font-face\s*\{([^}]*)\}/g)].map((m) => m[1]);
assert.ok(blocks.length >= 10);
for (const block of blocks) {
assert.match(block, /font-family:\s*"Jyotisha Serif SC"/);
assert.match(block, /font-display:\s*swap/);
assert.match(block, /font-weight:\s*500 700/);
// 原值:url("./jyotisha-serif-sc-NN.woff2"),"slices are bundled by Next, not served from public/"。
// 新值:url("/fonts/serif-sc/jyotisha-serif-sc-NN.<10 位哈希>.woff2"),由 public/ 提供、长期缓存。
// 原因:产品 2026-09-30「宋体只下一次」(TASK-home-first-load-20260930 决策 2 明确推翻本断言):
// Next 打包的资源带每次部署都变的 ?dpl=,切片在每次发布后都会被重新下载(BUG-1128)。
assert.match(block, /url\("\/fonts\/serif-sc\/jyotisha-serif-sc-\d{2}\.[0-9a-f]{10}\.woff2"\)/, "slices are served from public/ under content-hashed names");
}
assert.doesNotMatch(read("../src/app/layout.tsx"), /rel="preload"[^>]*woff2|serif-sc.*preload/);
});
test("the heading slices keep one URL across deploys and are cached for good (BUG-1128)", () => {
const config = read("../next.config.ts");
assert.match(config, /source: "\/fonts\/serif-sc\/:path\*",\s*headers: \[\{ key: "Cache-Control", value: "public, max-age=31536000, immutable" \}\]/);
// Nothing bundles them any more, so no `?dpl=` can reach their URL.
assert.doesNotMatch(read("../src/app/fonts/serif-sc/serif-sc.css"), /url\("\.\//);
});