100 lines
3.5 KiB
TypeScript
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");
|
|
});
|
|
});
|