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\("\.\//); });