Files
Jyotisha/frontend/tests/character-remaining-contract.test.ts
T
Jesse_ChenandClaude Opus 5 66d9f59d84
Independent Staging Quality Gate / validate (push) Failing after 9m26s
Independent Staging Quality Gate / publish (push) Skipped
feat(ui): 标题不再落宋体,强调色分阶换 coral,删输入框常驻底栏(BUG-737/738)
BUG-737:--font-display 以两个从未加载的 Anthropic 授权字体开头
(Tiempos Headline / StyreneB:无 @font-face、public/ 无字体、layout.tsx
只 vendor 了 Inter),于是每个中文标题都掉到 Songti SC / SimSun——20 条
规则宽,含助手回答正文的 h2/h3。display 与 body 合并为同一条无衬线栈,
层级改由字重承担(33 条 display 规则 400→500)。

拉丁衬线方案实测否决:Newsreader 拉丁子集 132 KB(Inter 的 2.7 倍),
只为给一个品牌字上衬线,且会把「D10 事业盘怎么读」劈成两种字形。

BUG-738:亮色 #85432f 与暗色 #d78064 不同源;--color-ring 在
@theme inline 里硬编码不跟随 :root。产品拍板换 Claude coral #cc785c,
但实测它作文字色只有 3.14:1,而 62 个调用点里 53 个是 color:。
按角色拆两阶:--color-action #a9583e(文字 4.85:1)、
--color-action-strong #cc785c(填充/导轨/焦点环,3:1 非文字阈值)。
--report-accent 刻意保持 #85432f(报告是纸面,不跟应用强调色)。

T1.3/T1.4:模型选择器进 ChatComposer 的 toolbar 插槽,删掉常驻 44px 的
.composer-footer;顶栏 68→46px,「分析对象:」改为标题旁静默 chip。

防复发:新增 font-stack-loadable-contract,断言字体栈里每个 family 要么
vendored 要么是系统字体,且 --font-display 不得触达任何 CJK 衬线。

测试 3346→3350,fail 仍 31 且清单与基线逐条一致;/ 仍 Static;
CSS gzip 41,095→41,096(+0.002%);快速门 pytest 段 798 passed / 0 failed。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-16 04:53:14 +00:00

64 lines
3.8 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
characterRemainingAnnouncement,
characterRemainingLabel,
characterRemainingVisible,
remainingThreshold,
} from "../src/lib/character-remaining.ts";
import { homeSurface as page } from "./home-surface.ts";
const composer = readFileSync(new URL("../src/components/chat-composer.tsx", import.meta.url), "utf8");
const remainingSource = readFileSync(new URL("../src/components/character-remaining.tsx", import.meta.url), "utf8");
const guide = readFileSync(new URL("../src/components/birth-time-guide-turn.tsx", import.meta.url), "utf8");
const choice = readFileSync(new URL("../src/components/birth-time-choice-question.tsx", import.meta.url), "utf8");
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
test("thresholds stay quiet until the last 10 percent, floored at 8 and capped at 50", () => {
assert.equal(remainingThreshold(500), 50);
assert.equal(remainingThreshold(240), 24);
assert.equal(remainingThreshold(80), 8);
assert.equal(characterRemainingVisible(449, 500), false);
assert.equal(characterRemainingVisible(450, 500), true);
assert.equal(characterRemainingVisible(71, 80), false);
assert.equal(characterRemainingVisible(72, 80), true);
});
test("the visible label updates while the live announcement stays at two phrases", () => {
assert.equal(characterRemainingLabel(12, 500), "还剩 12 字");
assert.equal(characterRemainingLabel(0, 500), "已达 500 字上限");
assert.equal(characterRemainingAnnouncement(12, 500), "接近字数上限");
assert.equal(characterRemainingAnnouncement(0, 80), "已达 80 字上限");
});
test("the count region is polite, atomic, and only mounted inside the threshold", () => {
assert.match(remainingSource, /if \(!characterRemainingVisible\(length, maxLength\)\) return null;/);
assert.match(remainingSource, /aria-live="polite"/);
assert.match(remainingSource, /aria-atomic="true"/);
assert.match(remainingSource, /aria-hidden="true"/);
assert.match(remainingSource, /className="sr-only"/);
assert.match(css, /\.character-remaining\[data-limit\] \{ color: var\(--color-danger\); \}/);
});
test("the four capped fields wire describedby to a count that lives in existing chrome", () => {
// Former lock: `aria-describedby={showRemaining ? composerRemainingId : undefined}`. The composer
// is now shared with the rectification surface, which passes its own count id (BUG-477).
assert.match(composer, /remainingId = composerRemainingId/);
assert.match(composer, /showRemaining \? remainingId : undefined\]\.filter\(Boolean\)\.join\(" "\)/);
assert.match(composer, /aria-describedby=\{describedByIds\}/);
// 原值:page.tsx 里的 `<ComposerCharacterRemaining>` 加 `.composer-footer` 常驻横栏
// 新值:计数由 ChatComposer 在自己的底排内联渲染,只在 showRemaining 为真时进 DOM
// 原因:`.composer-footer` 的 min-height 是常驻 44px,没到字数上限时那一条是空的。
assert.match(composer, /\{showRemaining \? \(\s*<CharacterRemaining id=\{remainingId\} length=\{draft\.length\} maxLength=\{maxLength\} \/>/);
assert.doesNotMatch(page, /composer-footer/);
assert.match(page, /aria-describedby=\{characterRemainingVisible\(value\.name\.length, 80\) \? nameRemainingId : undefined\}/);
assert.match(guide, /aria-describedby=\{describedBy\}/);
assert.match(choice, /aria-describedby=\{characterRemainingVisible\(note\.length, 240\) \? unmatchedRemainingId : undefined\}/);
// 原值 `.composer-footer > p { display: none; }`(窄屏藏计数)/ 新值:`.composer-row` 无内容时不渲染
// / 原因:计数不再有常驻容器,窄屏不需要再单独藏它。
assert.match(css, /\.composer-row \{[^}]*display: flex/);
assert.doesNotMatch(page, /<p className="character-remaining"/);
});