/** * L4 — {@link LayoutGrid} wired through the real {@link DIProvider} with the * in-memory {@link MockLayoutGateway} (+ {@link MockTerminalGateway} for the * leaves' embedded {@link TerminalView}). * * Under jsdom xterm's `term.open` may bail gracefully (no real layout engine), * so these tests assert the *structure* the grid renders (cells, no throw) * rather than xterm's visual output. They stay robust whether or not xterm * bailed. */ import { describe, it, expect, vi } from "vitest"; import { render, screen, waitFor, fireEvent } from "@testing-library/react"; import type { Gateways } from "@/ports"; import { MockLayoutGateway, MockTerminalGateway } from "@/adapters/mock"; import { DIProvider } from "@/app/di"; import { leaves } from "./layout"; import { LayoutGrid } from "./LayoutGrid"; function renderGrid(layout: MockLayoutGateway, projectId = "p1") { const gateways = { layout, terminal: new MockTerminalGateway(), } as unknown as Gateways; return render( , ); } describe("LayoutGrid (with MockLayoutGateway)", () => { it("renders a single-leaf layout without throwing", async () => { const layout = new MockLayoutGateway(); expect(() => renderGrid(layout)).not.toThrow(); // The grid container always renders. expect(screen.getByTestId("layout-grid")).toBeTruthy(); // Once loaded, the single leaf cell is shown. await waitFor(() => { expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1); }); }); it("renders N cells for a pre-split tree", async () => { const layout = new MockLayoutGateway(); // Pre-split the project's tree into two leaves before rendering. const initial = await layout.loadLayout("p1"); await layout.mutateLayout("p1", { type: "split", target: leaves(initial)[0].id, direction: "row", newLeaf: "b", container: "c", }); renderGrid(layout); await waitFor(() => { expect(screen.getAllByTestId("layout-leaf")).toHaveLength(2); }); // The split container is present with its direction recorded. const split = screen.getByTestId("layout-split"); expect(split.getAttribute("data-direction")).toBe("row"); }); it("stays graceful (no throw) even though xterm bails under jsdom", async () => { const layout = new MockLayoutGateway(); // Render a 3-leaf tree: nested splits, each leaf hosts a TerminalView. const initial = await layout.loadLayout("p1"); const a = leaves(initial)[0].id; await layout.mutateLayout("p1", { type: "split", target: a, direction: "row", newLeaf: "b", container: "c", }); await layout.mutateLayout("p1", { type: "split", target: "b", direction: "column", newLeaf: "d", container: "e", }); expect(() => renderGrid(layout)).not.toThrow(); await waitFor(() => { expect(screen.getAllByTestId("layout-leaf")).toHaveLength(3); }); }); it("closing a cell removes THAT cell and keeps its sibling without killing its session", async () => { const layout = new MockLayoutGateway(); const terminal = new MockTerminalGateway(); const closeSpy = vi.spyOn(terminal, "closeTerminal"); const initial = await layout.loadLayout("p1"); const a = leaves(initial)[0].id; // Split A → container c with children [A (index 0), b (index 1)]. await layout.mutateLayout("p1", { type: "split", target: a, direction: "row", newLeaf: "b", container: "c", }); await layout.mutateLayout("p1", { type: "setSession", target: "b", session: "running-session-b", }); const gateways = { layout, terminal } as unknown as Gateways; render( , ); await waitFor(() => expect(screen.getAllByTestId("layout-leaf")).toHaveLength(2), ); // Click the close button on cell `b` — `b` must disappear, `a` must remain. fireEvent.click(screen.getByLabelText("close b")); await waitFor(() => expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1), ); expect( screen.getByTestId("layout-leaf").getAttribute("data-node-id"), ).toBe(a); expect(closeSpy).not.toHaveBeenCalled(); }); it("closing the other cell keeps the opposite sibling", async () => { const layout = new MockLayoutGateway(); const initial = await layout.loadLayout("p1"); const a = leaves(initial)[0].id; await layout.mutateLayout("p1", { type: "split", target: a, direction: "row", newLeaf: "b", container: "c", }); renderGrid(layout); await waitFor(() => expect(screen.getAllByTestId("layout-leaf")).toHaveLength(2), ); // Closing `a` (index 0) must keep `b` (index 1). fireEvent.click(screen.getByLabelText(`close ${a}`)); await waitFor(() => expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1), ); expect( screen.getByTestId("layout-leaf").getAttribute("data-node-id"), ).toBe("b"); }); it("the sole root cell has no close button (cannot be closed)", async () => { const layout = new MockLayoutGateway(); renderGrid(layout); await waitFor(() => expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1), ); expect(screen.queryByLabelText(/^close /)).toBeNull(); }); it("renders the loading/empty container when the layout gateway is absent", () => { // A DI subset without the layout gateway → useLayout is defensive (null). const gateways = { terminal: new MockTerminalGateway() } as unknown as Gateways; expect(() => render( , ), ).not.toThrow(); expect(screen.getByTestId("layout-grid")).toBeTruthy(); }); });