feat(ui): refonte menus & fenêtres — menu unique Panneaux et onglets projet (#26)

Sous-menus flyout dans MenuBar ; ProjectsView expose un menu unique
Panneaux et supprime les menus View/Window/File ainsi que le sélecteur
de projet ; ProjectTabs gère les onglets et le bouton +. Tests migrés
en conséquence.

QA vert : tsc --noEmit exit 0, vitest 62 fichiers / 616 tests passés,
invariants préservés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 15:08:34 +02:00
parent 805d4b70a2
commit 09e7f212b7
7 changed files with 305 additions and 217 deletions

View File

@ -49,23 +49,27 @@ async function renderWithProject() {
return { windowGateway, projectId: created.id };
}
function openMenuItem(menu: string, item: string) {
fireEvent.click(screen.getByRole("button", { name: menu }));
fireEvent.click(screen.getByRole("button", { name: item }));
/**
* 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("Window menu detaches a view: opens the OS window and marks the slot detached", async () => {
it("« Fenêtre détachée » detaches a view: opens the OS window and marks the slot detached", async () => {
const { windowGateway, projectId } = await renderWithProject();
// A docked view is visible in the main window first…
openMenuItem("View", "Git");
const dialog = await screen.findByRole("dialog", { name: "Git" });
fireEvent.click(within(dialog).getByRole("button", { name: "dock Git left" }));
pickPlacement("Git", "Ancré à gauche");
await screen.findByRole("complementary", { name: "left dock" });
// …then « Git → nouvelle fenêtre » pops it out.
openMenuItem("Window", "Git → nouvelle fenêtre");
// …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|${projectId}`)).toBe(true),
@ -80,26 +84,29 @@ describe("ProjectsView detach to OS window (#23)", () => {
it("re-toggles the placement out of detached when the OS window closes", async () => {
const { windowGateway, projectId } = await renderWithProject();
openMenuItem("Window", "Git → nouvelle fenêtre");
pickPlacement("Git", "Fenêtre détachée");
await waitFor(() =>
expect(windowGateway.open.has(`git|${projectId}`)).toBe(true),
);
// The « Window » item shows the active marker (●) while detached.
const openWindowMenu = () =>
fireEvent.click(screen.getByRole("button", { name: "Window" }));
const gitWindowItem = () =>
screen.getByRole("button", { name: "Git → nouvelle fenêtre" });
// 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" });
openWindowMenu();
await waitFor(() => expect(gitWindowItem().textContent).toContain("●"));
openWindowMenu(); // close the dropdown
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", projectId);
openWindowMenu();
openGitSubmenu();
await waitFor(() =>
expect(gitWindowItem().textContent).not.toContain("●"),
expect(detachedLeaf().textContent).not.toContain("●"),
);
});
});