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
114 lines
5.8 KiB
TypeScript
114 lines
5.8 KiB
TypeScript
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\("\.\//);
|
||
});
|