/** * @file MarkdownContent.test.tsx * @description Tests for the lightweight markdown renderer used by the conversation viewer. * Focuses on the block parser since the inline parser is well-exercised by snapshot-style * DOM assertions. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect } from "vitest"; import { vi } from "vitest"; import { fireEvent, render, screen } from "@testing-library/react"; import { MarkdownContent, OpenFileContext } from "../MarkdownContent"; import { resolveIn } from "../../../lib/resolvePath"; function withOpen(text: string) { const open = vi.fn(); render( ); return open; } describe("file links", () => { it("opens a relative path with its line suffix stripped", () => { const open = withOpen("See server/lib/x.js:42 for it."); fireEvent.click(screen.getByRole("button", { name: "server/lib/x.js:42" })); expect(open).toHaveBeenCalledWith("server/lib/x.js"); }); it("opens an absolute path with a line suffix and one in inline code", () => { const open = withOpen("At /a/b/c.ts:9 and `docs/API.md`."); fireEvent.click(screen.getByRole("button", { name: "/a/b/c.ts:9" })); fireEvent.click(screen.getByRole("button", { name: "docs/API.md" })); expect(open.mock.calls).toEqual([["/a/b/c.ts"], ["docs/API.md"]]); }); it("opens a markdown link to a file instead of navigating", () => { const open = withOpen("[the panel](client/src/A%20B.tsx#L12) and [web](https://x.dev)"); fireEvent.click(screen.getByRole("button", { name: "the panel" })); expect(open).toHaveBeenCalledWith("client/src/A B.tsx"); expect(screen.getByRole("link", { name: "web" })).toHaveAttribute("href", "https://x.dev"); }); it("leaves prose with slashes as text", () => { withOpen("and/or, 3/4.5, github.com/x.js, TCP/IP."); expect(screen.queryAllByRole("button")).toHaveLength(0); }); it("resolves a relative path against the pane's folder", () => { expect(resolveIn("/w/repo/", "./server/../docs/API.md")).toBe("/w/repo/docs/API.md"); expect(resolveIn("/w/repo", "/etc/x.conf")).toBe("/etc/x.conf"); }); }); describe("", () => { it("renders fenced code blocks with the language label", () => { render(); // The CodeBlock header shows the language expect(screen.getByText(/javascript/i)).toBeInTheDocument(); // The code text is present (split across syntax-highlighted spans, so use a substring) expect(screen.getByText(/const/)).toBeInTheDocument(); }); it("renders headings as semantic-looking elements", () => { render(); expect(screen.getByText("Title")).toBeInTheDocument(); expect(screen.getByText("body")).toBeInTheDocument(); }); it("renders unordered and ordered lists", () => { const { container } = render(); expect(container.querySelectorAll("ul li")).toHaveLength(2); expect(container.querySelectorAll("ol li")).toHaveLength(2); }); it("renders blockquotes", () => { const { container } = render( a quote"} />); expect(container.querySelector("blockquote")).not.toBeNull(); expect(screen.getByText("a quote")).toBeInTheDocument(); }); it("renders inline code, bold, and italic", () => { const { container } = render( ); expect(container.querySelector("code")).not.toBeNull(); expect(container.querySelector("strong")).not.toBeNull(); expect(container.querySelector("em")).not.toBeNull(); }); it("auto-links bare URLs and renders explicit markdown links", () => { const { container } = render( ); const links = container.querySelectorAll("a"); expect(links.length).toBe(2); expect(links[0]!.getAttribute("href")).toBe("https://example.com"); expect(links[1]!.getAttribute("href")).toBe("https://example.com/docs"); // Both should open in a new tab safely for (const a of links) { expect(a.getAttribute("target")).toBe("_blank"); expect(a.getAttribute("rel")).toContain("noopener"); } }); it("renders plain text without any markdown features", () => { render(); expect(screen.getByText("just a normal sentence.")).toBeInTheDocument(); }); it("highlights standalone numbers, never ones inside a word, sha or issue ref", () => { const { container } = render( ); const nums = [...container.querySelectorAll("span.tabular-nums")].map((n) => n.textContent); expect(nums).toEqual(["3.5s", "87%", "$1.20", "v2.7.1", "2026-10-12"]); }); it("highlights an ISO date-time as one number", () => { const { container } = render( ); const nums = [...container.querySelectorAll("span.tabular-nums")].map((n) => n.textContent); expect(nums).toEqual([ "2026-10-12T08:00", "2026-10-12T08:00:00Z", "2026-10-12T08:00:00.123+07:00", ]); }); it("does not highlight numbers inside inline code", () => { const { container } = render(); expect(container.querySelector("span.tabular-nums")).toBeNull(); }); it("colours a flag label by how serious it is", () => { render(); expect(screen.getByText("Lưu ý:").className).toContain("text-accent"); expect(screen.getByText("Warning:").className).toContain("text-status-warning"); expect(screen.getByText("Lỗi:").className).toContain("text-status-danger"); expect(screen.queryByText("Note")).toBeNull(); }); it("handles empty input safely", () => { const { container } = render(); // Wrapper exists but no block elements expect(container.firstChild).not.toBeNull(); expect(container.querySelectorAll("p, ul, ol, blockquote, pre, hr").length).toBe(0); }); it("renders a table that follows prose with no blank line (issue #83)", () => { const text = 'Cách kiểm tra:\n| Token xoá | Số chỗ |\n|---|---|\n| `width="N"` | 29 |'; const { container } = render(); expect(container.querySelector("p")?.textContent).toBe("Cách kiểm tra:"); expect(container.querySelectorAll("th").length).toBe(2); expect(container.querySelectorAll("tbody tr").length).toBe(1); expect(container.textContent).not.toContain("|---|"); }); it("accepts a GFM divider with single dashes", () => { const { container } = render(); expect(container.querySelectorAll("th").length).toBe(2); }); });