import { createElement, isValidElement, type ReactElement, type ReactNode } from "react"; import { cleanup, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { pluginRegistry } from "@/lib/plugins/registry"; import { renderSettingsRoute } from "./settings-routes"; vi.mock("@/components/settings/workspaces/workspace-settings-shell", () => ({ WorkspaceSettingsShell: ({ children }: { children: ReactNode }) => children, })); vi.mock("@/hooks/domains/plugins/use-plugins", () => ({ usePlugins: () => ({ items: [{ id: "plugin-a" }], loaded: true, loading: false, error: null, }), })); vi.mock("@/components/settings/plugins/plugin-shortcuts-card", () => ({ PluginShortcutsCard: () => createElement("div", null, "Host-owned plugin shortcuts"), })); const PLUGIN_ID = "plugin-a"; const PLUGIN_SETTINGS_PATH = "/settings/plugins/plugin-a/config"; const PLUGIN_INTEGRATION_ID = "source-control"; function cleanupPlugins(...pluginIds: string[]) { pluginIds.forEach((id) => pluginRegistry.unregisterPlugin(id)); } describe("renderSettingsRoute — plugin fallthrough", () => { afterEach(() => cleanupPlugins(PLUGIN_ID)); it("falls back to the unported-route placeholder when no plugin owns the path", () => { const route = renderSettingsRoute(PLUGIN_SETTINGS_PATH); expect(isValidElement(route)).toBe(true); // The fallback renders the raw pathname as text — assert via string search // on the rendered props rather than a brittle component-type check. expect((route as ReactElement<{ pathname?: string }>).props.pathname).toBe( PLUGIN_SETTINGS_PATH, ); }); it("renders the plugin-registered settings route once a plugin registers it", () => { function PluginSettingsPage() { return "PluginSettingsPage:rendered"; } pluginRegistry .forPlugin(PLUGIN_ID) .registerSettingsRoute(PLUGIN_SETTINGS_PATH, PluginSettingsPage); const route = renderSettingsRoute(PLUGIN_SETTINGS_PATH); expect(isValidElement(route)).toBe(true); render(route as ReactElement); expect(screen.getByText("PluginSettingsPage:rendered")).not.toBeNull(); cleanup(); }); it("keeps host-owned shortcuts on an exact plugin detail route replacement", () => { function PluginSettingsPage() { return createElement("div", null, "Plugin-owned detail content"); } const detailPath = `/settings/plugins/${PLUGIN_ID}`; pluginRegistry.forPlugin(PLUGIN_ID).registerSettingsRoute(detailPath, PluginSettingsPage); render(renderSettingsRoute(detailPath) as ReactElement); expect(screen.getByText("Plugin-owned detail content")).not.toBeNull(); expect(screen.getByText("Host-owned plugin shortcuts")).not.toBeNull(); cleanup(); }); it("renders a fallback instead of throwing when the plugin settings route component throws", () => { const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); function ThrowingSettingsPage(): never { throw new Error("boom"); } pluginRegistry .forPlugin(PLUGIN_ID) .registerSettingsRoute(PLUGIN_SETTINGS_PATH, ThrowingSettingsPage); const route = renderSettingsRoute(PLUGIN_SETTINGS_PATH); render(route as ReactElement); expect(screen.getByText(/this plugin page failed to load/i)).not.toBeNull(); cleanup(); errorSpy.mockRestore(); }); it("does not consult the registry for a path outside /settings/plugins/", () => { function ShouldNotMatch() { return null; } pluginRegistry.forPlugin(PLUGIN_ID).registerSettingsRoute("/settings/general", ShouldNotMatch); const route = renderSettingsRoute("/settings/general"); // "/settings/general" is a first-party route (a redirect to its first page), // not the plugin's. expect((route as ReactElement).type).not.toBe(ShouldNotMatch); }); }); describe("renderSettingsRoute — plugin integration settings", () => { afterEach(() => { cleanupPlugins(PLUGIN_ID); cleanup(); }); it("redirects a registered integration global route into the active workspace", () => { function IntegrationSettings({ workspaceId }: { workspaceId?: string }) { return `PluginIntegration:${workspaceId ?? "global"}`; } pluginRegistry.forPlugin(PLUGIN_ID).registerIntegrationSettings({ id: PLUGIN_INTEGRATION_ID, label: "Source Control", description: "Configure source control.", Component: IntegrationSettings, }); const route = renderSettingsRoute( `/settings/integrations/${PLUGIN_INTEGRATION_ID}`, ) as ReactElement; expect((route.type as { name?: string }).name).toBe("ActiveWorkspaceSectionRedirect"); expect(route.props).toEqual({ section: `integrations/${PLUGIN_INTEGRATION_ID}` }); }); it("passes the decoded workspace id to a registered integration", () => { function IntegrationSettings({ workspaceId }: { workspaceId?: string }) { return `PluginIntegration:${workspaceId}`; } pluginRegistry.forPlugin(PLUGIN_ID).registerIntegrationSettings({ id: PLUGIN_INTEGRATION_ID, label: "Source Control", description: "Configure source control.", Component: IntegrationSettings, }); render( renderSettingsRoute( `/settings/workspaces/workspace%20one/integrations/${PLUGIN_INTEGRATION_ID}`, ) as ReactElement, ); expect(screen.getByText("PluginIntegration:workspace one")).not.toBeNull(); }); it("wraps plugin content in the native integration settings section", () => { function PluginProviderIcon({ className }: { className?: string }) { return createElement("svg", { className, "data-testid": "plugin-provider-icon" }); } pluginRegistry.forPlugin(PLUGIN_ID).registerIntegrationSettings({ id: PLUGIN_INTEGRATION_ID, label: "Acme Source Control", description: "Configure Acme source control.", icon: PluginProviderIcon, Component: () => "Plugin-owned settings cards", }); render( renderSettingsRoute( `/settings/workspaces/workspace-1/integrations/${PLUGIN_INTEGRATION_ID}`, ) as ReactElement, ); expect(screen.getByRole("heading", { name: "Acme Source Control" })).not.toBeNull(); expect(screen.getByText("Configure Acme source control.")).not.toBeNull(); expect(screen.getByTestId("plugin-provider-icon")).not.toBeNull(); expect(screen.getByText("Plugin-owned settings cards")).not.toBeNull(); }); it("contains errors thrown by a plugin integration settings component", () => { const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); function ThrowingIntegration(): never { throw new Error("boom"); } pluginRegistry.forPlugin(PLUGIN_ID).registerIntegrationSettings({ id: PLUGIN_INTEGRATION_ID, label: "Source Control", description: "Configure source control.", Component: ThrowingIntegration, }); render( renderSettingsRoute( `/settings/workspaces/workspace-1/integrations/${PLUGIN_INTEGRATION_ID}`, ) as ReactElement, ); expect(screen.getByText(/this plugin page failed to load/i)).not.toBeNull(); errorSpy.mockRestore(); }); });