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("- first
- 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(" |