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>
170 lines
5.3 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|