136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
import type { GitHubPR, GitHubPRStatus } from "@/lib/types/github";
|
|
|
|
const getPRStatusesBatch = vi.fn();
|
|
|
|
vi.mock("@/lib/api/domains/github-api", () => ({
|
|
getPRStatusesBatch: (
|
|
workspaceId: string,
|
|
refs: { owner: string; repo: string; number: number }[],
|
|
) =>
|
|
getPRStatusesBatch(workspaceId, refs) as Promise<{
|
|
statuses: Record<string, GitHubPRStatus>;
|
|
}>,
|
|
}));
|
|
|
|
import { prStatusKey, usePRStatuses } from "./use-pr-statuses";
|
|
|
|
function makePR(owner: string, repo: string, number: number): GitHubPR {
|
|
return {
|
|
number,
|
|
title: `PR #${number}`,
|
|
url: "",
|
|
html_url: "",
|
|
state: "open",
|
|
head_branch: "feat",
|
|
base_branch: "main",
|
|
author_login: "u",
|
|
repo_owner: owner,
|
|
repo_name: repo,
|
|
draft: false,
|
|
mergeable: true,
|
|
additions: 0,
|
|
deletions: 0,
|
|
requested_reviewers: [],
|
|
created_at: "",
|
|
updated_at: "",
|
|
merged_at: null,
|
|
closed_at: null,
|
|
};
|
|
}
|
|
|
|
function makeStatus(pr: GitHubPR): GitHubPRStatus {
|
|
return {
|
|
pr,
|
|
review_state: "",
|
|
checks_state: "",
|
|
mergeable_state: "unknown",
|
|
review_count: 0,
|
|
pending_review_count: 0,
|
|
checks_total: 0,
|
|
checks_passing: 0,
|
|
} as GitHubPRStatus;
|
|
}
|
|
|
|
describe("prStatusKey", () => {
|
|
it("encodes owner/repo#number", () => {
|
|
expect(prStatusKey("acme", "widget", 7)).toBe("acme/widget#7");
|
|
});
|
|
});
|
|
|
|
describe("usePRStatuses", () => {
|
|
beforeEach(() => {
|
|
getPRStatusesBatch.mockReset();
|
|
});
|
|
|
|
it("skips fetch when prs list is empty", () => {
|
|
renderHook(() => usePRStatuses("ws-1", []));
|
|
expect(getPRStatusesBatch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fetches and populates map keyed by prStatusKey", async () => {
|
|
const pr = makePR("acme", "widget", 7);
|
|
getPRStatusesBatch.mockResolvedValueOnce({
|
|
statuses: { "acme/widget#7": makeStatus(pr) },
|
|
});
|
|
const { result } = renderHook(() => usePRStatuses("ws-1", [pr]));
|
|
await waitFor(() => {
|
|
expect(result.current.size).toBe(1);
|
|
});
|
|
expect(result.current.get("acme/widget#7")?.pr.number).toBe(7);
|
|
});
|
|
|
|
it("does not refetch when list identity changes but key stays the same", async () => {
|
|
const pr = makePR("acme", "widget", 7);
|
|
getPRStatusesBatch.mockResolvedValue({
|
|
statuses: { "acme/widget#7": makeStatus(pr) },
|
|
});
|
|
const { rerender, result } = renderHook(({ prs }) => usePRStatuses("ws-1", prs), {
|
|
initialProps: { prs: [pr] },
|
|
});
|
|
await waitFor(() => expect(result.current.size).toBe(1));
|
|
expect(getPRStatusesBatch).toHaveBeenCalledTimes(1);
|
|
|
|
// New array reference, same content.
|
|
rerender({ prs: [makePR("acme", "widget", 7)] });
|
|
// Give effects a tick.
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
expect(getPRStatusesBatch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("refetches when the list key changes", async () => {
|
|
const pr1 = makePR("acme", "widget", 7);
|
|
const pr2 = makePR("acme", "widget", 8);
|
|
getPRStatusesBatch.mockResolvedValue({
|
|
statuses: { "acme/widget#7": makeStatus(pr1), "acme/widget#8": makeStatus(pr2) },
|
|
});
|
|
const { rerender } = renderHook(({ prs }) => usePRStatuses("ws-1", prs), {
|
|
initialProps: { prs: [pr1] },
|
|
});
|
|
await waitFor(() => expect(getPRStatusesBatch).toHaveBeenCalledTimes(1));
|
|
rerender({ prs: [pr1, pr2] });
|
|
await waitFor(() => expect(getPRStatusesBatch).toHaveBeenCalledTimes(2));
|
|
});
|
|
|
|
it("clears stale statuses when key changes and fetch fails", async () => {
|
|
const pr = makePR("acme", "widget", 7);
|
|
getPRStatusesBatch.mockResolvedValueOnce({
|
|
statuses: { "acme/widget#7": makeStatus(pr) },
|
|
});
|
|
const { result, rerender } = renderHook(({ prs }) => usePRStatuses("ws-1", prs), {
|
|
initialProps: { prs: [pr] },
|
|
});
|
|
await waitFor(() => expect(result.current.size).toBe(1));
|
|
|
|
// New key, then fetch rejects: the stale badges from pr #7 belong to the
|
|
// old key and would be misleading, so the catch handler clears them.
|
|
const pr2 = makePR("acme", "widget", 8);
|
|
getPRStatusesBatch.mockRejectedValueOnce(new Error("boom"));
|
|
rerender({ prs: [pr2] });
|
|
await waitFor(() => expect(result.current.size).toBe(0));
|
|
});
|
|
});
|