fix(windows): fenêtres de panneau détachées suivent le projet en focus côté UI (#47)

Partie frontend. Ajoute un adaptateur focusedProject et un port dédié : la
ViewWindow détachée n'est plus liée à un project_id figé, elle s'abonne à
l'event focused-project émis par la fenêtre principale et affiche le panneau
du projet courant. ProjectsView propage le focus ; le détachement crée une
fenêtre panel-only. Couvert par les tests window/ViewWindow/focusedProject/
ProjectsView.focus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 12:37:45 +02:00
parent 6387fac34f
commit 221cc8be78
14 changed files with 522 additions and 130 deletions

View File

@ -0,0 +1,76 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
const invoke = vi.fn();
const listen = vi.fn();
vi.mock("@tauri-apps/api/core", () => ({
invoke: (...args: unknown[]) => invoke(...args),
}));
vi.mock("@tauri-apps/api/event", () => ({
listen: (...args: unknown[]) => listen(...args),
}));
import { TauriFocusedProjectGateway } from "./focusedProject";
describe("TauriFocusedProjectGateway (#47)", () => {
beforeEach(() => {
invoke.mockReset().mockResolvedValue(null);
listen.mockReset();
});
it("set/get relay to the backend focused-project commands", async () => {
const gw = new TauriFocusedProjectGateway();
await gw.setFocusedProject({ id: "p1", name: "alpha", root: "/p/a" });
expect(invoke).toHaveBeenCalledWith("set_focused_project", {
project: { id: "p1", name: "alpha", root: "/p/a" },
});
await gw.setFocusedProject(null);
expect(invoke).toHaveBeenCalledWith("set_focused_project", {
project: null,
});
invoke.mockResolvedValueOnce({ id: "p2", name: "beta", root: "/p/b" });
await expect(gw.getFocusedProject()).resolves.toEqual({
id: "p2",
name: "beta",
root: "/p/b",
});
expect(invoke).toHaveBeenCalledWith("get_focused_project");
// A `null`/undefined focus normalises to `null`.
invoke.mockResolvedValueOnce(null);
await expect(gw.getFocusedProject()).resolves.toBeNull();
});
it("onFocusedProjectChanged listens on the channel and forwards the project (or null)", async () => {
let raw: ((e: { payload: unknown }) => void) | undefined;
const unlisten = vi.fn();
listen.mockImplementation((_name: string, cb: typeof raw) => {
raw = cb;
return Promise.resolve(unlisten);
});
const gw = new TauriFocusedProjectGateway();
const handler = vi.fn();
const off = await gw.onFocusedProjectChanged(handler);
expect(listen).toHaveBeenCalledWith(
"focused-project://changed",
expect.any(Function),
);
raw?.({ payload: { project: { id: "p1", name: "alpha", root: "/p/a" } } });
expect(handler).toHaveBeenLastCalledWith({
id: "p1",
name: "alpha",
root: "/p/a",
});
raw?.({ payload: { project: null } });
expect(handler).toHaveBeenLastCalledWith(null);
off();
expect(unlisten).toHaveBeenCalled();
});
});