/** * LS7 — `ProjectsView` integration: clicking a conversation in the Work panel swaps * the terminal grid for the read-only `ConversationViewer`; the back button returns * to the grid; switching project resets the viewer; and with no conversation open * the terminal grid is shown unchanged (non-regression). */ import { describe, it, expect } from "vitest"; import { render, screen, fireEvent, waitFor, within } from "@testing-library/react"; import { MockAgentGateway, MockGitGateway, MockLayoutGateway, MockProfileGateway, MockProjectGateway, MockSystemGateway, MockTemplateGateway, MockTerminalGateway, MockWindowGateway, } from "@/adapters/mock"; import type { ConversationGateway, Gateways, WorkStateGateway, } from "@/ports"; import type { ProjectWorkState } from "@/domain"; import { DIProvider } from "@/app/di"; import { ProjectsView } from "./ProjectsView"; const CONV_ID = "conversation-open-me"; /** A work-state gateway that returns a fixed, clickable conversation for any project. */ function fixedWorkState(): WorkStateGateway { const state: ProjectWorkState = { agents: [ { agentId: "agent-1", name: "Worker", profileId: "codex", busy: { state: "idle" }, tickets: [ { ticketId: "ticket-1", conversationId: CONV_ID, position: 0, status: "inProgress", source: { kind: "human" }, requesterLabel: "Anthony", taskPreview: "do the thing", taskLen: 12, }, ], }, ], conversations: [ { conversationId: CONV_ID, status: "ready", objectivePreview: "Objectif du fil", summaryPreview: "Résumé du fil", summaryLen: 13, upTo: "turn-1", recentTurns: [], }, ], }; return { getProjectWorkState: async () => structuredClone(state), cancelBackgroundTask: async () => {}, retryBackgroundTask: async () => {}, }; } /** A conversation gateway that returns one identifiable turn for any thread. */ function fixedConversation(): ConversationGateway { return { readPage: async () => ({ turns: [ { id: "t1", atMs: 1_700_000_000_000, role: "prompt", source: { kind: "human" }, text: "contenu du fil ouvert", textLen: 21, }, ], hasMore: false, }), }; } function renderView(project: MockProjectGateway) { const agent = new MockAgentGateway(); const gateways = { system: new MockSystemGateway(), project, agent, profile: new MockProfileGateway(), template: new MockTemplateGateway(agent), git: new MockGitGateway(), layout: new MockLayoutGateway(), terminal: new MockTerminalGateway(), workState: fixedWorkState(), conversation: fixedConversation(), window: new MockWindowGateway(), } as unknown as Gateways; return render( , ); } /** Opens a menu-bar dropdown item (#16 chrome). */ function openMenuItem(menu: string, item: string) { fireEvent.click(screen.getByRole("button", { name: menu })); fireEvent.click(screen.getByRole("button", { name: item })); } async function openProjectAndWorkTab(label: string) { await screen.findByText(label); // Open the project whose root is `label` from the welcome projects list. const li = screen .getAllByRole("listitem") .find((node) => within(node).queryByText(label)); fireEvent.click(within(li!).getByRole("button", { name: "Open" })); await screen.findByRole("tab"); // Open the Work panel window from the View menu. openMenuItem("View", "Work"); } describe("ProjectsView — LS7 conversation viewer integration", () => { it("opens the viewer on a conversation click and returns to the grid on back", async () => { const project = new MockProjectGateway(); await project.createProject("alpha", "/p/a"); renderView(project); await openProjectAndWorkTab("/p/a"); // Before: the terminal grid owns the main area (non-regression baseline). expect(await screen.findByTestId("layout-grid")).toBeTruthy(); expect( screen.queryByRole("button", { name: "← Retour aux terminaux" }), ).toBeNull(); // Click the conversation row in the Work panel. const open = await screen.findByRole("button", { name: `open conversation ${CONV_ID}`, }); fireEvent.click(open); // After: the viewer replaced the grid in the main area. expect( await screen.findByRole("button", { name: "← Retour aux terminaux" }), ).toBeTruthy(); expect(await screen.findByText("contenu du fil ouvert")).toBeTruthy(); expect(screen.queryByTestId("layout-grid")).toBeNull(); // Back ⇒ the terminal grid is restored, the viewer is gone. fireEvent.click( screen.getByRole("button", { name: "← Retour aux terminaux" }), ); expect(await screen.findByTestId("layout-grid")).toBeTruthy(); expect( screen.queryByRole("button", { name: "← Retour aux terminaux" }), ).toBeNull(); }); it("resets the viewer when the active project changes", async () => { const project = new MockProjectGateway(); await project.createProject("alpha", "/p/a"); await project.createProject("beta", "/p/b"); renderView(project); // Open BOTH projects as tabs. Use the projects manager window so the list // persists across activation (the inline welcome list is replaced by the // grid once a project is active). await screen.findByText("/p/a"); openMenuItem("File", "Projects…"); for (const root of ["/p/a", "/p/b"]) { const li = screen .getAllByRole("listitem") .find((node) => within(node).queryByText(root)); fireEvent.click(within(li!).getByRole("button", { name: "Open" })); } await waitFor(() => expect(screen.getAllByRole("tab")).toHaveLength(2)); // Make alpha the active tab, then open its conversation viewer. fireEvent.click(screen.getByRole("tab", { name: "alpha" })); openMenuItem("View", "Work"); fireEvent.click( await screen.findByRole("button", { name: `open conversation ${CONV_ID}` }), ); await screen.findByRole("button", { name: "← Retour aux terminaux" }); // Switch the active project (top tab) ⇒ viewer resets to the grid. fireEvent.click(screen.getByRole("tab", { name: "beta" })); await waitFor(() => expect( screen.queryByRole("button", { name: "← Retour aux terminaux" }), ).toBeNull(), ); expect(await screen.findByTestId("layout-grid")).toBeTruthy(); }); it("shows the terminal grid unchanged when no conversation is open (non-regression)", async () => { const project = new MockProjectGateway(); await project.createProject("alpha", "/p/a"); renderView(project); await screen.findByText("/p/a"); fireEvent.click(screen.getByRole("button", { name: "Open" })); await screen.findByRole("tab"); // No conversation opened ⇒ the main area is the terminal grid, no viewer. expect(await screen.findByTestId("layout-grid")).toBeTruthy(); expect( screen.queryByRole("button", { name: "← Retour aux terminaux" }), ).toBeNull(); }); });