import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { KnowledgeGraphViewer } from "./KnowledgeGraphViewer"; import * as knowledgeApi from "@/lib/api/knowledge"; // Mock the knowledge API vi.mock("@/lib/api/knowledge"); // Mock Next.js router const mockPush = vi.fn(); vi.mock("next/navigation", () => ({ useRouter: (): { push: typeof mockPush; replace: () => void; prefetch: () => void; back: () => void; forward: () => void; refresh: () => void; } => ({ push: mockPush, replace: vi.fn(), prefetch: vi.fn(), back: vi.fn(), forward: vi.fn(), refresh: vi.fn(), }), })); // Mock ReactFlow since it requires DOM APIs not available in test environment vi.mock("@xyflow/react", () => ({ ReactFlow: ({ nodes, edges, children, }: { nodes: unknown[]; edges: unknown[]; children: React.ReactNode; }): React.JSX.Element => (
{nodes.length}
{edges.length}
{children}
), Background: (): React.JSX.Element =>
, Controls: (): React.JSX.Element =>
, MiniMap: (): React.JSX.Element =>
, Panel: ({ children }: { children: React.ReactNode }): React.JSX.Element => (
{children}
), useNodesState: (initial: unknown[]): [unknown[], () => void, () => void] => [ initial, vi.fn(), vi.fn(), ], useEdgesState: (initial: unknown[]): [unknown[], () => void, () => void] => [ initial, vi.fn(), vi.fn(), ], MarkerType: { ArrowClosed: "arrowclosed" }, BackgroundVariant: { Dots: "dots" }, })); const mockGraphData = { nodes: [ { id: "node-1", slug: "test-entry-1", title: "Test Entry 1", summary: "Test summary 1", status: "PUBLISHED", tags: [{ id: "tag-1", name: "Tag 1", slug: "tag-1", color: "#3B82F6" }], depth: 0, isOrphan: false, }, { id: "node-2", slug: "test-entry-2", title: "Test Entry 2", summary: "Test summary 2", status: "DRAFT", tags: [], depth: 1, isOrphan: false, }, { id: "node-3", slug: "test-entry-3", title: "Orphan Entry", summary: "No connections", status: "PUBLISHED", tags: [], depth: 0, isOrphan: true, }, ], edges: [ { id: "edge-1", sourceId: "node-1", targetId: "node-2", linkText: "link text", }, ], stats: { totalNodes: 3, totalEdges: 1, orphanCount: 1, }, }; describe("KnowledgeGraphViewer", (): void => { beforeEach((): void => { vi.clearAllMocks(); }); describe("rendering", (): void => { it("should render loading state initially", (): void => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockImplementation( // eslint-disable-next-line @typescript-eslint/no-empty-function () => new Promise(() => {}) // Never resolves ); render(); expect(screen.getByTestId("loading-spinner")).toBeInTheDocument(); }); it("should render error state when fetch fails", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockRejectedValue( new Error("Failed to fetch graph") ); render(); await waitFor(() => { expect(screen.getByText(/error loading graph/i)).toBeInTheDocument(); expect(screen.getByText(/failed to fetch graph/i)).toBeInTheDocument(); }); }); it("should render graph when data loads successfully", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); expect(screen.getByTestId("node-count")).toHaveTextContent("3"); expect(screen.getByTestId("edge-count")).toHaveTextContent("1"); }); }); it("should render graph controls", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByTestId("controls")).toBeInTheDocument(); expect(screen.getByTestId("minimap")).toBeInTheDocument(); }); }); it("should display statistics in panel", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByText(/3 entries/i)).toBeInTheDocument(); expect(screen.getByText(/1 connection/i)).toBeInTheDocument(); }); }); }); describe("node sizing", (): void => { it("should size nodes based on number of connections", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { const reactFlow = screen.getByTestId("react-flow"); expect(reactFlow).toBeInTheDocument(); }); // Node sizes should be calculated based on connection count // node-1 has 1 connection, node-2 has 1 connection, node-3 has 0 connections }); }); describe("node coloring", (): void => { it("should color nodes based on entry status", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); }); // Colors should follow PDA-friendly guidelines: // PUBLISHED -> green (active) // DRAFT -> blue (scheduled/upcoming) // ARCHIVED -> gray (dormant) }); it("should highlight orphan nodes differently", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); }); // Orphan nodes should have visual indication }); }); describe("layout controls", (): void => { it("should render layout toggle buttons", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByRole("button", { name: /force/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /hierarchical/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /circular/i })).toBeInTheDocument(); }); }); it("should switch layout when button is clicked", async (): Promise => { const user = userEvent.setup(); vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByRole("button", { name: /force/i })).toBeInTheDocument(); }); const hierarchicalButton = screen.getByRole("button", { name: /hierarchical/i }); await user.click(hierarchicalButton); // Should apply hierarchical layout expect(hierarchicalButton).toHaveClass("bg-blue-500"); }); }); describe("filtering", (): void => { it("should allow filtering by tags", async (): Promise => { const user = userEvent.setup(); vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); }); const filterInput = screen.getByPlaceholderText(/filter by tags/i); await user.type(filterInput, "Tag 1"); // Should filter nodes }); it("should allow filtering by status", async (): Promise => { const user = userEvent.setup(); vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); }); const statusFilter = screen.getByRole("combobox", { name: /status/i }); await user.selectOptions(statusFilter, "PUBLISHED"); // Should filter nodes by status }); it("should show orphan nodes when filter is enabled", async (): Promise => { const user = userEvent.setup(); vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); }); const orphanToggle = screen.getByRole("switch", { name: /show orphans/i }); await user.click(orphanToggle); // Should toggle orphan visibility }); }); describe("node interaction", (): void => { it("should navigate to entry when node is clicked", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); }); // Click functionality will be tested in integration tests // Unit test verifies the handler is set up correctly }); it("should show node details on hover", async (): Promise => { vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(mockGraphData); render(); await waitFor(() => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); }); // Hover behavior will be verified in the actual component }); }); describe("performance", (): void => { it("should handle large graphs with 500+ nodes", async (): Promise => { const largeGraph = { nodes: Array.from({ length: 500 }, (_, i) => ({ id: `node-${String(i)}`, slug: `entry-${String(i)}`, title: `Entry ${String(i)}`, summary: `Summary ${String(i)}`, status: "PUBLISHED", tags: [], depth: 0, isOrphan: false, })), edges: Array.from({ length: 600 }, (_, i) => ({ id: `edge-${String(i)}`, sourceId: `node-${String(i % 500)}`, targetId: `node-${String((i + 1) % 500)}`, linkText: "link", })), stats: { totalNodes: 500, totalEdges: 600, orphanCount: 0, }, }; vi.mocked(knowledgeApi.fetchKnowledgeGraph).mockResolvedValue(largeGraph); const startTime = performance.now(); render(); await waitFor(() => { expect(screen.getByTestId("react-flow")).toBeInTheDocument(); }); const endTime = performance.now(); const renderTime = endTime - startTime; // Should render in reasonable time (< 3 seconds) expect(renderTime).toBeLessThan(3000); }); }); });