Files
Jyotisha/frontend/tests/site-style-isolation-contract.test.ts
T
Jesse_ChenandClaude Fable 5.1 5a1dcbd2a1
Independent Staging Quality Gate / validate (push) Failing after 8m58s
Independent Staging Quality Gate / publish (push) Skipped
feat(chart): 星盘改用行星符号与颜色,度数进行星表
北印星盘(星盘页 / 校正右栏 / 我的报告共用 VedicChartSvg)宫内不再画
「水 19°」「罗逆 29°」这类显示文本——一宫三颗星就叠三行、字号压到 8。
改成九个彩色符号横排,每行最多 3 个;逆行是符号下方一道同色横线,
第 1 宫加 As 小标,盘下方两行图例。度数、星宿、顺逆下沉到星盘页的
行星表,列从四列扩到八列,首行是上升,度数精确到分。

- 符号字体:vendored 的 9 字形 Noto Sans Symbols 子集(OFL,1.5 KB),
  经 next/font/local 挂成 --font-planet-glyphs;♀ ♂ 带 U+FE0E 防 iOS
  画成表情。构建镜像不能联网,所以必须自带,与 Inter 同一条理由。
- 颜色:--color-planet-* 九个 token,:root 与两个深色块各一份。只用于
  认星不表吉凶,罗计共用中性灰。挂 is-* 修饰类而非行内 style——报告
  文档的标记合同禁止渲染结果出现 style=,星盘不做例外。
- 数据来源:toNorthIndianChart() 加结构化 occupantGlyphs(星盘页与
  markdown fence 走这条);校正与报告两条路线的模型只有引擎 / golden
  显示文本,按首字解析,解析由测试锁住。
- 星宿沿用引擎返回名:仓库无中文宿名表,印度 27 宿与中国二十八宿不是
  同一套,不自造对照。

tsc 0 错,lint 0 error,npm test 3471→3484 条、36 红与基线逐条相同,
/ 仍 Static,首屏 JS gzip +0.29%。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-18 00:39:13 +00:00

95 lines
4.9 KiB
TypeScript

import assert from "node:assert/strict";
import { readdirSync, readFileSync } from "node:fs";
import { extname, join } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
const layout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
const siteStyles = readFileSync(new URL("../src/app/site-styles.ts", import.meta.url), "utf8");
const adminLayout = readFileSync(new URL("../src/app/admin/layout.tsx", import.meta.url), "utf8");
const adminCss = readFileSync(new URL("../src/app/admin/admin.css", import.meta.url), "utf8");
const homePage = readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8");
const loginPage = readFileSync(new URL("../src/app/login/page.tsx", import.meta.url), "utf8");
test("Inter is loaded through next/font and applied as a CSS variable", () => {
assert.match(layout, /import localFont from "next\/font\/local"/);
assert.doesNotMatch(layout, /from ["']next\/font\/google["']/);
assert.match(layout, /src: "\.\/fonts\/InterVariable-latin\.woff2"/);
assert.match(layout, /display: "swap"/);
assert.match(layout, /variable: "--font-inter"/);
// 原值:/className=\{inter\.variable\}/ —— <html> 上只挂一个字体变量。
// 新值:/className=\{`\$\{inter\.variable\}[^`]*`\}/ —— 允许同一个模板串里再挂别的
// 字体变量,但 inter.variable 必须仍是第一个。
// 原因:本轮新增了 vendored 的 9 字形行星符号子集(--font-planet-glyphs),两个
// next/font 变量必须挂在同一个 <html> 上。断言主语(Inter 以 CSS 变量形式
// 挂在根元素)没变,只是不再要求 className 只有它一个。
assert.match(layout, /className=\{`\$\{inter\.variable\}[^`]*`\}/);
assert.match(readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"), /var\(--font-inter, Inter\)/);
assert.match(
readFileSync(new URL("../src/components/admin/admin-app.tsx", import.meta.url), "utf8"),
/var\(--font-inter, Inter\)/,
);
});
test("Inter is vendored locally so production builds do not fetch Google Fonts", () => {
const font = readFileSync(new URL("../src/app/fonts/InterVariable-latin.woff2", import.meta.url));
assert.equal(Buffer.from(font.subarray(0, 4)).toString(), "wOF2");
assert.ok(font.byteLength > 20_000);
const srcRoot = fileURLToPath(new URL("../src/", import.meta.url));
const stack = [srcRoot];
while (stack.length > 0) {
const dir = stack.pop()!;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
continue;
}
if (![".ts", ".tsx", ".js", ".jsx", ".css"].includes(extname(entry.name))) continue;
const source = readFileSync(fullPath, "utf8");
assert.doesNotMatch(
source,
/from ["']next\/font\/google["']|fonts\.googleapis\.com/,
`${fullPath} must not fetch Google Fonts at build time`,
);
}
}
});
test("site chrome owns globals.css; admin does not import it", () => {
assert.match(siteStyles, /import "\.\/globals\.css"/);
assert.match(siteStyles, /import "\.\/birth-time-choice\.css"/);
assert.doesNotMatch(layout, /globals\.css|birth-time-choice\.css|site-styles/);
// 原值:首页 page.tsx import site-styles
// 新值:(app)/layout.tsx import,首页不再各自引进
// 原因:T2 四页共用一份外壳
const appLayout = readFileSync(new URL("../src/app/(app)/layout.tsx", import.meta.url), "utf8");
assert.match(appLayout, /import "\.\.\/site-styles"/);
assert.doesNotMatch(homePage, /site-styles/);
assert.match(loginPage, /import "\.\.\/site-styles"/);
assert.doesNotMatch(adminLayout, /globals\.css|site-styles/);
assert.doesNotMatch(
readFileSync(new URL("../src/app/error.tsx", import.meta.url), "utf8"),
/site-styles|globals\.css/,
);
assert.doesNotMatch(
readFileSync(new URL("../src/app/not-found.tsx", import.meta.url), "utf8"),
/site-styles|globals\.css/,
);
assert.match(adminCss, // 原值 `--font-body: StyreneB, var(--font-inter, Inter)` / 新值:去掉 StyreneB
// 原因:StyreneB 是 Anthropic 授权字体,本仓从未加载它(无 @font-face、public/ 无字体文件),
// 留在栈首只是死配置。见 BUG-737。
/--font-body: var\(--font-inter, Inter\)/);
});
test("root error and not-found pages do not depend on Tailwind utilities or Button", () => {
const errorPage = readFileSync(new URL("../src/app/error.tsx", import.meta.url), "utf8");
const notFoundPage = readFileSync(new URL("../src/app/not-found.tsx", import.meta.url), "utf8");
for (const source of [errorPage, notFoundPage]) {
assert.doesNotMatch(source, /className="flex/);
assert.doesNotMatch(source, /min-h-svh/);
assert.doesNotMatch(source, /text-muted-foreground/);
assert.doesNotMatch(source, /@\/components\/ui\/button/);
}
});