feat(ui): fenêtres View système séparées — WebviewWindow Tauri + port WindowGateway (#23)
Détache les vues dans de vraies fenêtres système Tauri (multi-écran, fullscreen). Backend : commandes de gestion de WebviewWindow, capabilities et composition root. Frontend : nouveau port WindowGateway et son adaptateur window, entrée panel-only ViewWindow/ViewPanelBody, détachement câblé dans ProjectsView. QA vert : app-tauri 63, frontend typecheck + vitest 546/546. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
105
frontend/src/features/projects/ProjectsView.detach.test.tsx
Normal file
105
frontend/src/features/projects/ProjectsView.detach.test.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* #23 — detaching a View into its own OS window from `ProjectsView`. The
|
||||
* "Window" menu (and the docked/floating header ⤢ control) call the
|
||||
* {@link WindowGateway}; the slot becomes "detached" (nothing renders in the
|
||||
* main window), and the backend's close event re-toggles it back out of
|
||||
* "detached". The one-slot invariant holds throughout.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen, within, waitFor, fireEvent } from "@testing-library/react";
|
||||
|
||||
import {
|
||||
MockAgentGateway,
|
||||
MockGitGateway,
|
||||
MockProfileGateway,
|
||||
MockProjectGateway,
|
||||
MockSystemGateway,
|
||||
MockTemplateGateway,
|
||||
MockWindowGateway,
|
||||
MockWorkStateGateway,
|
||||
} from "@/adapters/mock";
|
||||
import type { Gateways } from "@/ports";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { ProjectsView } from "./ProjectsView";
|
||||
|
||||
async function renderWithProject() {
|
||||
const project = new MockProjectGateway();
|
||||
const created = await project.createProject("alpha", "/p/a");
|
||||
const agentGateway = new MockAgentGateway();
|
||||
const windowGateway = new MockWindowGateway();
|
||||
const gateways = {
|
||||
system: new MockSystemGateway(),
|
||||
project,
|
||||
agent: agentGateway,
|
||||
profile: new MockProfileGateway(),
|
||||
template: new MockTemplateGateway(agentGateway),
|
||||
git: new MockGitGateway(),
|
||||
workState: new MockWorkStateGateway(),
|
||||
window: windowGateway,
|
||||
} as unknown as Gateways;
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<ProjectsView />
|
||||
</DIProvider>,
|
||||
);
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Open" }));
|
||||
await waitFor(() =>
|
||||
expect(within(screen.getByRole("tablist")).getAllByRole("tab")).toHaveLength(1),
|
||||
);
|
||||
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 }));
|
||||
}
|
||||
|
||||
describe("ProjectsView detach to OS window (#23)", () => {
|
||||
it("Window menu 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" }));
|
||||
await screen.findByRole("complementary", { name: "left dock" });
|
||||
|
||||
// …then « Git → nouvelle fenêtre » pops it out.
|
||||
openMenuItem("Window", "Git → nouvelle fenêtre");
|
||||
|
||||
await waitFor(() =>
|
||||
expect(windowGateway.open.has(`git|${projectId}`)).toBe(true),
|
||||
);
|
||||
// The slot is detached ⇒ nothing renders for Git in the main window.
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole("complementary", { name: "left dock" })).toBeNull(),
|
||||
);
|
||||
expect(screen.queryByRole("dialog", { name: "Git" })).toBeNull();
|
||||
});
|
||||
|
||||
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");
|
||||
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" });
|
||||
|
||||
openWindowMenu();
|
||||
await waitFor(() => expect(gitWindowItem().textContent).toContain("●"));
|
||||
openWindowMenu(); // close the dropdown
|
||||
|
||||
// The backend reports the OS window closed ⇒ the slot leaves "detached".
|
||||
windowGateway.simulateOsClose("git", projectId);
|
||||
openWindowMenu();
|
||||
await waitFor(() =>
|
||||
expect(gitWindowItem().textContent).not.toContain("●"),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user