/** * Md (chat markdown) rendering contract, via react-dom/server static markup (node env, no DOM): * - every link — explicit [text](url), bare autolinked URL (remark-gfm), relative or #anchor — * opens in a new tab: target="_blank" + rel="noreferrer" (a chat link must never navigate the * SPA away from the live conversation); * - fenced code still routes through the module-scope pre override into CodeBlock (its chrome * renders; Shiki only loads in an effect, which static markup never runs). */ import { describe, expect, it } from "vitest"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { Md } from "../src/features/chat/md"; const render = (text: string, streaming = false) => renderToStaticMarkup(createElement(Md, { text, streaming })); /** All rendered opening tags. */ const anchors = (html: string) => html.match(/]*>/g) ?? []; const expectNewTab = (tag: string | undefined) => { expect(tag).toContain('target="_blank"'); expect(tag).toContain('rel="noreferrer"'); }; describe("Md links", () => { it("explicit markdown links open in a new tab", () => { const html = render("See [the docs](https://example.com/docs) for details."); const tags = anchors(html); expect(tags).toHaveLength(1); expect(tags[0]).toContain('href="https://example.com/docs"'); expectNewTab(tags[0]); expect(html).toContain(">the docs"); }); it("bare autolinked URLs in CJK prose open in a new tab", () => { const html = render("前往 https://example.com/a/very/long/path 查看结果。"); const tags = anchors(html); expect(tags).toHaveLength(1); expect(tags[0]).toContain('href="https://example.com/a/very/long/path"'); expectNewTab(tags[0]); }); it('preserves the markdown link title from [text](url "title")', () => { const html = render('Read [docs](https://example.com "API docs") first.'); const tags = anchors(html); expect(tags).toHaveLength(1); expect(tags[0]).toContain('href="https://example.com"'); expect(tags[0]).toContain('title="API docs"'); expectNewTab(tags[0]); }); it("relative and #anchor hrefs also open in a new tab (never SPA-navigate)", () => { const html = render("[rel](./file.md) and [frag](#section)"); const tags = anchors(html); expect(tags).toHaveLength(2); for (const tag of tags) expectNewTab(tag); }); it("applies in both streaming and settled component maps", () => { for (const streaming of [true, false]) { const tags = anchors(render("[x](https://example.com/)", streaming)); expect(tags).toHaveLength(1); expectNewTab(tags[0]); } }); }); describe("Md code blocks", () => { it("fenced code still renders through the CodeBlock pre override", () => { const html = render("```js\nconst a = 1;\n```"); expect(html).toContain("code-block"); // CodeBlock chrome wrapper class expect(html).toContain("const a = 1;"); }); });