/** * Unit tests for theme: getTheme, applyThemeParamsToCss, applyTheme, initTheme. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; describe("theme", () => { afterEach(() => { vi.restoreAllMocks(); }); describe("getTheme", () => { it("returns Telegram.WebApp.colorScheme when set", async () => { globalThis.window.Telegram = { WebApp: { colorScheme: "light" } }; vi.spyOn(document.documentElement.style, "getPropertyValue").mockReturnValue(""); const { getTheme } = await import("./theme.js"); expect(getTheme()).toBe("light"); }); it("falls back to --tg-color-scheme CSS when TWA has no colorScheme", async () => { globalThis.window.Telegram = { WebApp: {} }; vi.spyOn(globalThis, "getComputedStyle").mockReturnValue({ getPropertyValue: vi.fn().mockReturnValue("dark"), }); const { getTheme } = await import("./theme.js"); expect(getTheme()).toBe("dark"); }); it("falls back to matchMedia prefers-color-scheme dark", async () => { globalThis.window.Telegram = { WebApp: {} }; vi.spyOn(globalThis, "getComputedStyle").mockReturnValue({ getPropertyValue: vi.fn().mockReturnValue(""), }); vi.spyOn(globalThis, "matchMedia").mockReturnValue({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn(), }); const { getTheme } = await import("./theme.js"); expect(getTheme()).toBe("dark"); }); it("returns light when matchMedia prefers light", async () => { globalThis.window.Telegram = { WebApp: {} }; vi.spyOn(globalThis, "getComputedStyle").mockReturnValue({ getPropertyValue: vi.fn().mockReturnValue(""), }); vi.spyOn(globalThis, "matchMedia").mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn(), }); const { getTheme } = await import("./theme.js"); expect(getTheme()).toBe("light"); }); it("falls back to matchMedia when getComputedStyle throws", async () => { globalThis.window.Telegram = { WebApp: {} }; vi.spyOn(globalThis, "getComputedStyle").mockImplementation(() => { throw new Error("getComputedStyle not available"); }); vi.spyOn(globalThis, "matchMedia").mockReturnValue({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn(), }); const { getTheme } = await import("./theme.js"); expect(getTheme()).toBe("dark"); }); }); describe("applyThemeParamsToCss", () => { it("does nothing when Telegram.WebApp or themeParams missing", async () => { globalThis.window.Telegram = undefined; const setProperty = vi.fn(); document.documentElement.style.setProperty = setProperty; const { applyThemeParamsToCss } = await import("./theme.js"); applyThemeParamsToCss(); expect(setProperty).not.toHaveBeenCalled(); }); it("sets --tg-theme-* CSS variables from themeParams", async () => { globalThis.window.Telegram = { WebApp: { themeParams: { bg_color: "#ffffff", text_color: "#000000", hint_color: "#888888", }, }, }; const setProperty = vi.fn(); document.documentElement.style.setProperty = setProperty; const { applyThemeParamsToCss } = await import("./theme.js"); applyThemeParamsToCss(); expect(setProperty).toHaveBeenCalledWith("--tg-theme-bg-color", "#ffffff"); expect(setProperty).toHaveBeenCalledWith("--tg-theme-text-color", "#000000"); expect(setProperty).toHaveBeenCalledWith("--tg-theme-hint-color", "#888888"); }); }); describe("applyTheme", () => { beforeEach(() => { document.documentElement.dataset.theme = ""; }); it("sets data-theme on documentElement from getTheme", async () => { const theme = await import("./theme.js"); vi.spyOn(theme, "getTheme").mockReturnValue("light"); theme.applyTheme(); expect(document.documentElement.dataset.theme).toBe("light"); }); it("calls setBackgroundColor and setHeaderColor when TWA present", async () => { const setBackgroundColor = vi.fn(); const setHeaderColor = vi.fn(); globalThis.window.Telegram = { WebApp: { setBackgroundColor: setBackgroundColor, setHeaderColor: setHeaderColor, themeParams: null, }, }; const { applyTheme } = await import("./theme.js"); applyTheme(); expect(setBackgroundColor).toHaveBeenCalledWith("bg_color"); expect(setHeaderColor).toHaveBeenCalledWith("bg_color"); }); }); describe("initTheme", () => { it("runs without throwing when TWA present", async () => { globalThis.window.Telegram = { WebApp: {} }; const { initTheme } = await import("./theme.js"); expect(() => initTheme()).not.toThrow(); }); it("adds matchMedia change listener when no TWA", async () => { globalThis.window.Telegram = undefined; const addEventListener = vi.fn(); vi.spyOn(globalThis, "matchMedia").mockReturnValue({ matches: false, addEventListener, removeEventListener: vi.fn(), }); const { initTheme } = await import("./theme.js"); initTheme(); expect(addEventListener).toHaveBeenCalledWith("change", expect.any(Function)); }); }); });