Files
Cyrene_v2/frontend/web/src/design/uiMode.test.ts
T
AskaEth 8d70f6d331
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(web): 双档 UI 模式(性能档 = 现状 / Smooth Corner 华丽档)+ 开关挂载(裁决 16.4.4)
- design/uiMode.ts:档位纯逻辑(localStorage `cyrene.ui-mode` 持久化、跨标签页同步、未知值回落默认档),
  只改 <html> 类名与 data 属性 ⇒ 不碰 React 树、不重挂载、几何尺寸不变
- 性能档=**现状**:tokens.css / styles/index.css 的改动**全为新增(0 删除)**,且全部限定在
  `:root.ui-mode-fancy` 下;75 元素签名比对证明性能档渲染与改动前逐元素一致
- 华丽档对齐六大核心:连续曲率 corner-shape / 多层 Surface 环境光 / 弹簧动效 / 深色优先(浅色单列补偿)/
  毛玻璃升级 / Liquid Glass(4 折射 + RGB 色差 + 鼠标跟踪 + 双层动态边框)
- 开关挂载:main.tsx 在 createRoot 之前 initUiMode(零闪烁);TopBar/Nav/AdminTopBar 三处挂载,
  窄屏(<640px)按既有约定收起(实测否则 375px 角色块被压到 27px、面板溢出 73px),移动端由侧栏抽屉覆盖
- 复核:tsc 0 错 + vite build 成功;uiMode 38 项 node 测试 + headless Chrome 真渲染 30 项(真点击/真持久化/
  真指针事件);变异反证(去掉读回 → 持久化往返必红)
2026-10-04 13:41:09 +08:00

151 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<string, string> = {}): UiModeStorage & { raw: Map<string, string> } {
const raw = new Map<string, string>(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<string>; attrs: Map<string, string>; el: HTMLElement } {
const classes = new Set<string>();
const attrs = new Map<string, string>();
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<string | null | undefined> = ["", " ", "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");
// 类名/属性落地:切档只改 <html>(不碰组件树 ⇒ 不丢会话状态)。
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`);