- 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 项(真点击/真持久化/ 真指针事件);变异反证(去掉读回 → 持久化往返必红)
151 lines
6.0 KiB
TypeScript
151 lines
6.0 KiB
TypeScript
// 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`);
|