- détachement: le panneau Paramètres du projet devient détachable (mapping historique panel=context → projectSettings dans parseViewWindowParams, DETACHABLE/ViewPanelBody alignés, ProjectsView ne court-circuitant plus le refus); Retry d'échec de détachement préservé - couleurs projet: projectTheme publie idea:project-theme-changed localement + BroadcastChannel inter-webviews (fenêtres OS détachées), garde de révision par projet dans ProjectsView/ViewWindow contre les lectures settings en course — plus de flash/retour au bleu par défaut - conversation custom CLI: transcript_restore_expected ne considère plus l'absence de scrollback volatile comme une restauration échouée quand la session rouvre en SessionPlan::None (continuation vide valide); CustomAgentChatView n'affiche plus le faux « conversation liée n'a pas pu être restaurée » - images: conservation des images de conversation après changement de projet puis retour - menus Panneaux: retrait des entrées Projects et Project Context (panelOrder WebWorkspace, création/switch via tab-bar +, contexte projet fusionné dans Paramètres du projet) - QA verte: tests frontend ciblés window/ViewWindow/ProjectSettingsPanel/ProjectsView.detach/projects/CustomAgentChatView/WebDesktopWorkspace + cargo test -p application lifecycle transcript_restore_expected; fichiers .ideai/* (tickets 308-315, index/counter, agents/test, chat-drafts, system-permissions, project-settings.json, attachments/) et scripts/ hors périmètre préservés en worktree; aucun push
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import { beforeEach, describe, it, expect } from "vitest";
|
|
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
|
|
import type { Gateways } from "@/ports";
|
|
import { DIProvider } from "@/app/di";
|
|
import { createMockGateways, MockWorkStateGateway } 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();
|
|
const project = await gateways.project.createProject("Demo", "/srv/demo");
|
|
(gateways.workState as MockWorkStateGateway)._setProjectWorkState(project.id, {
|
|
agents: [
|
|
{ agentId: "a1", name: "Archi", profileId: "p1", busy: { state: "idle" }, tickets: [] },
|
|
],
|
|
conversations: [],
|
|
});
|
|
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>,
|
|
);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
window.innerWidth = 1280;
|
|
window.dispatchEvent(new Event("resize"));
|
|
});
|
|
|
|
describe("WebWorkspace desktop layout", () => {
|
|
it("mounts the desktop AppImage-style shell with LayoutTabs/LayoutGrid and no detached action", async () => {
|
|
renderPairedWebApp(await seededGateways());
|
|
|
|
expect(
|
|
(await screen.findByTestId("web-workspace")).getAttribute("data-shell"),
|
|
).toBe(
|
|
"desktop-layout",
|
|
);
|
|
expect(screen.queryByTestId("web-mobile-task-canvas")).toBeNull();
|
|
|
|
fireEvent.click(await screen.findByText("Demo"));
|
|
|
|
expect(await screen.findByTestId("layout-tabs")).toBeTruthy();
|
|
expect(await screen.findByTestId("layout-grid")).toBeTruthy();
|
|
expect(screen.getByLabelText("right dock")).toBeTruthy();
|
|
expect(screen.queryByRole("button", { name: /detach/i })).toBeNull();
|
|
expect(screen.queryByText(/Fenêtre/)).toBeNull();
|
|
});
|
|
|
|
it("opens panels as floating or docked from the desktop menu bar without a detached web mode", async () => {
|
|
renderPairedWebApp(await seededGateways());
|
|
fireEvent.click(await screen.findByText("Demo"));
|
|
await screen.findByTestId("layout-grid");
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Panneaux" }));
|
|
const menu = await screen.findByRole("menu", { name: "Panneaux" });
|
|
|
|
fireEvent.click(within(menu).getByRole("button", { name: "Tickets" }));
|
|
const submenu = await screen.findByRole("menu", { name: "Tickets" });
|
|
fireEvent.click(within(submenu).getByRole("button", { name: "Flottant" }));
|
|
|
|
expect(await screen.findByRole("dialog", { name: "Tickets" })).toBeTruthy();
|
|
expect(screen.queryByRole("button", { name: /detached/i })).toBeNull();
|
|
});
|
|
|
|
it("omits Projects and Project context from Panneaux while keeping project access", async () => {
|
|
renderPairedWebApp(await seededGateways());
|
|
fireEvent.click(await screen.findByText("Demo"));
|
|
await screen.findByTestId("layout-grid");
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Panneaux" }));
|
|
const menu = await screen.findByRole("menu", { name: "Panneaux" });
|
|
expect(within(menu).queryByRole("button", { name: "Contexte projet" })).toBeNull();
|
|
expect(within(menu).queryByRole("button", { name: "Projets" })).toBeNull();
|
|
expect(screen.getByRole("button", { name: "Projets" })).toBeTruthy();
|
|
});
|
|
});
|