From 3d5cde77d5a25c152c270978055a2a8746faf1c5 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Tue, 13 Jan 2026 07:43:52 +1300 Subject: [PATCH] test(16-01): add unit tests for localStorage and nodeDefaults modules - Add 19 tests for localStorage utilities: - loadSaveConfigs, saveSaveConfig - loadWorkflowCostData, saveWorkflowCostData - loadGenerateImageDefaults, saveGenerateImageDefaults - getProviderSettings, saveProviderSettings - generateWorkflowId - Add 16 tests for nodeDefaults utilities: - defaultNodeDimensions coverage for all node types - GROUP_COLORS and GROUP_COLOR_ORDER validation - createDefaultNodeData for all 8 node types - Total test count: 147 (up from 112) --- .../utils/__tests__/localStorage.test.ts | 282 ++++++++++++++++++ .../utils/__tests__/nodeDefaults.test.ts | 205 +++++++++++++ 2 files changed, 487 insertions(+) create mode 100644 src/store/utils/__tests__/localStorage.test.ts create mode 100644 src/store/utils/__tests__/nodeDefaults.test.ts diff --git a/src/store/utils/__tests__/localStorage.test.ts b/src/store/utils/__tests__/localStorage.test.ts new file mode 100644 index 00000000..d3434ec8 --- /dev/null +++ b/src/store/utils/__tests__/localStorage.test.ts @@ -0,0 +1,282 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { + STORAGE_KEY, + COST_DATA_STORAGE_KEY, + GENERATE_IMAGE_DEFAULTS_KEY, + PROVIDER_SETTINGS_KEY, + loadSaveConfigs, + saveSaveConfig, + loadWorkflowCostData, + saveWorkflowCostData, + loadGenerateImageDefaults, + saveGenerateImageDefaults, + getProviderSettings, + saveProviderSettings, + defaultProviderSettings, + generateWorkflowId, +} from "../localStorage"; + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value; + }), + removeItem: vi.fn((key: string) => { + delete store[key]; + }), + clear: vi.fn(() => { + store = {}; + }), + }; +})(); + +Object.defineProperty(globalThis, "localStorage", { + value: localStorageMock, + writable: true, +}); + +describe("localStorage utilities", () => { + beforeEach(() => { + localStorageMock.clear(); + vi.clearAllMocks(); + }); + + describe("loadSaveConfigs", () => { + it("returns empty object when localStorage is empty", () => { + const result = loadSaveConfigs(); + expect(result).toEqual({}); + }); + + it("returns stored configs when data exists", () => { + const mockConfigs = { + "wf_123": { + workflowId: "wf_123", + name: "Test Workflow", + path: "/path/to/workflow", + lastSaved: Date.now(), + }, + }; + localStorageMock.setItem(STORAGE_KEY, JSON.stringify(mockConfigs)); + + const result = loadSaveConfigs(); + expect(result).toEqual(mockConfigs); + }); + }); + + describe("saveSaveConfig", () => { + it("stores and retrieves config", () => { + const config = { + workflowId: "wf_456", + name: "New Workflow", + path: "/path/to/new", + lastSaved: Date.now(), + }; + + saveSaveConfig(config); + + const stored = JSON.parse(localStorageMock.getItem(STORAGE_KEY)!); + expect(stored["wf_456"]).toEqual(config); + }); + + it("merges with existing configs", () => { + const existingConfig = { + workflowId: "wf_existing", + name: "Existing", + path: "/existing", + lastSaved: Date.now(), + }; + localStorageMock.setItem( + STORAGE_KEY, + JSON.stringify({ "wf_existing": existingConfig }) + ); + + const newConfig = { + workflowId: "wf_new", + name: "New", + path: "/new", + lastSaved: Date.now(), + }; + saveSaveConfig(newConfig); + + const stored = JSON.parse(localStorageMock.getItem(STORAGE_KEY)!); + expect(stored["wf_existing"]).toEqual(existingConfig); + expect(stored["wf_new"]).toEqual(newConfig); + }); + }); + + describe("loadWorkflowCostData", () => { + it("returns null when localStorage is empty", () => { + const result = loadWorkflowCostData("wf_123"); + expect(result).toBeNull(); + }); + + it("returns null when workflow not found", () => { + localStorageMock.setItem( + COST_DATA_STORAGE_KEY, + JSON.stringify({ "wf_other": { workflowId: "wf_other", totalCost: 10 } }) + ); + + const result = loadWorkflowCostData("wf_missing"); + expect(result).toBeNull(); + }); + + it("returns cost data when found", () => { + const costData = { workflowId: "wf_123", totalCost: 5.5 }; + localStorageMock.setItem( + COST_DATA_STORAGE_KEY, + JSON.stringify({ "wf_123": costData }) + ); + + const result = loadWorkflowCostData("wf_123"); + expect(result).toEqual(costData); + }); + + it("returns null on invalid JSON", () => { + localStorageMock.setItem(COST_DATA_STORAGE_KEY, "invalid json"); + + const result = loadWorkflowCostData("wf_123"); + expect(result).toBeNull(); + }); + }); + + describe("saveWorkflowCostData", () => { + it("stores cost data", () => { + const costData = { workflowId: "wf_789", totalCost: 2.5 }; + + saveWorkflowCostData(costData); + + const stored = JSON.parse(localStorageMock.getItem(COST_DATA_STORAGE_KEY)!); + expect(stored["wf_789"]).toEqual(costData); + }); + }); + + describe("loadGenerateImageDefaults", () => { + it("returns default settings when localStorage is empty", () => { + const result = loadGenerateImageDefaults(); + expect(result).toEqual({ + aspectRatio: "1:1", + resolution: "1K", + model: "nano-banana-pro", + useGoogleSearch: false, + }); + }); + + it("returns stored settings when data exists", () => { + const customSettings = { + aspectRatio: "16:9", + resolution: "2K", + model: "nano-banana", + useGoogleSearch: true, + }; + localStorageMock.setItem( + GENERATE_IMAGE_DEFAULTS_KEY, + JSON.stringify(customSettings) + ); + + const result = loadGenerateImageDefaults(); + expect(result).toEqual(customSettings); + }); + + it("returns default on invalid JSON", () => { + localStorageMock.setItem(GENERATE_IMAGE_DEFAULTS_KEY, "not valid json"); + + const result = loadGenerateImageDefaults(); + expect(result).toEqual({ + aspectRatio: "1:1", + resolution: "1K", + model: "nano-banana-pro", + useGoogleSearch: false, + }); + }); + }); + + describe("saveGenerateImageDefaults", () => { + it("merges partial settings with existing", () => { + const existing = { + aspectRatio: "1:1", + resolution: "1K", + model: "nano-banana-pro", + useGoogleSearch: false, + }; + localStorageMock.setItem( + GENERATE_IMAGE_DEFAULTS_KEY, + JSON.stringify(existing) + ); + + saveGenerateImageDefaults({ aspectRatio: "4:3" }); + + const stored = JSON.parse(localStorageMock.getItem(GENERATE_IMAGE_DEFAULTS_KEY)!); + expect(stored).toEqual({ + ...existing, + aspectRatio: "4:3", + }); + }); + }); + + describe("getProviderSettings", () => { + it("returns default settings when localStorage is empty", () => { + const result = getProviderSettings(); + expect(result).toEqual(defaultProviderSettings); + }); + + it("merges with defaults for new providers", () => { + // Simulate old settings missing a new provider + const oldSettings = { + providers: { + gemini: { id: "gemini", name: "Google Gemini", enabled: true, apiKey: "test-key" }, + }, + }; + localStorageMock.setItem( + PROVIDER_SETTINGS_KEY, + JSON.stringify(oldSettings) + ); + + const result = getProviderSettings(); + // Should have the stored gemini settings + expect(result.providers.gemini.apiKey).toBe("test-key"); + // Should also have the default providers that were missing + expect(result.providers.replicate).toBeDefined(); + expect(result.providers.fal).toBeDefined(); + }); + + it("returns default on invalid JSON", () => { + localStorageMock.setItem(PROVIDER_SETTINGS_KEY, "invalid"); + + const result = getProviderSettings(); + expect(result).toEqual(defaultProviderSettings); + }); + }); + + describe("saveProviderSettings", () => { + it("stores provider settings", () => { + const settings = { + providers: { + gemini: { id: "gemini", name: "Gemini", enabled: true, apiKey: "key123" }, + }, + }; + + saveProviderSettings(settings as any); + + const stored = JSON.parse(localStorageMock.getItem(PROVIDER_SETTINGS_KEY)!); + expect(stored).toEqual(settings); + }); + }); + + describe("generateWorkflowId", () => { + it("generates unique IDs", () => { + const id1 = generateWorkflowId(); + const id2 = generateWorkflowId(); + + expect(id1).not.toBe(id2); + }); + + it("has correct format", () => { + const id = generateWorkflowId(); + + expect(id).toMatch(/^wf_\d+_[a-z0-9]+$/); + }); + }); +}); diff --git a/src/store/utils/__tests__/nodeDefaults.test.ts b/src/store/utils/__tests__/nodeDefaults.test.ts new file mode 100644 index 00000000..e4f2960c --- /dev/null +++ b/src/store/utils/__tests__/nodeDefaults.test.ts @@ -0,0 +1,205 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { + createDefaultNodeData, + defaultNodeDimensions, + GROUP_COLORS, + GROUP_COLOR_ORDER, +} from "../nodeDefaults"; + +// Mock localStorage for loadGenerateImageDefaults +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value; + }), + clear: vi.fn(() => { + store = {}; + }), + }; +})(); + +Object.defineProperty(globalThis, "localStorage", { + value: localStorageMock, + writable: true, +}); + +describe("nodeDefaults utilities", () => { + beforeEach(() => { + localStorageMock.clear(); + vi.clearAllMocks(); + }); + + describe("defaultNodeDimensions", () => { + it("has all expected node types", () => { + const expectedTypes = [ + "imageInput", + "annotation", + "prompt", + "nanoBanana", + "generateVideo", + "llmGenerate", + "splitGrid", + "output", + ]; + + expectedTypes.forEach((type) => { + expect(defaultNodeDimensions[type as keyof typeof defaultNodeDimensions]).toBeDefined(); + }); + }); + + it("has width and height for each node type", () => { + Object.values(defaultNodeDimensions).forEach((dims) => { + expect(dims).toHaveProperty("width"); + expect(dims).toHaveProperty("height"); + expect(typeof dims.width).toBe("number"); + expect(typeof dims.height).toBe("number"); + expect(dims.width).toBeGreaterThan(0); + expect(dims.height).toBeGreaterThan(0); + }); + }); + }); + + describe("GROUP_COLORS", () => { + it("has all expected color keys", () => { + const expectedColors = ["neutral", "blue", "green", "purple", "orange", "red"]; + + expectedColors.forEach((color) => { + expect(GROUP_COLORS[color as keyof typeof GROUP_COLORS]).toBeDefined(); + }); + }); + + it("has valid hex color values", () => { + Object.values(GROUP_COLORS).forEach((color) => { + expect(color).toMatch(/^#[0-9a-fA-F]{6}$/); + }); + }); + }); + + describe("GROUP_COLOR_ORDER", () => { + it("has all colors from GROUP_COLORS", () => { + GROUP_COLOR_ORDER.forEach((color) => { + expect(GROUP_COLORS[color]).toBeDefined(); + }); + }); + + it("has same length as GROUP_COLORS", () => { + expect(GROUP_COLOR_ORDER.length).toBe(Object.keys(GROUP_COLORS).length); + }); + + it("starts with neutral", () => { + expect(GROUP_COLOR_ORDER[0]).toBe("neutral"); + }); + }); + + describe("createDefaultNodeData", () => { + it("creates correct structure for imageInput", () => { + const data = createDefaultNodeData("imageInput"); + + expect(data).toHaveProperty("image", null); + expect(data).toHaveProperty("filename", null); + expect(data).toHaveProperty("dimensions", null); + }); + + it("creates correct structure for annotation", () => { + const data = createDefaultNodeData("annotation"); + + expect(data).toHaveProperty("sourceImage", null); + expect(data).toHaveProperty("annotations"); + expect(Array.isArray((data as any).annotations)).toBe(true); + expect(data).toHaveProperty("outputImage", null); + }); + + it("creates correct structure for prompt", () => { + const data = createDefaultNodeData("prompt"); + + expect(data).toHaveProperty("prompt", ""); + }); + + it("creates correct structure for nanoBanana", () => { + const data = createDefaultNodeData("nanoBanana"); + + expect(data).toHaveProperty("inputImages"); + expect(data).toHaveProperty("inputPrompt", null); + expect(data).toHaveProperty("outputImage", null); + expect(data).toHaveProperty("aspectRatio"); + expect(data).toHaveProperty("resolution"); + expect(data).toHaveProperty("model"); + expect(data).toHaveProperty("selectedModel"); + expect(data).toHaveProperty("useGoogleSearch"); + expect(data).toHaveProperty("status", "idle"); + expect(data).toHaveProperty("error", null); + expect(data).toHaveProperty("imageHistory"); + expect(data).toHaveProperty("selectedHistoryIndex", 0); + }); + + it("creates correct structure for generateVideo", () => { + const data = createDefaultNodeData("generateVideo"); + + expect(data).toHaveProperty("inputImages"); + expect(data).toHaveProperty("inputPrompt", null); + expect(data).toHaveProperty("outputVideo", null); + expect(data).toHaveProperty("selectedModel"); + expect(data).toHaveProperty("status", "idle"); + expect(data).toHaveProperty("error", null); + expect(data).toHaveProperty("videoHistory"); + expect(data).toHaveProperty("selectedVideoHistoryIndex", 0); + }); + + it("creates correct structure for llmGenerate", () => { + const data = createDefaultNodeData("llmGenerate"); + + expect(data).toHaveProperty("inputPrompt", null); + expect(data).toHaveProperty("inputImages"); + expect(data).toHaveProperty("outputText", null); + expect(data).toHaveProperty("provider", "google"); + expect(data).toHaveProperty("model"); + expect(data).toHaveProperty("temperature"); + expect(data).toHaveProperty("maxTokens"); + expect(data).toHaveProperty("status", "idle"); + expect(data).toHaveProperty("error", null); + }); + + it("creates correct structure for splitGrid", () => { + const data = createDefaultNodeData("splitGrid"); + + expect(data).toHaveProperty("sourceImage", null); + expect(data).toHaveProperty("targetCount", 6); + expect(data).toHaveProperty("defaultPrompt", ""); + expect(data).toHaveProperty("generateSettings"); + expect(data).toHaveProperty("childNodeIds"); + expect(data).toHaveProperty("gridRows", 2); + expect(data).toHaveProperty("gridCols", 3); + expect(data).toHaveProperty("isConfigured", false); + expect(data).toHaveProperty("status", "idle"); + expect(data).toHaveProperty("error", null); + }); + + it("creates correct structure for output", () => { + const data = createDefaultNodeData("output"); + + expect(data).toHaveProperty("image", null); + }); + + it("uses stored defaults for nanoBanana when available", () => { + const customSettings = { + aspectRatio: "16:9", + resolution: "2K", + model: "nano-banana", + useGoogleSearch: true, + }; + localStorageMock.setItem( + "node-banana-nanoBanana-defaults", + JSON.stringify(customSettings) + ); + + const data = createDefaultNodeData("nanoBanana"); + + expect((data as any).aspectRatio).toBe("16:9"); + expect((data as any).resolution).toBe("2K"); + expect((data as any).model).toBe("nano-banana"); + expect((data as any).useGoogleSearch).toBe(true); + }); + }); +});