Browse Source
- 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
2 changed files with 487 additions and 0 deletions
@ -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]+$/); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -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…
Reference in new issue