Files
fusion/packages/dashboard/app/components/__tests__/PluginSlot.test.tsx
Fusion 513cab6fc4 feat(FN-1925): add plugin UI slot infrastructure
- Add fetchPluginUiSlots API wrapper with PluginUiSlotEntry type
- Add usePluginUiSlots hook with 30-second TTL cache for slot data
- Add PluginSlot component with error boundary and placeholder rendering
- Add comprehensive tests for hook (cache behavior, error handling, isolation)
- Add comprehensive tests for component (render states, error boundary)
2026-04-16 10:04:44 -07:00

139 lines
4.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render } from "@testing-library/react";
import { PluginSlot } from "../PluginSlot";
import type { PluginUiSlotEntry } from "../../api";
import { usePluginUiSlots } from "../../hooks/usePluginUiSlots";
vi.mock("../../hooks/usePluginUiSlots");
function createSlotEntry(slotId: string, pluginId = "test-plugin"): PluginUiSlotEntry {
return {
pluginId,
slot: {
slotId,
label: `Test slot ${slotId}`,
componentPath: `./components/${slotId}.js`,
},
};
}
describe("PluginSlot", () => {
beforeEach(() => {
vi.mocked(usePluginUiSlots).mockReset();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("renders nothing when no matching slots (empty array)", () => {
vi.mocked(usePluginUiSlots).mockReturnValue({
slots: [],
getSlotsForId: vi.fn(() => []),
loading: false,
error: null,
});
const { container } = render(<PluginSlot slotId="task-detail-tab" />);
expect(container.firstChild).toBeNull();
});
it("renders placeholder div for single matching slot", () => {
const entry = createSlotEntry("task-detail-tab", "plugin-a");
vi.mocked(usePluginUiSlots).mockReturnValue({
slots: [entry],
getSlotsForId: vi.fn(() => [entry]),
loading: false,
error: null,
});
const { container } = render(<PluginSlot slotId="task-detail-tab" />);
const divs = container.querySelectorAll("[data-plugin-slot]");
expect(divs).toHaveLength(1);
const div = divs[0];
expect(div).toHaveAttribute("data-slot-id", "task-detail-tab");
expect(div).toHaveAttribute("data-plugin-id", "plugin-a");
expect(div).toHaveAttribute("data-component-path", "./components/task-detail-tab.js");
expect(div).toHaveAttribute("aria-label", "Test slot task-detail-tab");
});
it("renders multiple placeholders for multiple plugins registered for same slotId", () => {
const entryA = createSlotEntry("board-column-footer", "plugin-x");
const entryB = createSlotEntry("board-column-footer", "plugin-y");
vi.mocked(usePluginUiSlots).mockReturnValue({
slots: [entryA, entryB],
getSlotsForId: vi.fn(() => [entryA, entryB]),
loading: false,
error: null,
});
const { container } = render(<PluginSlot slotId="board-column-footer" />);
const divs = container.querySelectorAll("[data-plugin-slot]");
expect(divs).toHaveLength(2);
// Verify both divs have correct attributes
expect(divs[0]).toHaveAttribute("data-plugin-id", "plugin-x");
expect(divs[0]).toHaveAttribute("data-slot-id", "board-column-footer");
expect(divs[1]).toHaveAttribute("data-plugin-id", "plugin-y");
expect(divs[1]).toHaveAttribute("data-slot-id", "board-column-footer");
});
it("returns null when loading", () => {
vi.mocked(usePluginUiSlots).mockReturnValue({
slots: [],
getSlotsForId: vi.fn(() => []),
loading: true,
error: null,
});
const { container } = render(<PluginSlot slotId="header-action" />);
expect(container.firstChild).toBeNull();
});
it("returns null on error", () => {
vi.mocked(usePluginUiSlots).mockReturnValue({
slots: [],
getSlotsForId: vi.fn(() => []),
loading: false,
error: "fetch failed",
});
const { container } = render(<PluginSlot slotId="header-action" />);
expect(container.firstChild).toBeNull();
});
it("passes projectId to usePluginUiSlots hook", () => {
vi.mocked(usePluginUiSlots).mockReturnValue({
slots: [],
getSlotsForId: vi.fn(() => []),
loading: false,
error: null,
});
render(<PluginSlot slotId="settings-section" projectId="proj-1" />);
expect(vi.mocked(usePluginUiSlots)).toHaveBeenCalledWith("proj-1");
});
it("returns null for empty string slotId", () => {
const getSlotsForId = vi.fn(() => []);
vi.mocked(usePluginUiSlots).mockReturnValue({
slots: [],
getSlotsForId,
loading: false,
error: null,
});
const { container } = render(<PluginSlot slotId="" />);
expect(container.firstChild).toBeNull();
// getSlotsForId should not be called when slotId is falsy
expect(getSlotsForId).not.toHaveBeenCalled();
});
// NOTE: Error boundary testing should be added when dynamic component loading
// is implemented. The ErrorBoundary wraps the rendered divs and catches any
// rendering errors from future plugin components.
});