Browse Source
ProjectSetupModal tests: - Add tests for visibility and tab navigation - Add tests for new project vs settings mode - Add tests for form validation (name, directory) - Add tests for save/create behavior with directory validation - Add tests for browse directory functionality - Add tests for keyboard shortcuts (Enter, Escape) - Add tests for Providers tab API key inputs CostDialog tests: - Add tests for basic rendering and sections - Add tests for cost display formatting - Add tests for per-model cost breakdown - Add tests for empty state handling - Add tests for reset costs functionality - Add tests for close behavior - Add tests for pricing reference display Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>handoff-20260429-1057
2 changed files with 1345 additions and 0 deletions
@ -0,0 +1,401 @@ |
|||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; |
||||
|
import { render, screen, fireEvent } from "@testing-library/react"; |
||||
|
import { CostDialog } from "@/components/CostDialog"; |
||||
|
import { PredictedCostResult } from "@/utils/costCalculator"; |
||||
|
|
||||
|
// Mock the workflow store
|
||||
|
const mockResetIncurredCost = vi.fn(); |
||||
|
const mockUseWorkflowStore = vi.fn(); |
||||
|
|
||||
|
vi.mock("@/store/workflowStore", () => ({ |
||||
|
useWorkflowStore: (selector?: (state: unknown) => unknown) => { |
||||
|
if (selector) { |
||||
|
return mockUseWorkflowStore(selector); |
||||
|
} |
||||
|
return mockUseWorkflowStore((s: unknown) => s); |
||||
|
}, |
||||
|
})); |
||||
|
|
||||
|
// Mock confirm
|
||||
|
const mockConfirm = vi.fn(() => true); |
||||
|
global.confirm = mockConfirm; |
||||
|
|
||||
|
describe("CostDialog", () => { |
||||
|
beforeEach(() => { |
||||
|
vi.clearAllMocks(); |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
const state = { |
||||
|
resetIncurredCost: mockResetIncurredCost, |
||||
|
}; |
||||
|
return selector(state); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
vi.restoreAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
const createPredictedCost = (overrides: Partial<PredictedCostResult> = {}): PredictedCostResult => ({ |
||||
|
totalCost: 0.50, |
||||
|
breakdown: [ |
||||
|
{ |
||||
|
model: "nano-banana", |
||||
|
resolution: "1K", |
||||
|
count: 5, |
||||
|
unitCost: 0.039, |
||||
|
subtotal: 0.195, |
||||
|
}, |
||||
|
{ |
||||
|
model: "nano-banana-pro", |
||||
|
resolution: "2K", |
||||
|
count: 2, |
||||
|
unitCost: 0.134, |
||||
|
subtotal: 0.268, |
||||
|
}, |
||||
|
], |
||||
|
nodeCount: 7, |
||||
|
...overrides, |
||||
|
}); |
||||
|
|
||||
|
describe("Basic Rendering", () => { |
||||
|
it("should render dialog with title", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Workflow Costs")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render close button", () => { |
||||
|
const { container } = render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Close button has an SVG with X icon
|
||||
|
const closeButton = container.querySelector("button"); |
||||
|
expect(closeButton).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render Predicted Cost section", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Predicted Cost")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render Incurred Cost section", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Incurred Cost")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render Pricing Reference section", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Pricing Reference:")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Cost Display", () => { |
||||
|
it("should display formatted predicted cost", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost({ totalCost: 1.25 })} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("$1.25")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should display formatted incurred cost", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={2.50} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("$2.50")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should display $0.00 for zero costs", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost({ totalCost: 0 })} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Should have two $0.00 - one for predicted, one for incurred
|
||||
|
const zeroValues = screen.getAllByText("$0.00"); |
||||
|
expect(zeroValues.length).toBe(2); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Cost Breakdown", () => { |
||||
|
it("should render per-model cost rows", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Check for model count and name
|
||||
|
expect(screen.getByText(/5x Nano Banana/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/2x Nano Banana Pro/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show resolution for Nano Banana Pro models", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Nano Banana Pro should show resolution
|
||||
|
expect(screen.getByText(/Nano Banana Pro.*\(2K\)/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should display subtotal for each model type", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Check for subtotals
|
||||
|
expect(screen.getByText("$0.20")).toBeInTheDocument(); // 0.195 rounded
|
||||
|
expect(screen.getByText("$0.27")).toBeInTheDocument(); // 0.268 rounded
|
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Empty State", () => { |
||||
|
it("should show empty state when no generation nodes exist", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost({ |
||||
|
totalCost: 0, |
||||
|
breakdown: [], |
||||
|
nodeCount: 0 |
||||
|
})} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("No generation nodes in workflow")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should not show breakdown section when nodeCount is 0", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost({ |
||||
|
totalCost: 0, |
||||
|
breakdown: [], |
||||
|
nodeCount: 0 |
||||
|
})} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.queryByText(/5x Nano Banana/)).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Reset Costs Button", () => { |
||||
|
it("should not show reset button when incurredCost is 0", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.queryByText("Reset to $0.00")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show reset button when incurredCost is greater than 0", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={1.00} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Reset to $0.00")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show confirmation dialog when reset is clicked", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={1.00} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Reset to $0.00")); |
||||
|
|
||||
|
expect(mockConfirm).toHaveBeenCalledWith("Reset incurred cost to $0.00?"); |
||||
|
}); |
||||
|
|
||||
|
it("should call resetIncurredCost when confirmed", () => { |
||||
|
mockConfirm.mockReturnValue(true); |
||||
|
|
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={1.00} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Reset to $0.00")); |
||||
|
|
||||
|
expect(mockResetIncurredCost).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should not call resetIncurredCost when cancelled", () => { |
||||
|
mockConfirm.mockReturnValue(false); |
||||
|
|
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={1.00} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Reset to $0.00")); |
||||
|
|
||||
|
expect(mockResetIncurredCost).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Close Behavior", () => { |
||||
|
it("should call onClose when close button is clicked", () => { |
||||
|
const onClose = vi.fn(); |
||||
|
|
||||
|
const { container } = render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={onClose} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Click the close button (first button in the dialog)
|
||||
|
const closeButton = container.querySelector("button"); |
||||
|
fireEvent.click(closeButton!); |
||||
|
|
||||
|
expect(onClose).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should call onClose when Escape key is pressed", () => { |
||||
|
const onClose = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={onClose} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.keyDown(window, { key: "Escape" }); |
||||
|
|
||||
|
expect(onClose).toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Pricing Reference", () => { |
||||
|
it("should display nano-banana pricing", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText(/Nano Banana \(Flash\):/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should display nano-banana-pro pricing tiers", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText(/Nano Banana Pro 1K\/2K:/)).toBeInTheDocument(); |
||||
|
expect(screen.getByText(/Nano Banana Pro 4K:/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should display currency note", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("All prices in USD")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Incurred Cost Description", () => { |
||||
|
it("should display description for incurred costs", () => { |
||||
|
render( |
||||
|
<CostDialog |
||||
|
predictedCost={createPredictedCost()} |
||||
|
incurredCost={0} |
||||
|
onClose={vi.fn()} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Actual API spend from successful generations")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,944 @@ |
|||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; |
||||
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; |
||||
|
import { ProjectSetupModal } from "@/components/ProjectSetupModal"; |
||||
|
import { ProviderSettings } from "@/types"; |
||||
|
|
||||
|
// Mock the workflow store
|
||||
|
const mockSetUseExternalImageStorage = vi.fn(); |
||||
|
const mockUpdateProviderApiKey = vi.fn(); |
||||
|
const mockToggleProvider = vi.fn(); |
||||
|
const mockUseWorkflowStore = vi.fn(); |
||||
|
|
||||
|
vi.mock("@/store/workflowStore", () => ({ |
||||
|
useWorkflowStore: (selector?: (state: unknown) => unknown) => { |
||||
|
if (selector) { |
||||
|
return mockUseWorkflowStore(selector); |
||||
|
} |
||||
|
return mockUseWorkflowStore((s: unknown) => s); |
||||
|
}, |
||||
|
generateWorkflowId: () => "mock-workflow-id", |
||||
|
})); |
||||
|
|
||||
|
// Mock fetch
|
||||
|
const mockFetch = vi.fn(); |
||||
|
global.fetch = mockFetch; |
||||
|
|
||||
|
// Mock confirm
|
||||
|
const mockConfirm = vi.fn(() => true); |
||||
|
global.confirm = mockConfirm; |
||||
|
|
||||
|
// Default provider settings
|
||||
|
const defaultProviderSettings: ProviderSettings = { |
||||
|
providers: { |
||||
|
gemini: { id: "gemini", name: "Gemini", enabled: true, apiKey: null, apiKeyEnvVar: "GEMINI_API_KEY" }, |
||||
|
openai: { id: "openai", name: "OpenAI", enabled: false, apiKey: null }, |
||||
|
replicate: { id: "replicate", name: "Replicate", enabled: false, apiKey: null }, |
||||
|
fal: { id: "fal", name: "fal.ai", enabled: false, apiKey: null }, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
// Default store state factory
|
||||
|
const createDefaultState = (overrides = {}) => ({ |
||||
|
workflowName: "", |
||||
|
workflowId: "", |
||||
|
saveDirectoryPath: "", |
||||
|
useExternalImageStorage: true, |
||||
|
providerSettings: defaultProviderSettings, |
||||
|
setUseExternalImageStorage: mockSetUseExternalImageStorage, |
||||
|
updateProviderApiKey: mockUpdateProviderApiKey, |
||||
|
toggleProvider: mockToggleProvider, |
||||
|
...overrides, |
||||
|
}); |
||||
|
|
||||
|
describe("ProjectSetupModal", () => { |
||||
|
beforeEach(() => { |
||||
|
vi.clearAllMocks(); |
||||
|
// Default mock for env-status API (called on modal open)
|
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
// Default success response for other APIs
|
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ success: true }), |
||||
|
}); |
||||
|
}); |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState()); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
vi.restoreAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
describe("Visibility", () => { |
||||
|
it("should not render when isOpen is false", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={false} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.queryByText("New Project")).not.toBeInTheDocument(); |
||||
|
expect(screen.queryByText("Project Settings")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render with 'New Project' title when mode is 'new'", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("New Project")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render with 'Project Settings' title when mode is 'settings'", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="settings" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Project Settings")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Tab Navigation", () => { |
||||
|
it("should render Project and Providers tabs", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Project")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("Providers")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should start on Project tab in new mode", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Project tab should show project name input
|
||||
|
expect(screen.getByPlaceholderText("my-project")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should switch to Providers tab when clicked", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Providers")); |
||||
|
|
||||
|
// Should show provider names
|
||||
|
expect(screen.getByText("Google Gemini")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("OpenAI")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("Replicate")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("fal.ai")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Project Tab - New Mode", () => { |
||||
|
it("should render empty form for new project", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
const nameInput = screen.getByPlaceholderText("my-project") as HTMLInputElement; |
||||
|
const directoryInput = screen.getByPlaceholderText("/Users/username/projects/my-project") as HTMLInputElement; |
||||
|
|
||||
|
expect(nameInput.value).toBe(""); |
||||
|
expect(directoryInput.value).toBe(""); |
||||
|
}); |
||||
|
|
||||
|
it("should render Create button in new mode", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Create")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render project name and directory inputs", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Project Name")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("Project Directory")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render Browse button for directory selection", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Browse")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render embed images checkbox", () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Embed images as base64")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Project Tab - Settings Mode", () => { |
||||
|
it("should pre-fill form with existing values in settings mode", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Existing Project", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
useExternalImageStorage: false, |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="settings" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
const nameInput = screen.getByPlaceholderText("my-project") as HTMLInputElement; |
||||
|
const directoryInput = screen.getByPlaceholderText("/Users/username/projects/my-project") as HTMLInputElement; |
||||
|
|
||||
|
expect(nameInput.value).toBe("My Existing Project"); |
||||
|
expect(directoryInput.value).toBe("/path/to/project"); |
||||
|
}); |
||||
|
|
||||
|
it("should render Save button in settings mode", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="settings" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Save")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Form Validation", () => { |
||||
|
it("should show error when project name is empty", async () => { |
||||
|
const onSave = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={onSave} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Fill directory but not name
|
||||
|
const directoryInput = screen.getByPlaceholderText("/Users/username/projects/my-project"); |
||||
|
fireEvent.change(directoryInput, { target: { value: "/path/to/project" } }); |
||||
|
|
||||
|
// Click Create
|
||||
|
fireEvent.click(screen.getByText("Create")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Project name is required")).toBeInTheDocument(); |
||||
|
}); |
||||
|
expect(onSave).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should show error when project directory is empty", async () => { |
||||
|
const onSave = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={onSave} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Fill name but not directory
|
||||
|
const nameInput = screen.getByPlaceholderText("my-project"); |
||||
|
fireEvent.change(nameInput, { target: { value: "My Project" } }); |
||||
|
|
||||
|
// Click Create
|
||||
|
fireEvent.click(screen.getByText("Create")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Project directory is required")).toBeInTheDocument(); |
||||
|
}); |
||||
|
expect(onSave).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should show error when directory does not exist", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url.startsWith("/api/workflow")) { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ exists: false }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
const onSave = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={onSave} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Fill both fields
|
||||
|
fireEvent.change(screen.getByPlaceholderText("my-project"), { |
||||
|
target: { value: "My Project" }, |
||||
|
}); |
||||
|
fireEvent.change(screen.getByPlaceholderText("/Users/username/projects/my-project"), { |
||||
|
target: { value: "/nonexistent/path" }, |
||||
|
}); |
||||
|
|
||||
|
// Click Create
|
||||
|
fireEvent.click(screen.getByText("Create")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Project directory does not exist")).toBeInTheDocument(); |
||||
|
}); |
||||
|
expect(onSave).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should show error when path is not a directory", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url.startsWith("/api/workflow")) { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ exists: true, isDirectory: false }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
const onSave = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={onSave} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Fill both fields
|
||||
|
fireEvent.change(screen.getByPlaceholderText("my-project"), { |
||||
|
target: { value: "My Project" }, |
||||
|
}); |
||||
|
fireEvent.change(screen.getByPlaceholderText("/Users/username/projects/my-project"), { |
||||
|
target: { value: "/path/to/file.txt" }, |
||||
|
}); |
||||
|
|
||||
|
// Click Create
|
||||
|
fireEvent.click(screen.getByText("Create")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Project path is not a directory")).toBeInTheDocument(); |
||||
|
}); |
||||
|
expect(onSave).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Save Behavior", () => { |
||||
|
it("should call onSave with project details when form is valid", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url.startsWith("/api/workflow")) { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ exists: true, isDirectory: true }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
const onSave = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={onSave} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Fill both fields
|
||||
|
fireEvent.change(screen.getByPlaceholderText("my-project"), { |
||||
|
target: { value: "My New Project" }, |
||||
|
}); |
||||
|
fireEvent.change(screen.getByPlaceholderText("/Users/username/projects/my-project"), { |
||||
|
target: { value: "/path/to/project" }, |
||||
|
}); |
||||
|
|
||||
|
// Click Create
|
||||
|
fireEvent.click(screen.getByText("Create")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(onSave).toHaveBeenCalledWith( |
||||
|
"mock-workflow-id", |
||||
|
"My New Project", |
||||
|
"/path/to/project" |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should show 'Validating...' while validating directory", async () => { |
||||
|
let resolveValidation: ((value: unknown) => void) | undefined; |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url.startsWith("/api/workflow")) { |
||||
|
return new Promise((resolve) => { |
||||
|
resolveValidation = resolve; |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Fill both fields
|
||||
|
fireEvent.change(screen.getByPlaceholderText("my-project"), { |
||||
|
target: { value: "My Project" }, |
||||
|
}); |
||||
|
fireEvent.change(screen.getByPlaceholderText("/Users/username/projects/my-project"), { |
||||
|
target: { value: "/path/to/project" }, |
||||
|
}); |
||||
|
|
||||
|
// Click Create
|
||||
|
fireEvent.click(screen.getByText("Create")); |
||||
|
|
||||
|
expect(screen.getByText("Validating...")).toBeInTheDocument(); |
||||
|
|
||||
|
// Resolve the validation
|
||||
|
resolveValidation!({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ exists: true, isDirectory: true }), |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should update external storage setting when saved", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url.startsWith("/api/workflow")) { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ exists: true, isDirectory: true }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Fill fields
|
||||
|
fireEvent.change(screen.getByPlaceholderText("my-project"), { |
||||
|
target: { value: "My Project" }, |
||||
|
}); |
||||
|
fireEvent.change(screen.getByPlaceholderText("/Users/username/projects/my-project"), { |
||||
|
target: { value: "/path/to/project" }, |
||||
|
}); |
||||
|
|
||||
|
// Toggle the embed checkbox (click it to enable embed/disable external)
|
||||
|
const embedCheckbox = screen.getByRole("checkbox"); |
||||
|
fireEvent.click(embedCheckbox); |
||||
|
|
||||
|
// Click Create
|
||||
|
fireEvent.click(screen.getByText("Create")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mockSetUseExternalImageStorage).toHaveBeenCalledWith(false); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Browse Button", () => { |
||||
|
it("should call browse-directory API when Browse is clicked", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url === "/api/browse-directory") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ success: true, path: "/selected/path" }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Browse")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mockFetch).toHaveBeenCalledWith("/api/browse-directory"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should update directory input when path is selected", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url === "/api/browse-directory") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ success: true, path: "/selected/path" }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Browse")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
const directoryInput = screen.getByPlaceholderText("/Users/username/projects/my-project") as HTMLInputElement; |
||||
|
expect(directoryInput.value).toBe("/selected/path"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should show '...' while browsing", async () => { |
||||
|
let resolvePromise: ((value: unknown) => void) | undefined; |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url === "/api/browse-directory") { |
||||
|
return new Promise((resolve) => { |
||||
|
resolvePromise = resolve; |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Browse")); |
||||
|
|
||||
|
expect(screen.getByText("...")).toBeInTheDocument(); |
||||
|
|
||||
|
resolvePromise!({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ success: true, cancelled: true }), |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should handle cancelled browse dialog", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url === "/api/browse-directory") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ success: true, cancelled: true }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Pre-fill directory
|
||||
|
const directoryInput = screen.getByPlaceholderText("/Users/username/projects/my-project") as HTMLInputElement; |
||||
|
fireEvent.change(directoryInput, { target: { value: "/original/path" } }); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Browse")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
// Should keep original value when cancelled
|
||||
|
expect(directoryInput.value).toBe("/original/path"); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Cancel Button", () => { |
||||
|
it("should call onClose when Cancel is clicked", () => { |
||||
|
const onClose = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={onClose} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Cancel")); |
||||
|
|
||||
|
expect(onClose).toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Keyboard Shortcuts", () => { |
||||
|
it("should close modal when Escape is pressed", () => { |
||||
|
const onClose = vi.fn(); |
||||
|
|
||||
|
const { container } = render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={onClose} |
||||
|
onSave={vi.fn()} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
const modalDiv = container.querySelector(".bg-neutral-800"); |
||||
|
fireEvent.keyDown(modalDiv!, { key: "Escape" }); |
||||
|
|
||||
|
expect(onClose).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should submit form when Enter is pressed", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: false, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
if (url.startsWith("/api/workflow")) { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ exists: true, isDirectory: true }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
const onSave = vi.fn(); |
||||
|
|
||||
|
const { container } = render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={onSave} |
||||
|
mode="new" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
// Fill fields
|
||||
|
fireEvent.change(screen.getByPlaceholderText("my-project"), { |
||||
|
target: { value: "My Project" }, |
||||
|
}); |
||||
|
fireEvent.change(screen.getByPlaceholderText("/Users/username/projects/my-project"), { |
||||
|
target: { value: "/path/to/project" }, |
||||
|
}); |
||||
|
|
||||
|
const modalDiv = container.querySelector(".bg-neutral-800"); |
||||
|
fireEvent.keyDown(modalDiv!, { key: "Enter" }); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(onSave).toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Providers Tab", () => { |
||||
|
// The default beforeEach already sets up proper mocks for env-status
|
||||
|
|
||||
|
it("should render all provider sections", async () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="settings" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Providers")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Google Gemini")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("OpenAI")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("Replicate")).toBeInTheDocument(); |
||||
|
expect(screen.getByText("fal.ai")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should show API key inputs for each provider", async () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="settings" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Providers")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
// Check for placeholder texts
|
||||
|
expect(screen.getByPlaceholderText("AIza...")).toBeInTheDocument(); |
||||
|
expect(screen.getByPlaceholderText("sk-...")).toBeInTheDocument(); |
||||
|
expect(screen.getByPlaceholderText("r8_...")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should show 'Configured via .env' when provider has env key", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: true, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="settings" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Providers")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Configured via .env")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should show Override button for env-configured providers", async () => { |
||||
|
mockFetch.mockImplementation((url: string) => { |
||||
|
if (url === "/api/env-status") { |
||||
|
return Promise.resolve({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ gemini: true, openai: false, replicate: false, fal: false }), |
||||
|
}); |
||||
|
} |
||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) }); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="settings" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Providers")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Override")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should toggle Show/Hide for API key visibility", async () => { |
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={vi.fn()} |
||||
|
onSave={vi.fn()} |
||||
|
mode="settings" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Providers")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
const showButtons = screen.getAllByText("Show"); |
||||
|
expect(showButtons.length).toBeGreaterThan(0); |
||||
|
}); |
||||
|
|
||||
|
// Click Show for first provider
|
||||
|
fireEvent.click(screen.getAllByText("Show")[0]); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Hide")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should call onClose when Save is clicked on Providers tab", async () => { |
||||
|
const onClose = vi.fn(); |
||||
|
|
||||
|
render( |
||||
|
<ProjectSetupModal |
||||
|
isOpen={true} |
||||
|
onClose={onClose} |
||||
|
onSave={vi.fn()} |
||||
|
mode="settings" |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Providers")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(screen.getByText("Google Gemini")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
fireEvent.click(screen.getByText("Save")); |
||||
|
|
||||
|
expect(onClose).toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue