UNPKG

theme-o-rama

Version:

A TypeScript library for dynamic theme management in react + shadcn + tailwind applications

513 lines (512 loc) 23 kB
import { describe, it, expect, beforeEach } from "vitest"; import { applyTheme, applyThemeIsolated } from "./theme"; describe("theme", () => { let mockRoot; beforeEach(() => { // Create a fresh mock element for each test mockRoot = document.createElement("div"); document.body.appendChild(mockRoot); }); describe("applyTheme", () => { it("should apply basic theme properties", () => { const theme = { name: "test-theme", displayName: "Test Theme", schemaVersion: 1, mostLike: "light", }; applyTheme(theme, mockRoot); expect(mockRoot.classList.contains("theme-test-theme")).toBe(true); expect(mockRoot.style.colorScheme).toBe("light"); }); it("should set color scheme to dark when mostLike is dark", () => { const theme = { name: "dark-theme", displayName: "Dark Theme", schemaVersion: 1, mostLike: "dark", }; applyTheme(theme, mockRoot); expect(mockRoot.style.colorScheme).toBe("dark"); }); it("should apply color variables", () => { const theme = { name: "color-theme", displayName: "Color Theme", schemaVersion: 1, colors: { background: "hsl(0 0% 100%)", foreground: "hsl(0 0% 0%)", primary: "hsl(221 83% 53%)", primaryForeground: "hsl(0 0% 100%)", }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--background")).toBe("hsl(0 0% 100%)"); expect(mockRoot.style.getPropertyValue("--foreground")).toBe("hsl(0 0% 0%)"); expect(mockRoot.style.getPropertyValue("--primary")).toBe("hsl(221 83% 53%)"); expect(mockRoot.style.getPropertyValue("--primary-foreground")).toBe("hsl(0 0% 100%)"); }); it("should apply font variables", () => { const theme = { name: "font-theme", displayName: "Font Theme", schemaVersion: 1, fonts: { sans: "Inter, sans-serif", serif: "Georgia, serif", mono: "Monaco, monospace", heading: "Poppins, sans-serif", body: "Inter, sans-serif", }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--font-sans")).toBe("Inter, sans-serif"); expect(mockRoot.style.getPropertyValue("--font-serif")).toBe("Georgia, serif"); expect(mockRoot.style.getPropertyValue("--font-mono")).toBe("Monaco, monospace"); expect(mockRoot.style.getPropertyValue("--font-heading")).toBe("Poppins, sans-serif"); expect(mockRoot.style.getPropertyValue("--font-body")).toBe("Inter, sans-serif"); }); it("should apply corner radius variables", () => { const theme = { name: "corners-theme", displayName: "Corners Theme", schemaVersion: 1, corners: { none: "0px", sm: "0.125rem", md: "0.375rem", lg: "0.5rem", xl: "0.75rem", full: "9999px", }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--corner-none")).toBe("0px"); expect(mockRoot.style.getPropertyValue("--corner-sm")).toBe("0.125rem"); expect(mockRoot.style.getPropertyValue("--corner-md")).toBe("0.375rem"); expect(mockRoot.style.getPropertyValue("--corner-lg")).toBe("0.5rem"); expect(mockRoot.style.getPropertyValue("--corner-xl")).toBe("0.75rem"); expect(mockRoot.style.getPropertyValue("--corner-full")).toBe("9999px"); }); it("should apply shadow variables", () => { const theme = { name: "shadow-theme", displayName: "Shadow Theme", schemaVersion: 1, shadows: { none: "none", sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)", md: "0 4px 6px -1px rgb(0 0 0 / 0.1)", lg: "0 10px 15px -3px rgb(0 0 0 / 0.1)", }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--shadow-none")).toBe("none"); expect(mockRoot.style.getPropertyValue("--shadow-sm")).toBe("0 1px 2px 0 rgb(0 0 0 / 0.05)"); expect(mockRoot.style.getPropertyValue("--shadow-md")).toBe("0 4px 6px -1px rgb(0 0 0 / 0.1)"); expect(mockRoot.style.getPropertyValue("--shadow-lg")).toBe("0 10px 15px -3px rgb(0 0 0 / 0.1)"); }); it("should apply background image with proper properties", () => { const theme = { name: "bg-theme", displayName: "Background Theme", schemaVersion: 1, backgroundImage: "/images/background.jpg", backgroundSize: "cover", backgroundPosition: "center", backgroundRepeat: "no-repeat", }; applyTheme(theme, mockRoot); // Browser normalizes url() by adding quotes and expands "center" to "center center" expect(mockRoot.style.backgroundImage).toBe('url("/images/background.jpg")'); expect(mockRoot.style.backgroundSize).toBe("cover"); expect(mockRoot.style.backgroundPosition).toBe("center center"); expect(mockRoot.style.backgroundRepeat).toBe("no-repeat"); expect(mockRoot.style.backgroundAttachment).toBe("fixed"); expect(mockRoot.classList.contains("has-background-image")).toBe(true); }); it("should remove background image when not specified", () => { // First apply a theme with background const themeWithBg = { name: "with-bg", displayName: "With Background", schemaVersion: 1, backgroundImage: "/images/bg.jpg", }; applyTheme(themeWithBg, mockRoot); expect(mockRoot.classList.contains("has-background-image")).toBe(true); // Now apply a theme without background const themeWithoutBg = { name: "without-bg", displayName: "Without Background", schemaVersion: 1, }; applyTheme(themeWithoutBg, mockRoot); expect(mockRoot.classList.contains("has-background-image")).toBe(false); expect(mockRoot.style.backgroundImage).toBe(""); }); it("should apply button style data attribute", () => { const theme = { name: "button-theme", displayName: "Button Theme", schemaVersion: 1, buttonStyle: "gradient", }; applyTheme(theme, mockRoot); expect(mockRoot.getAttribute("data-theme-style")).toBe("gradient"); }); it("should set button style feature flags", () => { const theme = { name: "gradient-theme", displayName: "Gradient Theme", schemaVersion: 1, buttonStyle: "gradient", }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--theme-has-gradient-buttons")).toBe("1"); expect(mockRoot.style.getPropertyValue("--theme-has-shimmer-effects")).toBe("0"); }); it("should apply table variables", () => { const theme = { name: "table-theme", displayName: "Table Theme", schemaVersion: 1, tables: { background: "hsl(0 0% 100%)", border: "1px solid hsl(0 0% 89.8%)", header: { background: "hsl(0 0% 96.1%)", color: "hsl(0 0% 3.9%)", border: "1px solid hsl(0 0% 89.8%)", }, row: { background: "hsl(0 0% 100%)", color: "hsl(0 0% 3.9%)", hover: { background: "hsl(0 0% 98%)", color: "hsl(0 0% 3.9%)", }, }, }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--table-background")).toBe("hsl(0 0% 100%)"); expect(mockRoot.style.getPropertyValue("--table-border")).toBe("1px solid hsl(0 0% 89.8%)"); expect(mockRoot.style.getPropertyValue("--table-header-background")).toBe("hsl(0 0% 96.1%)"); expect(mockRoot.style.getPropertyValue("--table-row-hover-background")).toBe("hsl(0 0% 98%)"); }); it("should apply sidebar variables", () => { const theme = { name: "sidebar-theme", displayName: "Sidebar Theme", schemaVersion: 1, sidebar: { background: "hsl(0 0% 100%)", border: "1px solid hsl(0 0% 89.8%)", backdropFilter: "blur(10px)", }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--sidebar-background")).toBe("hsl(0 0% 100%)"); expect(mockRoot.style.getPropertyValue("--sidebar-border")).toBe("1px solid hsl(0 0% 89.8%)"); expect(mockRoot.style.getPropertyValue("--sidebar-backdrop-filter")).toBe("blur(10px)"); expect(mockRoot.style.getPropertyValue("--sidebar-backdrop-filter-webkit")).toBe("blur(10px)"); }); it("should apply button variant variables", () => { const theme = { name: "btn-theme", displayName: "Button Theme", schemaVersion: 1, buttons: { default: { background: "hsl(0 0% 9%)", color: "hsl(0 0% 98%)", borderRadius: "0.375rem", hover: { background: "hsl(0 0% 15%)", }, }, }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--btn-default-background")).toBe("hsl(0 0% 9%)"); expect(mockRoot.style.getPropertyValue("--btn-default-color")).toBe("hsl(0 0% 98%)"); expect(mockRoot.style.getPropertyValue("--btn-default-radius")).toBe("0.375rem"); expect(mockRoot.style.getPropertyValue("--btn-default-hover-background")).toBe("hsl(0 0% 15%)"); }); it("should apply switch variables", () => { const theme = { name: "switch-theme", displayName: "Switch Theme", schemaVersion: 1, switches: { checked: { background: "hsl(221 83% 53%)", }, unchecked: { background: "hsl(0 0% 89.8%)", }, thumb: { background: "hsl(0 0% 100%)", }, }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--switch-checked-background")).toBe("hsl(221 83% 53%)"); expect(mockRoot.style.getPropertyValue("--switch-unchecked-background")).toBe("hsl(0 0% 89.8%)"); expect(mockRoot.style.getPropertyValue("--switch-thumb-background")).toBe("hsl(0 0% 100%)"); }); it("should apply backdrop filter variables", () => { const theme = { name: "backdrop-theme", displayName: "Backdrop Theme", schemaVersion: 1, colors: { cardBackdropFilter: "blur(10px)", popoverBackdropFilter: "blur(5px)", inputBackdropFilter: "blur(8px)", }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--card-backdrop-filter")).toBe("blur(10px)"); expect(mockRoot.style.getPropertyValue("--popover-backdrop-filter")).toBe("blur(5px)"); expect(mockRoot.style.getPropertyValue("--input-backdrop-filter")).toBe("blur(8px)"); }); it("should clear previous theme variables when applying new theme", () => { const theme1 = { name: "theme1", displayName: "Theme 1", schemaVersion: 1, colors: { background: "hsl(0 0% 100%)", primary: "hsl(221 83% 53%)", }, }; applyTheme(theme1, mockRoot); expect(mockRoot.classList.contains("theme-theme1")).toBe(true); expect(mockRoot.style.getPropertyValue("--background")).toBe("hsl(0 0% 100%)"); const theme2 = { name: "theme2", displayName: "Theme 2", schemaVersion: 1, colors: { background: "hsl(0 0% 0%)", }, }; applyTheme(theme2, mockRoot); expect(mockRoot.classList.contains("theme-theme1")).toBe(false); expect(mockRoot.classList.contains("theme-theme2")).toBe(true); expect(mockRoot.style.getPropertyValue("--background")).toBe("hsl(0 0% 0%)"); }); it("should handle inputBackground color variable", () => { const theme = { name: "input-theme", displayName: "Input Theme", schemaVersion: 1, colors: { input: "hsl(0 0% 89.8%)", inputBackground: "hsl(0 0% 100%)", }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--input-background")).toBe("hsl(0 0% 100%)"); }); it("should fallback to input color when inputBackground is not specified", () => { const theme = { name: "input-fallback-theme", displayName: "Input Fallback Theme", schemaVersion: 1, colors: { input: "hsl(0 0% 89.8%)", }, }; applyTheme(theme, mockRoot); expect(mockRoot.style.getPropertyValue("--input-background")).toBe("hsl(0 0% 89.8%)"); }); it("should handle theme names with invalid characters gracefully", () => { const theme = { name: "theme with spaces!@#", displayName: "Invalid Name Theme", schemaVersion: 1, }; applyTheme(theme, mockRoot); // Should add fallback class instead of crashing expect(mockRoot.classList.contains("theme-invalid-name}")).toBe(true); }); }); describe("applyThemeIsolated", () => { it("should apply theme in isolated mode without background attachment", () => { const theme = { name: "isolated-theme", displayName: "Isolated Theme", schemaVersion: 1, backgroundImage: "/images/bg.jpg", colors: { background: "hsl(0 0% 100%)", foreground: "hsl(0 0% 0%)", }, }; applyThemeIsolated(theme, mockRoot); // Should have background image but not fixed attachment expect(mockRoot.style.backgroundImage).toBe('url("/images/bg.jpg")'); expect(mockRoot.style.backgroundAttachment).not.toBe("fixed"); expect(mockRoot.classList.contains("has-background-image")).toBe(true); }); it("should set explicit background color in isolated mode", () => { const theme = { name: "isolated-bg", displayName: "Isolated Background", schemaVersion: 1, colors: { background: "hsl(0 0% 100%)", foreground: "hsl(0 0% 0%)", }, }; applyThemeIsolated(theme, mockRoot); // Browser normalizes HSL to RGB expect(mockRoot.style.backgroundColor).toBe("rgb(255, 255, 255)"); expect(mockRoot.style.color).toBe("rgb(0, 0, 0)"); }); it("should set explicit font family in isolated mode", () => { const theme = { name: "isolated-font", displayName: "Isolated Font", schemaVersion: 1, fonts: { body: "Inter, sans-serif", }, }; applyThemeIsolated(theme, mockRoot); expect(mockRoot.style.fontFamily).toBe("Inter, sans-serif"); }); it("should fallback to sans font if body font not specified", () => { const theme = { name: "isolated-sans", displayName: "Isolated Sans", schemaVersion: 1, fonts: { sans: "Helvetica, sans-serif", }, }; applyThemeIsolated(theme, mockRoot); expect(mockRoot.style.fontFamily).toBe("Helvetica, sans-serif"); }); it("should apply Tailwind v4 color mappings in isolated mode", () => { const theme = { name: "tailwind-v4", displayName: "Tailwind v4", schemaVersion: 1, colors: { background: "hsl(0 0% 100%)", foreground: "hsl(0 0% 0%)", primary: "hsl(221 83% 53%)", }, }; applyThemeIsolated(theme, mockRoot); // Wait for next tick to allow getComputedStyle to work // In a real browser, these would be mapped expect(mockRoot.style.getPropertyValue("--background")).toBe("hsl(0 0% 100%)"); expect(mockRoot.style.getPropertyValue("--primary")).toBe("hsl(221 83% 53%)"); }); it("should not apply document-wide background settings in isolated mode", () => { const theme = { name: "isolated-no-fixed", displayName: "Isolated No Fixed", schemaVersion: 1, backgroundImage: "/images/bg.jpg", }; applyThemeIsolated(theme, mockRoot); // Should apply to element directly, not with fixed attachment expect(mockRoot.style.backgroundImage).toBe('url("/images/bg.jpg")'); expect(mockRoot.style.backgroundAttachment).toBe(""); }); it("should clear previous theme classes in isolated mode", () => { const theme1 = { name: "iso-theme1", displayName: "Isolated Theme 1", schemaVersion: 1, }; applyThemeIsolated(theme1, mockRoot); expect(mockRoot.classList.contains("theme-iso-theme1")).toBe(true); const theme2 = { name: "iso-theme2", displayName: "Isolated Theme 2", schemaVersion: 1, }; applyThemeIsolated(theme2, mockRoot); expect(mockRoot.classList.contains("theme-iso-theme1")).toBe(false); expect(mockRoot.classList.contains("theme-iso-theme2")).toBe(true); }); }); describe("complex theme scenarios", () => { it("should handle complete theme with all properties", () => { const fullTheme = { name: "complete", displayName: "Complete Theme", schemaVersion: 1, mostLike: "light", backgroundImage: "/bg.jpg", backgroundSize: "cover", backgroundPosition: "center", backgroundRepeat: "no-repeat", buttonStyle: "gradient", colors: { background: "hsl(0 0% 100%)", foreground: "hsl(0 0% 0%)", primary: "hsl(221 83% 53%)", primaryForeground: "hsl(0 0% 100%)", }, fonts: { sans: "Inter, sans-serif", body: "Inter, sans-serif", }, corners: { md: "0.375rem", lg: "0.5rem", }, shadows: { sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)", }, sidebar: { background: "hsl(0 0% 98%)", }, tables: { background: "hsl(0 0% 100%)", }, buttons: { default: { background: "hsl(0 0% 9%)", color: "hsl(0 0% 98%)", }, }, switches: { checked: { background: "hsl(221 83% 53%)", }, unchecked: { background: "hsl(0 0% 89.8%)", }, }, }; applyTheme(fullTheme, mockRoot); // Verify multiple aspects expect(mockRoot.classList.contains("theme-complete")).toBe(true); expect(mockRoot.style.colorScheme).toBe("light"); expect(mockRoot.style.getPropertyValue("--background")).toBe("hsl(0 0% 100%)"); expect(mockRoot.style.getPropertyValue("--font-sans")).toBe("Inter, sans-serif"); expect(mockRoot.style.getPropertyValue("--corner-md")).toBe("0.375rem"); expect(mockRoot.style.backgroundImage).toBe('url("/bg.jpg")'); }); it("should handle theme with partial properties", () => { const minimalTheme = { name: "minimal", displayName: "Minimal Theme", schemaVersion: 1, }; applyTheme(minimalTheme, mockRoot); expect(mockRoot.classList.contains("theme-minimal")).toBe(true); // Should not crash with missing properties }); }); });