Skip to content

Commit 7deeb27

Browse files
committed
test(shell): bind nav-h fallback to token source
1 parent 556515b commit 7deeb27

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

‎app/src/shell/navHeight.consumption.test.ts‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,104 @@ describe("壳层纵向基准 --ed-nav-h", () => {
6262
expect(empty, `清单里的文件读不到或过短(路径写错了?):\n${empty.join("\n")}`).toEqual([]);
6363
});
6464
});
65+
66+
/* ── T17 / C10#13 追加段:兜底绑定(**纯追加**:既有 3 个 it 与既有 import 一行未动)──────────────
67+
* 病灶(批 4 交接项 #13):上面那份显式清单**不含任何 CSS**,而 `ui/primitives/Toast.css:60` 用
68+
* `top: calc(var(--ed-nav-h, 56px) + 8px)` —— **兜底 56px 没有任何判据绑到 token 真源**
69+
* (`ui/tokens.css:73` 的 `--ed-nav-h: 56px`);该文件 :56 的注释自己承认这条洞:
70+
* 「兜底 56px 与 `TopBar.css` 的 `var(--ed-nav-h, 56px)` 同值(两处兜底漂移不会有任何报错)」。
71+
*
72+
* 三条判据各自独立成 it(陷阱 #67:一个 it 里多条硬断言只有第一条拿得到"有没有牙"的读数):
73+
* ① 域与锚:域非空 · 两份已知消费者在域内 · 域内确有 `--ed-nav-h` 消费 · 域内无 FORBIDDEN 裸 56;
74+
* ② 绑定:域内每个 `var(--ed-nav-h, <N>px)` 的 N **等于**真源 M,且"带逗号的消费"全被解析器认下;
75+
* ③ 反空真:域内命中 > 0 且**无意义串命中 0**(同域双侧自证 —— 仪器纪律第 1 条)。
76+
*
77+
* 🔴 域 = `app/src` 下**递归**的全部 `*.css`(动态枚举,今日 14 个)—— **比计划逐字给的
78+
* `ui/primitives/*.css` 大**:计划引用的洞正是"**两处**兜底漂移",只扫原语层会漏掉
79+
* `shell/TopBar.css` 那一份。代价 = 邻居新增 CSS 若漂移会红(那是**真发现**,不是假红);
80+
* 不动既有 3 个 it 的语义(计划逐字要求)。
81+
* ⚠️ 本段注释里**不许**出现 `**` 紧跟 `/` 的 glob 字面量:`*` + `/` 会**提前终止块注释**
82+
* (实测 esbuild 报 `Unexpected "*"`)—— 同类"文本级仪器被字面量骗到"的 #54/#64 家族。
83+
* 残留(登记):`.tsx` 的**行内 style 兜底**不在本判据域内(口径与读数见 `task-17-report.md`)。
84+
* ──────────────────────────────────────────────────────────────────────────────────────────── */
85+
import { readdirSync, statSync } from "node:fs"; // 独立成行:既有那行 import 保持逐字原样(计划 V2:删除行 = 0)
86+
87+
/** 域内**必须**出现的两份已知兜底消费者(动态域的锚:改名/搬走必须被看见) */
88+
const FALLBACK_ANCHORS = ["ui/primitives/Toast.css", "shell/TopBar.css"] as const;
89+
/** `var(--ed-nav-h, <N>px)`:逗号与数字两边允许空白(与上面 FORBIDDEN/REQUIRED 的宽松口径一致) */
90+
const FALLBACK_PX = /var\(\s*--ed-nav-h\s*,\s*(\d+)px\s*\)/g;
91+
/** 任何"带兜底的消费"(**不看兜底形态**)——抓「兜底写成非 `<N>px` ⇒ 被 FALLBACK_PX 静默漏掉」 */
92+
const FALLBACK_ANY = /var\(\s*--ed-nav-h\s*,/g;
93+
/** token 真源:`ui/tokens.css` 的 `--ed-nav-h: <M>px` */
94+
const TOKEN_DECL = /--ed-nav-h\s*:\s*(\d+)px/;
95+
96+
/** `app/src` 下全部 `*.css`(相对 `app/src` 的正斜杠路径,排序稳定 ⇒ 失败信息可比对) */
97+
function cssFilesUnder(dir: string, prefix = ""): string[] {
98+
const out: string[] = [];
99+
for (const name of readdirSync(dir)) {
100+
const abs = join(dir, name);
101+
// ⚠️ 本仓 `src/node-builtins.d.ts` 只声明了单参 `readdirSync(path)` ⇒ `{ withFileTypes: true }`
102+
// 在 tsc 下报 TS2554/TS2339(`columnRegistry.test.ts:43` 已记同一缺口)。零新增依赖 ⇒ 走 statSync。
103+
if (statSync(abs).isDirectory()) out.push(...cssFilesUnder(abs, `${prefix}${name}/`));
104+
else if (name.endsWith(".css")) out.push(`${prefix}${name}`);
105+
}
106+
return out.sort();
107+
}
108+
109+
/**
110+
* CSS 注释**就地掩码**(非换行字符替换成空格 ⇒ 行号不错位)。
111+
* Why 必须剥:`Toast.css:56` 的**注释**里逐字写着 `var(--ed-nav-h, 56px)`(它正是本判据要解释的那段
112+
* 说明)—— 不剥就是「注释里的字面量骗过扫描器」(陷阱 #54/#64 家族,本批已三犯),
113+
* 后果是**改注释措辞会假红**(T8 已裁:判据不许对措辞过敏)。既有 3 个 it 仍用未剥的 `lines()`(V2)。
114+
*/
115+
const maskCssComments = (s: string): string => s.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
116+
117+
/** 域内文件逐行:`{ n: 行号, code: 去注释行(判据用), raw: 原始行(失败信息用)}` */
118+
function cssLines(rel: string): { n: number; code: string; raw: string }[] {
119+
const text = readFileSync(join(SRC, rel), "utf8");
120+
const raw = text.split(/\r?\n/);
121+
return maskCssComments(text)
122+
.split(/\r?\n/)
123+
.map((code, i) => ({ n: i + 1, code, raw: raw[i] ?? "" }));
124+
}
125+
126+
const CSS_DOMAIN = cssFilesUnder(SRC);
127+
/** 域正文(去注释后的代码面;用本文件既有的读盘口径,不另起一套路径解析) */
128+
const cssDomainText = (): string => CSS_DOMAIN.map((rel) => cssLines(rel).map((r) => r.code).join("\n")).join("\n");
129+
130+
describe("兜底绑定(T17 / C10#13):var(--ed-nav-h, <N>px) 的 N 必须等于 token 真源", () => {
131+
it("① 域与锚:域非空 · 两份已知消费者在域内 · 域内确有消费 · 域内无裸 56", () => {
132+
expect(CSS_DOMAIN.length, "域为空 ⇒ ②③ 会静默变成空真").toBeGreaterThan(0);
133+
const missing = FALLBACK_ANCHORS.filter((a) => !CSS_DOMAIN.includes(a));
134+
expect(missing, `已知的兜底消费者不在域内(改名/搬走了?):\n${missing.join("\n")}`).toEqual([]);
135+
const consuming = FALLBACK_ANCHORS.filter((a) => cssLines(a).some((r) => REQUIRED.test(r.code)));
136+
expect(consuming, "锚清单里每一份都必须真的消费 --ed-nav-h(少一份 ⇒「两处兜底漂移」里的那一处就没了判据)").toEqual([...FALLBACK_ANCHORS]);
137+
const bare: string[] = [];
138+
for (const rel of CSS_DOMAIN) for (const r of cssLines(rel)) if (FORBIDDEN.some((re) => re.test(r.code))) bare.push(`${rel}:${r.n} ${r.raw.trim()}`);
139+
expect(bare, `CSS 里仍硬编码 56(请改用 var(--ed-nav-h)):\n${bare.join("\n")}`).toEqual([]);
140+
});
141+
142+
it("② 绑定:每个兜底 <N> == tokens.css 的 <M>,且带逗号的消费全部被解析(形态漂移也红)", () => {
143+
const decl = readFileSync(join(SRC, "ui/tokens.css"), "utf8").match(TOKEN_DECL);
144+
expect(decl, "tokens.css 里读不到 `--ed-nav-h: <M>px` ⇒ 真源不可读,本条不该静默通过").not.toBeNull();
145+
const truth = Number(decl?.[1]);
146+
const bad: string[] = [];
147+
for (const rel of CSS_DOMAIN) {
148+
for (const r of cssLines(rel)) {
149+
for (const hit of r.code.matchAll(FALLBACK_PX)) if (Number(hit[1]) !== truth) bad.push(`${rel}:${r.n} 兜底 ${hit[1]}px ≠ 真源 ${truth}px ${r.raw.trim()}`);
150+
}
151+
}
152+
expect(bad, `兜底与 token 真源漂移(「两处兜底漂移不会有任何报错」—— 本条就是那个报错):\n${bad.join("\n")}`).toEqual([]);
153+
const text = cssDomainText();
154+
const any = [...text.matchAll(FALLBACK_ANY)].length;
155+
const parsed = [...text.matchAll(FALLBACK_PX)].length;
156+
expect(parsed, "域内一处兜底都解析不到 ⇒ 本条是空真(真源读到了,判据却没在判)—— 确实要删兜底请连本条一起改").toBeGreaterThan(0);
157+
expect(parsed, `有 ${any} 处带兜底的消费,却只解析出 ${parsed} 处 <N>px ⇒ 兜底换了形态(判据会漏检)`).toBe(any);
158+
});
159+
160+
it("③ 反空真:域内命中 > 0 · 无意义串命中 0(同域双侧自证)", () => {
161+
const text = cssDomainText();
162+
expect([...text.matchAll(/--ed-nav-h/g)].length, "域内 0 命中 ⇒ 是域选错了,不是判据绿").toBeGreaterThan(0);
163+
expect([...text.matchAll(/zzz-not-a-token-zzz/g)].length, "无意义串也命中 ⇒ 这台仪器在乱报").toBe(0);
164+
});
165+
});

0 commit comments

Comments
 (0)