import { describe, it, expect } from "vitest"; import { generateExportHtml } from "../../src/server/services/html-exporter.js"; import type { ExportRequest, ParsedMessage } from "../../src/shared/types.js"; function makeMessage( overrides: Partial & { uuid: string } ): ParsedMessage { return { category: "assistant_text", content: "Test content", rawIndex: 0, ...overrides, }; } function makeExportRequest( messages: ParsedMessage[], visible?: string[], redacted?: string[] ): ExportRequest { return { session: { id: "test-session", project: "test-project", messages, }, visibleMessageUuids: visible || messages.map((m) => m.uuid), redactedMessageUuids: redacted || [], }; } describe("html-exporter", () => { it("generates valid HTML with DOCTYPE", async () => { const msg = makeMessage({ uuid: "msg-1", content: "Hello" }); const html = await generateExportHtml(makeExportRequest([msg])); expect(html).toMatch(/^/); expect(html).toContain(""); }); it("contains no external URL references", async () => { const msg = makeMessage({ uuid: "msg-1", content: "Hello" }); const html = await generateExportHtml(makeExportRequest([msg])); // No script src or link href pointing to external URLs expect(html).not.toMatch(/ { const msg = makeMessage({ uuid: "msg-1", content: "Hello **world**", category: "assistant_text", }); const html = await generateExportHtml(makeExportRequest([msg])); expect(html).toContain("world"); }); it("excludes filtered messages", async () => { const msg1 = makeMessage({ uuid: "msg-1", content: "Visible" }); const msg2 = makeMessage({ uuid: "msg-2", content: "Invisible" }); const html = await generateExportHtml( makeExportRequest([msg1, msg2], ["msg-1"], []) ); expect(html).toContain("Visible"); expect(html).not.toContain("Invisible"); }); it("excludes redacted messages", async () => { const msg1 = makeMessage({ uuid: "msg-1", content: "Kept" }); const msg2 = makeMessage({ uuid: "msg-2", content: "Redacted" }); const html = await generateExportHtml( makeExportRequest([msg1, msg2], ["msg-1", "msg-2"], ["msg-2"]) ); expect(html).toContain("Kept"); expect(html).not.toContain("Redacted"); }); it("includes redacted divider between non-redacted messages where redaction occurred", async () => { const msg1 = makeMessage({ uuid: "msg-1", content: "Before" }); const msg2 = makeMessage({ uuid: "msg-2", content: "Middle removed" }); const msg3 = makeMessage({ uuid: "msg-3", content: "After" }); const html = await generateExportHtml( makeExportRequest( [msg1, msg2, msg3], ["msg-1", "msg-2", "msg-3"], ["msg-2"] ) ); expect(html).toContain("content redacted"); expect(html).toContain("Before"); expect(html).toContain("After"); expect(html).not.toContain("Middle removed"); }); it("renders markdown to HTML", async () => { const msg = makeMessage({ uuid: "msg-1", content: "# Heading\n\n- item 1\n- item 2\n\n```js\nconst x = 1;\n```", }); const html = await generateExportHtml(makeExportRequest([msg])); expect(html).toContain(""); expect(html).toContain(" { const msg = makeMessage({ uuid: "msg-1", content: "```js\nconst x = 1;\n```" }); const html = await generateExportHtml(makeExportRequest([msg])); expect(html).toContain(".hljs"); }); it("includes session metadata header", async () => { const msg = makeMessage({ uuid: "msg-1", content: "Hello" }); const html = await generateExportHtml(makeExportRequest([msg])); expect(html).toContain("test-project"); expect(html).toContain("Session Export"); expect(html).toContain("1 message"); // Verify singular — should NOT contain "1 messages" expect(html).not.toMatch(/\b1 messages\b/); }); });