import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import MarkdownTextRenderer from "@/components/MarkdownTextRenderer"; describe("MarkdownTextRenderer", () => { it("renders clickable markdown links in blue", () => { render([local server](http://127.0.0.1:7891/)); const link = screen.getByRole("link", { name: "local server" }); expect(link).toHaveAttribute("href", "http://127.0.0.1:7891/"); expect(link).toHaveClass("text-blue-500", "dark:text-blue-300"); }); it("renders local file links as previewable file references", () => { const onOpenFilePreview = vi.fn(); render( {"Edited [hook.py](/Users/test/project/nanobot/agent/hook.py:12)"} , ); const reference = screen.getByTestId("inline-file-path"); expect(reference).toHaveTextContent("hook.py"); expect(reference).toHaveAttribute( "aria-label", "/Users/test/project/nanobot/agent/hook.py", ); fireEvent.click(reference); expect(onOpenFilePreview).toHaveBeenCalledWith( "/Users/test/project/nanobot/agent/hook.py", ); }); it("does not treat non-file hrefs as previews just because the label looks like a file", () => { const onOpenFilePreview = vi.fn(); render( {"Download [index.html](/api/media/sig/html)"} , ); expect(screen.queryByTestId("inline-file-path")).not.toBeInTheDocument(); expect(screen.getByRole("link", { name: "index.html" })).toHaveAttribute( "href", "/api/media/sig/html", ); }); it("renders glob file links as plain text instead of preview targets", () => { const onOpenFilePreview = vi.fn(); const { container } = render( {"原始对话通常还在 [*.json](*.json)。"} , ); expect(screen.queryByTestId("inline-file-path")).not.toBeInTheDocument(); expect(screen.queryByRole("link", { name: "*.json" })).not.toBeInTheDocument(); expect(container).toHaveTextContent("*.json"); }); it("keeps glob inline code as code instead of a file preview chip", () => { render( {"检查 `src/**/*.json`。"} , ); expect(screen.queryByTestId("inline-file-path")).not.toBeInTheDocument(); expect(screen.getByText("src/**/*.json").tagName).toBe("CODE"); }); it("does not wrap complete fenced code blocks in an extra pre", () => { const { container } = render( {"当前目录:\n\n```text\n/Users/renxubin/.nanobot/workspace\n```"} , ); expect(screen.getByText("/Users/renxubin/.nanobot/workspace")).toBeInTheDocument(); expect(container.querySelectorAll("pre")).toHaveLength(1); expect(container.querySelector("pre div")).toBeNull(); }); it("renders bare fenced code blocks without crashing", () => { const { container } = render( {"Some text\n\n```\ncode without language\n```"} , ); expect(screen.getByText("code without language")).toBeInTheDocument(); expect(screen.getByText("text")).toBeInTheDocument(); expect(container.querySelectorAll("pre")).toHaveLength(1); }); it("keeps streaming unfinished fenced code blocks to a single shell", () => { const { container } = render( {"当前目录:\n\n```text\n/Users/renxubin/.nanobot/workspace"} , ); expect(screen.getByText("/Users/renxubin/.nanobot/workspace")).toBeInTheDocument(); expect(container.querySelectorAll("pre")).toHaveLength(1); expect(container.querySelector("pre div")).toBeNull(); }); it("renders markdown images as inline previews", () => { render(![Diagram](/api/media/sig/payload)); const image = screen.getByRole("img", { name: "Diagram" }); expect(image).toHaveAttribute("src", "/api/media/sig/payload"); expect(screen.getByRole("link", { name: "Open Diagram" })).toHaveAttribute( "href", "/api/media/sig/payload", ); }); it("renders markdown videos as inline players", () => { render(![nanobot-intro.mp4](/api/media/sig/video)); const video = screen.getByLabelText("Video attachment: nanobot-intro.mp4"); expect(video.tagName).toBe("VIDEO"); expect(video).toHaveAttribute("src", "/api/media/sig/video"); expect(video).toHaveAttribute("controls"); expect(screen.queryByRole("img", { name: "nanobot-intro.mp4" })).not.toBeInTheDocument(); }); it("renders markdown links with file-looking names as file attachments", () => { render(![index.html](/api/media/sig/html)); expect(screen.getByLabelText("File attachment")).toHaveTextContent("index.html"); expect(screen.queryByRole("img", { name: "index.html" })).not.toBeInTheDocument(); }); it("renders title plus url list items as compact link rows", () => { render( { "Sources:\n\n- Polymarket — “When will GPT-5.6 be released?”\n https://polymarket.com/event/when-will-gpt-5pt6-be-released\n- Polymarket — “GPT-5.6 released by...?”\n https://polymarket.com/event/gpt-5pt6-released-by" } , ); expect( screen.getByRole("link", { name: "Open link: Polymarket — When will GPT-5.6 be released?", }), ).toHaveAttribute( "href", "https://polymarket.com/event/when-will-gpt-5pt6-be-released", ); expect( screen.getByRole("link", { name: "Open link: Polymarket — GPT-5.6 released by...?", }), ).toHaveAttribute("href", "https://polymarket.com/event/gpt-5pt6-released-by"); expect(screen.queryByText("Polymarket · polymarket.com")).not.toBeInTheDocument(); }); it("does not require a source heading for compact link rows", () => { render( { "Useful links:\n\n- Polymarket — “When will GPT-5.6 be released?”\n https://polymarket.com/event/when-will-gpt-5pt6-be-released" } , ); expect( screen.getByRole("link", { name: "Open link: Polymarket — When will GPT-5.6 be released?", }), ).toHaveAttribute("href", "https://polymarket.com/event/when-will-gpt-5pt6-be-released"); }); it("falls back through favicon sources before showing a globe for compact link rows", () => { const { container } = render( { "Useful links:\n\n- Savills Hong Kong Corporate Relocation — Corporate relocation services\n https://www.savills.com.hk/services/corporate-relocation.aspx" } , ); const link = screen.getByRole("link", { name: "Open link: Savills Hong Kong Corporate Relocation — Corporate relocation services", }); const favicon = () => link.querySelector("img"); expect(favicon()).toHaveAttribute( "src", "https://www.savills.com.hk/favicon.ico", ); fireEvent.error(favicon()!); expect(favicon()).toHaveAttribute( "src", "https://icons.duckduckgo.com/ip3/www.savills.com.hk.ico", ); fireEvent.error(favicon()!); expect(favicon()).toHaveAttribute( "src", "https://www.google.com/s2/favicons?domain=www.savills.com.hk&sz=64", ); fireEvent.error(favicon()!); expect(favicon()).not.toBeInTheDocument(); expect(link.querySelector("svg")).toBeInTheDocument(); expect(container).not.toHaveTextContent("SC"); }); it("renders media attachments without an extra preview/code wrapper", () => { render(![Diagram](/api/media/sig/payload)); expect(screen.getByRole("img", { name: "Diagram" })).toHaveAttribute( "src", "/api/media/sig/payload", ); expect(screen.getByRole("link", { name: "Open Diagram" })).toHaveAttribute( "href", "/api/media/sig/payload", ); expect(screen.queryByRole("button", { name: "Code" })).not.toBeInTheDocument(); }); it("renders a safe subset of inline HTML", () => { const { container } = render( {"高亮文本\n\n上标:x2\n下标:H2O"} , ); expect(container.querySelector("mark")).toHaveTextContent("高亮文本"); expect(container.querySelector("sup")).toHaveTextContent("2"); expect(container.querySelector("sub")).toHaveTextContent("2"); }); it("keeps unsafe HTML as text", () => { const { container } = render( {"\n\nbad"} , ); expect(container.querySelector("script")).toBeNull(); expect(container.querySelector("mark")).toBeNull(); expect(container).toHaveTextContent(""); expect(container).toHaveTextContent("bad"); }); it("renders safe details blocks", () => { const { container } = render( { "
点击展开更多内容\n\n这里是被折叠的内容。\n\n- 可以放列表\n\n
" }
, ); expect(container.querySelector("details")).toBeInTheDocument(); expect(container.querySelector("summary")).toHaveTextContent("点击展开更多内容"); expect(screen.getByText("这里是被折叠的内容。")).toBeInTheDocument(); expect(screen.getByText("可以放列表")).toBeInTheDocument(); expect(container).not.toHaveTextContent(""); }); it("renders task list checkboxes as quiet status marks", () => { const { container } = render( {"- [x] 写 Markdown 示例\n- [x] 加点 emoji\n- [ ] 测试渲染效果"} , ); expect(container.querySelectorAll("input[type='checkbox']")).toHaveLength(0); expect(screen.getAllByTestId("markdown-task-checkbox")).toHaveLength(3); expect(container.querySelectorAll(".task-list-item")).toHaveLength(3); }); it("keeps dollar amounts from being parsed as inline math", () => { const { container } = render( { "VBeats mentions $24 million, while Globe states a total of $130.6 million since founding." } , ); expect(container).toHaveTextContent( "VBeats mentions $24 million, while Globe states a total of $130.6 million since founding.", ); expect(container.querySelector(".katex")).toBeNull(); }); it("renders guarded single-dollar inline math", () => { const { container } = render( { "Variables $x$ and powers $2^n$ render inline, while a price range $10-20$ stays literal." } , ); expect(container.querySelectorAll(".katex")).toHaveLength(2); expect(container).not.toHaveTextContent("$x$"); expect(container).not.toHaveTextContent("$2^n$"); expect(container).toHaveTextContent("$10-20$"); }); it("renders model-style single-dollar formula lists", () => { const { container } = render( {[ "- Fourier transform: $\\hat{f}(\\xi) = \\int_{-\\infty}^{+\\infty} f(x)e^{-2\\pi i x \\xi}\\, dx$", "- Taylor expansion: $e^x = \\sum_{n=0}^{\\infty} \\frac{x^n}{n!}$", "- KL divergence: $D_\\text{KL}(P || Q) = \\sum_x P(x) \\log \\frac{P(x)}{Q(x)}$", "- Quantum state: $\\psi = \\alpha|0\\rangle + \\beta|1\\rangle$", ].join("\n")} , ); expect(container.querySelectorAll(".katex")).toHaveLength(4); expect(container).not.toHaveTextContent("$\\hat{f}"); expect(container).not.toHaveTextContent("$D_\\text"); }); it("renders TeX inline math delimiters", () => { const { container } = render( {"Einstein wrote \\(E = mc^2\\) for mass-energy equivalence."}, ); expect(container.querySelector(".katex")).toBeInTheDocument(); expect(container.querySelector(".katex-display")).toBeNull(); expect(container).not.toHaveTextContent("\\("); expect(container).not.toHaveTextContent("\\)"); }); it("renders TeX display math delimiters", () => { const { container } = render( {"\\[x^2 + y^2 = z^2\\]"}, ); expect(container.querySelector(".katex-display")).toBeInTheDocument(); expect(container).not.toHaveTextContent("\\["); expect(container).not.toHaveTextContent("\\]"); }); it("keeps TeX delimiters inside code literal", () => { const { container } = render( {"Inline `\\(x\\)` stays literal.\n\n```text\n\\[x^2\\]\n```"} , ); expect(container.querySelector(".katex")).toBeNull(); expect(screen.getByText("\\(x\\)").tagName).toBe("CODE"); expect(screen.getByText("\\[x^2\\]")).toBeInTheDocument(); }); it("still renders explicit math blocks", () => { const { container } = render( {"$$x^2 + y^2 = z^2$$"}, ); expect(container.querySelector(".katex")).toBeInTheDocument(); }); });