TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import { describe, it, expect } from "vitest";2import { markdownToHtml, renderInline, escapeHtml } from "@/lib/export/markdown-html";3import { buildExportHtml } from "@/lib/export/html-document";45describe("markdownToHtml", () => {6 it("escapes raw HTML instead of passing it through", () => {7 expect(markdownToHtml('<script>alert("x")</script>')).toBe("<p><script>alert("x")</script></p>");8 });910 it("renders headings, paragraphs, emphasis and code spans", () => {11 const html = markdownToHtml("# Title\n\nSome **bold**, *em*, `code` and ~~gone~~ text.\nSecond line.");12 expect(html).toContain("<h1>Title</h1>");13 expect(html).toContain("<strong>bold</strong>");14 expect(html).toContain("<em>em</em>");15 expect(html).toContain("<code>code</code>");16 expect(html).toContain("<del>gone</del>");17 expect(html).toContain("text.\nSecond line.</p>");18 });1920 it("renders fenced code blocks verbatim (escaped) with a language", () => {21 const html = markdownToHtml("```ts\nconst a = <T,>(x: T) => x && 1;\n```");22 expect(html).toBe('<pre data-lang="ts"><code class="language-ts">const a = <T,>(x: T) => x && 1;</code></pre>');23 });2425 it("does not apply emphasis inside code spans", () => {26 expect(renderInline("use `a * b * c` here")).toBe("use <code>a * b * c</code> here");27 });2829 it("renders nested lists and task items", () => {30 const html = markdownToHtml("- one\n- two\n - nested\n - again\n- [x] done\n\n1. first\n2. second");31 expect(html).toContain("<ul><li>one</li><li>two<ul><li>nested</li><li>again</li></ul></li><li><input type=\"checkbox\" disabled checked /> done</li></ul>");32 expect(html).toContain("<ol><li>first</li><li>second</li></ol>");33 });3435 it("renders GFM tables with alignment", () => {36 const html = markdownToHtml("| Model | Price |\n|:--|--:|\n| GPT | $5 |\n| Claude | $3 |");37 expect(html).toContain('<th style="text-align:left">Model</th>');38 expect(html).toContain('<td style="text-align:right">$5</td>');39 expect(html).toContain("<tr><td");40 expect(html.match(/<tr>/g)?.length).toBe(3);41 });4243 it("renders blockquotes and horizontal rules", () => {44 const html = markdownToHtml("> quoted\n> more\n\n---\n\nafter");45 expect(html).toContain("<blockquote><p>quoted\nmore</p></blockquote>");46 expect(html).toContain("<hr />");47 expect(html).toContain("<p>after</p>");48 });4950 it("only links to safe URL schemes and adds rel", () => {51 expect(renderInline("[ok](https://a.b/c) [bad](javascript:alert(1)) see https://x.y/z.")).toBe('<a href="https://a.b/c" rel="noopener noreferrer nofollow">ok</a> bad see <a href="https://x.y/z" rel="noopener noreferrer nofollow">https://x.y/z</a>.');52 });5354 it("escapes attributes", () => {55 expect(escapeHtml(`<a href="x" onclick='y'>`)).toBe("<a href="x" onclick='y'>");56 });57});5859describe("buildExportHtml", () => {60 const doc = buildExportHtml({61 title: "Rate <limits> & quotas",62 createdAt: "2026-09-01T10:00:00Z",63 exportedAt: "2026-09-11T10:00:00Z",64 systemPrompt: "Be terse <b>",65 appUrl: "https://www.polyllm.io",66 messages: [67 { role: "user", content: "How do **Gemini** quotas work?", modelKey: null, createdAt: "2026-09-01T10:00:00Z" },68 { 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 },69 ],70 totals: { costUsd: 0.0021, inputTokens: 1000, outputTokens: 234 },71 autoPrint: true,72 });7374 it("is a self-contained document with escaped title, brand and messages", () => {75 expect(doc.startsWith("<!doctype html>")).toBe(true);76 expect(doc).toContain("<title>Rate <limits> & quotas · PolyLLM</title>");77 expect(doc).toContain('<meta name="robots" content="noindex, nofollow" />');78 expect(doc).toContain("<style>");79 expect(doc).not.toContain('<link rel="stylesheet"');80 expect(doc).toContain("Be terse <b>");81 expect(doc).toContain('<article class="msg user">');82 expect(doc).toContain('<article class="msg assistant">');83 expect(doc).toContain("<strong>Gemini</strong>");84 expect(doc).toContain('<pre data-lang="json">');85 expect(doc).toContain('<span class="chip">gemini-3-flash</span>');86 expect(doc).toContain("1,234 tokens · $0.0021 · 840 ms");87 expect(doc).toContain("window.print()");88 });8990 it("omits the auto-print script when not requested", () => {91 const d = buildExportHtml({ title: "t", createdAt: "2026-09-01T10:00:00Z", exportedAt: "2026-09-11T10:00:00Z", appUrl: "https://www.polyllm.io", messages: [] });92 expect(d).not.toContain('window.addEventListener("load"');93 expect(d).toContain("0 messages");94 });95});96