SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.7 KB · 96 lines typescript
Raw Blame History
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>&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;</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 = &lt;T,&gt;(x: T) =&gt; x &amp;&amp; 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("&lt;a href=&quot;x&quot; onclick=&#39;y&#39;&gt;");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 &lt;limits&gt; &amp; 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 &lt;b&gt;");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