import { describe, it, expect } from "vitest"; import { markdownToHtml, renderInline, escapeHtml } from "@/lib/export/markdown-html"; import { buildExportHtml } from "@/lib/export/html-document"; describe("markdownToHtml", () => { it("escapes raw HTML instead of passing it through", () => { expect(markdownToHtml('')).toBe("

<script>alert("x")</script>

"); }); it("renders headings, paragraphs, emphasis and code spans", () => { const html = markdownToHtml("# Title\n\nSome **bold**, *em*, `code` and ~~gone~~ text.\nSecond line."); expect(html).toContain("

Title

"); expect(html).toContain("bold"); expect(html).toContain("em"); expect(html).toContain("code"); expect(html).toContain("gone"); expect(html).toContain("text.\nSecond line.

"); }); it("renders fenced code blocks verbatim (escaped) with a language", () => { const html = markdownToHtml("```ts\nconst a = (x: T) => x && 1;\n```"); expect(html).toBe('
const a = <T,>(x: T) => x && 1;
'); }); it("does not apply emphasis inside code spans", () => { expect(renderInline("use `a * b * c` here")).toBe("use a * b * c here"); }); it("renders nested lists and task items", () => { const html = markdownToHtml("- one\n- two\n - nested\n - again\n- [x] done\n\n1. first\n2. second"); expect(html).toContain(""); expect(html).toContain("
  1. first
  2. second
"); }); it("renders GFM tables with alignment", () => { const html = markdownToHtml("| Model | Price |\n|:--|--:|\n| GPT | $5 |\n| Claude | $3 |"); expect(html).toContain('Model'); expect(html).toContain('$5'); expect(html).toContain("/g)?.length).toBe(3); }); it("renders blockquotes and horizontal rules", () => { const html = markdownToHtml("> quoted\n> more\n\n---\n\nafter"); expect(html).toContain("

quoted\nmore

"); expect(html).toContain("
"); expect(html).toContain("

after

"); }); it("only links to safe URL schemes and adds rel", () => { expect(renderInline("[ok](https://a.b/c) [bad](javascript:alert(1)) see https://x.y/z.")).toBe('ok bad see https://x.y/z.'); }); it("escapes attributes", () => { expect(escapeHtml(``)).toBe("<a href="x" onclick='y'>"); }); }); describe("buildExportHtml", () => { const doc = buildExportHtml({ title: "Rate & quotas", createdAt: "2026-09-01T10:00:00Z", exportedAt: "2026-09-11T10:00:00Z", systemPrompt: "Be terse ", appUrl: "https://www.polyllm.io", messages: [ { role: "user", content: "How do **Gemini** quotas work?", modelKey: null, createdAt: "2026-09-01T10:00:00Z" }, { role: "assistant", content: "Per model, per day.\n\n```json\n{\"limit\": 20}\n```", modelKey: "gemini/gemini-3-flash", createdAt: "2026-09-01T10:00:05Z", reasoning: "thinking…", usage: { totalTokens: 1234 }, costUsd: 0.0021, latencyMs: 840 }, ], totals: { costUsd: 0.0021, inputTokens: 1000, outputTokens: 234 }, autoPrint: true, }); it("is a self-contained document with escaped title, brand and messages", () => { expect(doc.startsWith("")).toBe(true); expect(doc).toContain("Rate <limits> & quotas · PolyLLM"); expect(doc).toContain(''); expect(doc).toContain("