UNPKG

theme-o-rama

Version:

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

394 lines (393 loc) 16.3 kB
import { describe, it, expect, beforeEach, vi } from "vitest"; import { ThemeLoader } from "./theme-loader"; describe("ThemeLoader", () => { let loader; beforeEach(() => { loader = new ThemeLoader(); }); describe("loadTheme", () => { it("should load a basic theme", async () => { const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, }; await loader.loadTheme(theme); const loaded = loader.getTheme("test"); expect(loaded).toBeDefined(); expect(loaded?.name).toBe("test"); }); it("should load theme with all properties", async () => { const theme = { name: "complete", displayName: "Complete Theme", schemaVersion: 1, mostLike: "light", colors: { background: "hsl(0 0% 100%)", foreground: "hsl(0 0% 0%)", }, fonts: { sans: "Inter, sans-serif", }, }; await loader.loadTheme(theme); const loaded = loader.getTheme("complete"); expect(loaded?.colors?.background).toBe("hsl(0 0% 100%)"); expect(loaded?.fonts?.sans).toBe("Inter, sans-serif"); }); it("should create deep copy of theme to avoid mutations", async () => { const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, colors: { background: "hsl(0 0% 100%)", }, }; await loader.loadTheme(theme); // Mutate original theme.colors.background = "hsl(0 0% 0%)"; // Loaded theme should not be affected const loaded = loader.getTheme("test"); expect(loaded?.colors?.background).toBe("hsl(0 0% 100%)"); }); }); describe("loadThemes", () => { it("should load multiple themes", async () => { const themes = [ { name: "theme1", displayName: "Theme 1", schemaVersion: 1 }, { name: "theme2", displayName: "Theme 2", schemaVersion: 1 }, { name: "theme3", displayName: "Theme 3", schemaVersion: 1 }, ]; await loader.loadThemes(themes); expect(loader.getTheme("theme1")).toBeDefined(); expect(loader.getTheme("theme2")).toBeDefined(); expect(loader.getTheme("theme3")).toBeDefined(); }); it("should load themes in parallel", async () => { const themes = Array.from({ length: 10 }, (_, i) => ({ name: `theme${i}`, displayName: `Theme ${i}`, schemaVersion: 1, })); const startTime = Date.now(); await loader.loadThemes(themes); const duration = Date.now() - startTime; // Should complete quickly if parallel (arbitrary threshold) expect(duration).toBeLessThan(1000); expect(loader.getThemes()).toHaveLength(10); }); }); describe("loadThemeFromJson", () => { it("should load theme from valid JSON string", async () => { const themeJson = JSON.stringify({ name: "json-theme", displayName: "JSON Theme", schemaVersion: 1, }); const theme = await loader.loadThemeFromJson(themeJson); expect(theme.name).toBe("json-theme"); expect(loader.getTheme("json-theme")).toBeDefined(); }); it("should load and cache theme from JSON", async () => { const themeJson = JSON.stringify({ name: "cached-theme", displayName: "Cached Theme", schemaVersion: 1, colors: { background: "hsl(0 0% 50%)", }, }); await loader.loadThemeFromJson(themeJson); const cached = loader.getTheme("cached-theme"); expect(cached?.colors?.background).toBe("hsl(0 0% 50%)"); }); it("should throw error for invalid JSON", async () => { await expect(loader.loadThemeFromJson("invalid json")).rejects.toThrow(); }); it("should throw error for JSON missing required fields", async () => { const invalidJson = JSON.stringify({ name: "test", // missing displayName schemaVersion: 1, }); await expect(loader.loadThemeFromJson(invalidJson)).rejects.toThrow(); }); }); describe("getTheme and getThemes", () => { it("should return theme by name", async () => { const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, }; await loader.loadTheme(theme); const retrieved = loader.getTheme("test"); expect(retrieved?.name).toBe("test"); }); it("should return all loaded themes", async () => { const themes = [ { name: "theme1", displayName: "Theme 1", schemaVersion: 1 }, { name: "theme2", displayName: "Theme 2", schemaVersion: 1 }, ]; await loader.loadThemes(themes); const allThemes = loader.getThemes(); expect(allThemes).toHaveLength(2); expect(allThemes.map((t) => t.name)).toContain("theme1"); expect(allThemes.map((t) => t.name)).toContain("theme2"); }); it("should return fallback theme for non-existent theme", () => { const theme = loader.getTheme("non-existent"); expect(theme).toBeDefined(); expect(theme.name).toBe("light"); }); }); describe("clearCache", () => { it("should clear all cached themes", async () => { const themes = [ { name: "theme1", displayName: "Theme 1", schemaVersion: 1 }, { name: "theme2", displayName: "Theme 2", schemaVersion: 1 }, ]; await loader.loadThemes(themes); expect(loader.getThemes()).toHaveLength(2); loader.clearCache(); expect(loader.getThemes()).toHaveLength(0); }); }); describe("theme inheritance", () => { it("should inherit from parent theme", async () => { const parentTheme = { name: "parent", displayName: "Parent Theme", schemaVersion: 1, colors: { background: "hsl(0 0% 100%)", foreground: "hsl(0 0% 0%)", primary: "hsl(221 83% 53%)", }, }; const childTheme = { name: "child", displayName: "Child Theme", schemaVersion: 1, inherits: "parent", colors: { primary: "hsl(0 100% 50%)", // Override primary }, }; await loader.loadTheme(parentTheme); await loader.loadTheme(childTheme); const child = loader.getTheme("child"); expect(child?.colors?.background).toBe("hsl(0 0% 100%)"); // Inherited expect(child?.colors?.foreground).toBe("hsl(0 0% 0%)"); // Inherited expect(child?.colors?.primary).toBe("hsl(0 100% 50%)"); // Overridden }); it("should handle multi-level inheritance", async () => { const grandparentTheme = { name: "grandparent", displayName: "Grandparent Theme", schemaVersion: 1, colors: { background: "hsl(0 0% 100%)", }, }; const parentTheme = { name: "parent", displayName: "Parent Theme", schemaVersion: 1, inherits: "grandparent", colors: { foreground: "hsl(0 0% 0%)", }, }; const childTheme = { name: "child", displayName: "Child Theme", schemaVersion: 1, inherits: "parent", colors: { primary: "hsl(221 83% 53%)", }, }; await loader.loadTheme(grandparentTheme); await loader.loadTheme(parentTheme); await loader.loadTheme(childTheme); const child = loader.getTheme("child"); expect(child?.colors?.background).toBe("hsl(0 0% 100%)"); // From grandparent expect(child?.colors?.foreground).toBe("hsl(0 0% 0%)"); // From parent expect(child?.colors?.primary).toBe("hsl(221 83% 53%)"); // From child }); it("should preserve child theme tags when inheriting", async () => { const parentTheme = { name: "parent", displayName: "Parent Theme", schemaVersion: 1, tags: ["parent-tag"], }; const childTheme = { name: "child", displayName: "Child Theme", schemaVersion: 1, inherits: "parent", tags: ["child-tag"], }; await loader.loadTheme(parentTheme); await loader.loadTheme(childTheme); const child = loader.getTheme("child"); expect(child?.tags).toEqual(["child-tag"]); }); }); describe("image resolution", () => { it("should resolve relative background image paths", async () => { const imageResolver = vi.fn(async (themeName, imagePath) => { return `/resolved/${themeName}/${imagePath}`; }); const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, backgroundImage: "background.jpg", }; await loader.loadTheme(theme, imageResolver); expect(imageResolver).toHaveBeenCalledWith("test", "background.jpg"); const loaded = loader.getTheme("test"); expect(loaded?.backgroundImage).toBe("/resolved/test/background.jpg"); }); it("should not resolve http:// URLs", async () => { const imageResolver = vi.fn(async (themeName, imagePath) => { return `/resolved/${themeName}/${imagePath}`; }); const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, backgroundImage: "http://example.com/image.jpg", }; await loader.loadTheme(theme, imageResolver); expect(imageResolver).not.toHaveBeenCalled(); const loaded = loader.getTheme("test"); expect(loaded?.backgroundImage).toBe("http://example.com/image.jpg"); }); it("should not resolve https:// URLs", async () => { const imageResolver = vi.fn(async (themeName, imagePath) => { return `/resolved/${themeName}/${imagePath}`; }); const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, backgroundImage: "https://example.com/image.jpg", }; await loader.loadTheme(theme, imageResolver); expect(imageResolver).not.toHaveBeenCalled(); const loaded = loader.getTheme("test"); expect(loaded?.backgroundImage).toBe("https://example.com/image.jpg"); }); it("should not resolve data: URLs", async () => { const imageResolver = vi.fn(async (themeName, imagePath) => { return `/resolved/${themeName}/${imagePath}`; }); const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, backgroundImage: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", }; await loader.loadTheme(theme, imageResolver); expect(imageResolver).not.toHaveBeenCalled(); const loaded = loader.getTheme("test"); expect(loaded?.backgroundImage).toContain("data:image/png"); }); it("should handle image resolver errors gracefully", async () => { const imageResolver = vi.fn(async () => { throw new Error("Failed to resolve image"); }); const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, backgroundImage: "background.jpg", }; await loader.loadTheme(theme, imageResolver); const loaded = loader.getTheme("test"); expect(loaded?.backgroundImage).toBeUndefined(); }); it("should not call imageResolver when no backgroundImage", async () => { const imageResolver = vi.fn(async (themeName, imagePath) => { return `/resolved/${themeName}/${imagePath}`; }); const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, }; await loader.loadTheme(theme, imageResolver); expect(imageResolver).not.toHaveBeenCalled(); }); }); describe("initializeTheme", () => { it("should initialize theme without modifications when no inheritance or images", async () => { const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, colors: { background: "hsl(0 0% 100%)", }, }; const initialized = await loader.initializeTheme(theme); expect(initialized).toEqual(theme); }); it("should initialize theme with inheritance", async () => { const parentTheme = { name: "parent", displayName: "Parent Theme", schemaVersion: 1, colors: { background: "hsl(0 0% 100%)", }, }; await loader.loadTheme(parentTheme); const childTheme = { name: "child", displayName: "Child Theme", schemaVersion: 1, inherits: "parent", colors: { foreground: "hsl(0 0% 0%)", }, }; const initialized = await loader.initializeTheme(childTheme); expect(initialized.colors?.background).toBe("hsl(0 0% 100%)"); expect(initialized.colors?.foreground).toBe("hsl(0 0% 0%)"); }); it("should initialize theme with image resolution", async () => { const imageResolver = vi.fn(async (themeName, imagePath) => { return `/assets/${themeName}/${imagePath}`; }); const theme = { name: "test", displayName: "Test Theme", schemaVersion: 1, backgroundImage: "bg.jpg", }; const initialized = await loader.initializeTheme(theme, imageResolver); expect(initialized.backgroundImage).toBe("/assets/test/bg.jpg"); }); }); describe("error handling", () => { it("should handle errors during theme loading gracefully", async () => { const corruptTheme = { name: "corrupt", displayName: "Corrupt Theme", schemaVersion: 1, get colors() { throw new Error("Property access error"); }, }; // Should not throw await expect(loader.loadTheme(corruptTheme)).resolves.toBeUndefined(); }); }); });