Files
browser/platform_web/theme/tokens.test.ts
T
2026-07-11 14:56:10 +08:00

122 lines
4.6 KiB
TypeScript

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<T> {
readonly type: string;
readonly detail: T | null;
constructor(type: string, init?: CustomEventInit<T>) {
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]));
}