Files
IdeA/frontend/src/features/projects/pluginGitRepository.test.tsx
Blomios ac726d075e feat(frontend): système de plugins — runtime, menus, layouts custom (#43)
Lots F1-F4 : runtime de chargement/registre plugin, extension des menus
existants, panneau de gestion des plugins, types de layout custom
(sélecteur, fallback, cellule dédiée) branchés sur le port plugin.
Suite npm typecheck/test verte (947/947).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 07:37:11 +02:00

170 lines
5.3 KiB
TypeScript

/**
* #43, F3 — `ProjectsView` wires the `gitRepository` `when` signal to a real
* check (`GitGateway.branches`), not a hardcoded `false` (Architect-mandated,
* blocking for #43 closure). This exercises a plugin menu item declared
* `when: "projectOpen && gitRepository"` end-to-end: disabled for a project
* whose git check fails (not a repo), enabled for one whose check succeeds.
*/
import { describe, it, expect } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import {
MockAgentGateway,
MockDesktopServerGateway,
MockProfileGateway,
MockProjectGateway,
MockSystemGateway,
MockTemplateGateway,
MockWindowGateway,
MockWorkStateGateway,
} from "@/adapters/mock";
import type { GatewayError, GitBranches, GitCommit, GitFileStatus, GraphCommit } from "@/domain";
import type { Gateways, GitGateway } from "@/ports";
import { DIProvider } from "@/app/di";
import {
PluginCommandRegistry,
PluginLayoutRegistry,
PluginMenuRegistry,
PluginRuntimeRegistry,
type LoadedPlugin,
} from "@/plugins/runtime";
import { PluginRuntimeProvider } from "@/features/plugins";
import { ProjectsView } from "./ProjectsView";
/** Minimal `GitGateway` double whose `branches()` always rejects — "not a repo". */
class NonRepoGitGateway implements GitGateway {
status(): Promise<GitFileStatus[]> {
return Promise.resolve([]);
}
stage(): Promise<void> {
return Promise.resolve();
}
unstage(): Promise<void> {
return Promise.resolve();
}
commit(): Promise<GitCommit> {
return Promise.reject({ code: "NOT_IMPLEMENTED", message: "n/a" } satisfies GatewayError);
}
branches(): Promise<GitBranches> {
const err: GatewayError = { code: "NOT_A_REPOSITORY", message: "not a git repository" };
return Promise.reject(err);
}
checkout(): Promise<void> {
return Promise.resolve();
}
log(): Promise<GitCommit[]> {
return Promise.resolve([]);
}
init(): Promise<void> {
return Promise.resolve();
}
graph(): Promise<GraphCommit[]> {
return Promise.resolve([]);
}
}
/** Minimal `GitGateway` double whose `branches()` always resolves — a real repo. */
class RealRepoGitGateway extends NonRepoGitGateway {
override branches(): Promise<GitBranches> {
return Promise.resolve({ branches: ["main"], current: "main" });
}
}
function gitAwarePlugin(): LoadedPlugin {
const contributes = {
menus: [],
menuItems: [
{
id: "dev.acme.gitgraph.open",
targetMenuId: "panels" as const,
label: "Ouvrir le graphe Git",
command: "dev.acme.gitgraph.open",
when: "projectOpen && gitRepository",
},
],
layouts: [],
mcpServers: [],
};
return {
pluginId: "dev.acme.gitgraph",
displayName: "Git Graph",
contributes,
commands: new PluginCommandRegistry(
"dev.acme.gitgraph",
new Set(["dev.acme.gitgraph.open"]),
),
layouts: new PluginLayoutRegistry("dev.acme.gitgraph", new Set()),
menu: new PluginMenuRegistry("dev.acme.gitgraph"),
dispose: async () => {},
};
}
function renderWithPlugin(git: GitGateway) {
const agentGateway = new MockAgentGateway();
const gateways = {
system: new MockSystemGateway(),
project: new MockProjectGateway(),
agent: agentGateway,
profile: new MockProfileGateway(),
template: new MockTemplateGateway(agentGateway),
git,
workState: new MockWorkStateGateway(),
window: new MockWindowGateway(),
desktopServer: new MockDesktopServerGateway(),
} as unknown as Gateways;
const registry = new PluginRuntimeRegistry();
registry.add(gitAwarePlugin());
return render(
<DIProvider gateways={gateways}>
<PluginRuntimeProvider value={{ registry, failures: [], loading: false }}>
<ProjectsView />
</PluginRuntimeProvider>
</DIProvider>,
);
}
async function waitForIdle() {
await waitFor(() =>
expect(
(screen.getByRole("button", { name: "Refresh" }) as HTMLButtonElement).disabled,
).toBe(false),
);
}
async function createProject(name: string, root: string) {
await waitForIdle();
fireEvent.change(screen.getByLabelText("project name"), { target: { value: name } });
fireEvent.change(screen.getByLabelText("project root"), { target: { value: root } });
fireEvent.click(screen.getByRole("button", { name: "Create project" }));
}
function panelsMenuItem(): HTMLButtonElement {
return screen.getByRole("button", { name: /Ouvrir le graphe Git/ }) as HTMLButtonElement;
}
describe("ProjectsView — plugin `gitRepository` when-context wiring", () => {
it("disables the item when the project's git check fails (not a repo)", async () => {
renderWithPlugin(new NonRepoGitGateway());
await createProject("alpha", "/home/me/non-repo");
await screen.findByRole("tab");
fireEvent.click(screen.getByRole("button", { name: "Panneaux" }));
await waitFor(() => {
expect(panelsMenuItem().disabled).toBe(true);
});
});
it("enables the item once the project's git check succeeds (real repo)", async () => {
renderWithPlugin(new RealRepoGitGateway());
await createProject("beta", "/home/me/real-repo");
await screen.findByRole("tab");
fireEvent.click(screen.getByRole("button", { name: "Panneaux" }));
await waitFor(() => {
expect(panelsMenuItem().disabled).toBe(false);
});
});
});