Why: Approved redesign phase 4: process folds, long-reply folds, thinking blocks and context-observation fold move to the @ui Collapsible primitive (two-layer height+fade animation, delayed unmount, open-state memory); tool calls render as ToolBlock rows with running sweep and paired results; the live bar gains the ZCode worked-time line with sweep; input placeholder states the backend's honest busy semantics (serialized per conversation). Evidence: npx tsc -b clean; npm test -> 39 passed (details-guard included); npm run build succeeds; combined make test recorded in the Dev task card. Impact: Conversation.tsx replaces all <details> usage (guard-enforced); ProcessParts pairs tool_call with its trailing tool_result into ToolBlock; styles.css rewrites fold selectors for the Collapsible structure and adds message mount animation (mk-rise-in); no contract or API changes. Refs: Agent-System-Dev task feature/ui-tokens-v3 (phase 4).
89 lines
4.1 KiB
TypeScript
89 lines
4.1 KiB
TypeScript
// 设计系统守护测试(父仓 DESIGN.md §11 禁止清单 · 宿主侧,2026-09-30)。
|
||
// 棘轮基线:现存豁免登记在下方;新增同类违规直接失败。收紧 = 修掉违规后删基线条目。
|
||
// 范围:src/styles.css 与 src/**/*.tsx。@ui 组件库侧由 Agent-System-UI 的守护测试覆盖。
|
||
import { describe, it, expect } from 'vitest'
|
||
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
|
||
import { join, relative, dirname } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url)) // test/
|
||
const FE_ROOT = join(HERE, '..')
|
||
const SRC = join(FE_ROOT, 'src')
|
||
const DEV_ROOT = join(FE_ROOT, '..', '..') // Dev 父仓根(DESIGN.md;单独检出本仓时不存在)
|
||
|
||
function walk(dir: string, out: string[] = []): string[] {
|
||
for (const name of readdirSync(dir)) {
|
||
const p = join(dir, name)
|
||
if (statSync(p).isDirectory()) walk(p, out)
|
||
else out.push(p)
|
||
}
|
||
return out
|
||
}
|
||
const read = (p: string) => readFileSync(p, 'utf8')
|
||
const stripCssComments = (t: string) => t.replace(/\/\*[\s\S]*?\*\//g, '')
|
||
const collectHex = (t: string) => [...t.matchAll(/#[0-9a-fA-F]{3,8}\b/g)].map(m => m[0].toLowerCase())
|
||
|
||
// ---- 1. 宪法在位(组合检出时) ----
|
||
describe('DESIGN.md 宪法在位', () => {
|
||
it('DESIGN.md 存在且 AGENTS.md 带强制阅读门禁', () => {
|
||
const designPath = join(DEV_ROOT, 'DESIGN.md')
|
||
if (!existsSync(designPath)) { console.warn('(单独检出 Frontend 仓:跳过宪法检查)'); return }
|
||
expect(read(designPath)).toContain('Agent-System-Dev 设计系统')
|
||
const agents = read(join(DEV_ROOT, 'AGENTS.md'))
|
||
expect(agents).toContain('DESIGN.md')
|
||
})
|
||
})
|
||
|
||
// ---- 2. 动效唯一所有者 ----
|
||
describe('动效唯一所有者', () => {
|
||
it('styles.css 无 @keyframes(全部走 @ui/motion.css 的 mk-* 类)', () => {
|
||
expect(stripCssComments(read(join(SRC, 'styles.css')))).not.toContain('@keyframes')
|
||
})
|
||
})
|
||
|
||
// ---- 3. 裸色值棘轮 ----
|
||
const HEX_BASELINE_STYLES = [
|
||
'#0e1116', '#101826', '#0b1f38', // 登录品牌面板固定深底渐变(面板自带深底,主题无关)
|
||
'#dfe4f2', // 同上面板的固定浅字
|
||
] as const
|
||
describe('裸色值棘轮', () => {
|
||
it('styles.css 不引入基线外裸色值', () => {
|
||
const found = collectHex(read(join(SRC, 'styles.css')))
|
||
const allow = new Set<string>(HEX_BASELINE_STYLES)
|
||
const extra = found.filter(h => !allow.has(h))
|
||
expect(extra, `新增裸色值 ${[...new Set(extra)].join(', ')}——改用 tokens 令牌;确需豁免更新 HEX_BASELINE_STYLES 并写明理由`).toEqual([])
|
||
})
|
||
it('组件 TSX 无裸色值', () => {
|
||
const extra: string[] = []
|
||
for (const p of walk(SRC).filter(p => /\.(tsx|ts)$/.test(p) && !p.endsWith('.d.ts'))) {
|
||
for (const h of collectHex(read(p))) extra.push(`${relative(SRC, p)}: ${h}`)
|
||
}
|
||
expect(extra, '组件里出现裸色值——走 tokens 令牌').toEqual([])
|
||
})
|
||
})
|
||
|
||
// ---- 4. 行内字号棘轮(--fs-ui-* 标尺) ----
|
||
const FONTSIZE_LITERAL_BASELINE = ['40'] as const // Conversation 空态装饰字形(⌘ 展示字号,非 UI 层级)
|
||
describe('行内字号棘轮', () => {
|
||
it('组件 TSX 无基线外 fontSize 数值字面量', () => {
|
||
const allow = new Set<string>(FONTSIZE_LITERAL_BASELINE)
|
||
const extra: string[] = []
|
||
for (const p of walk(SRC).filter(p => p.endsWith('.tsx'))) {
|
||
const lines = read(p).split('\n')
|
||
lines.forEach((l: string, i: number) => {
|
||
const m = l.match(/fontSize:\s*['"`]?([\d.]+)/)
|
||
if (m && !allow.has(m[1])) extra.push(`${relative(SRC, p)}:${i + 1} (${m[1]})`)
|
||
})
|
||
}
|
||
expect(extra, '行内 fontSize 数值——改用 var(--fs-ui-*);确需豁免更新基线并写明理由').toEqual([])
|
||
})
|
||
it('组件 TSX 无 <details> 折叠(走 @ui Collapsible,DESIGN.md §11.7)', () => {
|
||
const extra: string[] = []
|
||
for (const p of walk(SRC).filter(p => p.endsWith('.tsx'))) {
|
||
const code = read(p).replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
||
if (/<details[\s>]/.test(code)) extra.push(relative(SRC, p))
|
||
}
|
||
expect(extra, '<details> 折叠——改用 @ui Collapsible').toEqual([])
|
||
})
|
||
})
|