import { beforeEach, describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { MobileNavBar } from "../MobileNavBar"; vi.mock("../../api", () => ({ fetchScripts: vi.fn(), })); import { fetchScripts } from "../../api"; function mockViewport(mode: "mobile" | "desktop") { Object.defineProperty(window, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query: string) => { const isMobileQuery = query === "(max-width: 768px)"; const isTabletQuery = query === "(min-width: 769px) and (max-width: 1024px)"; return { matches: mode === "mobile" ? isMobileQuery : false, media: query, addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), }; }), }); } const createDefaultProps = () => ({ view: "board" as const, onChangeView: vi.fn(), footerVisible: false, modalOpen: false, onOpenSettings: vi.fn(), onOpenActivityLog: vi.fn(), onOpenMailbox: vi.fn(), mailboxUnreadCount: 0, onOpenGitManager: vi.fn(), onOpenWorkflowSteps: vi.fn(), onOpenSchedules: vi.fn(), onOpenScripts: vi.fn(), onToggleTerminal: vi.fn(), onOpenFiles: vi.fn(), onOpenGitHubImport: vi.fn(), onOpenPlanning: vi.fn(), onResumePlanning: vi.fn(), activePlanningSessionCount: 0, onOpenUsage: vi.fn(), onViewAllProjects: vi.fn(), onRunScript: vi.fn(), projectId: "proj_1", }); describe("MobileNavBar", () => { beforeEach(() => { mockViewport("mobile"); }); it("renders five tab buttons (board + list + agents + missions + more)", () => { render(); expect(screen.getByTestId("mobile-nav-tab-board")).toBeDefined(); expect(screen.getByTestId("mobile-nav-tab-list")).toBeDefined(); expect(screen.getByTestId("mobile-nav-tab-agents")).toBeDefined(); expect(screen.getByTestId("mobile-nav-tab-missions")).toBeDefined(); expect(screen.getByTestId("mobile-nav-tab-more")).toBeDefined(); }); it("active tab is highlighted for agents", () => { render(); expect(screen.getByTestId("mobile-nav-tab-agents").className).toContain("mobile-nav-tab--active"); }); it("board tab calls onChangeView with 'board'", () => { const props = createDefaultProps(); render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-board")); expect(props.onChangeView).toHaveBeenCalledWith("board"); }); it("list tab calls onChangeView with 'list'", () => { const props = createDefaultProps(); render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-list")); expect(props.onChangeView).toHaveBeenCalledWith("list"); }); it("board tab is active when view is 'board'", () => { render(); expect(screen.getByTestId("mobile-nav-tab-board").className).toContain("mobile-nav-tab--active"); expect(screen.getByTestId("mobile-nav-tab-list").className).not.toContain("mobile-nav-tab--active"); }); it("list tab is active when view is 'list'", () => { render(); expect(screen.getByTestId("mobile-nav-tab-list").className).toContain("mobile-nav-tab--active"); expect(screen.getByTestId("mobile-nav-tab-board").className).not.toContain("mobile-nav-tab--active"); }); it("board and list tabs are not active when view is 'agents'", () => { render(); expect(screen.getByTestId("mobile-nav-tab-board").className).not.toContain("mobile-nav-tab--active"); expect(screen.getByTestId("mobile-nav-tab-list").className).not.toContain("mobile-nav-tab--active"); }); it("missions tab calls onChangeView with 'missions'", () => { const props = createDefaultProps(); render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-missions")); expect(props.onChangeView).toHaveBeenCalledWith("missions"); }); it("missions tab is active when view is 'missions'", () => { render(); expect(screen.getByTestId("mobile-nav-tab-missions").className).toContain("mobile-nav-tab--active"); }); it("missions tab is not active when view is 'board'", () => { render(); expect(screen.getByTestId("mobile-nav-tab-missions").className).not.toContain("mobile-nav-tab--active"); }); it("opens and toggles the more sheet", () => { const props = createDefaultProps(); const { container } = render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); expect(container.querySelector(".mobile-more-sheet")).not.toBeNull(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); expect(container.querySelector(".mobile-more-sheet")).toBeNull(); }); it("sheet contains expected navigation items including activity log", () => { render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); expect(screen.getByTestId("mobile-more-item-mailbox")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-activity")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-git")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-terminal")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-files")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-planning")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-workflow")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-schedules")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-github")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-usage")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-projects")).toBeDefined(); expect(screen.getByTestId("mobile-more-item-settings")).toBeDefined(); }); it("activity log item in more sheet calls onOpenActivityLog", () => { const props = createDefaultProps(); const { container } = render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); fireEvent.click(screen.getByTestId("mobile-more-item-activity")); expect(container.querySelector(".mobile-more-sheet")).toBeNull(); expect(props.onOpenActivityLog).toHaveBeenCalledOnce(); }); it("closes sheet and calls handler when item is clicked", () => { const props = createDefaultProps(); const { container } = render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); fireEvent.click(screen.getByTestId("mobile-more-item-settings")); expect(container.querySelector(".mobile-more-sheet")).toBeNull(); expect(props.onOpenSettings).toHaveBeenCalledOnce(); }); it("calls onViewAllProjects from the Projects more-sheet item", () => { const props = createDefaultProps(); const { container } = render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); fireEvent.click(screen.getByTestId("mobile-more-item-projects")); expect(container.querySelector(".mobile-more-sheet")).toBeNull(); expect(props.onViewAllProjects).toHaveBeenCalledOnce(); }); it("closes sheet on backdrop click", () => { const { container } = render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); const backdrop = container.querySelector(".mobile-more-sheet-backdrop"); expect(backdrop).not.toBeNull(); fireEvent.click(backdrop!); expect(container.querySelector(".mobile-more-sheet")).toBeNull(); }); it("closes sheet on Escape", async () => { const { container } = render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); fireEvent.keyDown(document, { key: "Escape" }); await waitFor(() => { expect(container.querySelector(".mobile-more-sheet")).toBeNull(); }); }); it("returns null when modalOpen is true", () => { const { container } = render(); expect(container.querySelector(".mobile-nav-bar")).toBeNull(); }); it("tab click calls onChangeView", () => { const props = createDefaultProps(); render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-agents")); expect(props.onChangeView).toHaveBeenCalledWith("agents"); }); it("applies footer-visible class when footer is shown", () => { const { container } = render(); expect(container.querySelector(".mobile-nav-bar--with-footer")).not.toBeNull(); }); it("returns null on desktop viewport", () => { mockViewport("desktop"); const { container } = render(); expect(container.querySelector(".mobile-nav-bar")).toBeNull(); }); describe("scripts submenu", () => { beforeEach(() => { vi.mocked(fetchScripts).mockReset(); }); it("terminal item has a split toggle that opens scripts submenu", async () => { vi.mocked(fetchScripts).mockResolvedValue({}); render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); const toggle = screen.getByTestId("mobile-more-terminal-split-toggle"); expect(toggle).toBeDefined(); fireEvent.click(toggle); await waitFor(() => { expect(screen.getByTestId("mobile-more-scripts-manage")).toBeDefined(); }); }); it("scripts are fetched when submenu opens", async () => { vi.mocked(fetchScripts).mockResolvedValue({ build: "pnpm build", test: "pnpm test", }); render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); fireEvent.click(screen.getByTestId("mobile-more-terminal-split-toggle")); await waitFor(() => { expect(screen.getByTestId("mobile-more-script-item-build")).toBeDefined(); expect(screen.getByTestId("mobile-more-script-item-test")).toBeDefined(); }); }); it("clicking a script item calls onRunScript and closes sheet", async () => { vi.mocked(fetchScripts).mockResolvedValue({ build: "pnpm build", }); const props = createDefaultProps(); const { container } = render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); fireEvent.click(screen.getByTestId("mobile-more-terminal-split-toggle")); await waitFor(() => { expect(screen.getByTestId("mobile-more-script-item-build")).toBeDefined(); }); fireEvent.click(screen.getByTestId("mobile-more-script-item-build")); expect(props.onRunScript).toHaveBeenCalledWith("build", "pnpm build"); expect(container.querySelector(".mobile-more-sheet")).toBeNull(); }); it("manage scripts button calls onOpenScripts and closes sheet", async () => { vi.mocked(fetchScripts).mockResolvedValue({ build: "pnpm build", }); const props = createDefaultProps(); const { container } = render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); fireEvent.click(screen.getByTestId("mobile-more-terminal-split-toggle")); await waitFor(() => { expect(screen.getByTestId("mobile-more-scripts-manage")).toBeDefined(); }); fireEvent.click(screen.getByTestId("mobile-more-scripts-manage")); expect(props.onOpenScripts).toHaveBeenCalledOnce(); expect(container.querySelector(".mobile-more-sheet")).toBeNull(); }); it("empty scripts state shows 'No scripts' item", async () => { vi.mocked(fetchScripts).mockResolvedValue({}); render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); fireEvent.click(screen.getByTestId("mobile-more-terminal-split-toggle")); await waitFor(() => { const manageBtn = screen.getByTestId("mobile-more-scripts-manage"); expect(manageBtn).toBeDefined(); expect(manageBtn.textContent).toContain("No scripts — add one…"); }); }); it("loading state shows spinner while fetching", async () => { let resolveFetch!: (value: Record) => void; vi.mocked(fetchScripts).mockImplementation( () => new Promise((resolve) => { resolveFetch = resolve; }), ); render(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); fireEvent.click(screen.getByTestId("mobile-more-terminal-split-toggle")); expect(screen.getByTestId("mobile-more-scripts-loading")).toBeDefined(); // Resolve to clean up resolveFetch({}); await waitFor(() => { expect(screen.queryByTestId("mobile-more-scripts-loading")).toBeNull(); }); }); }); });