feat(FN-950): add useTaskDiffStats hook for consistent done file count in TaskCard
- Create useTaskDiffStats hook to extract and compute diff stats from task data - Use hook in TaskCard component for consistent done file count display - Add diffStats priority tests ensuring diff stats take precedence over steps - Add useTaskDiffStats mock and comprehensive unit tests for the hook
This commit is contained in:
162
packages/dashboard/app/hooks/__tests__/useTaskDiffStats.test.ts
Normal file
162
packages/dashboard/app/hooks/__tests__/useTaskDiffStats.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { useTaskDiffStats } from "../useTaskDiffStats";
|
||||
import * as api from "../../api";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
fetchTaskDiff: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockFetchTaskDiff = vi.mocked(api.fetchTaskDiff);
|
||||
|
||||
describe("useTaskDiffStats", () => {
|
||||
beforeEach(() => {
|
||||
mockFetchTaskDiff.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("fetches diff stats for done tasks with a commit SHA", async () => {
|
||||
mockFetchTaskDiff.mockResolvedValueOnce({
|
||||
files: [
|
||||
{ path: "src/a.ts", status: "modified", additions: 10, deletions: 2, patch: "" },
|
||||
{ path: "src/b.ts", status: "added", additions: 5, deletions: 0, patch: "" },
|
||||
],
|
||||
stats: { filesChanged: 2, additions: 15, deletions: 2 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "done", "abc1234", undefined),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
expect(result.current.stats).toEqual({ filesChanged: 2, additions: 15, deletions: 2 });
|
||||
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-123", undefined, undefined);
|
||||
});
|
||||
|
||||
it("passes projectId to fetchTaskDiff", async () => {
|
||||
mockFetchTaskDiff.mockResolvedValueOnce({
|
||||
files: [],
|
||||
stats: { filesChanged: 0, additions: 0, deletions: 0 },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "done", "abc1234", "proj-1"),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-123", undefined, "proj-1");
|
||||
});
|
||||
|
||||
it("does not fetch for non-done columns", async () => {
|
||||
const { result: inProgress } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "in-progress", "abc1234", undefined),
|
||||
);
|
||||
const { result: todo } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "todo", "abc1234", undefined),
|
||||
);
|
||||
const { result: inReview } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "in-review", "abc1234", undefined),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(inProgress.current.loading).toBe(false));
|
||||
await waitFor(() => expect(todo.current.loading).toBe(false));
|
||||
await waitFor(() => expect(inReview.current.loading).toBe(false));
|
||||
|
||||
expect(inProgress.current.stats).toBeNull();
|
||||
expect(todo.current.stats).toBeNull();
|
||||
expect(inReview.current.stats).toBeNull();
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not fetch for done tasks without a commit SHA", async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "done", undefined, undefined),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
expect(result.current.stats).toBeNull();
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not fetch for empty task ID", async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTaskDiffStats("", "done", "abc1234", undefined),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
expect(result.current.stats).toBeNull();
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null stats on fetch failure", async () => {
|
||||
mockFetchTaskDiff.mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "done", "abc1234", undefined),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
expect(result.current.stats).toBeNull();
|
||||
});
|
||||
|
||||
it("cancels in-flight request on dependency change", async () => {
|
||||
let resolveFirst: (value: unknown) => void;
|
||||
const firstPromise = new Promise((resolve) => {
|
||||
resolveFirst = resolve;
|
||||
});
|
||||
mockFetchTaskDiff.mockReturnValueOnce(firstPromise as any);
|
||||
mockFetchTaskDiff.mockResolvedValueOnce({
|
||||
files: [],
|
||||
stats: { filesChanged: 3, additions: 5, deletions: 1 },
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ taskId }) => useTaskDiffStats(taskId, "done", "abc1234", undefined),
|
||||
{ initialProps: { taskId: "FN-100" } },
|
||||
);
|
||||
|
||||
// Rerender with a different taskId before the first fetch resolves
|
||||
rerender({ taskId: "FN-200" });
|
||||
|
||||
// Resolve the first (now cancelled) request
|
||||
resolveFirst!({
|
||||
files: [],
|
||||
stats: { filesChanged: 99, additions: 99, deletions: 99 },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
// The cancelled response should not have been stored
|
||||
expect(result.current.stats).toEqual({ filesChanged: 3, additions: 5, deletions: 1 });
|
||||
expect(mockFetchTaskDiff).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("resets stats when column changes from done to non-done", async () => {
|
||||
mockFetchTaskDiff.mockResolvedValueOnce({
|
||||
files: [],
|
||||
stats: { filesChanged: 5, additions: 10, deletions: 3 },
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ column }) => useTaskDiffStats("FN-123", column, "abc1234", undefined),
|
||||
{ initialProps: { column: "done" as string } },
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.stats).toEqual({ filesChanged: 5, additions: 10, deletions: 3 });
|
||||
|
||||
// Switch to a non-done column
|
||||
rerender({ column: "in-progress" });
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.stats).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user