import { render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../api/client", async () => { const actual = await vi.importActual("../api/client"); return { ...actual, api: { ...actual.api, getWorkItemContent: vi.fn(), getTestResults: vi.fn(), getTokenCost: vi.fn(), }, }; }); vi.mock("../api/agents", () => ({ agentsApi: { listAgents: vi.fn(), getAgentConfig: vi.fn(), stopAgent: vi.fn(), startAgent: vi.fn(), }, subscribeAgentStream: vi.fn(() => () => {}), })); import { agentsApi, subscribeAgentStream } from "../api/agents"; import { api } from "../api/client"; const { WorkItemDetailPanel } = await import("./WorkItemDetailPanel"); const mockedGetWorkItemContent = vi.mocked(api.getWorkItemContent); const mockedGetTestResults = vi.mocked(api.getTestResults); const mockedGetTokenCost = vi.mocked(api.getTokenCost); const mockedListAgents = vi.mocked(agentsApi.listAgents); const mockedGetAgentConfig = vi.mocked(agentsApi.getAgentConfig); const mockedSubscribeAgentStream = vi.mocked(subscribeAgentStream); const DEFAULT_CONTENT = { content: "# Big Title\n\nSome content here.", stage: "current", name: "Big Title Story", agent: null, }; beforeEach(() => { vi.clearAllMocks(); mockedGetWorkItemContent.mockResolvedValue(DEFAULT_CONTENT); mockedGetTestResults.mockResolvedValue(null); mockedGetTokenCost.mockResolvedValue({ total_cost_usd: 0, agents: [] }); mockedListAgents.mockResolvedValue([]); mockedGetAgentConfig.mockResolvedValue([]); mockedSubscribeAgentStream.mockReturnValue(() => {}); }); afterEach(() => { vi.restoreAllMocks(); }); describe("WorkItemDetailPanel", () => { it("renders the story name in the header with type and ID prefix", async () => { render( {}} />, ); await waitFor(() => { expect(screen.getByTestId("detail-panel-title")).toHaveTextContent( "Bug 237: Big Title Story", ); }); }); it("shows loading state initially", () => { render( {}} />, ); expect(screen.getByTestId("detail-panel-loading")).toBeInTheDocument(); }); it("calls onClose when close button is clicked", async () => { const onClose = vi.fn(); render( , ); const closeButton = screen.getByTestId("detail-panel-close"); closeButton.click(); expect(onClose).toHaveBeenCalledTimes(1); }); it("renders markdown headings with constrained inline font size", async () => { mockedGetWorkItemContent.mockResolvedValue({ ...DEFAULT_CONTENT, content: "# Title Heading\n\n## Section Heading\n\nSome content.", }); render( {}} />, ); await waitFor(() => { const content = screen.getByTestId("detail-panel-content"); // H1 is stripped by stripDisplayContent; h2 should be constrained const h2 = content.querySelector("h2"); expect(h2).not.toBeNull(); expect(h2?.style.fontSize).toBeTruthy(); }); }); it("strips YAML front matter so 'name' is not shown as a prefix in content", async () => { mockedGetWorkItemContent.mockResolvedValue({ content: '---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nAs a user...', stage: "current", name: "My Story Name", agent: null, }); render( {}} />, ); const content = await screen.findByTestId("detail-panel-content"); expect(content.textContent).not.toMatch(/name:/i); }); it("strips the first H1 heading so the story title is not shown twice", async () => { mockedGetWorkItemContent.mockResolvedValue({ content: '---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nAs a user...', stage: "current", name: "My Story Name", agent: null, }); render( {}} />, ); const content = await screen.findByTestId("detail-panel-content"); expect(content.querySelector("h1")).toBeNull(); }); it("shows 'Type N: Name' format in the panel header title (story ID/title left-justified)", async () => { mockedGetWorkItemContent.mockResolvedValue({ content: "## User Story\n\nAs a user...", stage: "current", name: "My Story Name", agent: null, }); render( {}} />, ); await waitFor(() => { expect(screen.getByTestId("detail-panel-title")).toHaveTextContent( "Story 42: My Story Name", ); }); }); it("does not show the work item type label twice when front matter and H1 are stripped", async () => { mockedGetWorkItemContent.mockResolvedValue({ content: '---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nContent.', stage: "current", name: "My Story Name", agent: null, }); render( {}} />, ); await screen.findByTestId("detail-panel-content"); // "Story" type label appears exactly once — in the panel header title const title = screen.getByTestId("detail-panel-title"); expect(title.textContent).toContain("Story 42:"); // The content body should not contain an H1 repeating the type + title const content = screen.getByTestId("detail-panel-content"); expect(content.querySelector("h1")).toBeNull(); }); }); describe("WorkItemDetailPanel - Assigned Agent", () => { it("shows assigned agent name when agent front matter field is set", async () => { mockedGetWorkItemContent.mockResolvedValue({ ...DEFAULT_CONTENT, agent: "coder-opus", }); render( {}} />, ); const agentEl = await screen.findByTestId("detail-panel-assigned-agent"); expect(agentEl).toHaveTextContent("coder-opus"); }); it("omits assigned agent field when no agent is set in front matter", async () => { render( {}} />, ); await screen.findByTestId("detail-panel-content"); expect( screen.queryByTestId("detail-panel-assigned-agent"), ).not.toBeInTheDocument(); }); it("shows the specific agent name not just 'assigned'", async () => { mockedGetWorkItemContent.mockResolvedValue({ ...DEFAULT_CONTENT, agent: "coder-haiku", }); render( {}} />, ); const agentEl = await screen.findByTestId("detail-panel-assigned-agent"); expect(agentEl).toHaveTextContent("coder-haiku"); expect(agentEl).not.toHaveTextContent("assigned"); }); });