Files
IdeaSDK/frontend/src/features/plugins/PluginLayoutSelectorSection.test.tsx

100 lines
3.5 KiB
TypeScript

/**
* 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(
<PluginLayoutSelectorSection registry={new PluginRuntimeRegistry()} onSelect={() => {}} />,
);
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(
<PluginLayoutSelectorSection
registry={registry}
onSelect={(choice) => {
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");
});
});