[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 353 lines 13 kB view raw
1import { describe, expect, it } from 'vitest' 2 3// Utility to use more human-readable strings in tests 4function escapeHtml(text: string): string { 5 return text 6 .replace(/&/g, '&amp;') 7 .replace(/</g, '&lt;') 8 .replace(/>/g, '&gt;') 9 .replace(/"/g, '&quot;') 10 .replace(/'/g, '&#039;') 11} 12 13describe('useMarkdown', () => { 14 describe('plain text', () => { 15 it('renders plain text unchanged', () => { 16 const processed = useMarkdown({ text: 'Hello world' }) 17 expect(processed.value).toBe('Hello world') 18 }) 19 20 it('returns empty for empty text', () => { 21 const processed = useMarkdown({ text: '' }) 22 expect(processed.value).toBe('') 23 }) 24 }) 25 26 describe('HTML escaping', () => { 27 it('strips HTML tags to prevent XSS', () => { 28 const processed = useMarkdown({ text: '<script>alert("xss")</script>' }) 29 // HTML tags should be stripped (not rendered) 30 expect(processed.value).not.toContain('<script>') 31 // Only the text content remains 32 expect(processed.value).toBe(escapeHtml('alert("xss")')) 33 }) 34 35 it('escapes special characters', () => { 36 const processed = useMarkdown({ text: 'a < b && c > d' }) 37 expect(processed.value).toBe(escapeHtml('a < b && c > d')) 38 }) 39 }) 40 41 describe('bold formatting', () => { 42 it('renders **text** as bold', () => { 43 const processed = useMarkdown({ text: 'This is **bold** text' }) 44 expect(processed.value).toContain('<strong>') 45 expect(processed.value).toContain('bold') 46 }) 47 48 it('renders __text__ as bold', () => { 49 const processed = useMarkdown({ text: 'This is __bold__ text' }) 50 expect(processed.value).toContain('<strong>') 51 expect(processed.value).toContain('bold') 52 }) 53 }) 54 55 describe('italic formatting', () => { 56 it('renders *text* as italic', () => { 57 const processed = useMarkdown({ text: 'This is *italic* text' }) 58 expect(processed.value).toContain('<em>') 59 expect(processed.value).toContain('italic') 60 }) 61 62 it('renders _text_ as italic', () => { 63 const processed = useMarkdown({ text: 'This is _italic_ text' }) 64 expect(processed.value).toContain('<em>') 65 expect(processed.value).toContain('italic') 66 }) 67 }) 68 69 describe('inline code', () => { 70 it('renders `code` in code tags', () => { 71 const processed = useMarkdown({ text: 'Run `npm install` to start' }) 72 expect(processed.value).toContain('<code>') 73 expect(processed.value).toContain('npm install') 74 }) 75 }) 76 77 describe('strikethrough', () => { 78 it('renders ~~text~~ as strikethrough', () => { 79 const processed = useMarkdown({ text: 'This is ~~deleted~~ text' }) 80 expect(processed.value).toContain('<del>') 81 expect(processed.value).toContain('deleted') 82 }) 83 }) 84 85 describe('links', () => { 86 it('renders [text](https://url) as a link', () => { 87 const processed = useMarkdown({ text: 'Visit [our site](https://example.com) for more' }) 88 expect(processed.value).toContain( 89 '<a href="https://example.com/" rel="nofollow noreferrer noopener" target="_blank">our site</a>', 90 ) 91 }) 92 93 it('adds security attributes to links', () => { 94 const processed = useMarkdown({ text: '[link](https://example.com)' }) 95 expect(processed.value).toBe( 96 '<a href="https://example.com/" rel="nofollow noreferrer noopener" target="_blank">link</a>', 97 ) 98 }) 99 100 it('allows mailto: links', () => { 101 const processed = useMarkdown({ text: 'Contact [us](mailto:test@example.com)' }) 102 expect(processed.value).toContain( 103 '<a href="mailto:test@example.com" rel="nofollow noreferrer noopener" target="_blank">us</a>', 104 ) 105 }) 106 107 it('blocks javascript: protocol links', () => { 108 const processed = useMarkdown({ text: '[click me](javascript:alert("xss"))' }) 109 expect(processed.value).toBe(`click me ${escapeHtml('(javascript:alert("xss"))')}`) 110 }) 111 112 it('blocks http: links (only https allowed)', () => { 113 const processed = useMarkdown({ text: '[site](http://example.com)' }) 114 expect(processed.value).toBe('site (http://example.com)') 115 }) 116 117 it('handles invalid URLs gracefully', () => { 118 const processed = useMarkdown({ text: '[link](not a valid url)' }) 119 expect(processed.value).toBe('link (not a valid url)') 120 }) 121 122 it('handles URLs with ampersands', () => { 123 const processed = useMarkdown({ text: '[search](https://example.com?a=1&b=2)' }) 124 expect(processed.value).toBe( 125 '<a href="https://example.com/?a=1&b=2" rel="nofollow noreferrer noopener" target="_blank">search</a>', 126 ) 127 }) 128 }) 129 130 describe('plain prop', () => { 131 it('renders link text without anchor tag when plain=true', () => { 132 const processed = useMarkdown({ 133 text: 'Visit [our site](https://example.com) for more', 134 plain: true, 135 }) 136 expect(processed.value).toBe('Visit our site for more') 137 }) 138 139 it('still renders other formatting when plain=true', () => { 140 const processed = useMarkdown({ 141 text: '**bold** and [link](https://example.com)', 142 plain: true, 143 }) 144 expect(processed.value).toBe('<strong>bold</strong> and link') 145 }) 146 }) 147 148 describe('combined formatting', () => { 149 it('handles multiple formatting in one string', () => { 150 const processed = useMarkdown({ text: '**bold** and *italic* and `code`' }) 151 expect(processed.value).toContain('<strong>') 152 expect(processed.value).toContain('<em>') 153 expect(processed.value).toContain('<code>') 154 }) 155 }) 156 157 describe('markdown image stripping', () => { 158 it('strips standalone markdown images', () => { 159 const processed = useMarkdown({ 160 text: '![badge](https://img.shields.io/badge.svg) A library', 161 }) 162 expect(processed.value).toBe('A library') 163 }) 164 165 it('strips linked markdown images (badges)', () => { 166 const processed = useMarkdown({ 167 text: '[![Build Status](https://travis-ci.org/user/repo.svg)](https://travis-ci.org/user/repo) A library', 168 }) 169 expect(processed.value).toBe('A library') 170 }) 171 172 it('strips multiple badges', () => { 173 const processed = useMarkdown({ 174 text: '[![npm](https://badge.svg)](https://npm.com) [![build](https://ci.svg)](https://ci.com) A library', 175 }) 176 expect(processed.value).toBe('A library') 177 }) 178 179 it('preserves malformed image syntax without closing paren', () => { 180 // Incomplete/malformed markdown images are left as-is for safety 181 const processed = useMarkdown({ text: '![badge](https://example.svg A library' }) 182 // The image syntax is not stripped because it's malformed (no closing paren) 183 expect(processed.value).toBe('![badge](https://example.svg A library') 184 }) 185 186 it('strips empty link syntax', () => { 187 const processed = useMarkdown({ text: '[](https://example.com) A library' }) 188 expect(processed.value).toBe('A library') 189 }) 190 191 it('preserves regular markdown links', () => { 192 const processed = useMarkdown({ text: '[documentation](https://docs.example.com) is here' }) 193 expect(processed.value).toBe( 194 '<a href="https://docs.example.com/" rel="nofollow noreferrer noopener" target="_blank">documentation</a> is here', 195 ) 196 }) 197 }) 198 199 describe('packageName prop', () => { 200 it('strips package name from the beginning of plain text', () => { 201 const processed = useMarkdown({ 202 text: 'my-package - A great library', 203 packageName: 'my-package', 204 }) 205 expect(processed.value).toBe('A great library') 206 }) 207 208 it('strips package name with colon separator', () => { 209 const processed = useMarkdown({ 210 text: 'my-package: A great library', 211 packageName: 'my-package', 212 }) 213 expect(processed.value).toBe('A great library') 214 }) 215 216 it('strips package name with em dash separator', () => { 217 const processed = useMarkdown({ 218 text: 'my-package — A great library', 219 packageName: 'my-package', 220 }) 221 expect(processed.value).toBe('A great library') 222 }) 223 224 it('strips package name without separator', () => { 225 const processed = useMarkdown({ 226 text: 'my-package A great library', 227 packageName: 'my-package', 228 }) 229 expect(processed.value).toBe('A great library') 230 }) 231 232 it('is case-insensitive', () => { 233 const processed = useMarkdown({ 234 text: 'MY-PACKAGE - A great library', 235 packageName: 'my-package', 236 }) 237 expect(processed.value).toBe('A great library') 238 }) 239 240 it('does not strip package name from middle of text', () => { 241 const processed = useMarkdown({ 242 text: 'A great my-package library', 243 packageName: 'my-package', 244 }) 245 expect(processed.value).toBe('A great my-package library') 246 }) 247 248 it('handles scoped package names', () => { 249 const processed = useMarkdown({ 250 text: '@org/my-package - A great library', 251 packageName: '@org/my-package', 252 }) 253 expect(processed.value).toBe('A great library') 254 }) 255 256 it('handles package names with special regex characters', () => { 257 const processed = useMarkdown({ 258 text: 'pkg.name+test - A great library', 259 packageName: 'pkg.name+test', 260 }) 261 expect(processed.value).toBe('A great library') 262 }) 263 264 it('strips package name from HTML-containing descriptions', () => { 265 const processed = useMarkdown({ 266 text: '<b>my-package</b> - A great library', 267 packageName: 'my-package', 268 }) 269 expect(processed.value).toBe('A great library') 270 }) 271 272 it('strips package name from descriptions with markdown images', () => { 273 const processed = useMarkdown({ 274 text: '![badge](https://badge.svg) my-package - A great library', 275 packageName: 'my-package', 276 }) 277 expect(processed.value).toBe('A great library') 278 }) 279 280 it('does nothing when packageName is not provided', () => { 281 const processed = useMarkdown({ 282 text: 'my-package - A great library', 283 }) 284 expect(processed.value).toBe('my-package - A great library') 285 }) 286 }) 287 288 describe('HTML tag stripping', () => { 289 it('strips simple HTML tags but keeps content', () => { 290 const processed = useMarkdown({ text: '<b>bold text</b> here' }) 291 expect(processed.value).toBe('bold text here') 292 expect(processed.value).not.toContain('<b>') 293 }) 294 295 it('strips nested HTML tags', () => { 296 const processed = useMarkdown({ text: '<div><span>nested</span> content</div>' }) 297 expect(processed.value).toBe('nested content') 298 }) 299 300 it('strips self-closing tags', () => { 301 const processed = useMarkdown({ text: 'before<br/>after' }) 302 expect(processed.value).toBe('beforeafter') 303 }) 304 305 it('strips tags with attributes', () => { 306 const processed = useMarkdown({ text: '<a href="https://evil.com">click me</a>' }) 307 expect(processed.value).toBe('click me') 308 expect(processed.value).not.toContain('<a href="https://evil.com">') 309 }) 310 311 it('preserves text that looks like comparison operators', () => { 312 const processed = useMarkdown({ text: 'x < y > z and a < b && c > d' }) 313 expect(processed.value).toBe(escapeHtml('x < y > z and a < b && c > d')) 314 }) 315 316 it('handles mixed HTML and markdown', () => { 317 const processed = useMarkdown({ text: '<b>bold</b> and **also bold**' }) 318 expect(processed.value).toBe('bold and <strong>also bold</strong>') 319 }) 320 }) 321 322 describe('HTML comment stripping', () => { 323 it('strips HTML comments', () => { 324 const processed = useMarkdown({ text: '<!-- automd:badges color=yellow -->A library' }) 325 expect(processed.value).toBe('A library') 326 }) 327 328 it('strips HTML comments from the middle of text', () => { 329 const processed = useMarkdown({ text: 'Before <!-- comment --> after' }) 330 expect(processed.value).toBe('Before after') 331 }) 332 333 it('strips multiple HTML comments', () => { 334 const processed = useMarkdown({ text: '<!-- first -->Text <!-- second -->here' }) 335 expect(processed.value).toBe('Text here') 336 }) 337 338 it('strips multiline HTML comments', () => { 339 const processed = useMarkdown({ text: '<!-- multi\nline\ncomment -->Text' }) 340 expect(processed.value).toBe('Text') 341 }) 342 343 it('returns empty string when description is only a comment', () => { 344 const processed = useMarkdown({ text: '<!-- automd:badges color=yellow -->' }) 345 expect(processed.value).toBe('') 346 }) 347 348 it('strips unclosed HTML comments (truncated)', () => { 349 const processed = useMarkdown({ text: 'A library <!-- automd:badges color=yel' }) 350 expect(processed.value).toBe('A library ') 351 }) 352 }) 353})