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>
This commit is contained in:
102
frontend/src/features/plugins/plugins.test.tsx
Normal file
102
frontend/src/features/plugins/plugins.test.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* F2 — `PluginsPanel` (ticket #43, carnet §10 F2 acceptance criteria): install
|
||||
* from archive/directory with pre-install review, full-trust mention visible,
|
||||
* enable/disable/uninstall flows, and `restartRequired` rendered.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen, waitFor, fireEvent, within } from "@testing-library/react";
|
||||
|
||||
import { MockPluginGateway, MockSystemGateway } from "@/adapters/mock";
|
||||
import type { Gateways } from "@/ports";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { PluginsPanel } from "./PluginsPanel";
|
||||
|
||||
function renderPanel(plugin?: MockPluginGateway, system?: MockSystemGateway) {
|
||||
const p = plugin ?? new MockPluginGateway();
|
||||
const s = system ?? new MockSystemGateway();
|
||||
const gateways = { plugin: p, system: s } as unknown as Gateways;
|
||||
return {
|
||||
plugin: p,
|
||||
system: s,
|
||||
...render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<PluginsPanel />
|
||||
</DIProvider>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
describe("PluginsPanel", () => {
|
||||
it("shows an empty state with no plugins installed", async () => {
|
||||
renderPanel();
|
||||
expect(await screen.findByText("Aucun plugin installé.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("installs from an archive via the review dialog, mentioning full-trust", async () => {
|
||||
renderPanel();
|
||||
await screen.findByText("Aucun plugin installé.");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Installer depuis une archive…" }));
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
expect(within(dialog).getByText(/full-trust/)).toBeTruthy();
|
||||
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Installer" }));
|
||||
|
||||
// The installed plugin now shows in the list, restart required (carnet §1.4).
|
||||
expect(await screen.findByText("mock-plugin")).toBeTruthy();
|
||||
expect(screen.getByText("Redémarrage requis")).toBeTruthy();
|
||||
expect(screen.getByText("Activé")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables an installed plugin after confirmation", async () => {
|
||||
const plugin = new MockPluginGateway();
|
||||
plugin._seedPlugin({
|
||||
id: "dev.acme.one",
|
||||
displayName: "Acme One",
|
||||
version: "1.0.0",
|
||||
sourceKind: "archive",
|
||||
lifecycleState: "enabled",
|
||||
enabled: true,
|
||||
pendingUninstall: false,
|
||||
restartRequired: false,
|
||||
trustLevel: "full",
|
||||
contributionSummary: { topLevelMenus: 0, menuItems: 0, layouts: 0, mcpServers: 0 },
|
||||
});
|
||||
renderPanel(plugin);
|
||||
|
||||
await screen.findByText("Acme One");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Désactiver" }));
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Désactiver" }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText("Désactivé")).toBeTruthy());
|
||||
});
|
||||
|
||||
it("uninstalls an installed plugin after confirmation", async () => {
|
||||
const plugin = new MockPluginGateway();
|
||||
plugin._seedPlugin({
|
||||
id: "dev.acme.two",
|
||||
displayName: "Acme Two",
|
||||
version: "1.0.0",
|
||||
sourceKind: "directory",
|
||||
lifecycleState: "enabled",
|
||||
enabled: true,
|
||||
pendingUninstall: false,
|
||||
restartRequired: false,
|
||||
trustLevel: "full",
|
||||
contributionSummary: { topLevelMenus: 0, menuItems: 0, layouts: 0, mcpServers: 0 },
|
||||
});
|
||||
renderPanel(plugin);
|
||||
|
||||
await screen.findByText("Acme Two");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Désinstaller" }));
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Désinstaller" }));
|
||||
|
||||
await waitFor(() => expect(screen.queryByText("Acme Two")).toBeNull());
|
||||
expect(await screen.findByText("Aucun plugin installé.")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user