/** * #4 — LayoutTabs / useLayouts: named-layout tab bar driven through the * MockLayoutGateway. Covers: list, create, rename, delete, switch. */ import { describe, it, expect, vi } from "vitest"; import { render, screen, waitFor, fireEvent, act } from "@testing-library/react"; import type { Gateways } from "@/ports"; import { MockLayoutGateway, MockAgentGateway, MockTerminalGateway } from "@/adapters/mock"; import { DIProvider } from "@/app/di"; import { LayoutTabs } from "./LayoutTabs"; function renderTabs( layout: MockLayoutGateway, projectId = "p1", onActiveLayoutChange = vi.fn(), ) { const gateways = { layout, terminal: new MockTerminalGateway(), agent: new MockAgentGateway(), } as unknown as Gateways; return render( , ); } describe("LayoutTabs", () => { it("renders the default 'Default' tab on first load", async () => { const layout = new MockLayoutGateway(); renderTabs(layout); await waitFor(() => { expect(screen.getByText("Default")).toBeTruthy(); }); }); it("shows a create (+) button", async () => { const layout = new MockLayoutGateway(); renderTabs(layout); await waitFor(() => { expect(screen.getByLabelText("create layout")).toBeTruthy(); }); }); it("switching tabs reports the new active layout (id + kind) to the parent", async () => { const layout = new MockLayoutGateway(); // Seed a second layout. await layout.createLayout("p1", "Beta"); const onActiveLayoutChange = vi.fn(); renderTabs(layout, "p1", onActiveLayoutChange); await waitFor(() => { expect(screen.getByText("Beta")).toBeTruthy(); }); // Click the Beta tab. fireEvent.click(screen.getByText("Beta")); await waitFor(() => { // Called with the full LayoutInfo (so the parent knows the kind too). expect(onActiveLayoutChange).toHaveBeenCalledWith( expect.objectContaining({ name: "Beta", kind: "terminal" }), ); }); }); it("renaming a tab (double-click → type → blur) updates the name", async () => { const layout = new MockLayoutGateway(); renderTabs(layout); await waitFor(() => { expect(screen.getByText("Default")).toBeTruthy(); }); // Double-click to start rename. fireEvent.dblClick(screen.getByText("Default")); await waitFor(() => { expect(screen.getByLabelText(/rename layout/i)).toBeTruthy(); }); const input = screen.getByLabelText(/rename layout/i) as HTMLInputElement; fireEvent.change(input, { target: { value: "My Layout" } }); fireEvent.blur(input); await waitFor(() => { expect(screen.getByText("My Layout")).toBeTruthy(); }); }); it("delete button is disabled when only one layout exists", async () => { const layout = new MockLayoutGateway(); renderTabs(layout); await waitFor(() => { expect(screen.getByLabelText(/delete layout/i)).toBeTruthy(); }); const closeBtn = screen.getByLabelText(/delete layout/i) as HTMLButtonElement; expect(closeBtn.disabled).toBe(true); }); it("creating a layout adds a tab", async () => { const layout = new MockLayoutGateway(); // Directly create a second layout via the gateway (simulates the + flow). await layout.createLayout("p1", "Extra"); renderTabs(layout); await waitFor(() => { expect(screen.getByText("Default")).toBeTruthy(); expect(screen.getByText("Extra")).toBeTruthy(); }); }); it("deleting a layout removes its tab (when 2 exist)", async () => { const layout = new MockLayoutGateway(); await layout.createLayout("p1", "Second"); renderTabs(layout); await waitFor(() => { expect(screen.getByText("Second")).toBeTruthy(); }); const deleteBtns = screen.getAllByLabelText(/delete layout/i); // Both tabs exist, pick the second (Second). await act(async () => { fireEvent.click(deleteBtns[1]); }); await waitFor(() => { expect(screen.queryByText("Second")).toBeNull(); }); }); });