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