/** * #23 — detaching a View into its own OS window from `ProjectsView`. The * "Window" menu (and the docked/floating header ⤢ control) call the * {@link WindowGateway}; the slot becomes "detached" (nothing renders in the * main window), and the backend's close event re-toggles it back out of * "detached". The one-slot invariant holds throughout. */ import { describe, it, expect } from "vitest"; import { render, screen, within, waitFor, fireEvent } from "@testing-library/react"; import { MockAgentGateway, MockGitGateway, MockProfileGateway, MockFocusedProjectGateway, MockProjectGateway, MockSystemGateway, MockTemplateGateway, MockWindowGateway, MockWorkStateGateway, } from "@/adapters/mock"; import type { Gateways } from "@/ports"; import { DIProvider } from "@/app/di"; import { ProjectsView } from "./ProjectsView"; async function renderWithProject() { const project = new MockProjectGateway(); await project.createProject("alpha", "/p/a"); const agentGateway = new MockAgentGateway(); const windowGateway = new MockWindowGateway(); const gateways = { system: new MockSystemGateway(), project, agent: agentGateway, profile: new MockProfileGateway(), template: new MockTemplateGateway(agentGateway), git: new MockGitGateway(), workState: new MockWorkStateGateway(), window: windowGateway, focusedProject: new MockFocusedProjectGateway(), } as unknown as Gateways; render( , ); fireEvent.click(await screen.findByRole("button", { name: "Open" })); await waitFor(() => expect(within(screen.getByRole("tablist")).getAllByRole("tab")).toHaveLength(1), ); return { windowGateway }; } /** * Picks a placement for a panel from the single « Panneaux » menu (#26): menu → * panel entry (opens its placement submenu) → the placement leaf ("Flottant", * "Ancré à gauche", "Fenêtre détachée", …). */ function pickPlacement(panelTitle: string, placementLabel: string) { fireEvent.click(screen.getByRole("button", { name: "Panneaux" })); fireEvent.click(screen.getByRole("button", { name: panelTitle })); fireEvent.click(screen.getByRole("button", { name: placementLabel })); } describe("ProjectsView detach to OS window (#23)", () => { it("« Fenêtre détachée » detaches a view: opens the OS window and marks the slot detached", async () => { const { windowGateway } = await renderWithProject(); // A docked view is visible in the main window first… pickPlacement("Git", "Ancré à gauche"); await screen.findByRole("complementary", { name: "left dock" }); // …then « Panneaux → Git → Fenêtre détachée » pops it out. pickPlacement("Git", "Fenêtre détachée"); await waitFor(() => expect(windowGateway.open.has("git")).toBe(true), ); // The slot is detached ⇒ nothing renders for Git in the main window. await waitFor(() => expect(screen.queryByRole("complementary", { name: "left dock" })).toBeNull(), ); expect(screen.queryByRole("dialog", { name: "Git" })).toBeNull(); }); it("re-toggles the placement out of detached when the OS window closes", async () => { const { windowGateway } = await renderWithProject(); pickPlacement("Git", "Fenêtre détachée"); await waitFor(() => expect(windowGateway.open.has("git")).toBe(true), ); // The « Fenêtre détachée » submenu leaf shows the active marker (●) while // detached. Re-navigate the submenu each time to read it. const openGitSubmenu = () => { fireEvent.click(screen.getByRole("button", { name: "Panneaux" })); fireEvent.click(screen.getByRole("button", { name: "Git" })); }; const detachedLeaf = () => screen.getByRole("button", { name: "Fenêtre détachée" }); openGitSubmenu(); await waitFor(() => expect(detachedLeaf().textContent).toContain("●")); fireEvent.click(screen.getByRole("button", { name: "Panneaux" })); // close // The backend reports the OS window closed ⇒ the slot leaves "detached". windowGateway.simulateOsClose("git"); openGitSubmenu(); await waitFor(() => expect(detachedLeaf().textContent).not.toContain("●"), ); }); });