Files
kandev/apps/web/src/settings-routes.plugin.test.ts
T
2026-09-04 17:38:05 +00:00

197 lines
7.1 KiB
TypeScript

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();
});
});