feat(FN-3091): add plugin view remount routing test coverage

Adds test coverage for plugin view remount routing behavior in the App component.

Fusion-Task-Id: FN-3091
This commit is contained in:
Fusion
2026-05-07 07:08:11 -07:00
committed by gsxdsm
parent 5ffd1485c3
commit 4338b590c6
10 changed files with 480 additions and 0 deletions

View File

@@ -1730,6 +1730,44 @@ export default plugin;
"views-b:timeline",
]);
expect(loader.getPluginUiSlots()).toHaveLength(1);
expect(loader.getPluginTools()).toEqual([]);
expect(loader.getPluginRoutes()).toEqual([]);
});
it("returns pluginId and complete view payload for each dashboard view entry", async () => {
await pluginStore.init();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
(loader as any).plugins.set("views-shape", {
manifest: makeManifest({ id: "views-shape" }),
state: "started",
hooks: {},
dashboardViews: [
{
viewId: "graph",
label: "Graph",
componentPath: "./graph.js",
icon: "Network",
placement: "more",
description: "Task dependency graph",
order: 40,
},
],
} as FusionPlugin);
expect(loader.getPluginDashboardViews()).toEqual([
{
pluginId: "views-shape",
view: {
viewId: "graph",
label: "Graph",
componentPath: "./graph.js",
icon: "Network",
placement: "more",
description: "Task dependency graph",
order: 40,
},
},
]);
});
});

View File

@@ -1784,6 +1784,48 @@ describe("App view switching", () => {
localStorage.removeItem("kb-dashboard-view-mode");
});
it("restores board and plugin routes when persisted taskView changes across remounts", async () => {
localStorage.setItem("kb-dashboard-view-mode", "project");
localStorage.setItem(taskViewStorageKey(), "plugin:fusion-plugin-dependency-graph:graph");
(fetchPluginDashboardViews as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
{
pluginId: "fusion-plugin-dependency-graph",
view: { viewId: "graph", label: "Graph", componentPath: "./GraphView", placement: "more" },
},
]);
const first = render(<App />);
await waitFor(() => {
expect(screen.getByText("Plugin view unavailable")).toBeInTheDocument();
});
first.unmount();
localStorage.setItem(taskViewStorageKey(), "board");
const second = render(<App />);
await waitFor(() => {
expect(screen.getByTitle("Board view").className).toContain("active");
});
second.unmount();
localStorage.setItem(taskViewStorageKey(), "plugin:fusion-plugin-dependency-graph:graph");
(fetchPluginDashboardViews as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
{
pluginId: "fusion-plugin-dependency-graph",
view: { viewId: "graph", label: "Graph", componentPath: "./GraphView", placement: "more" },
},
]);
render(<App />);
await waitFor(() => {
expect(screen.getByText("Plugin view unavailable")).toBeInTheDocument();
});
localStorage.removeItem(taskViewStorageKey());
localStorage.removeItem("kb-dashboard-view-mode");
});
it("opens planning mode when TodoView triggers planning from todo item", async () => {
localStorage.setItem("kb-dashboard-view-mode", "project");
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({

View File

@@ -101,4 +101,19 @@ describe("usePluginDashboardViews", () => {
await waitFor(() => expect(second.result.current.loading).toBe(false));
expect(mockFetch).toHaveBeenCalledWith("project-b");
});
it("supports filtering view entries by pluginId in consumers", async () => {
mockFetch.mockResolvedValueOnce([
{ pluginId: "fusion-plugin-dependency-graph", view: { viewId: "graph", label: "Graph", componentPath: "./Graph.js" } },
{ pluginId: "fusion-plugin-queue", view: { viewId: "queue", label: "Queue", componentPath: "./Queue.js" } },
]);
const { result } = renderHook(() => usePluginDashboardViews("project-a"));
await waitFor(() => expect(result.current.loading).toBe(false));
const graphViews = result.current.views.filter((entry) => entry.pluginId === "fusion-plugin-dependency-graph");
expect(graphViews).toEqual([
{ pluginId: "fusion-plugin-dependency-graph", view: { viewId: "graph", label: "Graph", componentPath: "./Graph.js" } },
]);
});
});

View File

@@ -367,6 +367,34 @@ describe("useViewState", () => {
});
});
it("round-trips between built-in and plugin task views", async () => {
localStorage.setItem("kb:proj_123:kb-dashboard-task-view", "board");
const { result } = renderHook(() =>
useViewState(
createOptions({
currentProject: PROJECT,
}),
),
);
await waitFor(() => {
expect(result.current.taskView).toBe("board");
});
await act(async () => {
result.current.handleChangeTaskView("plugin:fusion-plugin-dependency-graph:graph");
});
expect(result.current.taskView).toBe("plugin:fusion-plugin-dependency-graph:graph");
expect(localStorage.getItem("kb:proj_123:kb-dashboard-task-view")).toBe("plugin:fusion-plugin-dependency-graph:graph");
await act(async () => {
result.current.handleChangeTaskView("board");
});
expect(result.current.taskView).toBe("board");
expect(localStorage.getItem("kb:proj_123:kb-dashboard-task-view")).toBe("board");
});
it("restores legacy views (board/list/agents/missions/chat) from scoped storage", async () => {
const legacyViews = ["board", "list", "agents", "missions", "chat"] as const;

View File

@@ -34,6 +34,22 @@ describe("pluginViewRegistry", () => {
expect(getPluginViewComponent("plugin-b", "missing")).toBeNull();
});
it("keeps all registered plugin views discoverable by key iteration", () => {
const ViewA = lazy(async () => ({ default: () => <div>A</div> }));
const ViewB = lazy(async () => ({ default: () => <div>B</div> }));
registerPluginView("plugin-a", "main", ViewA);
registerPluginView("plugin-b", "graph", ViewB);
const knownIds = [getPluginViewId("plugin-a", "main"), getPluginViewId("plugin-b", "graph")];
const resolved = knownIds.map((id) => {
const parsed = parsePluginViewId(id);
if (!parsed) return null;
return getPluginViewComponent(parsed.pluginId, parsed.viewId);
});
expect(resolved).toEqual([ViewA, ViewB]);
});
it("renders registered components", async () => {
const View = lazy(async () => ({ default: () => <div>Rendered Plugin View</div> }));
registerPluginView("plugin-a", "main", View);

View File

@@ -784,6 +784,8 @@ describe("GET /api/plugins/dashboard-views", () => {
componentPath: "./views/Graph.js",
icon: "Network",
placement: "more",
order: 40,
description: "Dependency graph",
},
},
];
@@ -794,6 +796,28 @@ describe("GET /api/plugins/dashboard-views", () => {
expect(res.status).toBe(200);
expect(res.body).toEqual(mockViews);
});
it("returns exactly pluginLoader dashboard-view entries (no synthesized plugin rows)", async () => {
(pluginLoader.getPluginDashboardViews as ReturnType<typeof vi.fn>).mockReturnValue([
{
pluginId: "with-view",
view: {
viewId: "graph",
label: "Graph",
componentPath: "./Graph.js",
},
},
]);
const res = await performGet(buildApp(), "/api/plugins/dashboard-views");
expect(res.status).toBe(200);
expect(res.body).toHaveLength(1);
expect(res.body[0]).toMatchObject({
pluginId: "with-view",
view: { viewId: "graph", label: "Graph", componentPath: "./Graph.js" },
});
});
});
describe("GET /api/plugins/ui-slots", () => {