/** * Markdown Component Tests * * Tests the pure helper functions extracted from the Markdown component: * - processSpoilers: wraps ||text|| syntax in spoiler spans, leaving code blocks untouched * - highlightSearchMatches: injects tags for search terms, skipping code blocks * * Manual testing checklist: * - [ ] Code blocks render with syntax highlighting and a copy button * - [ ] ||spoiler text|| renders as a hidden span revealed on click * - [ ] Search query highlights matching text in non-code content * - [ ] Regular links open in the system browser via the Tauri opener * - [ ] Binary file links invoke open_binary_file (WSL-path-aware) instead of openPath */ import { describe, it, expect } from "vitest"; // Mirror functions from Markdown.svelte for isolated testing function processSpoilers(html: string): string { const codeBlockPlaceholders: string[] = []; let processed = html.replace(/<(pre|code)[^>]*>[\s\S]*?<\/\1>/gi, (match) => { codeBlockPlaceholders.push(match); return `__CODE_PLACEHOLDER_${codeBlockPlaceholders.length - 1}__`; }); processed = processed.replace( /\|\|(.+?)\|\|/g, '$1' ); processed = processed.replace(/__CODE_PLACEHOLDER_(\d+)__/g, (_, index) => { return codeBlockPlaceholders[parseInt(index)]; }); return processed; } function highlightSearchMatches(html: string, query: string): string { if (!query) return html; const codeBlockPlaceholders: string[] = []; const tagPlaceholders: string[] = []; let processed = html.replace(/<(pre|code)[^>]*>[\s\S]*?<\/\1>/gi, (match) => { codeBlockPlaceholders.push(match); return `__CODE_SEARCH_PLACEHOLDER_${codeBlockPlaceholders.length - 1}__`; }); processed = processed.replace(/<[^>]+>/g, (match) => { tagPlaceholders.push(match); return `__TAG_PLACEHOLDER_${tagPlaceholders.length - 1}__`; }); const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(`(${escapedQuery})`, "gi"); processed = processed.replace(regex, '$1'); processed = processed.replace(/__TAG_PLACEHOLDER_(\d+)__/g, (_, index) => { return tagPlaceholders[parseInt(index)]; }); processed = processed.replace(/__CODE_SEARCH_PLACEHOLDER_(\d+)__/g, (_, index) => { return codeBlockPlaceholders[parseInt(index)]; }); return processed; } // --- describe("processSpoilers", () => { it("wraps ||text|| in a spoiler span", () => { const result = processSpoilers("

||secret||

"); expect(result).toContain(' { const result = processSpoilers("

||hidden||

"); expect(result).toContain('role="button"'); expect(result).toContain('tabindex="0"'); }); it("leaves content without spoiler markers unchanged", () => { const html = "

Normal text here

"; expect(processSpoilers(html)).toBe(html); }); it("handles multiple spoilers in the same string", () => { const result = processSpoilers("

||a|| and ||b||

"); const matches = result.match(/class="spoiler"/g); expect(matches).toHaveLength(2); }); it("does not apply spoiler syntax inside code blocks", () => { const html = "
||not a spoiler||
"; const result = processSpoilers(html); expect(result).not.toContain('class="spoiler"'); expect(result).toContain("||not a spoiler||"); }); it("does not apply spoiler syntax inside inline code", () => { const html = "

Example: ||inline||

"; const result = processSpoilers(html); expect(result).not.toContain('class="spoiler"'); }); it("handles spoilers adjacent to code blocks correctly", () => { const html = "
code

||revealed||

"; const result = processSpoilers(html); expect(result).toContain('code"); }); }); describe("highlightSearchMatches", () => { it("returns unchanged html when query is empty string", () => { const html = "

hello world

"; expect(highlightSearchMatches(html, "")).toBe(html); }); it("wraps matched text in a mark element", () => { const result = highlightSearchMatches("

hello world

", "hello"); expect(result).toContain('hello'); }); it("is case-insensitive", () => { const result = highlightSearchMatches("

Hello World

", "hello"); expect(result).toContain('Hello'); }); it("highlights all occurrences", () => { const result = highlightSearchMatches("

cat and cat

", "cat"); const matches = result.match(//g); expect(matches).toHaveLength(2); }); it("does not highlight inside code blocks", () => { const html = "
hello inside code
"; const result = highlightSearchMatches(html, "hello"); expect(result).not.toContain(''); expect(result).toContain("hello inside code"); }); it("does not corrupt HTML tags", () => { const result = highlightSearchMatches('

hello

', "hello"); expect(result).toContain('

'); expect(result).toContain('hello'); }); it("escapes regex special characters in the query", () => { const result = highlightSearchMatches("

price: $1.00

", "$1"); expect(result).toContain('$1'); }); it("highlights text outside code blocks whilst leaving code intact", () => { const html = "
match here

match here too

"; const result = highlightSearchMatches(html, "match"); expect(result).toContain("
match here
"); expect(result).toContain('match'); }); });