/** * Tests for the DOM cf-markdown builds from a markdown document. * * These need a browser, and run under deno-web-test rather than `deno test`. * The harness registers tests through `Deno.test` and calls each one with no * arguments, so the BDD functions the rest of the repository uses are not * available here. */ import { assert, assertEquals, assertStringIncludes } from "@std/assert"; import { CFMarkdown } from "./index.ts"; interface Rendered { content: HTMLElement; element: CFMarkdown; done(): void; } async function render(markdown: string): Promise { const element = new CFMarkdown(); element.content = markdown; document.body.appendChild(element); await element.updateComplete; const content = element.shadowRoot?.querySelector( ".markdown-content", ) as HTMLElement; assert(content, "the component renders a .markdown-content wrapper"); return { content, element, done: () => element.remove() }; } /** Every element the component put in the document, the wrapper aside. */ function elementsOf(content: HTMLElement): Element[] { return Array.from(content.querySelectorAll("*")); } /** * A document collecting the ways markdown can carry markup, a URL that runs * script, or an event handler. */ const HOSTILE = [ "", '', '
hover me
', '', 'an anchor', '', "[a link](javascript:alert(1))", "[a link](JaVaScRiPt:alert(1))", "![an image](javascript:alert(1))", "[a link](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)", "[a link](vbscript:msgbox(1))", "", ].join("\n\n"); Deno.test("cf-markdown renders no element that can run script", async () => { const { content, done } = await render(HOSTILE); try { for (const forbidden of ["script", "iframe", "img", "svg", "style"]) { assertEquals( content.querySelectorAll(forbidden).length, 0, `renders no <${forbidden}>`, ); } } finally { done(); } }); Deno.test("cf-markdown renders no event-handler attribute", async () => { const { content, done } = await render(HOSTILE); try { for (const element of elementsOf(content)) { for (const attribute of Array.from(element.attributes)) { assert( !attribute.name.toLowerCase().startsWith("on"), `<${element.localName}> carries ${attribute.name}`, ); } } } finally { done(); } }); Deno.test("cf-markdown renders no URL that runs script", async () => { const { content, done } = await render(HOSTILE); try { for (const element of elementsOf(content)) { for (const attribute of Array.from(element.attributes)) { const value = attribute.value.replace(/[\s]/g, "").toLowerCase(); assert( !value.startsWith("javascript:") && !value.startsWith("vbscript:") && !value.startsWith("data:text/html"), `<${element.localName}> carries ${attribute.name}="${attribute.value}"`, ); } } } finally { done(); } }); Deno.test("cf-markdown renders a hostile document's text as text", async () => { const { content, done } = await render("A bold word and