feat(FN-4757): complete Step 3 — wire PrPanel and remove PrSection
Fusion-Task-Id: FN-4757 Fusion-Task-Lineage: 32236172-0b3b-4c19-b3a1-8ee0b91ea3ac
This commit is contained in:
committed by
gsxdsm
parent
3c6be85b96
commit
0c8cc02ca3
@@ -7,7 +7,7 @@ const themeDataPath = path.resolve(__dirname, "../public/theme-data.css");
|
||||
|
||||
/**
|
||||
* Theme-safety regression tests for status color tokens across
|
||||
* TaskCard, GitHubBadge, and PrSection components.
|
||||
* TaskCard, GitHubBadge, and PrPanel components.
|
||||
*
|
||||
* These tests verify that hardcoded rgba/hex colors have been replaced
|
||||
* with theme-aware CSS custom properties using color-mix(), ensuring
|
||||
@@ -228,8 +228,8 @@ describe("GitHubBadge theme safety", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PrSection theme safety", () => {
|
||||
const componentPath = path.resolve(__dirname, "../components/PrSection.tsx");
|
||||
describe("PrPanel theme safety", () => {
|
||||
const componentPath = path.resolve(__dirname, "../components/PrPanel.tsx");
|
||||
let source: string;
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { GitPullRequest, ExternalLink, RefreshCw, Plus, MessageSquare, CircleDot, XCircle, GitMerge } from "lucide-react";
|
||||
import type { PrInfo } from "@fusion/core";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
import { createPr, refreshPrStatus, type PrRefreshResponse } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
interface PrSectionProps {
|
||||
taskId: string;
|
||||
projectId?: string;
|
||||
prInfo?: PrInfo;
|
||||
automationStatus?: string | null;
|
||||
autoMerge?: boolean;
|
||||
isManualPrFlow?: boolean;
|
||||
prAuthAvailable: boolean;
|
||||
onPrCreated: (prInfo: PrInfo) => void;
|
||||
onPrUpdated: (prInfo: PrInfo) => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
}
|
||||
|
||||
const STATUS_ICONS: Record<string, React.ReactNode> = {
|
||||
open: <CircleDot size={16} />,
|
||||
closed: <XCircle size={16} />,
|
||||
merged: <GitMerge size={16} />,
|
||||
};
|
||||
|
||||
export function PrSection({
|
||||
taskId,
|
||||
projectId,
|
||||
prInfo,
|
||||
automationStatus,
|
||||
autoMerge = false,
|
||||
isManualPrFlow = false,
|
||||
prAuthAvailable,
|
||||
onPrCreated,
|
||||
onPrUpdated,
|
||||
addToast,
|
||||
}: PrSectionProps) {
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [prTitle, setPrTitle] = useState("");
|
||||
const [prBody, setPrBody] = useState("");
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [refreshState, setRefreshState] = useState<PrRefreshResponse | null>(null);
|
||||
|
||||
const handleCreate = useCallback(async () => {
|
||||
if (!prTitle.trim()) return;
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const newPr = await createPr(taskId, {
|
||||
title: prTitle.trim(),
|
||||
body: prBody.trim() || undefined,
|
||||
}, projectId);
|
||||
onPrCreated(newPr);
|
||||
setShowCreateForm(false);
|
||||
setPrTitle("");
|
||||
setPrBody("");
|
||||
addToast(`Created PR #${newPr.number}`, "success");
|
||||
} catch (err) {
|
||||
addToast(getErrorMessage(err) || "Failed to create PR", "error");
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [taskId, prTitle, prBody, projectId, onPrCreated, addToast]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
if (!prInfo) return;
|
||||
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
const updated = await refreshPrStatus(taskId, projectId);
|
||||
setRefreshState(updated);
|
||||
onPrUpdated(updated.prInfo);
|
||||
addToast("PR status refreshed", "success");
|
||||
} catch (err) {
|
||||
addToast(getErrorMessage(err) || "Failed to refresh PR", "error");
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [taskId, prInfo, projectId, onPrUpdated, addToast]);
|
||||
|
||||
// No PR yet - show create button or automation state
|
||||
if (!prInfo) {
|
||||
if (automationStatus === "creating-pr") {
|
||||
return (
|
||||
<div className="pr-section">
|
||||
<h4>
|
||||
<GitPullRequest size={16} className="pr-section-icon" />
|
||||
Pull Request
|
||||
</h4>
|
||||
<div className="pr-hint pr-hint--muted">
|
||||
fn is creating a pull request automatically for this task.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (autoMerge) {
|
||||
return (
|
||||
<div className="pr-section">
|
||||
<h4>
|
||||
<GitPullRequest size={16} className="pr-section-icon" />
|
||||
Pull Request
|
||||
</h4>
|
||||
<div className="pr-hint pr-hint--muted">
|
||||
Auto-merge will handle this task automatically.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showCreateForm) {
|
||||
return (
|
||||
<div className="pr-section">
|
||||
<h4>
|
||||
<GitPullRequest size={16} className="pr-section-icon" />
|
||||
Create Pull Request
|
||||
</h4>
|
||||
<div className="pr-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="PR title"
|
||||
value={prTitle}
|
||||
onChange={(e) => setPrTitle(e.target.value)}
|
||||
disabled={isCreating}
|
||||
className="pr-input"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="PR description (optional)"
|
||||
value={prBody}
|
||||
onChange={(e) => setPrBody(e.target.value)}
|
||||
disabled={isCreating}
|
||||
className="pr-textarea"
|
||||
rows={3}
|
||||
/>
|
||||
<div className="pr-actions">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => setShowCreateForm(false)}
|
||||
disabled={isCreating}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={handleCreate}
|
||||
disabled={!prTitle.trim() || isCreating}
|
||||
>
|
||||
{isCreating ? "Creating…" : "Create PR"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pr-section">
|
||||
<h4>
|
||||
<GitPullRequest size={16} className="pr-section-icon" />
|
||||
Pull Request
|
||||
</h4>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setShowCreateForm(true)}
|
||||
disabled={!prAuthAvailable}
|
||||
title={prAuthAvailable ? "Create a PR for this task" : "PR auth unavailable — run 'gh auth login'"}
|
||||
>
|
||||
<Plus size={14} className="pr-section-icon--sm" />
|
||||
Create PR
|
||||
</button>
|
||||
{isManualPrFlow && (
|
||||
<div className="pr-hint pr-hint--subtle">
|
||||
Use the footer action to run PR-first completion for this task.
|
||||
</div>
|
||||
)}
|
||||
{!prAuthAvailable && (
|
||||
<div className="pr-hint pr-hint--subtle">
|
||||
Run <code>gh auth login</code> to enable PR creation.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// PR exists - show PR card
|
||||
const statusIcon = STATUS_ICONS[prInfo.status] ?? <CircleDot size={16} />;
|
||||
const blockingReasons = refreshState?.blockingReasons ?? [];
|
||||
|
||||
return (
|
||||
<div className="pr-section">
|
||||
<h4>
|
||||
<GitPullRequest size={16} className="pr-section-icon" />
|
||||
Pull Request
|
||||
</h4>
|
||||
<div
|
||||
className={`pr-card pr-card--status-${prInfo.status}`}
|
||||
>
|
||||
<div className="pr-header">
|
||||
<span className="pr-status-icon">{statusIcon}</span>
|
||||
<span
|
||||
className={`pr-status-badge pr-status-badge--${prInfo.status}`}
|
||||
>
|
||||
{prInfo.status}
|
||||
</span>
|
||||
<span className="pr-number">#{prInfo.number}</span>
|
||||
<div className="pr-spacer" />
|
||||
<button
|
||||
className="btn btn-sm pr-refresh-btn"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
title="Refresh PR status"
|
||||
>
|
||||
<RefreshCw size={14} style={{ verticalAlign: "middle", opacity: isRefreshing ? 0.5 : 1 }} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="pr-title">{prInfo.title}</div>
|
||||
<div className="pr-meta">
|
||||
<span>{prInfo.headBranch}</span>
|
||||
<span className="pr-meta-arrow">→</span>
|
||||
<span>{prInfo.baseBranch}</span>
|
||||
</div>
|
||||
{automationStatus === "merging-pr" && (
|
||||
<div className="pr-hint pr-hint--info">
|
||||
fn is merging this pull request automatically.
|
||||
</div>
|
||||
)}
|
||||
{automationStatus === "awaiting-pr-checks" && (
|
||||
<div className="pr-hint pr-hint--info">
|
||||
{blockingReasons.length > 0
|
||||
? `Waiting for: ${blockingReasons.join("; ")}`
|
||||
: "Waiting for required checks or review feedback before auto-merge."}
|
||||
</div>
|
||||
)}
|
||||
{prInfo.status === "merged" && (
|
||||
<div className="pr-hint pr-hint--info">
|
||||
This PR is merged. fn will finish local cleanup and move the task to Done.
|
||||
</div>
|
||||
)}
|
||||
<div className="pr-footer">
|
||||
{prInfo.commentCount > 0 && (
|
||||
<span className="pr-comments">
|
||||
<MessageSquare size={14} />
|
||||
{prInfo.commentCount}
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
href={prInfo.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="pr-link"
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import { useAgentLogs } from "../hooks/useAgentLogs";
|
||||
import { useConfirm } from "../hooks/useConfirm";
|
||||
import { AgentLogViewer } from "./AgentLogViewer";
|
||||
import { ModelSelectorTab } from "./ModelSelectorTab";
|
||||
import { PrSection } from "./PrSection";
|
||||
import { PrPanel } from "./PrPanel";
|
||||
import { TaskComments } from "./TaskComments";
|
||||
import { TaskReviewTab } from "./TaskReviewTab";
|
||||
import { MergeDetails } from "./MergeDetails";
|
||||
@@ -3284,7 +3284,7 @@ export function TaskDetailContent({
|
||||
);
|
||||
})()}
|
||||
<div className="detail-section detail-pr-section">
|
||||
<PrSection
|
||||
<PrPanel
|
||||
taskId={task.id}
|
||||
projectId={projectId}
|
||||
prInfo={task.prInfo}
|
||||
@@ -3292,10 +3292,8 @@ export function TaskDetailContent({
|
||||
autoMerge={settings?.autoMerge ?? false}
|
||||
isManualPrFlow={isManualPrFlow}
|
||||
prAuthAvailable={prAuthAvailable ?? false}
|
||||
onPrCreated={(prInfo) => {
|
||||
// Update task locally to show new PR
|
||||
(task as TaskDetail).prInfo = prInfo;
|
||||
}}
|
||||
// TODO(FN-4758): wire create-PR modal trigger
|
||||
onRequestCreatePr={undefined}
|
||||
onPrUpdated={(prInfo) => {
|
||||
(task as TaskDetail).prInfo = prInfo;
|
||||
}}
|
||||
|
||||
@@ -1,407 +0,0 @@
|
||||
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: "fusion/fn-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 PR auth is available", () => {
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
prAuthAvailable={true}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Create PR")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows disabled button and hint when PR auth is unavailable", () => {
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
prAuthAvailable={false}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const button = screen.getByText("Create PR") as HTMLButtonElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
expect(button.title).toContain("gh auth login");
|
||||
expect(screen.getByText(/gh auth login/i)).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows create form when clicking create button", () => {
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
prAuthAvailable={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="FN-001"
|
||||
prAuthAvailable={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="FN-001"
|
||||
prAuthAvailable={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("FN-001", {
|
||||
title: "My PR Title",
|
||||
body: undefined,
|
||||
}, 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="FN-001"
|
||||
prAuthAvailable={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 autoMerge is enabled", () => {
|
||||
it("hides manual PR controls and shows auto-merge messaging when no PR exists", () => {
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
autoMerge={true}
|
||||
prAuthAvailable={true}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Create PR" })).toBeNull();
|
||||
expect(screen.queryByText(/gh auth login/i)).toBeNull();
|
||||
expect(screen.getByText("Auto-merge will handle this task automatically.")).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not show PR auth hint when auto-merge is enabled and PR auth is unavailable", () => {
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
autoMerge={true}
|
||||
prAuthAvailable={false}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/gh auth login/i)).toBeNull();
|
||||
expect(screen.getByText("Auto-merge will handle this task automatically.")).toBeDefined();
|
||||
});
|
||||
|
||||
it("still shows the creating-pr automation message when automation is active", () => {
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
autoMerge={true}
|
||||
automationStatus="creating-pr"
|
||||
prAuthAvailable={false}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/creating a pull request automatically/i)).toBeDefined();
|
||||
expect(screen.queryByText("Auto-merge will handle this task automatically.")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Create PR" })).toBeNull();
|
||||
});
|
||||
|
||||
it("shows manual PR-footer hint only when manual PR flow is active", () => {
|
||||
const { rerender } = render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
autoMerge={false}
|
||||
isManualPrFlow={true}
|
||||
prAuthAvailable={false}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Create PR" })).toBeDefined();
|
||||
expect(screen.getByText(/Use the footer action to run PR-first completion/i)).toBeDefined();
|
||||
expect(screen.getByText(/gh auth login/i)).toBeDefined();
|
||||
expect(screen.queryByText("Auto-merge will handle this task automatically.")).toBeNull();
|
||||
|
||||
rerender(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
autoMerge={false}
|
||||
isManualPrFlow={false}
|
||||
prAuthAvailable={false}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Use the footer action to run PR-first completion/i)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when task has a PR", () => {
|
||||
it("displays PR info for open PR", () => {
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
prInfo={mockPrInfo}
|
||||
prAuthAvailable={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="FN-001"
|
||||
prInfo={{ ...mockPrInfo, status: "merged" }}
|
||||
prAuthAvailable={true}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
prInfo={{ ...mockPrInfo, status: "closed" }}
|
||||
prAuthAvailable={true}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("closed")).toBeDefined();
|
||||
});
|
||||
|
||||
it("displays comment count when PR has comments", () => {
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
prInfo={{ ...mockPrInfo, commentCount: 5 }}
|
||||
prAuthAvailable={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({
|
||||
prInfo: updatedPr,
|
||||
mergeReady: true,
|
||||
blockingReasons: [],
|
||||
reviewDecision: "APPROVED",
|
||||
checks: [{ name: "ci", required: true, state: "success" }],
|
||||
automationStatus: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
prInfo={mockPrInfo}
|
||||
prAuthAvailable={true}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const refreshButton = screen.getByTitle("Refresh PR status");
|
||||
fireEvent.click(refreshButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(refreshPrStatus).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
|
||||
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="FN-001"
|
||||
prInfo={mockPrInfo}
|
||||
prAuthAvailable={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");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows automatic PR creation message while PR-first automation is creating a PR", () => {
|
||||
render(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
automationStatus="creating-pr"
|
||||
prAuthAvailable={true}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
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<typeof vi.fn>).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(
|
||||
<PrSection
|
||||
taskId="FN-001"
|
||||
prInfo={mockPrInfo}
|
||||
automationStatus="awaiting-pr-checks"
|
||||
prAuthAvailable={true}
|
||||
onPrCreated={mockOnPrCreated}
|
||||
onPrUpdated={mockOnPrUpdated}
|
||||
addToast={mockAddToast}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTitle("Refresh PR status"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Waiting for: required checks not successful: ci \(pending\)/)).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,7 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Mock lucide-react icons used by TaskDetailModal, TaskForm, PrSection, CustomModelDropdown
|
||||
// Mock lucide-react icons used by TaskDetailModal, TaskForm, PrPanel, CustomModelDropdown
|
||||
vi.mock("lucide-react", () => ({
|
||||
Pencil: () => null,
|
||||
Sparkles: () => null,
|
||||
|
||||
@@ -1927,11 +1927,6 @@ input[type="range"]:focus-visible {
|
||||
margin-right: var(--space-sm);
|
||||
}
|
||||
|
||||
.pr-section-icon--sm {
|
||||
vertical-align: middle;
|
||||
margin-right: var(--space-xs);
|
||||
}
|
||||
|
||||
.pr-hint--muted {
|
||||
opacity: 0.8;
|
||||
font-size: 13px;
|
||||
|
||||
Reference in New Issue
Block a user