// @vitest-environment node import type { ReactNode } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; import { buildAgentMentionHref, buildProjectMentionHref, buildSkillMentionHref } from "@paperclipai/shared"; import { ThemeProvider } from "../context/ThemeContext"; import { MarkdownBody } from "./MarkdownBody"; import { queryKeys } from "../lib/queryKeys"; const mockIssuesApi = vi.hoisted(() => ({ get: vi.fn(), })); vi.mock("@/lib/router", () => ({ Link: ({ children, to }: { children: ReactNode; to: string }) => {children}, })); vi.mock("../api/issues", () => ({ issuesApi: mockIssuesApi, })); function renderMarkdown(children: string, seededIssues: Array<{ identifier: string; status: string }> = []) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }); for (const issue of seededIssues) { queryClient.setQueryData(queryKeys.issues.detail(issue.identifier), { id: issue.identifier, identifier: issue.identifier, status: issue.status, }); } return renderToStaticMarkup( {children} , ); } describe("MarkdownBody", () => { it("renders markdown images without a resolver", () => { const html = renderToStaticMarkup( {"![](/api/attachments/test/content)"} , ); expect(html).toContain(''); }); it("resolves relative image paths when a resolver is provided", () => { const html = renderToStaticMarkup( `/resolved/${src}`}> {"![Org chart](images/org-chart.png)"} , ); expect(html).toContain('src="/resolved/images/org-chart.png"'); expect(html).toContain('alt="Org chart"'); }); it("renders agent, project, and skill mentions as chips", () => { const html = renderToStaticMarkup( {`[@CodexCoder](${buildAgentMentionHref("agent-123", "code")}) [@Paperclip App](${buildProjectMentionHref("project-456", "#336699")}) [/release-changelog](${buildSkillMentionHref("skill-789", "release-changelog")})`} , ); expect(html).toContain('href="/agents/agent-123"'); expect(html).toContain('data-mention-kind="agent"'); expect(html).toContain("--paperclip-mention-icon-mask"); expect(html).toContain('href="/projects/project-456"'); expect(html).toContain('data-mention-kind="project"'); expect(html).toContain("--paperclip-mention-project-color:#336699"); expect(html).toContain('href="/skills/skill-789"'); expect(html).toContain('data-mention-kind="skill"'); }); it("uses soft-break styling by default", () => { const html = renderMarkdown("First line\nSecond line"); expect(html).toContain("First line
"); expect(html).toContain("Second line"); }); it("can opt out of soft-break styling", () => { const html = renderToStaticMarkup( {"First line\nSecond line"} , ); expect(html).not.toContain("
"); }); it("does not inject extra line-break nodes into nested lists", () => { const html = renderMarkdown("1. Parent item\n - child a\n - child b\n\n2. Second item"); expect(html).not.toContain("[&_p]:whitespace-pre-line"); expect(html).not.toContain("Parent item
"); expect(html).toContain("
    "); expect(html).toContain("