feat(frontend): système de plugins — runtime, menus, layouts custom (#43)
Lots F1-F4 : runtime de chargement/registre plugin, extension des menus existants, panneau de gestion des plugins, types de layout custom (sélecteur, fallback, cellule dédiée) branchés sur le port plugin. Suite npm typecheck/test verte (947/947). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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 { 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user