Browse Source
- Basic rendering tests (title, icon, links) - Unconfigured project state display - Configured project state with CostIndicator - Save state display (saving, saved timestamp) - Unsaved changes indicator (red dot) - New Project button modal trigger - Open file button and file input - Save button with disabled state - Settings button modal trigger - Open Project Folder button visibility and API call Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>handoff-20260429-1057
1 changed files with 413 additions and 0 deletions
@ -0,0 +1,413 @@ |
|||||
|
import { describe, it, expect, vi, beforeEach } from "vitest"; |
||||
|
import { render, screen, fireEvent } from "@testing-library/react"; |
||||
|
import { Header } from "@/components/Header"; |
||||
|
|
||||
|
// Mock the workflow store
|
||||
|
const mockSetWorkflowMetadata = vi.fn(); |
||||
|
const mockSaveToFile = vi.fn(); |
||||
|
const mockLoadWorkflow = 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 ProjectSetupModal
|
||||
|
vi.mock("@/components/ProjectSetupModal", () => ({ |
||||
|
ProjectSetupModal: ({ isOpen, mode }: { isOpen: boolean; mode: string }) => ( |
||||
|
isOpen ? <div data-testid="project-setup-modal" data-mode={mode}>Project Setup Modal</div> : null |
||||
|
), |
||||
|
})); |
||||
|
|
||||
|
// Mock CostIndicator
|
||||
|
vi.mock("@/components/CostIndicator", () => ({ |
||||
|
CostIndicator: () => <div data-testid="cost-indicator">$0.00</div>, |
||||
|
})); |
||||
|
|
||||
|
// Default store state factory
|
||||
|
const createDefaultState = (overrides = {}) => ({ |
||||
|
workflowName: "", |
||||
|
workflowId: "", |
||||
|
saveDirectoryPath: "", |
||||
|
hasUnsavedChanges: false, |
||||
|
lastSavedAt: null, |
||||
|
isSaving: false, |
||||
|
setWorkflowMetadata: mockSetWorkflowMetadata, |
||||
|
saveToFile: mockSaveToFile, |
||||
|
loadWorkflow: mockLoadWorkflow, |
||||
|
...overrides, |
||||
|
}); |
||||
|
|
||||
|
describe("Header", () => { |
||||
|
beforeEach(() => { |
||||
|
vi.clearAllMocks(); |
||||
|
// Default mock implementation - unconfigured project
|
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState()); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Basic Rendering", () => { |
||||
|
it("should render the app title", () => { |
||||
|
render(<Header />); |
||||
|
expect(screen.getByText("Node Banana")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render the banana icon", () => { |
||||
|
render(<Header />); |
||||
|
const icon = screen.getByAltText("Banana"); |
||||
|
expect(icon).toBeInTheDocument(); |
||||
|
expect(icon).toHaveAttribute("src", "/banana_icon.png"); |
||||
|
}); |
||||
|
|
||||
|
it("should render 'Made by Willie' link", () => { |
||||
|
render(<Header />); |
||||
|
const link = screen.getByText("Made by Willie"); |
||||
|
expect(link).toHaveAttribute("href", "https://x.com/ReflctWillie"); |
||||
|
}); |
||||
|
|
||||
|
it("should render Discord support link", () => { |
||||
|
render(<Header />); |
||||
|
const link = screen.getByTitle("Support"); |
||||
|
expect(link).toHaveAttribute("href", "https://discord.com/invite/89Nr6EKkTf"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Unconfigured Project State", () => { |
||||
|
it("should show 'Untitled' when no project name is set", () => { |
||||
|
render(<Header />); |
||||
|
expect(screen.getByText("Untitled")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show 'Not saved' status when project is not configured", () => { |
||||
|
render(<Header />); |
||||
|
expect(screen.getByText("Not saved")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show save button with unsaved indicator (red dot)", () => { |
||||
|
const { container } = render(<Header />); |
||||
|
const redDot = container.querySelector(".bg-red-500.rounded-full"); |
||||
|
expect(redDot).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should not render CostIndicator when project is not configured", () => { |
||||
|
render(<Header />); |
||||
|
expect(screen.queryByTestId("cost-indicator")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Configured Project State", () => { |
||||
|
beforeEach(() => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
})); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should show project name when configured", () => { |
||||
|
render(<Header />); |
||||
|
expect(screen.getByText("My Project")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render CostIndicator when project is configured", () => { |
||||
|
render(<Header />); |
||||
|
expect(screen.getByTestId("cost-indicator")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show 'Not saved' when no lastSavedAt timestamp", () => { |
||||
|
render(<Header />); |
||||
|
expect(screen.getByText("Not saved")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render Open Project Folder button when saveDirectoryPath is set", () => { |
||||
|
render(<Header />); |
||||
|
const folderButton = screen.getByTitle("Open Project Folder"); |
||||
|
expect(folderButton).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Save State Display", () => { |
||||
|
it("should show 'Saving...' when isSaving is true", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
isSaving: true, |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
render(<Header />); |
||||
|
expect(screen.getByText("Saving...")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show formatted save time when lastSavedAt is set", () => { |
||||
|
const timestamp = new Date("2024-01-15T14:30:00").getTime(); |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
lastSavedAt: timestamp, |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
render(<Header />); |
||||
|
// The exact format depends on locale, but should contain "Saved"
|
||||
|
expect(screen.getByText(/Saved/)).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Unsaved Changes Indicator", () => { |
||||
|
it("should show red dot when hasUnsavedChanges is true", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
hasUnsavedChanges: true, |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
const { container } = render(<Header />); |
||||
|
const redDot = container.querySelector(".bg-red-500.rounded-full"); |
||||
|
expect(redDot).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should not show red dot when hasUnsavedChanges is false and project is configured", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
hasUnsavedChanges: false, |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
const { container } = render(<Header />); |
||||
|
// Find the save button area and check there's no red dot inside it
|
||||
|
const saveButton = screen.getByTitle("Save project"); |
||||
|
const redDotInSaveButton = saveButton.querySelector(".bg-red-500.rounded-full"); |
||||
|
expect(redDotInSaveButton).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should not show red dot when isSaving is true even if hasUnsavedChanges", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
hasUnsavedChanges: true, |
||||
|
isSaving: true, |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
const { container } = render(<Header />); |
||||
|
const saveButton = screen.getByTitle("Saving..."); |
||||
|
const redDotInSaveButton = saveButton.querySelector(".bg-red-500.rounded-full"); |
||||
|
expect(redDotInSaveButton).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("New Project Button", () => { |
||||
|
it("should open ProjectSetupModal in 'new' mode when save button clicked (unconfigured)", () => { |
||||
|
render(<Header />); |
||||
|
|
||||
|
const saveButton = screen.getByTitle("Save project"); |
||||
|
fireEvent.click(saveButton); |
||||
|
|
||||
|
const modal = screen.getByTestId("project-setup-modal"); |
||||
|
expect(modal).toBeInTheDocument(); |
||||
|
expect(modal).toHaveAttribute("data-mode", "new"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Open File Button", () => { |
||||
|
it("should render open file button when project is not configured", () => { |
||||
|
render(<Header />); |
||||
|
const openButton = screen.getByTitle("Open project"); |
||||
|
expect(openButton).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should have hidden file input for loading workflows", () => { |
||||
|
const { container } = render(<Header />); |
||||
|
const fileInput = container.querySelector('input[type="file"]'); |
||||
|
expect(fileInput).toBeInTheDocument(); |
||||
|
expect(fileInput).toHaveAttribute("accept", ".json"); |
||||
|
expect(fileInput).toHaveClass("hidden"); |
||||
|
}); |
||||
|
|
||||
|
it("should trigger file input click when open button is clicked", () => { |
||||
|
const { container } = render(<Header />); |
||||
|
const openButton = screen.getByTitle("Open project"); |
||||
|
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; |
||||
|
|
||||
|
// Mock click on file input
|
||||
|
const clickSpy = vi.spyOn(fileInput, "click"); |
||||
|
fireEvent.click(openButton); |
||||
|
|
||||
|
expect(clickSpy).toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Save Button", () => { |
||||
|
beforeEach(() => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
})); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("should call saveToFile when clicked on configured project", () => { |
||||
|
render(<Header />); |
||||
|
const saveButton = screen.getByTitle("Save project"); |
||||
|
fireEvent.click(saveButton); |
||||
|
|
||||
|
expect(mockSaveToFile).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should be disabled when isSaving is true", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
isSaving: true, |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
render(<Header />); |
||||
|
const saveButton = screen.getByTitle("Saving..."); |
||||
|
expect(saveButton).toBeDisabled(); |
||||
|
}); |
||||
|
|
||||
|
it("should open settings modal when project name is set but saveDirectoryPath is empty", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "", |
||||
|
saveDirectoryPath: "", |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
render(<Header />); |
||||
|
const saveButton = screen.getByTitle("Configure save location"); |
||||
|
fireEvent.click(saveButton); |
||||
|
|
||||
|
const modal = screen.getByTestId("project-setup-modal"); |
||||
|
expect(modal).toBeInTheDocument(); |
||||
|
expect(modal).toHaveAttribute("data-mode", "settings"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Settings Button", () => { |
||||
|
it("should open ProjectSetupModal in 'settings' mode when settings button clicked", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
render(<Header />); |
||||
|
const settingsButton = screen.getByTitle("Project settings"); |
||||
|
fireEvent.click(settingsButton); |
||||
|
|
||||
|
const modal = screen.getByTestId("project-setup-modal"); |
||||
|
expect(modal).toBeInTheDocument(); |
||||
|
expect(modal).toHaveAttribute("data-mode", "settings"); |
||||
|
}); |
||||
|
|
||||
|
it("should be visible for unconfigured projects", () => { |
||||
|
render(<Header />); |
||||
|
const settingsButton = screen.getByTitle("Project settings"); |
||||
|
expect(settingsButton).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Open Project Folder Button", () => { |
||||
|
it("should not be visible when saveDirectoryPath is not set", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "", |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
render(<Header />); |
||||
|
expect(screen.queryByTitle("Open Project Folder")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should be visible when saveDirectoryPath is set", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
render(<Header />); |
||||
|
expect(screen.getByTitle("Open Project Folder")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should call fetch to open-directory API when clicked", async () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
return selector(createDefaultState({ |
||||
|
workflowName: "My Project", |
||||
|
workflowId: "project-123", |
||||
|
saveDirectoryPath: "/path/to/project", |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
const mockFetch = vi.fn().mockResolvedValue({ |
||||
|
ok: true, |
||||
|
json: () => Promise.resolve({ success: true }), |
||||
|
}); |
||||
|
global.fetch = mockFetch; |
||||
|
|
||||
|
render(<Header />); |
||||
|
const folderButton = screen.getByTitle("Open Project Folder"); |
||||
|
fireEvent.click(folderButton); |
||||
|
|
||||
|
expect(mockFetch).toHaveBeenCalledWith("/api/open-directory", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ path: "/path/to/project" }), |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("File Loading", () => { |
||||
|
it("should not call loadWorkflow when no file is selected", () => { |
||||
|
const { container } = render(<Header />); |
||||
|
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; |
||||
|
|
||||
|
// Trigger file change with empty files
|
||||
|
fireEvent.change(fileInput, { target: { files: [] } }); |
||||
|
|
||||
|
expect(mockLoadWorkflow).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should reset file input value after file selection to allow re-selecting same file", () => { |
||||
|
const { container } = render(<Header />); |
||||
|
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; |
||||
|
|
||||
|
// File input should accept .json files
|
||||
|
expect(fileInput).toHaveAttribute("accept", ".json"); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue