@@ -39,6 +39,73 @@ const flattenNodes = (input: React.ReactNode): React.ReactNode[] => {
3939const flattenChildren = ( value : React . ReactNode ) : React . ReactNode [ ] =>
4040 flattenNodes ( value )
4141
42+ // Helper to find all elements matching a predicate (recursively)
43+ const findAllElements = (
44+ input : React . ReactNode ,
45+ predicate : ( element : React . ReactElement ) => boolean ,
46+ ) : React . ReactElement [ ] => {
47+ const results : React . ReactElement [ ] = [ ]
48+
49+ const visit = ( value : React . ReactNode ) : void => {
50+ if (
51+ value === null ||
52+ value === undefined ||
53+ typeof value === 'boolean' ||
54+ typeof value === 'string' ||
55+ typeof value === 'number'
56+ ) {
57+ return
58+ }
59+
60+ if ( Array . isArray ( value ) ) {
61+ value . forEach ( visit )
62+ return
63+ }
64+
65+ if ( React . isValidElement ( value ) ) {
66+ // Check if this element matches
67+ if ( predicate ( value ) ) {
68+ results . push ( value )
69+ }
70+ // Always recurse into children
71+ if ( value . props ?. children ) {
72+ visit ( value . props . children )
73+ }
74+ }
75+ }
76+
77+ visit ( input )
78+ return results
79+ }
80+
81+ // Helper to extract all text content recursively
82+ const getAllTextContent = ( input : React . ReactNode ) : string => {
83+ const texts : string [ ] = [ ]
84+
85+ const visit = ( value : React . ReactNode ) : void => {
86+ if ( value === null || value === undefined || typeof value === 'boolean' ) {
87+ return
88+ }
89+
90+ if ( typeof value === 'string' || typeof value === 'number' ) {
91+ texts . push ( String ( value ) )
92+ return
93+ }
94+
95+ if ( Array . isArray ( value ) ) {
96+ value . forEach ( visit )
97+ return
98+ }
99+
100+ if ( React . isValidElement ( value ) ) {
101+ visit ( value . props ?. children )
102+ }
103+ }
104+
105+ visit ( input )
106+ return texts . join ( '' )
107+ }
108+
42109describe ( 'markdown renderer' , ( ) => {
43110 test ( 'renders bold and italic emphasis' , ( ) => {
44111 const output = renderMarkdown ( 'Hello **bold** and *italic*!' )
@@ -343,18 +410,9 @@ a) (DEFAULT) Implement webhook system first (most flexible for enterprise custom
343410b) Focus on email/in-app notifications first (simpler to implement)`
344411
345412 const output = renderMarkdown ( markdown )
346- const nodes = flattenNodes ( output )
347413
348- // Convert all nodes to text to check indentation
349- const textContent = nodes
350- . map ( ( node ) => {
351- if ( typeof node === 'string' ) return node
352- if ( React . isValidElement ( node ) ) {
353- return flattenChildren ( node . props . children ) . join ( '' )
354- }
355- return ''
356- } )
357- . join ( '' )
414+ // Convert to text content (recursively extracts all text including from nested spans)
415+ const textContent = getAllTextContent ( output )
358416
359417 // Lettered items should be indented (3 spaces when under numbered lists)
360418 expect ( textContent ) . toContain ( ' a) (DEFAULT) PostgreSQL' )
@@ -428,17 +486,9 @@ b) Custom configuration
428486c) Advanced configuration`
429487
430488 const output = renderMarkdown ( markdown )
431- const nodes = flattenNodes ( output )
432489
433- const textContent = nodes
434- . map ( ( node ) => {
435- if ( typeof node === 'string' ) return node
436- if ( React . isValidElement ( node ) ) {
437- return flattenChildren ( node . props . children ) . join ( '' )
438- }
439- return ''
440- } )
441- . join ( '' )
490+ // Convert to text content (recursively extracts all text including from nested spans)
491+ const textContent = getAllTextContent ( output )
442492
443493 // Should preserve DEFAULT markers and apply indentation (3 spaces under list)
444494 expect ( textContent ) . toContain ( ' a) (DEFAULT) Standard' )
@@ -586,5 +636,60 @@ Conclusion text at the end.`
586636 expect ( textContent ) . not . toContain ( ' a) something' )
587637 expect ( textContent ) . toContain ( 'a) something in the middle' )
588638 } )
639+
640+ test ( 'makes DEFAULT options bold' , ( ) => {
641+ const markdown = `1. Choose your option:
642+ a) (DEFAULT) Standard configuration
643+ b) Custom configuration
644+ c) Advanced configuration`
645+
646+ const output = renderMarkdown ( markdown )
647+
648+ // Find all span elements with BOLD attribute (recursively)
649+ const boldSpans = findAllElements (
650+ output ,
651+ ( el ) => el . type === 'span' && el . props . attributes === TextAttributes . BOLD
652+ )
653+
654+ // Should have at least one bold span
655+ expect ( boldSpans . length ) . toBeGreaterThan ( 0 )
656+
657+ // Check that bold span contains DEFAULT text
658+ const boldTexts = boldSpans . map ( ( span ) =>
659+ flattenChildren ( span . props . children ) . join ( '' )
660+ )
661+ const hasDEFAULT = boldTexts . some ( ( text ) => text . includes ( '(DEFAULT)' ) )
662+ expect ( hasDEFAULT ) . toBe ( true )
663+ } )
664+
665+ test ( 'bolds multiple DEFAULT options' , ( ) => {
666+ const markdown = `1. First question:
667+ a) (DEFAULT) Option A
668+ b) Option B
669+ 2. Second question:
670+ a) (DEFAULT) Another default
671+ b) Non-default option`
672+
673+ const output = renderMarkdown ( markdown )
674+
675+ // Find all span elements with BOLD attribute (recursively)
676+ const boldSpans = findAllElements (
677+ output ,
678+ ( el ) => el . type === 'span' && el . props . attributes === TextAttributes . BOLD
679+ )
680+
681+ // Should have at least 2 bold spans
682+ expect ( boldSpans . length ) . toBeGreaterThanOrEqual ( 2 )
683+
684+ // Both bold spans should contain DEFAULT text
685+ const boldTexts = boldSpans . map ( ( span ) =>
686+ flattenChildren ( span . props . children ) . join ( '' )
687+ )
688+ const defaultCount = boldTexts . filter ( ( text ) =>
689+ text . includes ( '(DEFAULT)' )
690+ ) . length
691+
692+ expect ( defaultCount ) . toBeGreaterThanOrEqual ( 2 )
693+ } )
589694 } )
590695} )
0 commit comments