Skip to content

Commit b8de23a

Browse files
committed
Add comprehensive tests for markdown renderer
- Add 263 lines of test coverage for markdown-renderer.test.tsx - Update markdown-renderer.tsx implementation (37 insertions, 2 deletions)
1 parent 95f2500 commit b8de23a

File tree

2 files changed

+298
-2
lines changed

2 files changed

+298
-2
lines changed

cli/src/utils/__tests__/markdown-renderer.test.tsx

Lines changed: 262 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,270 @@ codebuff "implement feature" --verbose
321321

322322
const inlineCode = nodes[1] as React.ReactElement
323323
const inlineContent = flattenChildren(inlineCode.props.children).join('')
324-
324+
325325
// Should preserve quotes and special characters within inline code
326326
expect(inlineContent).toContain('git commit -m "fix: bug"')
327327
expect(nodes[2]).toBe(' to commit.')
328328
})
329+
330+
describe('lettered sub-item indentation', () => {
331+
test('renders numbered questions with lettered sub-items (real world example)', () => {
332+
const markdown = `Questions:**
333+
1. What is your preferred storage backend for real-time metrics aggregation?
334+
a) (DEFAULT) PostgreSQL with time-series optimized indexes (leverages existing infrastructure)
335+
b) Dedicated time-series database (InfluxDB/TimescaleDB) for better performance at scale
336+
c) Redis for real-time aggregation with periodic PostgreSQL sync
337+
2. For the metrics dashboard visualization library:
338+
a) (DEFAULT) Recharts (already used in the project, consistent with existing charts)
339+
b) Apache ECharts (more powerful for complex visualizations)
340+
c) D3.js (maximum flexibility, steeper learning curve)
341+
3. Alert webhook delivery priority:
342+
a) (DEFAULT) Implement webhook system first (most flexible for enterprise customers)
343+
b) Focus on email/in-app notifications first (simpler to implement)`
344+
345+
const output = renderMarkdown(markdown)
346+
const nodes = flattenNodes(output)
347+
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('')
358+
359+
// Lettered items should be indented (3 spaces when under numbered lists)
360+
expect(textContent).toContain(' a) (DEFAULT) PostgreSQL')
361+
expect(textContent).toContain(' b) Dedicated time-series')
362+
expect(textContent).toContain(' c) Redis for real-time')
363+
expect(textContent).toContain(' a) (DEFAULT) Recharts')
364+
expect(textContent).toContain(' b) Apache ECharts')
365+
expect(textContent).toContain(' c) D3.js')
366+
expect(textContent).toContain(' a) (DEFAULT) Implement webhook')
367+
expect(textContent).toContain(' b) Focus on email')
368+
})
369+
370+
test('renders simple numbered list with lettered sub-items', () => {
371+
const markdown = `1. First question?
372+
a) First option
373+
b) Second option
374+
c) Third option
375+
2. Second question?
376+
a) Another option
377+
b) One more option`
378+
379+
const output = renderMarkdown(markdown)
380+
const nodes = flattenNodes(output)
381+
382+
const textContent = nodes
383+
.map((node) => {
384+
if (typeof node === 'string') return node
385+
if (React.isValidElement(node)) {
386+
return flattenChildren(node.props.children).join('')
387+
}
388+
return ''
389+
})
390+
.join('')
391+
392+
// All lettered items should have 3 spaces of indentation (under numbered lists)
393+
expect(textContent).toContain(' a) First option')
394+
expect(textContent).toContain(' b) Second option')
395+
expect(textContent).toContain(' c) Third option')
396+
expect(textContent).toContain(' a) Another option')
397+
expect(textContent).toContain(' b) One more option')
398+
})
399+
400+
test('renders lettered items without numbered parents', () => {
401+
const markdown = `a) First standalone option
402+
b) Second standalone option
403+
c) Third standalone option`
404+
405+
const output = renderMarkdown(markdown)
406+
const nodes = flattenNodes(output)
407+
408+
const textContent = nodes
409+
.map((node) => {
410+
if (typeof node === 'string') return node
411+
if (React.isValidElement(node)) {
412+
return flattenChildren(node.props.children).join('')
413+
}
414+
return ''
415+
})
416+
.join('')
417+
418+
// Should still be indented even without numbered parents
419+
expect(textContent).toContain(' a) First standalone')
420+
expect(textContent).toContain(' b) Second standalone')
421+
expect(textContent).toContain(' c) Third standalone')
422+
})
423+
424+
test('renders lettered items with DEFAULT markers', () => {
425+
const markdown = `1. Choose your option:
426+
a) (DEFAULT) Standard configuration
427+
b) Custom configuration
428+
c) Advanced configuration`
429+
430+
const output = renderMarkdown(markdown)
431+
const nodes = flattenNodes(output)
432+
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('')
442+
443+
// Should preserve DEFAULT markers and apply indentation (3 spaces under list)
444+
expect(textContent).toContain(' a) (DEFAULT) Standard')
445+
expect(textContent).toContain(' b) Custom')
446+
expect(textContent).toContain(' c) Advanced')
447+
})
448+
449+
test('renders lettered items with long text', () => {
450+
const markdown = `1. Which approach do you prefer?
451+
a) This is a very long option that contains lots of detailed information about the approach and its benefits
452+
b) Short option
453+
c) Another very detailed option explaining all the trade-offs and considerations you should think about`
454+
455+
const output = renderMarkdown(markdown)
456+
const nodes = flattenNodes(output)
457+
458+
const textContent = nodes
459+
.map((node) => {
460+
if (typeof node === 'string') return node
461+
if (React.isValidElement(node)) {
462+
return flattenChildren(node.props.children).join('')
463+
}
464+
return ''
465+
})
466+
.join('')
467+
468+
// Long text should still be indented (3 spaces under list)
469+
expect(textContent).toContain(' a) This is a very long option')
470+
expect(textContent).toContain(' b) Short option')
471+
expect(textContent).toContain(' c) Another very detailed')
472+
})
473+
474+
test('renders extended lettered lists (d, e, f)', () => {
475+
const markdown = `1. Pick one:
476+
a) Option A
477+
b) Option B
478+
c) Option C
479+
d) Option D
480+
e) Option E
481+
f) Option F`
482+
483+
const output = renderMarkdown(markdown)
484+
const nodes = flattenNodes(output)
485+
486+
const textContent = nodes
487+
.map((node) => {
488+
if (typeof node === 'string') return node
489+
if (React.isValidElement(node)) {
490+
return flattenChildren(node.props.children).join('')
491+
}
492+
return ''
493+
})
494+
.join('')
495+
496+
// All lettered items a-f should be indented (3 spaces under list)
497+
expect(textContent).toContain(' a) Option A')
498+
expect(textContent).toContain(' b) Option B')
499+
expect(textContent).toContain(' c) Option C')
500+
expect(textContent).toContain(' d) Option D')
501+
expect(textContent).toContain(' e) Option E')
502+
expect(textContent).toContain(' f) Option F')
503+
})
504+
505+
test('does not indent uppercase lettered items', () => {
506+
const markdown = `A) This should not be indented
507+
B) Neither should this`
508+
509+
const output = renderMarkdown(markdown)
510+
const nodes = flattenNodes(output)
511+
512+
const textContent = nodes
513+
.map((node) => {
514+
if (typeof node === 'string') return node
515+
if (React.isValidElement(node)) {
516+
return flattenChildren(node.props.children).join('')
517+
}
518+
return ''
519+
})
520+
.join('')
521+
522+
// Uppercase should NOT be indented (only lowercase a-z)
523+
expect(textContent).not.toContain(' A)')
524+
expect(textContent).not.toContain(' B)')
525+
expect(textContent).toContain('A) This should not')
526+
expect(textContent).toContain('B) Neither should')
527+
})
528+
529+
test('renders mixed content with paragraphs and lettered items', () => {
530+
const markdown = `Here's some context before the questions.
531+
532+
1. First question?
533+
a) Option one
534+
b) Option two
535+
536+
And some text in between questions.
537+
538+
2. Second question?
539+
a) Another option
540+
b) Final option
541+
542+
Conclusion text at the end.`
543+
544+
const output = renderMarkdown(markdown)
545+
const nodes = flattenNodes(output)
546+
547+
const textContent = nodes
548+
.map((node) => {
549+
if (typeof node === 'string') return node
550+
if (React.isValidElement(node)) {
551+
return flattenChildren(node.props.children).join('')
552+
}
553+
return ''
554+
})
555+
.join('')
556+
557+
// Context and conclusion should not be indented
558+
expect(textContent).toContain('Here\'s some context')
559+
expect(textContent).toContain('And some text in between')
560+
expect(textContent).toContain('Conclusion text at the end')
561+
562+
// Lettered items should be indented (3 spaces under list)
563+
expect(textContent).toContain(' a) Option one')
564+
expect(textContent).toContain(' b) Option two')
565+
expect(textContent).toContain(' a) Another option')
566+
expect(textContent).toContain(' b) Final option')
567+
})
568+
569+
test('does not indent text that happens to start with letter and parenthesis mid-sentence', () => {
570+
const markdown = `This is a sentence that mentions a) something in the middle.`
571+
572+
const output = renderMarkdown(markdown)
573+
const nodes = flattenNodes(output)
574+
575+
const textContent = nodes
576+
.map((node) => {
577+
if (typeof node === 'string') return node
578+
if (React.isValidElement(node)) {
579+
return flattenChildren(node.props.children).join('')
580+
}
581+
return ''
582+
})
583+
.join('')
584+
585+
// Should not add indentation for a) in the middle of a sentence
586+
expect(textContent).not.toContain(' a) something')
587+
expect(textContent).toContain('a) something in the middle')
588+
})
589+
})
329590
})

