224 lines
8.9 KiB
TypeScript
224 lines
8.9 KiB
TypeScript
/**
|
|
* 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<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 () => "{}",
|
|
});
|
|
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
window.innerWidth = 1024;
|
|
window.dispatchEvent(new Event("resize"));
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
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("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();
|
|
});
|
|
});
|