@@ -20,12 +20,14 @@ describe('MarkdownText', () => {
2020 } )
2121
2222 describe ( 'HTML escaping' , ( ) => {
23- it ( 'escapes HTML tags to prevent XSS' , async ( ) => {
23+ it ( 'strips HTML tags to prevent XSS' , async ( ) => {
2424 const component = await mountSuspended ( MarkdownText , {
2525 props : { text : '<script>alert("xss")</script>' } ,
2626 } )
27+ // HTML tags should be stripped (not rendered)
2728 expect ( component . html ( ) ) . not . toContain ( '<script>' )
28- expect ( component . text ( ) ) . toContain ( '<script>' )
29+ // Only the text content remains
30+ expect ( component . text ( ) ) . toBe ( 'alert("xss")' )
2931 } )
3032
3133 it ( 'escapes special characters' , async ( ) => {
@@ -202,4 +204,215 @@ describe('MarkdownText', () => {
202204 expect ( component . find ( 'code' ) . exists ( ) ) . toBe ( true )
203205 } )
204206 } )
207+
208+ describe ( 'markdown image stripping' , ( ) => {
209+ it ( 'strips standalone markdown images' , async ( ) => {
210+ const component = await mountSuspended ( MarkdownText , {
211+ props : { text : ' A library' } ,
212+ } )
213+ expect ( component . text ( ) ) . toBe ( 'A library' )
214+ } )
215+
216+ it ( 'strips linked markdown images (badges)' , async ( ) => {
217+ const component = await mountSuspended ( MarkdownText , {
218+ props : {
219+ text : '[](https://travis-ci.org/user/repo) A library' ,
220+ } ,
221+ } )
222+ expect ( component . text ( ) ) . toBe ( 'A library' )
223+ } )
224+
225+ it ( 'strips multiple badges' , async ( ) => {
226+ const component = await mountSuspended ( MarkdownText , {
227+ props : {
228+ text : '[](https://npm.com) [](https://ci.com) A library' ,
229+ } ,
230+ } )
231+ expect ( component . text ( ) ) . toBe ( 'A library' )
232+ } )
233+
234+ it ( 'preserves malformed image syntax without closing paren' , async ( ) => {
235+ // Incomplete/malformed markdown images are left as-is for safety
236+ const component = await mountSuspended ( MarkdownText , {
237+ props : { text : '
239+ // The image syntax is not stripped because it's malformed (no closing paren)
240+ expect ( component . text ( ) ) . toBe ( '
241+ } )
242+
243+ it ( 'strips empty link syntax' , async ( ) => {
244+ const component = await mountSuspended ( MarkdownText , {
245+ props : { text : '[](https://example.com) A library' } ,
246+ } )
247+ expect ( component . text ( ) ) . toBe ( 'A library' )
248+ } )
249+
250+ it ( 'preserves regular markdown links' , async ( ) => {
251+ const component = await mountSuspended ( MarkdownText , {
252+ props : { text : '[documentation](https://docs.example.com) is here' } ,
253+ } )
254+ const link = component . find ( 'a' )
255+ expect ( link . exists ( ) ) . toBe ( true )
256+ expect ( link . text ( ) ) . toBe ( 'documentation' )
257+ expect ( component . text ( ) ) . toBe ( 'documentation is here' )
258+ } )
259+ } )
260+
261+ describe ( 'packageName prop' , ( ) => {
262+ it ( 'strips package name from the beginning of plain text' , async ( ) => {
263+ const component = await mountSuspended ( MarkdownText , {
264+ props : {
265+ text : 'my-package - A great library' ,
266+ packageName : 'my-package' ,
267+ } ,
268+ } )
269+ expect ( component . text ( ) ) . toBe ( 'A great library' )
270+ } )
271+
272+ it ( 'strips package name with colon separator' , async ( ) => {
273+ const component = await mountSuspended ( MarkdownText , {
274+ props : {
275+ text : 'my-package: A great library' ,
276+ packageName : 'my-package' ,
277+ } ,
278+ } )
279+ expect ( component . text ( ) ) . toBe ( 'A great library' )
280+ } )
281+
282+ it ( 'strips package name with em dash separator' , async ( ) => {
283+ const component = await mountSuspended ( MarkdownText , {
284+ props : {
285+ text : 'my-package — A great library' ,
286+ packageName : 'my-package' ,
287+ } ,
288+ } )
289+ expect ( component . text ( ) ) . toBe ( 'A great library' )
290+ } )
291+
292+ it ( 'strips package name without separator' , async ( ) => {
293+ const component = await mountSuspended ( MarkdownText , {
294+ props : {
295+ text : 'my-package A great library' ,
296+ packageName : 'my-package' ,
297+ } ,
298+ } )
299+ expect ( component . text ( ) ) . toBe ( 'A great library' )
300+ } )
301+
302+ it ( 'is case-insensitive' , async ( ) => {
303+ const component = await mountSuspended ( MarkdownText , {
304+ props : {
305+ text : 'MY-PACKAGE - A great library' ,
306+ packageName : 'my-package' ,
307+ } ,
308+ } )
309+ expect ( component . text ( ) ) . toBe ( 'A great library' )
310+ } )
311+
312+ it ( 'does not strip package name from middle of text' , async ( ) => {
313+ const component = await mountSuspended ( MarkdownText , {
314+ props : {
315+ text : 'A great my-package library' ,
316+ packageName : 'my-package' ,
317+ } ,
318+ } )
319+ expect ( component . text ( ) ) . toBe ( 'A great my-package library' )
320+ } )
321+
322+ it ( 'handles scoped package names' , async ( ) => {
323+ const component = await mountSuspended ( MarkdownText , {
324+ props : {
325+ text : '@org/my-package - A great library' ,
326+ packageName : '@org/my-package' ,
327+ } ,
328+ } )
329+ expect ( component . text ( ) ) . toBe ( 'A great library' )
330+ } )
331+
332+ it ( 'handles package names with special regex characters' , async ( ) => {
333+ const component = await mountSuspended ( MarkdownText , {
334+ props : {
335+ text : 'pkg.name+test - A great library' ,
336+ packageName : 'pkg.name+test' ,
337+ } ,
338+ } )
339+ expect ( component . text ( ) ) . toBe ( 'A great library' )
340+ } )
341+
342+ it ( 'strips package name from HTML-containing descriptions' , async ( ) => {
343+ const component = await mountSuspended ( MarkdownText , {
344+ props : {
345+ text : '<b>my-package</b> - A great library' ,
346+ packageName : 'my-package' ,
347+ } ,
348+ } )
349+ expect ( component . text ( ) ) . toBe ( 'A great library' )
350+ } )
351+
352+ it ( 'strips package name from descriptions with markdown images' , async ( ) => {
353+ const component = await mountSuspended ( MarkdownText , {
354+ props : {
355+ text : ' my-package - A great library' ,
356+ packageName : 'my-package' ,
357+ } ,
358+ } )
359+ expect ( component . text ( ) ) . toBe ( 'A great library' )
360+ } )
361+
362+ it ( 'does nothing when packageName is not provided' , async ( ) => {
363+ const component = await mountSuspended ( MarkdownText , {
364+ props : {
365+ text : 'my-package - A great library' ,
366+ } ,
367+ } )
368+ expect ( component . text ( ) ) . toBe ( 'my-package - A great library' )
369+ } )
370+ } )
371+
372+ describe ( 'HTML tag stripping' , ( ) => {
373+ it ( 'strips simple HTML tags but keeps content' , async ( ) => {
374+ const component = await mountSuspended ( MarkdownText , {
375+ props : { text : '<b>bold text</b> here' } ,
376+ } )
377+ expect ( component . text ( ) ) . toBe ( 'bold text here' )
378+ expect ( component . html ( ) ) . not . toContain ( '<b>' )
379+ } )
380+
381+ it ( 'strips nested HTML tags' , async ( ) => {
382+ const component = await mountSuspended ( MarkdownText , {
383+ props : { text : '<div><span>nested</span> content</div>' } ,
384+ } )
385+ expect ( component . text ( ) ) . toBe ( 'nested content' )
386+ } )
387+
388+ it ( 'strips self-closing tags' , async ( ) => {
389+ const component = await mountSuspended ( MarkdownText , {
390+ props : { text : 'before<br/>after' } ,
391+ } )
392+ expect ( component . text ( ) ) . toBe ( 'beforeafter' )
393+ } )
394+
395+ it ( 'strips tags with attributes' , async ( ) => {
396+ const component = await mountSuspended ( MarkdownText , {
397+ props : { text : '<a href="https://evil.com">click me</a>' } ,
398+ } )
399+ expect ( component . text ( ) ) . toBe ( 'click me' )
400+ expect ( component . find ( 'a' ) . exists ( ) ) . toBe ( false )
401+ } )
402+
403+ it ( 'preserves text that looks like comparison operators' , async ( ) => {
404+ const component = await mountSuspended ( MarkdownText , {
405+ props : { text : 'x < y > z and a < b && c > d' } ,
406+ } )
407+ expect ( component . text ( ) ) . toBe ( 'x < y > z and a < b && c > d' )
408+ } )
409+
410+ it ( 'handles mixed HTML and markdown' , async ( ) => {
411+ const component = await mountSuspended ( MarkdownText , {
412+ props : { text : '<b>bold</b> and **also bold**' } ,
413+ } )
414+ expect ( component . text ( ) ) . toBe ( 'bold and also bold' )
415+ expect ( component . find ( 'strong' ) . exists ( ) ) . toBe ( true )
416+ } )
417+ } )
205418} )
0 commit comments