/** * Ticket #90 — the web "Panneaux" (WebWorkspace) and "Paramètres" (WebApp) menus. * * Both replace the previous ad-hoc navigation (a 3-tab tablist, a single * "Appareils" header toggle) with a full-screen {@link WebMenuSheet}: this pins * the new contract — entry list, active marker, disabled-with-reason state, and * that selecting an entry routes the main surface without ever crashing on a * desktop-only action. */ import { beforeEach, describe, it, expect, vi } from "vitest"; import { fireEvent, render, screen, within } from "@testing-library/react"; import type { Gateways } from "@/ports"; import { DIProvider } from "@/app/di"; import { createMockGateways } from "@/adapters/mock"; import { WebSession } from "@/adapters/http"; import type { FetchLike } from "@/adapters/http/httpInvoker"; import type { FlagStore } from "@/adapters/http/webSession"; import { WebApp } from "./WebApp"; function memStore(): FlagStore { const map = new Map(); return { getItem: (k) => map.get(k) ?? null, setItem: (k, v) => void map.set(k, v), removeItem: (k) => void map.delete(k), }; } const okFetch: FetchLike = async () => ({ ok: true, status: 200, json: async () => ({ ok: true }), text: async () => "{}", }); beforeEach(() => { window.localStorage.clear(); window.innerWidth = 1024; window.dispatchEvent(new Event("resize")); vi.restoreAllMocks(); }); async function seededGateways(): Promise { const gateways = createMockGateways(); await gateways.project.createProject("Demo", "/srv/demo"); return gateways; } function renderPairedWebApp(gateways: Gateways) { const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() }); session.markPaired(); return render( , ); } describe("WebWorkspace « Panneaux » menu (#90)", () => { it("renders the #203 desktop shell zones with persistent surface navigation", async () => { renderPairedWebApp(await seededGateways()); expect(await screen.findByTestId("web-desktop-shell")).toBeTruthy(); expect(screen.getByTestId("web-shell-topbar")).toBeTruthy(); expect(screen.getByTestId("web-shell-primary-rail")).toBeTruthy(); expect(screen.getByTestId("web-shell-canvas")).toBeTruthy(); expect(screen.getByTestId("web-shell-secondary-rail")).toBeTruthy(); fireEvent.click(await screen.findByText("Demo")); expect(await screen.findByText("Surface Travail")).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Surface Tickets" })); expect(await screen.findByText("Surface Tickets")).toBeTruthy(); expect(screen.getByRole("button", { name: "Surface Travail" })).toBeTruthy(); }); it("offers #204 open here/tab/window controls and falls back to a tab", async () => { const open = vi .spyOn(window, "open") .mockReturnValueOnce(null) .mockReturnValueOnce({ closed: false } as Window); renderPairedWebApp(await seededGateways()); fireEvent.click(await screen.findByText("Demo")); expect(await screen.findByText("Surface Travail")).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Fenêtre" })); expect(open).toHaveBeenCalledTimes(2); expect(open.mock.calls[0][0]).toContain("webPanel=work"); expect(open.mock.calls[0][1]).toBe("idea-web-work"); expect(open.mock.calls[1][1]).toBe("_blank"); expect(await screen.findByText("Fenêtre bloquée — ouverture en onglet.")).toBeTruthy(); open.mockRestore(); }); it("restores the #205 presentation state per project", async () => { const gateways = await seededGateways(); const first = renderPairedWebApp(gateways); fireEvent.click(await screen.findByText("Demo")); fireEvent.click(await screen.findByRole("button", { name: "Surface Tickets" })); fireEvent.click(screen.getByRole("button", { name: "Rail gauche" })); fireEvent.click(screen.getByRole("button", { name: "Rail droit" })); first.unmount(); renderPairedWebApp(gateways); fireEvent.click(await screen.findByText("Demo")); expect(await screen.findByText("Surface Tickets")).toBeTruthy(); expect(screen.getByRole("button", { name: "Rail gauche" }).getAttribute("aria-pressed")).toBe("false"); expect(screen.getByRole("button", { name: "Rail droit" }).getAttribute("aria-pressed")).toBe("false"); }); it("only enables Projets while no project is open, with a help note", async () => { renderPairedWebApp(await seededGateways()); fireEvent.click(await screen.findByRole("button", { name: "Panneaux : Projets" })); expect( await screen.findByText("Ouvrir un projet pour utiliser ce panneau."), ).toBeTruthy(); const contextEntry = screen.getByRole("button", { name: "Contexte projet" }); expect(contextEntry).toHaveProperty("disabled", true); const projectsEntry = screen.getByRole("button", { name: "Projets" }); expect(projectsEntry).toHaveProperty("disabled", false); // A disabled entry does nothing. fireEvent.click(contextEntry); expect( within(screen.getByRole("dialog", { name: "Panneaux" })).queryByText("Contexte projet"), ).toBeTruthy(); // sheet still open, menu entry still there }); it("routes the main surface to the selected panel once a project is open", async () => { renderPairedWebApp(await seededGateways()); fireEvent.click(await screen.findByText("Demo")); expect(await screen.findByRole("button", { name: "Panneaux : Travail" })).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Panneaux : Travail" })); const agentsEntry = await screen.findByRole("button", { name: "Agents" }); expect(agentsEntry).toHaveProperty("disabled", false); fireEvent.click(agentsEntry); expect(await screen.findByRole("button", { name: "Panneaux : Agents" })).toBeTruthy(); }); it("closes on Escape without changing the panel", async () => { renderPairedWebApp(await seededGateways()); fireEvent.click(await screen.findByText("Demo")); await screen.findByRole("button", { name: "Panneaux : Travail" }); fireEvent.click(screen.getByRole("button", { name: "Panneaux : Travail" })); await screen.findByRole("dialog", { name: "Panneaux" }); fireEvent.keyDown(window, { key: "Escape" }); expect(screen.queryByRole("dialog", { name: "Panneaux" })).toBeNull(); expect(screen.getByRole("button", { name: "Panneaux : Travail" })).toBeTruthy(); }); }); describe("WebProjectsPanel — web project creation (#90)", () => { it("creates a project via a manual path and opens it", async () => { renderPairedWebApp(await seededGateways()); await screen.findByText("Demo"); fireEvent.change(screen.getByLabelText("Nom du projet"), { target: { value: "Nouveau" }, }); fireEvent.change(screen.getByLabelText("Chemin du projet"), { target: { value: "/srv/nouveau" }, }); expect( screen.getByText("Saisir un chemin absolu accessible depuis le serveur IdeA."), ).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Créer le projet" })); // Creating opens it — routes straight to the Travail panel, like opening // an existing project from the list. expect(await screen.findByRole("button", { name: "Panneaux : Travail" })).toBeTruthy(); }); }); describe("WebApp « Paramètres » menu (#90)", () => { it("opens Profils IA and returns to the workspace via « ← Retour »", async () => { renderPairedWebApp(await seededGateways()); fireEvent.click(await screen.findByRole("button", { name: "Paramètres" })); fireEvent.click(await screen.findByRole("button", { name: "Profils IA" })); expect(await screen.findByRole("heading", { name: "Profils IA" })).toBeTruthy(); expect(screen.queryByRole("button", { name: /^Panneaux :/ })).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "← Retour" })); expect(await screen.findByRole("button", { name: /^Panneaux :/ })).toBeTruthy(); }); it("opens Appareils from the settings menu", async () => { renderPairedWebApp(await seededGateways()); fireEvent.click(await screen.findByRole("button", { name: "Paramètres" })); fireEvent.click(await screen.findByRole("button", { name: "Appareils" })); expect(await screen.findByRole("button", { name: "← Retour" })).toBeTruthy(); }); it("shows Déploiement as disabled, labelled desktop-only, and does nothing on click", async () => { renderPairedWebApp(await seededGateways()); fireEvent.click(await screen.findByRole("button", { name: "Paramètres" })); const deploymentEntry = await screen.findByRole("button", { name: /Déploiement/ }); expect(deploymentEntry.textContent).toContain("Desktop uniquement"); expect(deploymentEntry).toHaveProperty("disabled", true); fireEvent.click(deploymentEntry); // Still on the menu — no navigation happened. expect(screen.getByRole("dialog", { name: "Paramètres" })).toBeTruthy(); }); });