// @vitest-environment jsdom
import { flushSync } from "react-dom";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ThemeProvider } from "@/context/ThemeContext";
import { TaskChatMarker } from "./TaskChatMarker";
import { TaskChatThreadView } from "./TaskChatThreadView";
vi.mock("@/lib/router", () => ({
Link: ({ to, children }: { to: string; children: React.ReactNode }) => (
{children}
),
}));
describe("TaskChatMarker", () => {
let container: HTMLDivElement;
let root: Root | null = null;
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-09-01T12:05:00.000Z"));
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
flushSync(() => root?.unmount());
root = null;
container.remove();
vi.useRealTimers();
});
it("renders a failed run as a timestamped disclosure without divider rules", () => {
flushSync(() =>
root!.render(
,
),
);
const toggle = container.querySelector(
'[data-testid="task-chat-collapsible-marker"] button[aria-expanded]',
)!;
expect(toggle.textContent).toContain("Run failed");
expect(toggle.textContent).toContain("5m ago");
expect(
container
.querySelector('[data-testid="task-chat-collapsible-marker"]')
?.classList.contains("items-start"),
).toBe(true);
expect(container.querySelector(".border-dashed")).toBeNull();
expect(container.textContent).not.toContain("runner_exited");
flushSync(() => toggle.click());
expect(toggle.getAttribute("aria-expanded")).toBe("true");
expect(container.textContent).toContain("runner_exited");
expect(
container.querySelector('a[href="/agents/codex/runs/run-1"]')
?.textContent,
).toBe("View run");
});
it("keeps expected cancellation neutral when collapsed and expanded", () => {
flushSync(() =>
root!.render(
,
),
);
const toggle = container.querySelector(
"button[aria-expanded]",
)!;
expect(toggle.classList).toContain("text-muted-foreground");
expect(container.querySelector(".text-destructive")).toBeNull();
flushSync(() => toggle.click());
expect(container.textContent).toContain("Cancelled by you.");
expect(container.querySelector(".text-destructive")).toBeNull();
});
it("keeps Try again available without retry instructions in the detail", async () => {
const onTryAgain = vi.fn();
flushSync(() =>
root!.render(
,
),
);
const retry = container.querySelector(
'[data-testid="task-chat-run-failed-try-again"]',
)!;
expect(container.textContent).not.toContain(
"You can retry this message now",
);
flushSync(() => retry.click());
await Promise.resolve();
expect(onTryAgain).toHaveBeenCalledTimes(1);
});
it("routes Try again to the failed run retry callback", async () => {
const onRetryFailedRun = vi.fn();
flushSync(() =>
root!.render(
,
),
);
const retry = container.querySelector(
'[data-testid="task-chat-run-failed-try-again"]',
)!;
expect(retry).not.toBeNull();
flushSync(() => retry.click());
await Promise.resolve();
expect(onRetryFailedRun).toHaveBeenCalledWith("run-1");
});
});