Browse Source

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)
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
3d5cde77d5
  1. 282
      src/store/utils/__tests__/localStorage.test.ts
  2. 205
      src/store/utils/__tests__/nodeDefaults.test.ts

282
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<string, string> = {};
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]+$/);
});
});
});

205
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<string, string> = {};
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);
});
});
});
Loading…
Cancel
Save