|
| 1 | +/** |
| 2 | + * @ai-context 壳层纵向基准的**棘轮守卫**(规格 §1 决策 16 / §10 批 3 行「7 处魔数归零」第 1 处)。 |
| 3 | + * |
| 4 | + * Why 用测试而不是脚本:本仓 `scripts/*.mjs` 不在 `line-limits` 扫描域(批 2 follow-up #16), |
| 5 | + * 而 `app/src/**` 在 ⇒ 把守卫放进扫描域内,它自己也被 300 行红线看着。 |
| 6 | + * |
| 7 | + * Why 只扫壳层相关文件而不是全树:全树 `height: 56` 还有无关命中(如 `ReviewPage.tsx:204` 的 |
| 8 | + * `padding: "56px 0"`)。本守卫的**扫描域是一份显式清单**(下方 SHELL_FILES), |
| 9 | + * 这样「漏了一个文件」是**清单的错**、不是正则的错 —— 口径可读、可评审。 |
| 10 | + * |
| 11 | + * 口径:命中 = `height: 56` / `top: 56` / `100vh - 56px`(数字两边允许空白)。 |
| 12 | + * `var(--ed-nav-h)` 与 `calc(100vh - var(--ed-nav-h))` **不匹配**、不被误伤。 |
| 13 | + */ |
| 14 | +import { readFileSync } from "node:fs"; |
| 15 | +import { dirname, join } from "node:path"; |
| 16 | +import { fileURLToPath } from "node:url"; |
| 17 | +import { describe, expect, it } from "vitest"; |
| 18 | + |
| 19 | +const HERE = dirname(fileURLToPath(import.meta.url)); |
| 20 | +const SRC = join(HERE, ".."); |
| 21 | + |
| 22 | +/** 壳层纵向基准**必须**经由变量的文件(相对 `app/src`,正斜杠) */ |
| 23 | +const SHELL_FILES = [ |
| 24 | + "App.tsx", |
| 25 | + "components/AiConversationDock.tsx", |
| 26 | + "pages/ActionPage.tsx", |
| 27 | + "pages/ClassroomPage.tsx", |
| 28 | + "pages/KnowledgePage.tsx", |
| 29 | + "pages/NotesPage.tsx", |
| 30 | + "pages/ReviewPage.tsx", |
| 31 | + "pages/SessionsPage.tsx", |
| 32 | + "pages/SettingsPage.tsx", |
| 33 | +] as const; |
| 34 | + |
| 35 | +/** 三种消费形态的**旧写法**(命中即违规) */ |
| 36 | +const FORBIDDEN = [/height:\s*56\b/, /top:\s*56\b/, /100vh\s*-\s*56px/]; |
| 37 | +/** 新写法的正样本(守卫必须能认出来) */ |
| 38 | +const REQUIRED = /--ed-nav-h/; |
| 39 | + |
| 40 | +function lines(rel: string): [number, string][] { |
| 41 | + return readFileSync(join(SRC, rel), "utf8") |
| 42 | + .split(/\r?\n/) |
| 43 | + .map((l, i) => [i + 1, l] as [number, string]); |
| 44 | +} |
| 45 | + |
| 46 | +describe("壳层纵向基准 --ed-nav-h", () => { |
| 47 | + it("壳层文件里不得再出现裸 56(height / top / calc)", () => { |
| 48 | + const hits: string[] = []; |
| 49 | + for (const rel of SHELL_FILES) { |
| 50 | + for (const [n, l] of lines(rel)) if (FORBIDDEN.some((re) => re.test(l))) hits.push(`${rel}:${n} ${l.trim()}`); |
| 51 | + } |
| 52 | + expect(hits, `这些行仍硬编码 56(请改用 var(--ed-nav-h)):\n${hits.join("\n")}`).toEqual([]); |
| 53 | + }); |
| 54 | + |
| 55 | + it("每个壳层文件都**确实**消费了变量(防止用删代码的方式让上一条变绿)", () => { |
| 56 | + const missing = SHELL_FILES.filter((rel) => !lines(rel).some(([, l]) => REQUIRED.test(l))); |
| 57 | + expect(missing, `这些文件没有消费 --ed-nav-h(删掉旧写法不等于接上了变量):\n${missing.join("\n")}`).toEqual([]); |
| 58 | + }); |
| 59 | + |
| 60 | + it("扫描域自检:清单里的文件都真的存在且非空", () => { |
| 61 | + const empty = SHELL_FILES.filter((rel) => lines(rel).length < 10); |
| 62 | + expect(empty, `清单里的文件读不到或过短(路径写错了?):\n${empty.join("\n")}`).toEqual([]); |
| 63 | + }); |
| 64 | +}); |
0 commit comments