1
9import { describe, expect, it } from "vitest";
10import { renderMarkdown } from "./markdown.js";
11
12describe("renderMarkdown", () => {
13 it("escapes raw HTML (the XSS contract)", () => {
14 expect(renderMarkdown("<script>alert(1)</script>")).toBe("<p><script>alert(1)</script></p>");
15 expect(renderMarkdown('<img src=x onerror="alert(1)">')).toBe("<p><img src=x onerror="alert(1)"></p>");
16 expect(renderMarkdown("**bold**")).toContain("<strong>bold</strong>");
17 expect(renderMarkdown("**bold**")).not.toContain("<script");
18 });
19 it("renders headings", () => {
20 expect(renderMarkdown("# H1")).toBe("<h1>H1</h1>");
21 expect(renderMarkdown("## H2")).toBe("<h2>H2</h2>");
22 expect(renderMarkdown("### H3")).toBe("<h3>H3</h3>");
23 });
24 it("renders bold, italic, code", () => {
25 expect(renderMarkdown("**b**")).toContain("<strong>b</strong>");
26 expect(renderMarkdown("*i*")).toContain("<em>i</em>");
27 expect(renderMarkdown("a `c` b")).toContain("<code>c</code>");
28 });
29 it("renders unordered + ordered lists", () => {
30 expect(renderMarkdown("- a\n- b")).toBe("<ul>\n<li>a</li>\n<li>b</li>\n</ul>");
31 expect(renderMarkdown("1. a\n2. b")).toBe("<ol>\n<li>a</li>\n<li>b</li>\n</ol>");
32 });
33 it("links: http(s) only; javascript: defanged", () => {
34 expect(renderMarkdown("[t](https://x.io/a)")).toBe('<p><a href="https://x.io/a" rel="noopener noreferrer">t</a></p>');
35 expect(renderMarkdown("[t](javascript:alert(1))")).toContain('href="#"');
36 expect(renderMarkdown("[t](javascript:alert(1))")).not.toContain("javascript:");
37 });
38 it("paragraphs + line breaks", () => {
39 expect(renderMarkdown("line1\nline2")).toBe("<p>line1<br>line2</p>");
40 expect(renderMarkdown("a\n\nb")).toBe("<p>a</p>\n<p>b</p>");
41 });
42 it("empty string → empty output", () => {
43 expect(renderMarkdown("")).toBe("");
44 });
45 it("no raw < ever emitted from user text (except our own tags)", () => {
46 const out = renderMarkdown("<div class='x'>hi</div>\n<script>evil()</script>");
47 expect(out).not.toMatch(/<div|<script|<svg|<iframe/);
48 });
49});
50