feat: Dashboard 主题模式切换 — 系统跟随/明亮/黑暗,localStorage 持久化,TDesign theme-mode 驱动
新增 useThemePreference hook 和纯工具函数,支持系统/明亮/黑暗三态主题选择、 matchMedia 系统主题跟随、localStorage 持久化和启动期主题预应用,通过 <html theme-mode> 驱动 TDesign 主题变量切换。 Header 右侧控件重新组织为 .dashboard-header-controls 单行桌面布局,主题 RadioGroup 位于刷新频率 RadioGroup 前。 附带:build.ts import specifier 改为跨平台 sep 转换;config-loader 测试适配 Windows PATH 和 YAML 路径转义;test-utils 类型窄化修复。
This commit is contained in:
83
tests/web/hooks/use-theme-preference.test.ts
Normal file
83
tests/web/hooks/use-theme-preference.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import {
|
||||
applyThemeMode,
|
||||
parseThemePreference,
|
||||
readThemePreference,
|
||||
resolveEffectiveTheme,
|
||||
THEME_PREFERENCE_STORAGE_KEY,
|
||||
writeThemePreference,
|
||||
} from "../../../src/web/hooks/use-theme-preference";
|
||||
|
||||
function createMemoryStorage(initialValue?: string): Storage {
|
||||
const data = new Map<string, string>();
|
||||
if (initialValue !== undefined) data.set(THEME_PREFERENCE_STORAGE_KEY, initialValue);
|
||||
|
||||
return {
|
||||
clear: () => data.clear(),
|
||||
getItem: (key: string) => data.get(key) ?? null,
|
||||
key: (index: number) => Array.from(data.keys())[index] ?? null,
|
||||
get length() {
|
||||
return data.size;
|
||||
},
|
||||
removeItem: (key: string) => void data.delete(key),
|
||||
setItem: (key: string, value: string) => void data.set(key, value),
|
||||
};
|
||||
}
|
||||
|
||||
function createThrowingStorage(): Storage {
|
||||
return {
|
||||
clear: () => {
|
||||
throw new Error("storage unavailable");
|
||||
},
|
||||
getItem: () => {
|
||||
throw new Error("storage unavailable");
|
||||
},
|
||||
key: () => {
|
||||
throw new Error("storage unavailable");
|
||||
},
|
||||
get length(): number {
|
||||
throw new Error("storage unavailable");
|
||||
},
|
||||
removeItem: () => {
|
||||
throw new Error("storage unavailable");
|
||||
},
|
||||
setItem: () => {
|
||||
throw new Error("storage unavailable");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("use-theme-preference 工具函数", () => {
|
||||
test("解析有效主题偏好并对非法值回退为系统", () => {
|
||||
expect(parseThemePreference("system")).toBe("system");
|
||||
expect(parseThemePreference("light")).toBe("light");
|
||||
expect(parseThemePreference("dark")).toBe("dark");
|
||||
expect(parseThemePreference("unknown")).toBe("system");
|
||||
expect(parseThemePreference(null)).toBe("system");
|
||||
});
|
||||
|
||||
test("根据系统模式计算有效主题", () => {
|
||||
expect(resolveEffectiveTheme("system", true)).toBe("dark");
|
||||
expect(resolveEffectiveTheme("system", false)).toBe("light");
|
||||
expect(resolveEffectiveTheme("light", true)).toBe("light");
|
||||
expect(resolveEffectiveTheme("dark", false)).toBe("dark");
|
||||
});
|
||||
|
||||
test("读取本地存储偏好并在非法值时回退", () => {
|
||||
expect(readThemePreference(createMemoryStorage("dark"))).toBe("dark");
|
||||
expect(readThemePreference(createMemoryStorage("bad-value"))).toBe("system");
|
||||
});
|
||||
|
||||
test("本地存储不可用时读取和写入均不抛错", () => {
|
||||
const storage = createThrowingStorage();
|
||||
expect(readThemePreference(storage)).toBe("system");
|
||||
expect(() => writeThemePreference("dark", storage)).not.toThrow();
|
||||
});
|
||||
|
||||
test("应用有效主题到指定根元素", () => {
|
||||
const root = document.createElement("html");
|
||||
applyThemeMode("dark", root);
|
||||
expect(root.getAttribute("theme-mode")).toBe("dark");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user