feat(frontend): ajoute les menus Panneaux/Paramètres à la version web (#90)
WebWorkspace gagne un menu « Panneaux » qui route la surface principale du projet ouvert vers les panneaux transport-neutres déjà réutilisables (contexte, agents, templates, skills, permissions, mémoire, git) en plus des surfaces web existantes (travail live, tickets, sprints), ainsi qu'un variant web du panneau Projets (création par chemin serveur saisi manuellement, sans browse natif). WebApp gagne un menu « Paramètres » (Profils IA, Appareils, Déploiement désactivé "Desktop uniquement") qui remplace l'ancien bouton unique "Appareils". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
158
frontend/src/features/web/WebMenus.test.tsx
Normal file
158
frontend/src/features/web/WebMenus.test.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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 { describe, it, expect } from "vitest";
|
||||
import { fireEvent, render, screen } 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<string, string>();
|
||||
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 () => "{}",
|
||||
});
|
||||
|
||||
async function seededGateways(): Promise<Gateways> {
|
||||
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(
|
||||
<DIProvider gateways={gateways}>
|
||||
<WebApp session={session} />
|
||||
</DIProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("WebWorkspace « Panneaux » menu (#90)", () => {
|
||||
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(screen.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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user