// @vitest-environment happy-dom import "@testing-library/jest-dom/vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { vi, describe, it, expect, beforeEach } from "vitest"; import { FileCard } from "./FileCard"; import type { FileDiff, FileDiffDetail } from "./types"; // ── Module mocks ────────────────────────────────────────────────────────────── vi.mock("./HunkRows", () => ({ HunkRows: ({ hunk }: { hunk: { header: string } }) => ( {hunk.header} ), })); vi.mock("./use-syntax-highlight", () => ({ useHighlightedFile: () => null, })); // Hoist mocks so they can be referenced in vi.mock factories const { mockGetFileDiff, mockParseUnifiedDiff } = vi.hoisted(() => ({ mockGetFileDiff: vi.fn(), mockParseUnifiedDiff: vi.fn(), })); vi.mock("@/lib/trpc", () => ({ trpc: { getFileDiff: { useQuery: ( input: unknown, opts: { enabled: boolean; staleTime?: number }, ) => mockGetFileDiff(input, opts), }, }, })); vi.mock("./parse-diff", () => ({ parseUnifiedDiff: (rawDiff: string) => mockParseUnifiedDiff(rawDiff), })); // ── Helpers ─────────────────────────────────────────────────────────────────── function makeFile(overrides: Partial = {}): FileDiff { return { oldPath: "src/foo.ts", newPath: "src/foo.ts", status: "modified", additions: 10, deletions: 5, ...overrides, }; } const defaultProps = { phaseId: "phase-1", commitMode: false, commentsByLine: new Map(), onAddComment: vi.fn(), onResolveComment: vi.fn(), onUnresolveComment: vi.fn(), }; beforeEach(() => { mockGetFileDiff.mockReturnValue({ data: undefined, isLoading: false, isError: false, refetch: vi.fn(), }); // Default: return empty parse result mockParseUnifiedDiff.mockReturnValue([]); }); // ── Tests ───────────────────────────────────────────────────────────────────── describe("FileCard", () => { it("starts collapsed and does not enable getFileDiff query", () => { render(); // Query must be called with enabled: false while card is collapsed expect(mockGetFileDiff).toHaveBeenCalledWith( expect.objectContaining({ filePath: encodeURIComponent("src/foo.ts"), }), expect.objectContaining({ enabled: false }), ); // No hunk rows rendered in the collapsed state expect(screen.queryByTestId("hunk-row")).toBeNull(); }); it("enables query and shows loading spinner when expanded", () => { mockGetFileDiff.mockReturnValue({ data: undefined, isLoading: true, isError: false, refetch: vi.fn(), }); render(); fireEvent.click(screen.getByRole("button")); // After expanding, query should be called with enabled: true expect(mockGetFileDiff).toHaveBeenLastCalledWith( expect.anything(), expect.objectContaining({ enabled: true }), ); // Loading spinner should be visible expect(screen.getByText(/Loading diff/i)).toBeInTheDocument(); }); it("renders HunkRows when query succeeds", async () => { mockGetFileDiff.mockReturnValue({ data: { binary: false, rawDiff: "diff --git a/src/foo.ts b/src/foo.ts\n@@ -1,3 +1,3 @@\n context\n", }, isLoading: false, isError: false, refetch: vi.fn(), }); mockParseUnifiedDiff.mockReturnValue([ { oldPath: "src/foo.ts", newPath: "src/foo.ts", status: "modified", additions: 0, deletions: 0, hunks: [ { header: "@@ -1,3 +1,3 @@", oldStart: 1, oldCount: 3, newStart: 1, newCount: 3, lines: [], }, ], }, ]); render(); fireEvent.click(screen.getByRole("button")); await waitFor(() => { expect(screen.getByTestId("hunk-row")).toBeInTheDocument(); }); }); it("shows error state with Retry button; clicking retry calls refetch", () => { const refetch = vi.fn(); mockGetFileDiff.mockReturnValue({ data: undefined, isLoading: false, isError: true, refetch, }); render(); fireEvent.click(screen.getByRole("button")); expect(screen.getByText(/Failed to load diff/i)).toBeInTheDocument(); const retryBtn = screen.getByRole("button", { name: /retry/i }); fireEvent.click(retryBtn); expect(refetch).toHaveBeenCalledOnce(); }); it("shows binary message on expand and does not enable getFileDiff query", () => { render(); fireEvent.click(screen.getByRole("button")); expect(screen.getByText(/Binary file/i)).toBeInTheDocument(); // Query must never be enabled for binary files expect(mockGetFileDiff).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ enabled: false }), ); }); it("shows No content changes when parsed hunks array is empty", async () => { mockGetFileDiff.mockReturnValue({ data: { binary: false, rawDiff: "diff --git a/src/foo.ts b/src/foo.ts\nsome content\n", }, isLoading: false, isError: false, refetch: vi.fn(), }); mockParseUnifiedDiff.mockReturnValue([ { oldPath: "src/foo.ts", newPath: "src/foo.ts", status: "modified", additions: 0, deletions: 0, hunks: [], }, ]); render(); fireEvent.click(screen.getByRole("button")); await waitFor(() => { expect(screen.getByText(/No content changes/i)).toBeInTheDocument(); }); }); it("renders pre-parsed hunks from detail prop without fetching", () => { const detail: FileDiffDetail = { oldPath: "src/foo.ts", newPath: "src/foo.ts", status: "modified", additions: 5, deletions: 2, hunks: [ { header: "@@ -1 +1 @@", oldStart: 1, oldCount: 1, newStart: 1, newCount: 1, lines: [], }, ], }; render(); // Should start expanded because detail prop is provided expect(screen.getByTestId("hunk-row")).toBeInTheDocument(); // Query must not be enabled when detail prop is present expect(mockGetFileDiff).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ enabled: false }), ); }); it("does not refetch when collapsing and re-expanding", () => { // Simulate data already available (as if previously fetched and cached) mockGetFileDiff.mockReturnValue({ data: { binary: false, rawDiff: "" }, isLoading: false, isError: false, refetch: vi.fn(), }); render(); const headerBtn = screen.getByRole("button"); // Expand: query enabled, data shown immediately (no loading) fireEvent.click(headerBtn); expect(screen.queryByText(/Loading diff/i)).toBeNull(); // Collapse fireEvent.click(headerBtn); // Re-expand: should not enter loading state (data still available) fireEvent.click(headerBtn); expect(screen.queryByText(/Loading diff/i)).toBeNull(); }); });