Files
fusion/packages/dashboard/app/components/__tests__/PrSection.test.tsx
gsxdsm a53e4c1420 feat(KB-048): add collapsible list sections to dashboard
- Add section expansion state management with localStorage persistence
- Update section headers with chevron toggle controls
- Implement conditional task row rendering based on section state
- Add Expand All / Collapse All toolbar controls
- Add CSS styles for chevron rotation animation and section headers
- Add comprehensive tests for collapsible section behavior
2026-03-29 19:51:41 -07:00

268 lines
7.5 KiB
TypeScript

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(
<PrSection
taskId="KB-001"
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
expect(screen.getByText("Create PR")).toBeDefined();
});
it("shows disabled button and hint when GitHub token is missing", () => {
render(
<PrSection
taskId="KB-001"
hasGitHubToken={false}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
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(
<PrSection
taskId="KB-001"
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
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(
<PrSection
taskId="KB-001"
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
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<typeof vi.fn>).mockResolvedValue(mockPrInfo);
render(
<PrSection
taskId="KB-001"
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
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<typeof vi.fn>).mockRejectedValue(new Error("API error"));
render(
<PrSection
taskId="KB-001"
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
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(
<PrSection
taskId="KB-001"
prInfo={mockPrInfo}
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
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(
<PrSection
taskId="KB-001"
prInfo={{ ...mockPrInfo, status: "merged" }}
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
expect(screen.getByText("merged")).toBeDefined();
});
it("shows correct status badge for closed PR", () => {
render(
<PrSection
taskId="KB-001"
prInfo={{ ...mockPrInfo, status: "closed" }}
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
expect(screen.getByText("closed")).toBeDefined();
});
it("displays comment count when PR has comments", () => {
render(
<PrSection
taskId="KB-001"
prInfo={{ ...mockPrInfo, commentCount: 5 }}
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
// 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<typeof vi.fn>).mockResolvedValue(updatedPr);
render(
<PrSection
taskId="KB-001"
prInfo={mockPrInfo}
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
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<typeof vi.fn>).mockRejectedValue(new Error("Network error"));
render(
<PrSection
taskId="KB-001"
prInfo={mockPrInfo}
hasGitHubToken={true}
onPrCreated={mockOnPrCreated}
onPrUpdated={mockOnPrUpdated}
addToast={mockAddToast}
/>
);
const refreshButton = screen.getByTitle("Refresh PR status");
fireEvent.click(refreshButton);
await waitFor(() => {
expect(mockAddToast).toHaveBeenCalledWith("Network error", "error");
});
});
});
});