import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { Header } from "@/components/Header"; import { useLoginModalStore } from "@/store/loginModalStore"; import { useUserStore } from "@/store/userStore"; // Mock the workflow store const mockSetWorkflowMetadata = vi.fn(); const mockSetWorkflowName = vi.fn(); const mockSaveToFile = vi.fn(); const mockLoadWorkflow = vi.fn(); const mockUseWorkflowStore = vi.fn(); const mockSetCurrentCanvasWorkflow = vi.fn(); vi.mock("@/store/workflowStore", () => ({ useWorkflowStore: (selector?: (state: unknown) => unknown) => { if (selector) { return mockUseWorkflowStore(selector); } return mockUseWorkflowStore((s: unknown) => s); }, })); vi.mock("@/store/canvasWorkflowStore", () => ({ useCanvasWorkflowStore: (selector?: (state: unknown) => unknown) => { const state = { currentCanvasWorkflowId: null, currentCanvasWorkflowDescription: "", setCurrentCanvasWorkflow: mockSetCurrentCanvasWorkflow, }; return selector ? selector(state) : state; }, })); // Mock ProjectSetupModal vi.mock("@/components/ProjectSetupModal", () => ({ ProjectSetupModal: ({ isOpen, mode }: { isOpen: boolean; mode: string }) => ( isOpen ?
Project Setup Modal
: null ), })); // Mock WorkflowBrowserModal vi.mock("@/components/WorkflowBrowserModal", () => ({ WorkflowBrowserModal: ({ isOpen }: { isOpen: boolean }) => ( isOpen ?
Workflow Browser
: null ), })); // Mock CostIndicator vi.mock("@/components/CostIndicator", () => ({ CostIndicator: () =>
$0.00
, })); // Mock functions for comment navigation const mockGetNodesWithComments = vi.fn(); const mockGetUnviewedCommentCount = vi.fn(); const mockMarkCommentViewed = vi.fn(); const mockSetNavigationTarget = vi.fn(); // Default store state factory const createDefaultState = (overrides = {}) => ({ workflowName: "", workflowId: "", saveDirectoryPath: "", hasUnsavedChanges: false, lastSavedAt: null, isSaving: false, setWorkflowMetadata: mockSetWorkflowMetadata, setWorkflowName: mockSetWorkflowName, saveToFile: mockSaveToFile, loadWorkflow: mockLoadWorkflow, getNodesWithComments: mockGetNodesWithComments, getUnviewedCommentCount: mockGetUnviewedCommentCount, viewedCommentNodeIds: new Set(), markCommentViewed: mockMarkCommentViewed, setNavigationTarget: mockSetNavigationTarget, ...overrides, }); const createTestUserInfo = () => ({ id: 10561, code: "u10561", name: "Test User", avatar: "", gender: 0, birthday: "", phone: "", email: "", wechat: "", signature: "", country: "", province: "", city: "", isMember: false, memberLevel: 0, memberCoins: 0, otherCoins: 0, pointPackageCoins: 0, allCoins: 12, isUsedCoinsRecently: false, power: 0, powerConsumed: 0, powerRecharged: 0, followingNum: 0, fansNum: 0, likeNum: 0, postNum: 0, taskNum: 0, characterDesignGuide: false, status: 1, createTime: "", updateTime: "", }); describe("Header", () => { beforeEach(() => { vi.clearAllMocks(); // Default mock implementation - unconfigured project mockGetNodesWithComments.mockReturnValue([]); mockGetUnviewedCommentCount.mockReturnValue(0); useLoginModalStore.setState({ isOpen: false, reason: "manual" }); useUserStore.setState({ token: null, userInfo: null, gatewayApiKey: null, isLoadingUserInfo: false, isLoadingGatewayApiKey: false, userInfoError: null, gatewayApiKeyError: null, }); mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState()); }); }); describe("Basic Rendering", () => { it("should render the app title", () => { render(
); expect(screen.getByText("Popi.TV")).toBeInTheDocument(); }); it("should render the brand icon", () => { const { container } = render(
); const icon = container.querySelector('img[src="/banana_icon.png"]'); expect(icon).toBeInTheDocument(); expect(icon).toHaveAttribute("src", "/banana_icon.png"); expect(icon).toHaveAttribute("alt", ""); expect(icon).toHaveAttribute("aria-hidden", "true"); }); it("should hide external author link", () => { render(
); expect(screen.queryByText("Made by Willie")).not.toBeInTheDocument(); }); it("should hide Discord support link", () => { render(
); expect(screen.queryByTitle("Support")).not.toBeInTheDocument(); }); it("should open the shared login modal when login is clicked", () => { render(
); fireEvent.click(screen.getByRole("button", { name: "Login" })); expect(useLoginModalStore.getState().isOpen).toBe(true); expect(useLoginModalStore.getState().reason).toBe("manual"); }); it("should show user info when logged in", () => { useUserStore.setState({ token: "login-token", userInfo: createTestUserInfo(), }); render(
); expect(screen.getByText("Test User")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Login" })).not.toBeInTheDocument(); }); it("should close the user menu when clicking outside", () => { useUserStore.setState({ token: "login-token", userInfo: createTestUserInfo(), }); render(
); fireEvent.click(screen.getByTitle("Test User")); expect(screen.getByText("ID 10561")).toBeInTheDocument(); fireEvent.mouseDown(document.body); expect(screen.queryByText("ID 10561")).not.toBeInTheDocument(); }); }); describe("Unconfigured Project State", () => { it("should show 'Untitled' when no project name is set", () => { render(
); expect(screen.getByText("Untitled")).toBeInTheDocument(); }); it("should not show save status when project is not configured", () => { render(
); expect(screen.queryByText("Not saved")).not.toBeInTheDocument(); }); it("should show save button without unsaved indicator", () => { const { container } = render(
); const redDot = container.querySelector(".bg-red-500.rounded-full"); expect(redDot).not.toBeInTheDocument(); }); it("should not render CostIndicator when project is not configured", () => { render(
); 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(
); expect(screen.getByRole("textbox", { name: "Workflow name" })).toHaveValue("My Project"); }); it("should update project name on blur", () => { render(
); const nameInput = screen.getByRole("textbox", { name: "Workflow name" }); fireEvent.change(nameInput, { target: { value: "Renamed Project" } }); fireEvent.blur(nameInput); expect(mockSetWorkflowName).toHaveBeenCalledWith("Renamed Project"); }); it("should render CostIndicator when project is configured", () => { render(
); expect(screen.getByTestId("cost-indicator")).toBeInTheDocument(); }); it("should not show save status when no lastSavedAt timestamp", () => { render(
); expect(screen.queryByText("Not saved")).not.toBeInTheDocument(); }); it("should render Open Project Folder button when saveDirectoryPath is set", () => { render(
); const folderButton = screen.getByTitle("Open Project Folder"); expect(folderButton).toBeInTheDocument(); }); }); describe("Save State Display", () => { it("should not show visible saving status when isSaving is true", () => { mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState({ workflowName: "My Project", workflowId: "project-123", saveDirectoryPath: "/path/to/project", isSaving: true, })); }); render(
); expect(screen.queryByText("Saving...")).not.toBeInTheDocument(); }); it("should not 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(
); expect(screen.queryByText(/Saved/)).not.toBeInTheDocument(); }); }); describe("Unsaved Changes Indicator", () => { it("should not 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(
); const redDot = container.querySelector(".bg-red-500.rounded-full"); expect(redDot).not.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, })); }); render(
); // 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, })); }); render(
); 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(
); 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(
); const openButton = screen.getByTitle("Open project"); expect(openButton).toBeInTheDocument(); }); it("should open WorkflowBrowserModal when open button is clicked", () => { render(
); const openButton = screen.getByTitle("Open project"); fireEvent.click(openButton); expect(screen.getByTestId("workflow-browser-modal")).toBeInTheDocument(); }); }); 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(
); 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(
); 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(
); 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(
); 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(
); 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(
); 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(
); 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(
); 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("Comments Navigation Icon", () => { it("should not render comments icon when no comments exist", () => { mockGetNodesWithComments.mockReturnValue([]); mockGetUnviewedCommentCount.mockReturnValue(0); render(
); // No button with comment-related title should exist expect(screen.queryByTitle(/unviewed comment/)).not.toBeInTheDocument(); }); it("should render comments icon with badge when comments exist", () => { const mockNodes = [ { id: "node-1", position: { x: 0, y: 0 }, type: "prompt", data: { comment: "Test" } }, { id: "node-2", position: { x: 100, y: 0 }, type: "prompt", data: { comment: "Test 2" } }, ]; mockGetNodesWithComments.mockReturnValue(mockNodes); mockGetUnviewedCommentCount.mockReturnValue(2); mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState()); }); render(
); const commentsButton = screen.getByTitle(/2 unviewed comments/); expect(commentsButton).toBeInTheDocument(); }); it("should show 9+ when unviewed count exceeds 9", () => { const mockNodes = Array.from({ length: 10 }, (_, i) => ({ id: `node-${i}`, position: { x: i * 100, y: 0 }, type: "prompt", data: { comment: `Comment ${i}` }, })); mockGetNodesWithComments.mockReturnValue(mockNodes); mockGetUnviewedCommentCount.mockReturnValue(10); mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState()); }); render(
); // Badge should show 9+ expect(screen.getByText("9+")).toBeInTheDocument(); }); it("should call setNavigationTarget when clicked", () => { const mockNodes = [ { id: "node-1", position: { x: 0, y: 0 }, type: "prompt", data: { comment: "Test" } }, ]; mockGetNodesWithComments.mockReturnValue(mockNodes); mockGetUnviewedCommentCount.mockReturnValue(1); mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState()); }); render(
); const commentsButton = screen.getByTitle(/1 unviewed comment/); fireEvent.click(commentsButton); expect(mockSetNavigationTarget).toHaveBeenCalledWith("node-1"); }); it("should call markCommentViewed when clicked", () => { const mockNodes = [ { id: "node-1", position: { x: 0, y: 0 }, type: "prompt", data: { comment: "Test" } }, ]; mockGetNodesWithComments.mockReturnValue(mockNodes); mockGetUnviewedCommentCount.mockReturnValue(1); mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState()); }); render(
); const commentsButton = screen.getByTitle(/1 unviewed comment/); fireEvent.click(commentsButton); expect(mockMarkCommentViewed).toHaveBeenCalledWith("node-1"); }); it("should navigate to first unviewed comment when clicked", () => { const mockNodes = [ { id: "node-1", position: { x: 0, y: 0 }, type: "prompt", data: { comment: "Test" } }, { id: "node-2", position: { x: 100, y: 0 }, type: "prompt", data: { comment: "Test 2" } }, ]; mockGetNodesWithComments.mockReturnValue(mockNodes); mockGetUnviewedCommentCount.mockReturnValue(1); // node-1 is already viewed mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState({ viewedCommentNodeIds: new Set(["node-1"]), })); }); render(
); const commentsButton = screen.getByTitle(/1 unviewed comment/); fireEvent.click(commentsButton); // Should navigate to node-2 (first unviewed) expect(mockSetNavigationTarget).toHaveBeenCalledWith("node-2"); }); it("should navigate to first comment when all viewed", () => { const mockNodes = [ { id: "node-1", position: { x: 0, y: 0 }, type: "prompt", data: { comment: "Test" } }, { id: "node-2", position: { x: 100, y: 0 }, type: "prompt", data: { comment: "Test 2" } }, ]; mockGetNodesWithComments.mockReturnValue(mockNodes); mockGetUnviewedCommentCount.mockReturnValue(0); // All comments are viewed mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState({ viewedCommentNodeIds: new Set(["node-1", "node-2"]), })); }); render(
); const commentsButton = screen.getByTitle(/0 unviewed comments/); fireEvent.click(commentsButton); // Should navigate to node-1 (first comment) expect(mockSetNavigationTarget).toHaveBeenCalledWith("node-1"); }); }); });