Files
IdeA/frontend/src/features/plugins/PluginLayoutCellView.test.tsx
Blomios aa850374ce fix(plugins): isole les contributions plugin en erreur et durcit menus.ts
Le chargement de l'archive hello-plugin (build/hello-plugin-0.1.0.zip)
vidait la fenêtre principale : une contribution plugin fautive
remontait jusqu'au rendu global au lieu de rester locale à la cellule.
Ajoute un boundary local dans PluginLayoutCellView/PluginLayoutSelectorSection
et durcit menus.ts/loader.ts/registry.ts contre les entrées de menu ou
contributions malformées.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:08:14 +02:00

132 lines
4.3 KiB
TypeScript

/**
* F4 — `PluginLayoutCellView` (ticket #43, carnet §10 F4 acceptance criteria:
* "rendu composant mock, state roundtrip, fallback sans mutation du layout").
*/
import { describe, expect, it, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import type { CustomPluginLayoutCell } from "@/domain";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { createMockGateways } from "@/adapters/mock";
import {
PluginCommandRegistry,
PluginLayoutRegistry,
PluginMenuRegistry,
PluginRuntimeRegistry,
type LoadedPlugin,
type PluginLayoutProps,
} from "@/plugins/runtime";
import { PluginRuntimeProvider } from "./PluginRuntimeProvider";
import { PluginLayoutCellView } from "./PluginLayoutCellView";
function cell(overrides: Partial<CustomPluginLayoutCell> = {}): CustomPluginLayoutCell {
return {
id: "leaf-1",
pluginId: "dev.acme.gitgraph",
layoutType: "dev.acme.gitgraph.layout",
state: { commits: 3 },
...overrides,
};
}
function MockLayoutComponent(props: PluginLayoutProps) {
return (
<div>
<p data-testid="state">{JSON.stringify(props.state)}</p>
<button onClick={() => props.setState({ commits: 4 })}>bump</button>
</div>
);
}
function ThrowingLayoutComponent(_props: PluginLayoutProps): JSX.Element {
throw new Error("plugin layout render failed");
}
function stubPlugin(component = MockLayoutComponent): LoadedPlugin {
const contributes = {
menus: [],
menuItems: [],
layouts: [{ type: "dev.acme.gitgraph.layout", label: "Git Graph", component: "GitGraphLayout" }],
mcpServers: [],
};
const layouts = new PluginLayoutRegistry("dev.acme.gitgraph", new Set(["dev.acme.gitgraph.layout"]));
layouts.register({ type: "dev.acme.gitgraph.layout", component });
return {
pluginId: "dev.acme.gitgraph",
displayName: "Git Graph",
contributes,
commands: new PluginCommandRegistry("dev.acme.gitgraph", new Set()),
layouts,
menu: new PluginMenuRegistry("dev.acme.gitgraph"),
dispose: async () => {},
};
}
function renderCell(
registry: PluginRuntimeRegistry,
props: Partial<{ cell: CustomPluginLayoutCell; onStateChange: (s: unknown) => void }> = {},
) {
const gateways: Gateways = createMockGateways();
return render(
<DIProvider gateways={gateways}>
<PluginRuntimeProvider value={{ registry, failures: [], loading: false }}>
<PluginLayoutCellView
projectId="proj-1"
cell={props.cell ?? cell()}
onStateChange={props.onStateChange ?? (() => {})}
onOpenPlugins={() => {}}
onChooseAnotherLayout={() => {}}
/>
</PluginRuntimeProvider>
</DIProvider>,
);
}
describe("PluginLayoutCellView", () => {
it("renders the mock registered component when the provider is loaded", () => {
const registry = new PluginRuntimeRegistry();
registry.add(stubPlugin());
renderCell(registry);
expect(screen.getByTestId("state").textContent).toBe(JSON.stringify({ commits: 3 }));
});
it("round-trips state through setState → onStateChange", () => {
const registry = new PluginRuntimeRegistry();
registry.add(stubPlugin());
let lastState: unknown = null;
renderCell(registry, {
onStateChange: (s) => {
lastState = s;
},
});
fireEvent.click(screen.getByRole("button", { name: "bump" }));
expect(lastState).toEqual({ commits: 4 });
});
it("renders the non-destructive fallback when the provider is not loaded, without mutating the cell", () => {
const registry = new PluginRuntimeRegistry(); // empty — provider not loaded
const testCell = cell();
renderCell(registry, { cell: testCell });
expect(screen.getByText("Layout indisponible")).toBeTruthy();
// The cell object itself is untouched — the domain identity/state survive.
expect(testCell).toEqual(cell());
});
it("isolates a plugin layout render failure to the plugin cell fallback", () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const registry = new PluginRuntimeRegistry();
registry.add(stubPlugin(ThrowingLayoutComponent));
renderCell(registry);
expect(screen.getByText("Layout indisponible")).toBeTruthy();
expect(screen.queryByTestId("state")).toBeNull();
} finally {
consoleError.mockRestore();
}
});
});