paperclip/ui/src/components/IssueChatThread.test.tsx
dotta f593e116c1 Refine issue chat activity and message chrome
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-08 06:02:34 -05:00

205 lines
5.6 KiB
TypeScript

// @vitest-environment jsdom
import { act } from "react";
import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { IssueChatThread } from "./IssueChatThread";
vi.mock("@assistant-ui/react", () => ({
AssistantRuntimeProvider: ({ children }: { children: ReactNode }) => <div>{children}</div>,
ThreadPrimitive: {
Root: ({ children, className }: { children: ReactNode; className?: string }) => (
<div data-testid="thread-root" className={className}>{children}</div>
),
Viewport: ({ children, className }: { children: ReactNode; className?: string }) => (
<div data-testid="thread-viewport" className={className}>{children}</div>
),
Empty: ({ children }: { children: ReactNode }) => <div>{children}</div>,
Messages: () => <div data-testid="thread-messages" />,
},
MessagePrimitive: {
Root: ({ children }: { children: ReactNode }) => <div>{children}</div>,
Content: () => null,
Parts: () => null,
},
useAui: () => ({ thread: () => ({ append: vi.fn() }) }),
useAuiState: () => false,
useMessage: () => ({
id: "message",
role: "assistant",
createdAt: new Date("2026-04-06T12:00:00.000Z"),
content: [],
metadata: { custom: {} },
status: { type: "complete" },
}),
}));
vi.mock("./transcript/useLiveRunTranscripts", () => ({
useLiveRunTranscripts: () => ({
transcriptByRun: new Map(),
hasOutputForRun: () => false,
}),
}));
vi.mock("./MarkdownBody", () => ({
MarkdownBody: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));
vi.mock("./MarkdownEditor", () => ({
MarkdownEditor: ({
value = "",
onChange,
}: {
value?: string;
onChange?: (value: string) => void;
}) => (
<textarea
aria-label="Issue chat editor"
value={value}
onChange={(event) => onChange?.(event.target.value)}
/>
),
}));
vi.mock("./InlineEntitySelector", () => ({
InlineEntitySelector: () => null,
}));
vi.mock("./Identity", () => ({
Identity: ({ name }: { name: string }) => <span>{name}</span>,
}));
vi.mock("./OutputFeedbackButtons", () => ({
OutputFeedbackButtons: () => null,
}));
vi.mock("./AgentIconPicker", () => ({
AgentIcon: () => null,
}));
vi.mock("./StatusBadge", () => ({
StatusBadge: ({ status }: { status: string }) => <span>{status}</span>,
}));
vi.mock("../hooks/usePaperclipIssueRuntime", () => ({
usePaperclipIssueRuntime: () => ({}),
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
describe("IssueChatThread", () => {
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
localStorage.clear();
});
afterEach(() => {
container.remove();
vi.useRealTimers();
});
it("drops the count heading and does not use an internal scrollbox", () => {
const root = createRoot(container);
act(() => {
root.render(
<MemoryRouter>
<IssueChatThread
comments={[]}
linkedRuns={[]}
timelineEvents={[]}
liveRuns={[]}
onAdd={async () => {}}
showComposer={false}
enableLiveTranscriptPolling={false}
/>
</MemoryRouter>,
);
});
expect(container.textContent).toContain("Jump to latest");
expect(container.textContent).not.toContain("Chat (");
const viewport = container.querySelector('[data-testid="thread-viewport"]') as HTMLDivElement | null;
expect(viewport).not.toBeNull();
expect(viewport?.className).not.toContain("overflow-y-auto");
expect(viewport?.className).not.toContain("max-h-[70vh]");
act(() => {
root.unmount();
});
});
it("stores and restores the composer draft per issue key", () => {
vi.useFakeTimers();
const root = createRoot(container);
act(() => {
root.render(
<MemoryRouter>
<IssueChatThread
comments={[]}
linkedRuns={[]}
timelineEvents={[]}
liveRuns={[]}
onAdd={async () => {}}
draftKey="issue-chat-draft:test-1"
enableLiveTranscriptPolling={false}
/>
</MemoryRouter>,
);
});
const editor = container.querySelector('textarea[aria-label="Issue chat editor"]') as HTMLTextAreaElement | null;
expect(editor).not.toBeNull();
act(() => {
const valueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value",
)?.set;
valueSetter?.call(editor, "Draft survives refresh");
editor?.dispatchEvent(new Event("input", { bubbles: true }));
});
act(() => {
vi.advanceTimersByTime(900);
});
expect(localStorage.getItem("issue-chat-draft:test-1")).toBe("Draft survives refresh");
act(() => {
root.unmount();
});
const remount = createRoot(container);
act(() => {
remount.render(
<MemoryRouter>
<IssueChatThread
comments={[]}
linkedRuns={[]}
timelineEvents={[]}
liveRuns={[]}
onAdd={async () => {}}
draftKey="issue-chat-draft:test-1"
enableLiveTranscriptPolling={false}
/>
</MemoryRouter>,
);
});
const restoredEditor = container.querySelector('textarea[aria-label="Issue chat editor"]') as HTMLTextAreaElement | null;
expect(restoredEditor?.value).toBe("Draft survives refresh");
act(() => {
remount.unmount();
});
});
});