/** * 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 { return { id: "leaf-1", pluginId: "dev.acme.gitgraph", layoutType: "dev.acme.gitgraph.layout", state: { commits: 3 }, ...overrides, }; } function MockLayoutComponent(props: PluginLayoutProps) { return (

{JSON.stringify(props.state)}

); } 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( {})} onOpenPlugins={() => {}} onChooseAnotherLayout={() => {}} /> , ); } 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(); } }); });