/** * F4 — `PluginLayoutSelectorSection` (carnet §10 F4 acceptance criteria: * "layout disabled non proposé comme nouveau choix"). */ import { describe, expect, it } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { PluginCommandRegistry, PluginLayoutRegistry, PluginMenuRegistry, PluginRuntimeRegistry, type LoadedPlugin, } from "@/plugins/runtime"; import type { PluginContributionDto } from "@/domain"; import { PluginLayoutSelectorSection, listPluginLayoutChoices } from "./PluginLayoutSelectorSection"; function stubPlugin(pluginId: string, displayName: string, layoutType: string): LoadedPlugin { const contributes = { menus: [], menuItems: [], layouts: [{ type: layoutType, label: `${displayName} layout`, component: "X" }], mcpServers: [], }; return { pluginId, displayName, contributes, commands: new PluginCommandRegistry(pluginId, new Set()), layouts: new PluginLayoutRegistry(pluginId, new Set([layoutType])), menu: new PluginMenuRegistry(pluginId), dispose: async () => {}, }; } describe("listPluginLayoutChoices / PluginLayoutSelectorSection", () => { it("only lists loaded (enabled) plugins' layouts — a disabled plugin is never in the registry", () => { const registry = new PluginRuntimeRegistry(); registry.add(stubPlugin("dev.acme.one", "One", "dev.acme.one.layout")); // "dev.acme.two" is disabled ⇒ never loaded ⇒ never added to the registry // (carnet §1.3) — nothing to filter here beyond what's already loaded. const choices = listPluginLayoutChoices(registry); expect(choices).toHaveLength(1); expect(choices[0].pluginId).toBe("dev.acme.one"); }); it("renders nothing when there are no plugin layouts", () => { const { container } = render( {}} />, ); expect(container.innerHTML).toBe(""); }); it("calls onSelect with the chosen plugin layout", () => { const registry = new PluginRuntimeRegistry(); registry.add(stubPlugin("dev.acme.one", "One", "dev.acme.one.layout")); let selected: string | null = null; render( { selected = choice.layout.type; }} />, ); fireEvent.click(screen.getByRole("menuitem", { name: /One layout/ })); expect(selected).toBe("dev.acme.one.layout"); }); it("skips malformed layout contributions instead of throwing during sorting", () => { const registry = new PluginRuntimeRegistry(); const contributes = { menus: [], menuItems: [], layouts: [ { type: "dev.acme.good", label: "Good layout", component: "X" }, null, { type: "dev.acme.no-label", label: undefined, component: "X" }, { type: undefined, label: "No type", component: "X" }, ] as unknown as PluginContributionDto["layouts"], mcpServers: [], }; registry.add({ pluginId: "dev.acme.bad", displayName: undefined as unknown as string, contributes, commands: new PluginCommandRegistry("dev.acme.bad", new Set()), layouts: new PluginLayoutRegistry("dev.acme.bad", new Set()), menu: new PluginMenuRegistry("dev.acme.bad"), dispose: async () => {}, }); const choices = listPluginLayoutChoices(registry); expect(choices).toHaveLength(1); expect(choices[0].layout.label).toBe("Good layout"); expect(choices[0].pluginDisplayName).toBe("dev.acme.bad"); }); });