| 1 | // Run: tsx src/__tests__/github-link-rendering.test.tsx |
| 2 | |
| 3 | import { readFileSync } from "node:fs"; |
| 4 | import { fileURLToPath } from "node:url"; |
| 5 | import { JSDOM } from "jsdom"; |
| 6 | import { createElement } from "react"; |
| 7 | import { renderToStaticMarkup } from "react-dom/server"; |
| 8 | import { classifyLinkIcon, parseGitHubLink } from "../components/githubLink"; |
| 9 | import { RichMarkdownLink } from "../components/githubLink"; |
| 10 | |
| 11 | let passed = 0; |
| 12 | let failed = 0; |
| 13 | |
| 14 | function ok(value: unknown, label: string) { |
| 15 | eq(Boolean(value), true, label); |
| 16 | } |
| 17 | |
| 18 | function eq(actual: unknown, expected: unknown, label: string) { |
| 19 | if (JSON.stringify(actual) === JSON.stringify(expected)) { |
| 20 | process.stdout.write(` PASS ${label}\n`); |
| 21 | passed += 1; |
| 22 | } else { |
| 23 | process.stdout.write( |
| 24 | ` FAIL ${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}\n`, |
| 25 | ); |
| 26 | failed += 1; |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | console.log("\ngithub link rendering"); |
| 31 | |
| 32 | eq(parseGitHubLink("https://github.com/esengine/DeepSeek-Reasonix/issues/6856"), { |
| 33 | kind: "issue", |
| 34 | owner: "esengine", |
| 35 | repo: "DeepSeek-Reasonix", |
| 36 | value: "6856", |
| 37 | compactLabel: "#6856", |
| 38 | }, "recognizes GitHub issue links"); |
| 39 | |
| 40 | eq(parseGitHubLink("https://github.com/esengine/DeepSeek-Reasonix/pull/123?diff=split"), { |
| 41 | kind: "pull", |
| 42 | owner: "esengine", |
| 43 | repo: "DeepSeek-Reasonix", |
| 44 | value: "123", |
| 45 | compactLabel: "PR #123", |
| 46 | }, "recognizes GitHub pull request links with query parameters"); |
| 47 | |
| 48 | eq(parseGitHubLink("https://github.com/esengine/DeepSeek-Reasonix/commit/abcdef1234567890"), { |
| 49 | kind: "commit", |
| 50 | owner: "esengine", |
| 51 | repo: "DeepSeek-Reasonix", |
| 52 | value: "abcdef1234567890", |
| 53 | compactLabel: "abcdef1", |
| 54 | }, "recognizes GitHub commit links"); |
| 55 | |
| 56 | eq(parseGitHubLink("https://github.com/esengine/DeepSeek-Reasonix"), null, "leaves repository links unchanged"); |
| 57 | eq(parseGitHubLink("http://github.com/esengine/DeepSeek-Reasonix/issues/1"), null, "rejects insecure GitHub links"); |
| 58 | eq(parseGitHubLink("https://example.com/issues/6856"), null, "leaves non-GitHub links unchanged"); |
| 59 | eq(parseGitHubLink("javascript:alert(1)"), null, "does not enhance unsafe links"); |
| 60 | |
| 61 | eq(classifyLinkIcon("https://github.com/esengine/DeepSeek-Reasonix"), "github", "adds a GitHub icon to repository links"); |
| 62 | eq(classifyLinkIcon("https://example.com/docs"), "external", "adds an external-link icon to web links"); |
| 63 | eq(classifyLinkIcon("http://localhost:5173/docs"), "external", "adds an external-link icon to HTTP links"); |
| 64 | eq(classifyLinkIcon("mailto:hello@example.com"), "mail", "adds a mail icon to email links"); |
| 65 | eq(classifyLinkIcon("./docs/GUIDE.md"), null, "leaves relative links without an icon"); |
| 66 | eq(classifyLinkIcon("#section"), null, "leaves page fragments without an icon"); |
| 67 | eq(classifyLinkIcon("javascript:alert(1)"), null, "does not add icons to unsafe protocols"); |
| 68 | |
| 69 | function renderLink(href: string, label: string = href) { |
| 70 | const markup = renderToStaticMarkup( |
| 71 | createElement(RichMarkdownLink, { href, children: label }), |
| 72 | ); |
| 73 | return new JSDOM(markup).window.document.querySelector("a"); |
| 74 | } |
| 75 | |
| 76 | { |
| 77 | const href = "https://github.com/esengine/DeepSeek-Reasonix/pull/6976"; |
| 78 | const link = renderLink(href); |
| 79 | const label = link?.querySelector(".md-rich-link__label"); |
| 80 | eq(label?.textContent, href, "keeps the original GitHub URL in selectable DOM text"); |
| 81 | eq(label?.getAttribute("data-display-label"), "PR #6976", "exposes the compact visual PR label"); |
| 82 | eq( |
| 83 | link?.getAttribute("aria-label"), |
| 84 | "GitHub esengine/DeepSeek-Reasonix pull request #6976", |
| 85 | "gives compact PR links a contextual accessible name", |
| 86 | ); |
| 87 | ok(link?.querySelector("svg[aria-hidden=\"true\"]"), "hides the decorative GitHub icon from assistive technology"); |
| 88 | |
| 89 | const selection = link?.ownerDocument.defaultView?.getSelection(); |
| 90 | const range = link?.ownerDocument.createRange(); |
| 91 | if (link && selection && range) { |
| 92 | range.selectNodeContents(link); |
| 93 | selection.removeAllRanges(); |
| 94 | selection.addRange(range); |
| 95 | } |
| 96 | eq(selection?.toString(), href, "copies the original URL instead of the compact visual label"); |
| 97 | } |
| 98 | |
| 99 | { |
| 100 | const link = renderLink( |
| 101 | "https://github.com/esengine/DeepSeek-Reasonix/issues/6856", |
| 102 | "Readable issue title", |
| 103 | ); |
| 104 | eq(link?.textContent, "Readable issue title", "preserves an explicit Markdown link label"); |
| 105 | eq( |
| 106 | link?.querySelector(".md-rich-link__label")?.hasAttribute("data-display-label"), |
| 107 | false, |
| 108 | "does not compact explicit Markdown link labels", |
| 109 | ); |
| 110 | eq(link?.hasAttribute("aria-label"), false, "does not override an explicit accessible link label"); |
| 111 | } |
| 112 | |
| 113 | { |
| 114 | const link = renderLink("https://example.com/a/very/long/documentation/path"); |
| 115 | ok(link?.classList.contains("md-rich-link--external"), "renders ordinary HTTPS destinations as rich external links"); |
| 116 | ok(link?.querySelector("svg[aria-hidden=\"true\"]"), "renders a decorative external-link icon"); |
| 117 | } |
| 118 | |
| 119 | { |
| 120 | const relative = renderLink("./docs/GUIDE.md", "Guide"); |
| 121 | eq(relative?.className, "", "leaves relative links without rich-link styling"); |
| 122 | eq(relative?.querySelector("svg"), null, "leaves relative links without an icon"); |
| 123 | } |
| 124 | |
| 125 | { |
| 126 | const styles = readFileSync(fileURLToPath(new URL("../styles.css", import.meta.url)), "utf8"); |
| 127 | ok( |
| 128 | /\.md \.md-rich-link\s*\{[^}]*white-space:\s*normal;/.test(styles), |
| 129 | "allows rich links to wrap on narrow layouts", |
| 130 | ); |
| 131 | ok( |
| 132 | /\.md \.md-rich-link__label\s*\{[^}]*min-width:\s*0;[^}]*overflow-wrap:\s*anywhere;/.test(styles), |
| 133 | "lets long unbroken link labels shrink and wrap", |
| 134 | ); |
| 135 | } |
| 136 | |
| 137 | console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`); |
| 138 | if (failed > 0) process.exit(1); |
| 139 |