cli/src/utils/markdown-renderer.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,42 @@ const renderNode = (
727727
state,
728728
node.type,
729729
)
730-
const nodes = [...children]
730+
let nodes = [...children]
731+
732+
// Add indentation for lettered sub-items (a), b), c), etc.)
733+
// Process text nodes to add indentation at the start and after each newline
734+
// Check both root and listItem context
735+
if (parentType === 'root' || parentType === 'listItem') {
736+
const processedNodes: ReactNode[] = []
737+
738+
for (const child of nodes) {
739+
if (typeof child === 'string') {
740+
// Split by newlines and add indentation for lettered items
741+
const lines = child.split('\n')
742+
const processedLines: string[] = []
743+
744+
for (let i = 0; i < lines.length; i++) {
745+
const line = lines[i]
746+
const needsIndent = line.match(/^([a-z])\)\s/)
747+
748+
if (needsIndent) {
749+
// Use 3 spaces for sub-items under list items, 6 for root level
750+
const indent = parentType === 'listItem' ? ' ' : ' '
751+
processedLines.push(indent + line)
752+
} else {
753+
processedLines.push(line)
754+
}
755+
}
756+
757+
processedNodes.push(processedLines.join('\n'))
758+
} else {
759+
processedNodes.push(child)
760+
}
761+
}
762+
763+
nodes = processedNodes
764+
}
765+
731766
if (parentType === 'listItem') {
732767
nodes.push('\n')
733768
} else if (parentType === 'blockquote') {

0 commit comments

Comments
 (0)