import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { CommitDiffTab } from "../CommitDiffTab";
import type { MergeDetails } from "@fusion/core";
const mockFetchCommitDiff = vi.fn();
vi.mock("../../api", () => ({
fetchCommitDiff: (...args: any[]) => mockFetchCommitDiff(...args),
}));
vi.mock("lucide-react", () => ({
FileCode: () => null,
ChevronDown: ({ size }: any) => ,
ChevronRight: ({ size }: any) => ,
AlertCircle: () => null,
GitCommit: () => null,
}));
vi.mock("../../utils/highlightDiff", () => ({
highlightDiff: (diff: string) => diff,
}));
const SAMPLE_PATCH = `diff --git a/src/app.ts b/src/app.ts
index abc1234..def5678 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,3 +1,4 @@
import express from "express";
+import cors from "cors";
const app = express();
app.listen(3000);
diff --git a/src/new-file.ts b/src/new-file.ts
new file mode 100644
index 0000000..abc1234
--- /dev/null
+++ b/src/new-file.ts
@@ -0,0 +1,2 @@
+export function hello() {}
+export function world() {}
diff --git a/src/removed.ts b/src/removed.ts
deleted file mode 100644
index abc1234..0000000
--- a/src/removed.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const old = true;`;
const MERGE_DETAILS: MergeDetails = {
commitSha: "abc1234567890def",
filesChanged: 3,
insertions: 3,
deletions: 1,
mergeCommitMessage: "Merge branch 'fusion/fn-001' into main",
mergedAt: "2026-01-01T00:00:00Z",
};
beforeEach(() => {
mockFetchCommitDiff.mockReset();
});
describe("CommitDiffTab", () => {
it("renders loading state initially", () => {
mockFetchCommitDiff.mockReturnValue(new Promise(() => {})); // never resolves
render();
expect(screen.getByText("Loading commit diff...")).toBeTruthy();
});
it("renders error state when fetch fails", async () => {
mockFetchCommitDiff.mockRejectedValue(new Error("Network error"));
render();
await waitFor(() => {
expect(screen.getByText(/Error loading commit diff: Network error/)).toBeTruthy();
});
});
it("renders empty state when no commit SHA", () => {
render();
expect(screen.getByText("No commit SHA available.")).toBeTruthy();
expect(mockFetchCommitDiff).not.toHaveBeenCalled();
});
it("renders file list with correct file paths and statuses after successful fetch", async () => {
mockFetchCommitDiff.mockResolvedValue({ stat: "", patch: SAMPLE_PATCH });
render();
await waitFor(() => {
expect(screen.getByText("src/app.ts")).toBeTruthy();
});
expect(screen.getByText("src/new-file.ts")).toBeTruthy();
expect(screen.getByText("src/removed.ts")).toBeTruthy();
// Check status badges - M for modified, A for added, D for deleted
const statusElements = screen.getAllByTitle(/added|modified|deleted/);
expect(statusElements).toHaveLength(3);
expect(statusElements.find((el) => el.textContent === "M" && el.title === "modified")).toBeTruthy();
expect(statusElements.find((el) => el.textContent === "A" && el.title === "added")).toBeTruthy();
expect(statusElements.find((el) => el.textContent === "D" && el.title === "deleted")).toBeTruthy();
});
it("toggling file expansion shows/hides diff content", async () => {
mockFetchCommitDiff.mockResolvedValue({ stat: "", patch: SAMPLE_PATCH });
const { container } = render();
await waitFor(() => {
expect(screen.getByText("src/app.ts")).toBeTruthy();
});
// First file should be auto-expanded
let diffBlocks = container.querySelectorAll(".changes-file-content");
expect(diffBlocks.length).toBe(1);
// Click on first file to collapse it
fireEvent.click(screen.getByText("src/app.ts"));
diffBlocks = container.querySelectorAll(".changes-file-content");
expect(diffBlocks.length).toBe(0);
// Click on second file to expand it
fireEvent.click(screen.getByText("src/new-file.ts"));
diffBlocks = container.querySelectorAll(".changes-file-content");
expect(diffBlocks.length).toBe(1);
});
it("displays merge commit metadata (SHA and message)", async () => {
mockFetchCommitDiff.mockResolvedValue({ stat: "", patch: SAMPLE_PATCH });
render();
await waitFor(() => {
expect(screen.getByText("abc1234")).toBeTruthy(); // short SHA
});
expect(screen.getByText("Merge branch 'fusion/fn-001' into main")).toBeTruthy();
});
it("renders empty file list when patch has no diffs", async () => {
mockFetchCommitDiff.mockResolvedValue({ stat: "", patch: "" });
render();
await waitFor(() => {
expect(screen.getByText("No files changed in this commit.")).toBeTruthy();
});
});
it("uses mergeDetails stats for summary when available", async () => {
mockFetchCommitDiff.mockResolvedValue({ stat: "", patch: SAMPLE_PATCH });
render();
await waitFor(() => {
expect(screen.getByText("Files Changed (3)")).toBeTruthy();
});
expect(screen.getByText("+3")).toBeTruthy();
expect(screen.getByText("-1")).toBeTruthy();
});
});
describe("CommitDiffTab in TaskDetailModal", () => {
// These tests verify tab visibility logic via the CommitDiffTab integration
// Testing against the TaskDetailModal directly would require extensive mocking
// Instead we verify the component renders correctly for valid/invalid props
it("does not fetch when commitSha is empty (non-done task scenario)", () => {
render();
expect(mockFetchCommitDiff).not.toHaveBeenCalled();
expect(screen.getByText("No commit SHA available.")).toBeTruthy();
});
});
describe("CommitDiffTab — compact spacing isolation", () => {
it("does NOT render the TaskChangesTab compact modifier class", async () => {
mockFetchCommitDiff.mockResolvedValue({ stat: "", patch: SAMPLE_PATCH });
const { container } = render();
await waitFor(() => {
expect(screen.getByText("src/app.ts")).toBeTruthy();
});
// CommitDiffTab should not have the compact modifier — proves scoping isolation
const compactList = container.querySelector(".task-changes-file-list--compact");
expect(compactList).toBeNull();
});
});