Browse Source

test(17-08): add ProjectSetupModal and CostDialog tests

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
shrimbly 6 months ago
parent
commit
c5c2171b9a
  1. 401
      src/components/__tests__/CostDialog.test.tsx
  2. 944
      src/components/__tests__/ProjectSetupModal.test.tsx

401
src/components/__tests__/CostDialog.test.tsx

@ -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();
});
});
});

944
src/components/__tests__/ProjectSetupModal.test.tsx

@ -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…
Cancel
Save