Partie frontend. Ajoute un adaptateur focusedProject et un port dédié : la ViewWindow détachée n'est plus liée à un project_id figé, elle s'abonne à l'event focused-project émis par la fenêtre principale et affiche le panneau du projet courant. ProjectsView propage le focus ; le détachement crée une fenêtre panel-only. Couvert par les tests window/ViewWindow/focusedProject/ ProjectsView.focus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
/**
|
|
* #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(
|
|
<DIProvider gateways={gateways}>
|
|
<ProjectsView />
|
|
</DIProvider>,
|
|
);
|
|
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("●"),
|
|
);
|
|
});
|
|
});
|