import { afterEach, describe, expect, it, vi } from "vitest"; import { applyThemePalette, defaultThemeBackgroundId, defaultThemePaletteId, getThemeBackgroundPreset, getThemePalette, themeBackgroundPresets, themePaletteChangeEvent, themePalettes } from "./tokens"; afterEach(() => { vi.unstubAllGlobals(); }); describe("platform web theme palettes", () => { it("offers black mecha and magical-girl palettes with usable swatches", () => { expect(themePalettes.map((palette) => palette.label)).toEqual(["黑色机甲", "魔法少女"]); for (const palette of themePalettes) { expect(palette.swatches.length).toBeGreaterThanOrEqual(5); expect(palette.variables["--accent"]).toMatch(/^#[0-9a-f]{6}$/i); expect(palette.variables["--surface"]).toContain("rgba"); expect(palette.variables["--rim-light"]).toContain("rgba"); expect(palette.variables["--crystal-rim"]).toContain("rgba"); expect(palette.variables["--sparkle-gold"]).toContain("rgba"); expect(palette.variables["--panel-material"]).toContain("var"); expect(palette.variables["--menu-item-bg"]).toContain("gradient"); expect(palette.variables["--frame-accessory-top"]).toContain("data:image/svg+xml"); expect(palette.variables["--frame-accessory-bottom"]).toContain("data:image/svg+xml"); expect(palette.variables["--frame-accessory-opacity"]).toMatch(/^0\.\d{2}$/); expect(accessorySizeNumbers(palette.variables["--frame-accessory-size"])).toEqual( expect.arrayContaining([expect.any(Number)]) ); expect(palette.variables["--custom-background-overlay"]).toBe("transparent"); const accessoryOpacity = Number(palette.variables["--frame-accessory-opacity"]); const largestAccessoryDimension = Math.max(...accessorySizeNumbers(palette.variables["--frame-accessory-size"])); if (palette.id === "magical-girl") { expect(accessoryOpacity).toBeGreaterThanOrEqual(0.65); expect(largestAccessoryDimension).toBeGreaterThanOrEqual(56); expect(largestAccessoryDimension).toBeLessThanOrEqual(80); expect( new Set([ palette.variables["--frame-accessory-heart"], palette.variables["--frame-accessory-wand"], palette.variables["--frame-accessory-moon"], palette.variables["--frame-accessory-ribbon"], palette.variables["--frame-accessory-crystal"], palette.variables["--frame-accessory-circle"] ]).size ).toBe(6); } else { expect(accessoryOpacity).toBeLessThanOrEqual(0.4); expect(largestAccessoryDimension).toBeLessThanOrEqual(60); } } }); it("falls back to the default black mecha palette", () => { expect(defaultThemePaletteId).toBe("mecha-black"); expect(getThemePalette("missing").id).toBe("mecha-black"); }); it("clears stale palette variables and publishes the active palette", () => { const removeProperty = vi.fn(); const setProperty = vi.fn(); const dispatchEvent = vi.fn(() => true); class TestCustomEvent { readonly type: string; readonly detail: T | null; constructor(type: string, init?: CustomEventInit) { this.type = type; this.detail = init?.detail ?? null; } } vi.stubGlobal("document", { documentElement: { dataset: {}, style: { removeProperty, setProperty } } }); vi.stubGlobal("window", { dispatchEvent }); vi.stubGlobal("CustomEvent", TestCustomEvent); applyThemePalette("mecha-black"); expect(removeProperty).toHaveBeenCalledWith("--frame-accessory-heart"); expect(setProperty).toHaveBeenCalledWith("--accent", "#48e6ff"); expect(dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ type: themePaletteChangeEvent, detail: { paletteId: "mecha-black" } }) ); }); it("offers mecha and magical desktop presets with a mecha default", () => { expect(defaultThemeBackgroundId).toBe("mecha-grid"); expect(themeBackgroundPresets.map((preset) => preset.label)).toEqual(["机甲格纳库", "粉月魔法阵"]); for (const preset of themeBackgroundPresets) { expect(preset.preview).toContain("gradient"); expect(preset.variables["--workspace-background-pattern-image"]).toContain("gradient"); expect(preset.variables["--workspace-background-pattern-opacity"]).toMatch(/^0\./); } expect(getThemeBackgroundPreset("missing").id).toBe("mecha-grid"); }); }); function accessorySizeNumbers(sizeValue: string): number[] { return Array.from(sizeValue.matchAll(/(\d+)px/g), (match) => Number(match[1])); }