import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { PrSection } from "../PrSection";
// Mock the API module
vi.mock("../../api", () => ({
createPr: vi.fn(),
refreshPrStatus: vi.fn(),
}));
import { createPr, refreshPrStatus } from "../../api";
const mockAddToast = vi.fn();
const mockOnPrCreated = vi.fn();
const mockOnPrUpdated = vi.fn();
const mockPrInfo = {
url: "https://github.com/owner/repo/pull/42",
number: 42,
status: "open" as const,
title: "Fix the bug",
headBranch: "kb/kb-001",
baseBranch: "main",
commentCount: 3,
lastCommentAt: "2026-01-01T00:00:00.000Z",
};
describe("PrSection", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("when task has no PR", () => {
it("shows create PR button when GitHub token is available", () => {
render(
);
expect(screen.getByText("Create PR")).toBeDefined();
});
it("shows disabled button and hint when GitHub token is missing", () => {
render(
);
const button = screen.getByText("Create PR") as HTMLButtonElement;
expect(button.disabled).toBe(true);
expect(screen.getByText(/GITHUB_TOKEN env var/i)).toBeDefined();
});
it("shows create form when clicking create button", () => {
render(
);
fireEvent.click(screen.getByText("Create PR"));
expect(screen.getByPlaceholderText("PR title")).toBeDefined();
expect(screen.getByPlaceholderText("PR description (optional)")).toBeDefined();
expect(screen.getByText("Cancel")).toBeDefined();
});
it("hides form when clicking cancel", () => {
render(
);
fireEvent.click(screen.getByText("Create PR"));
fireEvent.click(screen.getByText("Cancel"));
expect(screen.queryByPlaceholderText("PR title")).toBeNull();
});
it("creates PR when form is submitted", async () => {
(createPr as ReturnType).mockResolvedValue(mockPrInfo);
render(
);
fireEvent.click(screen.getByText("Create PR"));
fireEvent.change(screen.getByPlaceholderText("PR title"), {
target: { value: "My PR Title" },
});
fireEvent.click(screen.getByText("Create PR"));
await waitFor(() => {
expect(createPr).toHaveBeenCalledWith("KB-001", {
title: "My PR Title",
body: undefined,
});
});
expect(mockOnPrCreated).toHaveBeenCalledWith(mockPrInfo);
expect(mockAddToast).toHaveBeenCalledWith("Created PR #42", "success");
});
it("shows error when PR creation fails", async () => {
(createPr as ReturnType).mockRejectedValue(new Error("API error"));
render(
);
fireEvent.click(screen.getByText("Create PR"));
fireEvent.change(screen.getByPlaceholderText("PR title"), {
target: { value: "My PR Title" },
});
fireEvent.click(screen.getByText("Create PR"));
await waitFor(() => {
expect(mockAddToast).toHaveBeenCalledWith("API error", "error");
});
});
});
describe("when task has a PR", () => {
it("displays PR info for open PR", () => {
render(
);
expect(screen.getByText("#42")).toBeDefined();
expect(screen.getByText("Fix the bug")).toBeDefined();
expect(screen.getByText("open")).toBeDefined();
expect(screen.getByText("View on GitHub")).toBeDefined();
});
it("shows correct status badge for merged PR", () => {
render(
);
expect(screen.getByText("merged")).toBeDefined();
expect(screen.getByText(/finish local cleanup and move the task to Done/i)).toBeDefined();
});
it("shows correct status badge for closed PR", () => {
render(
);
expect(screen.getByText("closed")).toBeDefined();
});
it("displays comment count when PR has comments", () => {
render(
);
// The comment count should be rendered
expect(screen.getByText("5")).toBeDefined();
});
it("refreshes PR status when refresh button is clicked", async () => {
const updatedPr = { ...mockPrInfo, status: "merged" as const };
(refreshPrStatus as ReturnType).mockResolvedValue({
prInfo: updatedPr,
mergeReady: true,
blockingReasons: [],
reviewDecision: "APPROVED",
checks: [{ name: "ci", required: true, state: "success" }],
automationStatus: null,
});
render(
);
const refreshButton = screen.getByTitle("Refresh PR status");
fireEvent.click(refreshButton);
await waitFor(() => {
expect(refreshPrStatus).toHaveBeenCalledWith("KB-001");
});
expect(mockOnPrUpdated).toHaveBeenCalledWith(updatedPr);
expect(mockAddToast).toHaveBeenCalledWith("PR status refreshed", "success");
});
it("shows error when refresh fails", async () => {
(refreshPrStatus as ReturnType).mockRejectedValue(new Error("Network error"));
render(
);
const refreshButton = screen.getByTitle("Refresh PR status");
fireEvent.click(refreshButton);
await waitFor(() => {
expect(mockAddToast).toHaveBeenCalledWith("Network error", "error");
});
});
it("shows automatic PR creation message while PR-first automation is creating a PR", () => {
render(
);
expect(screen.getByText(/creating a pull request automatically/i)).toBeDefined();
expect(screen.queryByText("Create PR")).toBeNull();
});
it("shows awaiting-checks messaging from refreshed merge blockers", async () => {
(refreshPrStatus as ReturnType).mockResolvedValue({
prInfo: mockPrInfo,
mergeReady: false,
blockingReasons: ["required checks not successful: ci (pending)"],
reviewDecision: null,
checks: [{ name: "ci", required: true, state: "pending" }],
automationStatus: "awaiting-pr-checks",
});
render(
);
fireEvent.click(screen.getByTitle("Refresh PR status"));
await waitFor(() => {
expect(screen.getByText(/Waiting for: required checks not successful: ci \(pending\)/)).toBeDefined();
});
});
});
});