import "../index.css"; import { page } from "vite-plus/test/browser"; import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { render } from "vitest-browser-react"; const { openInPreferredEditorMock, readLocalApiMock } = vi.hoisted(() => ({ openInPreferredEditorMock: vi.fn(async () => "vscode"), readLocalApiMock: vi.fn(() => ({ server: { getConfig: vi.fn(async () => ({ availableEditors: ["vscode"] })) }, shell: { openInEditor: vi.fn(async () => undefined) }, })), })); vi.mock("../editorPreferences", () => ({ openInPreferredEditor: openInPreferredEditorMock, })); vi.mock("../localApi", () => ({ ensureLocalApi: vi.fn(() => { throw new Error("ensureLocalApi not implemented in browser test"); }), readLocalApi: readLocalApiMock, })); import ChatMarkdown from "./ChatMarkdown"; import { serializeTableElementToCsv, serializeTableElementToMarkdown } from "../markdown-clipboard"; describe("ChatMarkdown", () => { afterEach(() => { openInPreferredEditorMock.mockClear(); readLocalApiMock.mockClear(); localStorage.clear(); document.body.innerHTML = ""; }); it("rewrites file uri hrefs into direct paths before rendering", async () => { const filePath = "/Users/yashsingh/p/sco/claude-code-extract/src/utils/permissions/PermissionRule.ts"; const screen = await render( , ); try { const link = page.getByRole("link", { name: "PermissionRule.ts" }); await expect.element(link).toBeInTheDocument(); await expect.element(link).toHaveAttribute("href", filePath); await link.click(); await vi.waitFor(() => { expect(openInPreferredEditorMock).toHaveBeenCalledWith(expect.anything(), filePath); }); } finally { await screen.unmount(); } }); it("keeps line anchors working after rewriting file uri hrefs", async () => { const filePath = "/Users/yashsingh/p/sco/claude-code-extract/src/utils/permissions/PermissionRule.ts"; const screen = await render( , ); try { const link = page.getByRole("link", { name: "PermissionRule.ts · L1" }); await expect.element(link).toBeInTheDocument(); await expect.element(link).toHaveAttribute("href", `${filePath}:1`); await link.click(); await vi.waitFor(() => { expect(openInPreferredEditorMock).toHaveBeenCalledWith(expect.anything(), `${filePath}:1`); }); } finally { await screen.unmount(); } }); it("shows column information inline when present", async () => { const filePath = "/Users/yashsingh/p/sco/claude-code-extract/src/utils/permissions/PermissionRule.ts"; const screen = await render( , ); try { const link = page.getByRole("link", { name: "PermissionRule.ts · L1:C7" }); await expect.element(link).toBeInTheDocument(); await expect.element(link).toHaveAttribute("href", `${filePath}:1:7`); await link.click(); await vi.waitFor(() => { expect(openInPreferredEditorMock).toHaveBeenCalledWith( expect.anything(), `${filePath}:1:7`, ); }); } finally { await screen.unmount(); } }); it("disambiguates duplicate file basenames inline", async () => { const firstPath = "/Users/yashsingh/p/t3code/apps/web/src/components/chat/MessagesTimeline.tsx"; const secondPath = "/Users/yashsingh/p/t3code/apps/web/src/components/MessagesTimeline.tsx"; const screen = await render( , ); try { await expect .element(page.getByRole("link", { name: "MessagesTimeline.tsx · components/chat" })) .toBeInTheDocument(); await expect .element(page.getByRole("link", { name: "MessagesTimeline.tsx · src/components" })) .toBeInTheDocument(); } finally { await screen.unmount(); } }); it("keeps normal web links unchanged", async () => { const screen = await render( , ); try { const link = page.getByRole("link", { name: "OpenAI" }); await expect.element(link).toBeInTheDocument(); await expect.element(link).toHaveAttribute("href", "https://openai.com/docs"); await expect.element(link).toHaveAttribute("target", "_blank"); const favicon = link.element().querySelector(".chat-markdown-link-favicon"); const leading = link.element().querySelector(".chat-markdown-link-leading"); expect(favicon).not.toBeNull(); expect(leading).not.toBeNull(); expect(leading?.contains(favicon)).toBe(true); expect(getComputedStyle(leading!).display).toBe("inline"); expect(getComputedStyle(leading!).whiteSpace).toBe("nowrap"); expect(getComputedStyle(favicon!).verticalAlign).not.toBe("baseline"); expect(leading?.textContent).toBe("O"); expect(link.element().textContent).toBe("OpenAI"); expect(getComputedStyle(link.element()).textDecorationLine).toBe("none"); expect(link.element().querySelector("img, svg")?.getBoundingClientRect().width).toBe(14); await link.hover(); expect(getComputedStyle(link.element()).backgroundImage).not.toBe("none"); await expect.element(page.getByText("https://openai.com/docs")).toBeVisible(); } finally { await screen.unmount(); } }); it("keeps a favicon with the leading segment of a wrapping URL", async () => { const url = "https://github.com/pingdotgg/t3code/pull/3017/changes"; const screen = await render(
, ); try { const link = page.getByRole("link", { name: url }); const leading = link.element().querySelector(".chat-markdown-link-leading"); const favicon = link.element().querySelector(".chat-markdown-link-favicon"); expect(leading).not.toBeNull(); expect(favicon).not.toBeNull(); expect(leading?.contains(favicon)).toBe(true); expect(leading?.textContent).toBe("https://"); expect(getComputedStyle(leading!).display).toBe("inline"); expect(getComputedStyle(leading!).whiteSpace).toBe("nowrap"); expect(getComputedStyle(favicon!).verticalAlign).not.toBe("baseline"); expect(link.element().textContent).toBe(url); expect(link.element().querySelectorAll("wbr").length).toBeGreaterThan(0); const markdownRoot = link.element().closest(".chat-markdown"); expect(markdownRoot).not.toBeNull(); expect(markdownRoot!.scrollWidth).toBeLessThanOrEqual(markdownRoot!.clientWidth); } finally { await screen.unmount(); } }); it("renders file links with the shared file tag chip treatment", async () => { const screen = await render( , ); try { const link = page.getByRole("link", { name: "package.json" }); await expect.element(link).toHaveClass(/chat-markdown-file-link/); const element = document.querySelector(".chat-markdown-file-link"); expect(element?.querySelector("img, svg")).not.toBeNull(); expect(getComputedStyle(element!).display).toBe("inline-flex"); expect(getComputedStyle(element!).textDecorationLine).toBe("none"); expect(getComputedStyle(element!).borderStyle).toBe("solid"); expect(getComputedStyle(element!).userSelect).not.toBe("none"); } finally { await screen.unmount(); } }); it("renders sanitized details with the design-system collapsible", async () => { const source = [ "
", "Expandable details section", "", "This content includes **formatted text**.", "", 'Safe inline HTML', "", "
", ].join("\n"); const screen = await render(); try { const details = document.querySelector("[data-markdown-details]"); const trigger = page.getByRole("button", { name: "Expandable details section" }); expect(details).not.toBeNull(); expect(details?.tagName).toBe("DIV"); await expect.element(trigger).toHaveAttribute("aria-expanded", "true"); expect(details?.querySelector("strong")?.textContent).toBe("formatted text"); expect(details?.querySelector("script")).toBeNull(); expect(details?.querySelector("[title]")).toBeNull(); await trigger.click(); await expect.element(trigger).toHaveAttribute("aria-expanded", "false"); await trigger.click(); await expect.element(trigger).toHaveAttribute("aria-expanded", "true"); } finally { await screen.unmount(); } }); it("renders footnotes as same-document references", async () => { const source = [ "A claim with supporting context.[^context]", "", "[^context]: Supporting **footnote text**.", ].join("\n"); const screen = await render(); try { const reference = document.querySelector( '.chat-markdown a[data-footnote-ref=""]', ); const footnotes = document.querySelector( ".chat-markdown section[data-footnotes]", ); expect(reference).not.toBeNull(); expect(reference?.getAttribute("href")).toMatch(/^#user-content-fn-/); expect(reference?.hasAttribute("target")).toBe(false); expect(footnotes).not.toBeNull(); expect(footnotes?.querySelector("strong")?.textContent).toBe("footnote text"); expect(footnotes?.querySelector("a[data-footnote-backref]")?.target).toBe( "", ); } finally { await screen.unmount(); } }); it("navigates hash links within the clicked markdown message", async () => { const source = [ "A claim with supporting context.[^context]", "", "[^context]: Supporting footnote text.", ].join("\n"); const originalUrl = window.location.href; const scrollIntoView = vi .spyOn(HTMLElement.prototype, "scrollIntoView") .mockImplementation(() => undefined); const screen = await render(
, ); try { const markdownRoots = document.querySelectorAll(".chat-markdown"); const secondRoot = markdownRoots[1]; const secondReference = secondRoot?.querySelector('a[data-footnote-ref=""]'); const secondFootnote = secondRoot?.querySelector( "section[data-footnotes] li[id]", ); expect(secondReference).not.toBeNull(); expect(secondFootnote).not.toBeNull(); secondReference?.click(); expect(scrollIntoView).toHaveBeenCalledTimes(1); expect(scrollIntoView.mock.instances[0]).toBe(secondFootnote); expect(window.location.hash).toBe(secondReference?.hash); const secondBackref = secondRoot?.querySelector( "a[data-footnote-backref]", ); expect(secondBackref).not.toBeNull(); secondBackref?.click(); const secondReferenceTarget = secondReference?.closest("[id]"); expect(scrollIntoView).toHaveBeenCalledTimes(2); expect(scrollIntoView.mock.instances[1]).toBe(secondReferenceTarget); } finally { scrollIntoView.mockRestore(); window.history.replaceState(window.history.state, "", originalUrl); await screen.unmount(); } }); describe("code block chrome", () => { it("shows icon-only language titles, text fallbacks, and filename overrides", async () => { const source = [ "```ts", "const a = 1;", "```", "", '```ts title="src/main.ts"', "const b = 2;", "```", "", "```text", "plain", "```", ].join("\n"); const screen = await render(); try { const titles = [...document.querySelectorAll(".chat-markdown-codeblock-title")]; expect(titles).toHaveLength(3); // Language with a known icon: icon XOR text — never the redundant pair. const languageOnly = titles[0]!; const hasIcon = languageOnly.querySelector("img") != null; const hasText = (languageOnly.textContent ?? "").includes("ts"); expect(hasIcon || hasText).toBe(true); expect(hasIcon && hasText).toBe(false); if (hasIcon) { const languageTrigger = page.getByLabelText("Language: ts").first(); await languageTrigger.hover(); await vi.waitFor(() => { const tooltip = document.querySelector('[data-slot="tooltip-popup"]'); expect(tooltip?.textContent).toContain("ts"); }); } // Explicit filename: text always shown. expect(titles[1]!.textContent).toBe("src/main.ts"); // Unknown language: no icon attempt, text label. expect(titles[2]!.querySelector("img")).toBeNull(); expect(titles[2]!.textContent).toBe("text"); } finally { await screen.unmount(); } }); it("toggles line wrapping per block", async () => { const screen = await render( , ); try { const block = document.querySelector(".chat-markdown-codeblock"); expect(block?.getAttribute("data-wrap")).toBe("false"); const toggle = page.getByRole("button", { name: "Wrap lines" }); await expect.element(toggle).not.toHaveAttribute("title"); await toggle.hover(); await vi.waitFor(() => { const tooltip = document.querySelector('[data-slot="tooltip-popup"]'); expect(tooltip?.textContent).toContain("Wrap lines"); }); await toggle.click(); expect(block?.getAttribute("data-wrap")).toBe("true"); await page.getByRole("button", { name: "Disable line wrap" }).click(); expect(block?.getAttribute("data-wrap")).toBe("false"); } finally { await screen.unmount(); } }); }); it("scrolls wide tables horizontally instead of letter-wrapping cells", async () => { const header = `| ${Array.from({ length: 8 }, (_, i) => `ColumnHeading${i}`).join(" | ")} |`; const separator = `| ${Array.from({ length: 8 }, () => "---").join(" | ")} |`; const row = `| ${Array.from({ length: 8 }, () => "averylongunbrokencellvalue@example-domain.com").join(" | ")} |`; const screen = await render( , ); try { const viewport = document.querySelector( '.chat-markdown-table-container [data-slot="scroll-area-viewport"]', ); expect(viewport).not.toBeNull(); expect(viewport!.querySelector("table")).not.toBeNull(); // Content exceeds the container — the scroll-fade viewport scrolls // horizontally rather than squishing columns. expect(viewport!.scrollWidth).toBeGreaterThan(viewport!.clientWidth); // And cells keep their longest word intact instead of breaking mid-word. const cell = viewport!.querySelector("td"); expect(cell!.getBoundingClientRect().width).toBeGreaterThan(100); } finally { await screen.unmount(); } }); describe("table chrome", () => { const longCell = "This service has been experiencing intermittent latency spikes during peak traffic hours and the on-call team is investigating."; it("truncates cells by default and expands them from the footer toggle", async () => { const source = ["| Name | Notes |", "| --- | --- |", `| api | ${longCell} |`].join("\n"); const screen = await render(); try { const container = document.querySelector(".chat-markdown-table-container"); expect(container?.getAttribute("data-expanded")).toBe("false"); const noteCell = [...document.querySelectorAll(".chat-markdown td")].at(-1)!; expect(getComputedStyle(noteCell).whiteSpace).toBe("nowrap"); expect(noteCell.scrollWidth).toBeGreaterThan(noteCell.clientWidth); const expandButton = page.getByRole("button", { name: "Expand table cells" }); await expect.element(expandButton).not.toHaveAttribute("title"); await expandButton.hover(); await vi.waitFor(() => { const tooltip = document.querySelector('[data-slot="tooltip-popup"]'); expect(tooltip?.textContent).toContain("Expand table cells"); }); await expandButton.click(); expect(container?.getAttribute("data-expanded")).toBe("true"); expect(getComputedStyle(noteCell).whiteSpace).not.toBe("nowrap"); await page.getByRole("button", { name: "Collapse table cells" }).click(); expect(container?.getAttribute("data-expanded")).toBe("false"); const copyButton = page.getByRole("button", { name: "Copy table" }); await expect.element(copyButton).not.toHaveAttribute("title"); await copyButton.hover(); await vi.waitFor(() => { const tooltip = document.querySelector('[data-slot="tooltip-popup"]'); expect(tooltip?.textContent).toContain("Copy table"); }); expect(document.querySelector(".chat-markdown [title]")).toBeNull(); } finally { await screen.unmount(); } }); it("retains column widths when cells expand", async () => { const source = [ "| ID | Owner | Status | Priority | Region | Summary | Long Description | Metrics | Payload | Notes |", "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |", '| 1001 | Ada Lovelace | Active | High | us-west-2 | Payment workflow migration | This cell has enough text to wrap across several lines when expanded without shrinking its column. | Requests: 128,440; Error rate: 0.04%; P95: 212ms | `{ "feature": "billing", "version": 3 }` | Needs post-release monitoring for 24 hours. |', ].join("\n"); const screen = await render(); try { const viewport = document.querySelector( '.chat-markdown-table-container [data-slot="scroll-area-viewport"]', )!; const table = viewport.querySelector("table")!; const collapsedWidths = [...table.querySelectorAll("thead th")].map( (cell) => cell.getBoundingClientRect().width, ); expect(viewport.scrollWidth).toBeGreaterThan(viewport.clientWidth); await page.getByRole("button", { name: "Expand table cells" }).click(); const expandedWidths = [...table.querySelectorAll("thead th")].map( (cell) => cell.getBoundingClientRect().width, ); expect(expandedWidths).toHaveLength(collapsedWidths.length); expandedWidths.forEach((width, index) => { expect(width).toBeGreaterThanOrEqual(collapsedWidths[index]! - 1); }); expect(viewport.scrollWidth).toBeGreaterThan(viewport.clientWidth); } finally { await screen.unmount(); } }); it("exports tables as markdown and csv", async () => { const source = [ "| Name | Count |", "| --- | ---: |", '| widget, "deluxe" | 2 |', "| plain | 1 |", ].join("\n"); const screen = await render(); try { const table = document.querySelector(".chat-markdown table")!; expect(serializeTableElementToMarkdown(table)).toBe( ["| Name | Count |", "| --- | ---: |", '| widget, "deluxe" | 2 |', "| plain | 1 |"].join( "\n", ), ); expect(serializeTableElementToCsv(table)).toBe( ["Name,Count", '"widget, ""deluxe""",2', "plain,1"].join("\n"), ); } finally { await screen.unmount(); } }); }); describe("copying rendered markdown", () => { function copySelectedMarkdown(): { text: string; html: string } { const root = document.querySelector(".chat-markdown"); if (!root) throw new Error("chat-markdown root not rendered"); const selection = window.getSelection(); if (!selection) throw new Error("selection unavailable"); selection.removeAllRanges(); const range = document.createRange(); range.selectNodeContents(root); selection.addRange(range); const clipboardData = new DataTransfer(); root.dispatchEvent( new ClipboardEvent("copy", { clipboardData, bubbles: true, cancelable: true }), ); selection.removeAllRanges(); return { text: clipboardData.getData("text/plain"), html: clipboardData.getData("text/html"), }; } it("round-trips links, emphasis, and inline code", async () => { const screen = await render( , ); try { const { text, html } = copySelectedMarkdown(); expect(text).toBe( "Check out [Anthropic](https://anthropic.com), **bold**, *italic*, and `code`.", ); expect(html).toContain('href="https://anthropic.com"'); } finally { await screen.unmount(); } }); it("round-trips block structure: headings, lists, quotes, and fences", async () => { const source = [ "## Heading", "", "- first", "- second", " - nested", "", "1. one", "2. two", "", "- [x] done", "- [ ] todo", "", "> quoted", "", "```ts", "const x = 1;", "", "const y = 2;", "```", ].join("\n"); const screen = await render(); try { const { text } = copySelectedMarkdown(); expect(text).toBe(source); } finally { await screen.unmount(); } }); it("round-trips tables with alignment", async () => { const source = ["| Name | Count |", "| --- | ---: |", "| a | 1 |", "| b | 2 |"].join("\n"); const screen = await render(); try { const { text } = copySelectedMarkdown(); expect(text).toBe(source); } finally { await screen.unmount(); } }); it("round-trips details rendered through the collapsible", async () => { const source = [ "
", "Expandable details section", "", "This content includes **formatted text**.", "
", ].join("\n"); const screen = await render(); try { const { text } = copySelectedMarkdown(); expect(text).toBe(source); } finally { await screen.unmount(); } }); it("excludes the code block header chrome from copied markdown", async () => { const source = ["```ts", "const x = 1;", "```"].join("\n"); const screen = await render(); try { const { text } = copySelectedMarkdown(); expect(text).toBe(source); } finally { await screen.unmount(); } }); it("copies file links as markdown and skips UI affordances", async () => { const filePath = "/Users/yashsingh/p/t3code/src/utils/permissions/PermissionRule.ts"; const screen = await render( , ); try { const { text, html } = copySelectedMarkdown(); expect(text).toBe( `See [PermissionRule.ts](/Users/yashsingh/p/t3code/src/utils/permissions/PermissionRule.ts) for details.`, ); expect(html).toContain("PermissionRule.ts"); expect(html).not.toContain(" { const source = "Use $agent-browser with [package.json](path/to/package.json) before continuing."; const screen = await render( , ); try { const root = document.querySelector(".chat-markdown")!; const selection = window.getSelection()!; selection.removeAllRanges(); const range = document.createRange(); range.selectNodeContents(root); selection.addRange(range); expect(selection.toString()).toContain("Agent Browser"); expect(selection.toString()).toContain("package.json"); selection.removeAllRanges(); const { text, html } = copySelectedMarkdown(); expect(text).toBe(source); expect(html).toContain("Agent Browser"); expect(html).toContain("package.json"); expect(html).not.toContain("