// @vitest-environment jsdom /** * 管理端主题档位状态回归(yh-theme 迁移方案 §6.5): * 持久化读写、脏数据回退默认档、applyThemeState 类名幂等互斥。 */ import { beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_THEME_STATE, THEME_STORAGE_KEY, applyThemeState, initTheme, loadThemeState, parseThemeState, saveThemeState, } from "@/utils/theme"; function memStore(init: Record = {}) { const map = new Map(Object.entries(init)); return { getItem: (k: string) => map.get(k) ?? null, setItem: (k: string, v: string) => void map.set(k, v), _dump: () => Object.fromEntries(map), }; } describe("parseThemeState", () => { it("null/坏 JSON/非对象一律回退默认档", () => { expect(parseThemeState(null)).toEqual(DEFAULT_THEME_STATE); expect(parseThemeState("{oops")).toEqual(DEFAULT_THEME_STATE); expect(parseThemeState("[1,2]")).toEqual(DEFAULT_THEME_STATE); }); it("单字段非法只回退该字段,其余保留", () => { const s = parseThemeState(JSON.stringify({ theme: "neon", gradOn: false })); expect(s.theme).toBe("blue"); expect(s.gradOn).toBe(false); expect(s.sideDeep).toBe(true); }); it("全合法字段按存档恢复", () => { const raw = JSON.stringify({ theme: "teal", sideDeep: false, gradOn: false, sideFollow: false }); expect(parseThemeState(raw)).toEqual({ theme: "teal", sideDeep: false, gradOn: false, sideFollow: false }); }); }); describe("load/saveThemeState", () => { it("save 后 load 往返一致,键名约定 yh-theme", () => { const store = memStore(); const state = { theme: "ocean" as const, sideDeep: true, gradOn: false, sideFollow: true }; saveThemeState(state, store); expect(Object.keys(store._dump())).toEqual([THEME_STORAGE_KEY]); expect(loadThemeState(store)).toEqual(state); }); it("storage 缺失/抛错不崩溃(隐私模式兜底)", () => { expect(loadThemeState(undefined)).toEqual(DEFAULT_THEME_STATE); const broken = { getItem: () => null, setItem: vi.fn(() => { throw new Error("quota"); }) }; expect(() => saveThemeState(DEFAULT_THEME_STATE, broken)).not.toThrow(); }); }); describe("applyThemeState", () => { const root = () => document.documentElement; beforeEach(() => { root().className = ""; delete root().dataset.theme; }); it("默认档挂 data-theme=blue + side-deep + grad-on,且不出现互斥类", () => { applyThemeState(DEFAULT_THEME_STATE, root()); expect(root().dataset.theme).toBe("blue"); expect(root().classList).toContain("side-deep"); expect(root().classList).not.toContain("side-light"); expect(root().classList).toContain("grad-on"); expect(root().classList).not.toContain("grad-off"); expect(root().classList).not.toContain("side-plain"); }); it("反复切换幂等:任意顺序调用后类名集合恰好匹配最后状态", () => { applyThemeState({ theme: "violet", sideDeep: false, gradOn: true, sideFollow: false }, root()); applyThemeState({ theme: "ink", sideDeep: true, gradOn: false, sideFollow: true }, root()); expect(root().dataset.theme).toBe("ink"); expect([...root().classList].sort()).toEqual(["grad-off", "side-deep"]); }); it("initTheme 读 localStorage 恢复现场(启动引导路径)", () => { window.localStorage.setItem( THEME_STORAGE_KEY, JSON.stringify({ theme: "teal", sideDeep: false, gradOn: true, sideFollow: true }) ); const st = initTheme(); expect(st.theme).toBe("teal"); expect(root().dataset.theme).toBe("teal"); expect([...root().classList].sort()).toEqual(["grad-on", "side-light"]); }); });