// Logic test for the Miuix dual-tier UI mode (裁决 16.4.4 / design/07 §0.1): // 默认档 = UI 性能模式、未知值回落、持久化往返、存储不可用降级、类名/属性落地。 // // The project has no frontend test runner, so this is a dependency-free plain-node // check that drives the real module and throws on the first failure. Run it with // the esbuild that already ships with vite (no new dependency, no package.json // change): // // cd frontend/web \ // && ./node_modules/.bin/esbuild src/design/uiMode.test.ts \ // --bundle --platform=node --format=esm \ // --outfile=/tmp/cyrene-uimode.test.mjs \ // && node /tmp/cyrene-uimode.test.mjs // // `tsc --noEmit` (part of `npm run build`) type-checks this file, which is why it // imports nothing from node:*. import { DEFAULT_UI_MODE, UI_MODE_ATTR, UI_MODE_CLASS, UI_MODE_HINT, UI_MODE_LABEL, UI_MODE_ORDER, UI_MODE_STORAGE_KEY, applyUiMode, isUiMode, normalizeUiMode, readUiMode, setUiMode, toggleUiMode, writeUiMode, type UiMode, type UiModeStorage, } from "./uiMode"; let checks = 0; function ok(cond: boolean, what: string): void { checks += 1; if (!cond) throw new Error(`FAILED: ${what}`); } function eq(got: string, want: string, what: string): void { ok(got === want, `${what} — got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`); } /** 内存版 localStorage。 */ function memoryStorage(seed: Record = {}): UiModeStorage & { raw: Map } { const raw = new Map(Object.entries(seed)); return { raw, getItem: (key: string) => raw.get(key) ?? null, setItem: (key: string, value: string) => { raw.set(key, value); }, }; } /** 隐私模式 / 禁用存储:读写都抛。 */ const throwingStorage: UiModeStorage = { getItem: () => { throw new Error("storage disabled"); }, setItem: () => { throw new Error("storage disabled"); }, }; /** 最小 DOM 替身:applyUiMode 只用 classList.add/remove + setAttribute。 */ function fakeRoot(): { classes: Set; attrs: Map; el: HTMLElement } { const classes = new Set(); const attrs = new Map(); const el = { classList: { add: (name: string) => classes.add(name), remove: (name: string) => classes.delete(name), contains: (name: string) => classes.has(name), }, setAttribute: (key: string, value: string) => attrs.set(key, value), getAttribute: (key: string) => attrs.get(key) ?? null, } as unknown as HTMLElement; return { classes, attrs, el }; } // 默认档 = UI 性能模式(裁决 16.4.4 / design/07 §0.1「默认 = UI 性能档」)。 eq(DEFAULT_UI_MODE, "performance", "默认档是 UI 性能模式"); eq(UI_MODE_ORDER[0], DEFAULT_UI_MODE, "档位顺序首项 = 默认档"); eq(String(UI_MODE_ORDER.length), "2", "只有两档"); // 空存储 → 默认档。 eq(readUiMode(memoryStorage()), "performance", "空存储读默认档"); // 未知 / 损坏 / 大小写不符的值一律回落默认档(前向兼容,不抛)。 const junk: Array = ["", " ", "FANCY", "Fancy", "fancyx", "performance ", "2", null, undefined]; for (const value of junk) { eq(normalizeUiMode(value), DEFAULT_UI_MODE, `未知值 ${JSON.stringify(value)} 回落默认档`); } eq(normalizeUiMode("fancy"), "fancy", "合法值 fancy 保留"); eq(normalizeUiMode("performance"), "performance", "合法值 performance 保留"); ok(isUiMode("fancy") && isUiMode("performance"), "isUiMode 认两档"); ok(!isUiMode("FANCY") && !isUiMode("") && !isUiMode(null), "isUiMode 拒绝非法值"); // 持久化往返:写进去必须读得回来(这是「开关状态存 localStorage」的核心断言)。 const store = memoryStorage(); writeUiMode("fancy", store); eq(readUiMode(store), "fancy", "fancy 持久化往返"); eq(store.raw.get(UI_MODE_STORAGE_KEY) ?? "", "fancy", "落盘键名与值"); writeUiMode("performance", store); eq(readUiMode(store), "performance", "回切 performance 也往返"); // setUiMode(切档入口)同样落盘;无 DOM 环境下仍不抛。 setUiMode("fancy", store); eq(readUiMode(store), "fancy", "setUiMode 落盘"); setUiMode("bogus" as UiMode, store); eq(readUiMode(store), "performance", "setUiMode 非法值回落默认档并落盘"); // 存储不可用(隐私模式)→ 读默认档、写不抛(退化为仅本次会话生效)。 eq(readUiMode(throwingStorage), "performance", "存储抛错时读默认档"); writeUiMode("fancy", throwingStorage); eq(readUiMode(throwingStorage), "performance", "存储抛错时不崩且仍读默认档"); eq(readUiMode(null), "performance", "无存储对象时读默认档"); // 两档互切。 eq(toggleUiMode("performance"), "fancy", "performance → fancy"); eq(toggleUiMode("fancy"), "performance", "fancy → performance"); // 类名/属性落地:切档只改 (不碰组件树 ⇒ 不丢会话状态)。 const root = fakeRoot(); applyUiMode("fancy", root.el); ok(root.classes.has(UI_MODE_CLASS.fancy), "华丽档加 ui-mode-fancy 类"); ok(!root.classes.has(UI_MODE_CLASS.performance), "华丽档移除 ui-mode-performance 类"); eq(root.attrs.get(UI_MODE_ATTR) ?? "", "fancy", "data-ui-mode = fancy"); applyUiMode("performance", root.el); ok(root.classes.has(UI_MODE_CLASS.performance), "性能档加 ui-mode-performance 类"); ok(!root.classes.has(UI_MODE_CLASS.fancy), "性能档移除 ui-mode-fancy 类"); eq(root.attrs.get(UI_MODE_ATTR) ?? "", "performance", "data-ui-mode = performance"); applyUiMode("nonsense" as UiMode, root.el); ok(root.classes.has(UI_MODE_CLASS.performance), "非法档位回落到性能档类名"); // 无 root(SSR / 无 DOM)时不抛。 applyUiMode("fancy", null); // 开关文案齐全(两档都有标签与说明,UI 不会出现空档位)。 for (const id of UI_MODE_ORDER) { ok(UI_MODE_LABEL[id].length > 0, `${id} 有标签`); ok(UI_MODE_HINT[id].length > 0, `${id} 有说明`); } console.log(`uiMode.test.ts OK — ${checks} checks`);