import { MantineProvider } from "@mantine/core"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, expect, test, vi } from "vitest"; import { nextRunLabel, prPresentation, PullRequestIcons, PullRequestInfo, CardStatusLine } from "./CardStatusIcons"; import type { PullRequestStatus } from "../types"; // Test TaskChef's rows and keyboard handlers without jsdom layout work. // Browser QA separately exercises Mantine's real popup and positioning. vi.mock("@mantine/core", async () => { const actual = await vi.importActual("@mantine/core"); const React = await import("react"); const Open = React.createContext(false); const Popover = Object.assign( ({opened, children}: {opened: boolean; children: React.ReactNode}) => {children}, { Target: ({children}: {children: React.ReactNode}) => children, Dropdown: React.forwardRef>((props, ref) => React.useContext(Open) ?
: null), }, ); return {...actual, Popover}; }); afterEach(cleanup); const url="https://github.com/example/repo/pull/12"; const pr:PullRequestStatus={url,state:"open",checks:"passed",title:"Improve search"}; test("cards keep PR status in an accessible icon without extra text",()=>{ render(); expect(screen.getByLabelText("#12 Improve search: Checks passed")).toBeInTheDocument(); expect(screen.getByLabelText("#13 Improve search: PR status not checked or unavailable")).toBeInTheDocument(); expect(screen.queryByText(/unavailable/)).not.toBeInTheDocument(); }); test("PR presentation follows Codex priority for drafts, conflicts, CI and merge readiness",()=>{ expect(prPresentation({...pr,state:"draft",checks:"failed"}).dot).toBeNull(); expect(prPresentation({...pr,hasMergeConflicts:true}).label).toBe("Merge conflicts"); expect(prPresentation({...pr,canMerge:true}).label).toBe("Ready to merge"); expect(prPresentation({...pr,checks:"pending"}).label).toBe("Checks pending"); expect(prPresentation({...pr,state:"merged",checks:"failed"}).label).toBe("Merged"); }); test("next run uses Today and Tomorrow and never invents missing times",()=>{ const now=new Date(2026,9,10,10); expect(nextRunLabel(new Date(2026,9,10,15).toISOString(),now)).toMatch(/^Next run: Today at /); expect(nextRunLabel(new Date(2026,9,11,9).toISOString(),now)).toMatch(/^Next run: Tomorrow at /); expect(nextRunLabel(null,now)).toBe("Next run time unavailable"); }); test("popup content keeps CI for merged and unknown PRs and links only supported URLs",()=>{ const {rerender}=render(); expect(screen.getByText("CI passed")).toBeInTheDocument(); expect(screen.getByRole("link",{name:"#12 Improve search"})).toHaveAttribute("href",url); rerender(); expect(screen.queryByText(/CI status unknown/)).not.toBeInTheDocument(); expect(screen.queryByRole("link")).not.toBeInTheDocument(); }); test("one status popup shows available lines, survives movement onto links, and closes with Escape", async () => { render(Oct 7th); const line = screen.getByLabelText("Turn status details"); fireEvent.mouseEnter(line); expect(screen.getByText("Worked for 15h 5m")).toBeInTheDocument(); const link = screen.getByRole("link", {name:"#12 Improve search",hidden:true}); expect(link).toHaveAttribute("href",url); expect(screen.queryByText("CI passed")).not.toBeInTheDocument(); expect(screen.getByText(/^Next run:/)).toBeInTheDocument(); expect(screen.queryByText(/Checked |Ready to merge/)).not.toBeInTheDocument(); fireEvent.focus(line); fireEvent.keyDown(line,{key:"Tab"}); expect(link).toHaveFocus(); fireEvent.keyDown(link,{key:"Escape"}); expect(line).toHaveFocus(); }); test("popup omits missing duration, CI and schedule time instead of unavailable lines", async () => { render(Oct 7th); fireEvent.mouseEnter(screen.getByLabelText("Turn status details")); expect(screen.getByRole("link",{name:"#12 Improve search",hidden:true})).toBeInTheDocument(); expect(screen.queryByText(/Worked for|Next run:|CI status|unavailable/)).not.toBeInTheDocument(); }); test("status line describes PR state to keyboard and screen-reader users", () => { render(Oct 7th); expect(screen.getByLabelText("Turn status details")).toHaveAccessibleDescription("#12 Improve search: Merged. #13 Improve search: Draft"); }); test("Tab preserves the time control before handing focus to the PR link", () => { render(); const line = screen.getByLabelText("Turn status details"); fireEvent.focus(line); expect(fireEvent.keyDown(line, {key:"Tab"})).toBe(true); const time = screen.getByRole("button", {name:"Oct 7th"}); time.focus(); fireEvent.keyDown(time, {key:"Tab"}); expect(screen.getByRole("link", {name:"#12 Improve search",hidden:true})).toHaveFocus(); }); test("shows subagents and repository access action with number first", () => { render(Oct 7th); fireEvent.mouseEnter(screen.getByLabelText("Turn status details")); expect(screen.queryByText("155 subagents")).toBeNull(); expect(screen.getByText(/Access denied to example\/repo/)).toBeInTheDocument(); expect(screen.getByRole("link",{name:"Grant access"})).toHaveAttribute("href","https://github.com/apps/taskchef/installations/new"); expect(screen.getByRole("link",{name:"#12 Improve search"})).toHaveClass("taskchef-status-pr-title"); }); test("card and popup use latest turn usage and omit incomplete markers", () => { const turnUsage = {tokens:{input_tokens:1000,cached_input_tokens:200,cache_write_input_tokens:0,output_tokens:100,reasoning_output_tokens:40,total_tokens:1100},costUsd:2.34,samples:1,partial:true,costPartial:true}; render(1h); expect(screen.getByText("1.10K • $2.34")).toBeVisible(); fireEvent.focus(screen.getByLabelText("Turn status details")); expect(screen.getByText("Turn usage: 1.10K • $2.34")).toBeVisible(); }); test("missing turn records show plain placeholders without an explanation", () => { const turnUsage={tokens:{input_tokens:0,cached_input_tokens:0,cache_write_input_tokens:0,output_tokens:0,reasoning_output_tokens:0,total_tokens:0},costUsd:0,samples:0,partial:true,costPartial:true}; render(1h); expect(screen.getByText("— • —")).toBeVisible(); fireEvent.focus(screen.getByLabelText("Turn status details")); expect(screen.getByText("Turn usage: — • —")).toBeVisible(); expect(screen.queryByText(/Some usage records/)).toBeNull(); }); test("earlier-turn PR is labeled in the popup and latest-turn PRs take priority", () => { const earlier={...pr,state:"merged" as const}; const view=render(1h); expect(screen.getByLabelText("#12 Improve search: Merged")).toBeInTheDocument(); fireEvent.mouseEnter(screen.getByLabelText("Turn status details")); expect(screen.getByText("(Earlier turn PR)")).toBeInTheDocument(); expect(screen.getByRole("link",{name:"#12 Improve search",hidden:true})).toHaveAttribute("href",url); view.rerender(1h); expect(screen.queryByText("(Earlier turn PR)")).toBeNull(); expect(screen.queryByRole("link",{name:"#12 Improve search",hidden:true})).toBeNull(); expect(screen.getByRole("link",{name:"#13 Current change",hidden:true})).toBeInTheDocument(); }); test("earlier PR note stays directly below the link before an access warning", () => { render(1h); fireEvent.mouseEnter(screen.getByLabelText('Turn status details')); const link=screen.getByRole('link',{name:'#12 Improve search',hidden:true}); const note=screen.getByText('(Earlier turn PR)'); const warning=screen.getByText(/Access denied to/); expect(link.compareDocumentPosition(note) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(note.compareDocumentPosition(warning) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); });