import { describe, expect, it } from 'vitest'
import { hasCompleteHtmlTagContent } from '../packages/markstream-angular/src/components/shared/node-helpers'
import {
coerceBuiltinHtmlNode,
coerceCustomHtmlNode,
resolveNodeOutletCodeMode,
resolveNodeOutletCustomComponent,
resolveNodeOutletCustomInputs,
} from '../packages/markstream-angular/src/components/shared/node-outlet-helpers'
class ExactLanguageComponent {}
class GenericCodeBlockComponent {}
class MermaidComponent {}
class D2Component {}
class D2LangComponent {}
describe('markstream-angular NodeOutlet', () => {
it('coerces custom html tags into tag-typed nodes for custom components', () => {
const node = {
type: 'html_block',
tag: 'thinking',
content: 'inner body',
raw: 'inner body',
} as any
expect((coerceCustomHtmlNode(node) as any).type).toBe('thinking')
expect((coerceCustomHtmlNode(node) as any).tag).toBe('thinking')
expect((coerceCustomHtmlNode(node) as any).content).toBe('inner body')
expect((coerceBuiltinHtmlNode(node, 'html_block') as any).type).toBe('html_block')
expect((coerceBuiltinHtmlNode(node, 'html_block') as any).content).toBe('inner body')
})
it('preserves standard html wrappers for builtin html nodes', () => {
const node = {
type: 'html_block',
tag: 'details',
content: 'More
Body
',
raw: 'More
Body
',
} as any
expect((coerceBuiltinHtmlNode(node, 'html_block') as any).content).toBe('More
Body
')
})
it('only escapes malformed unknown html tags', () => {
expect(hasCompleteHtmlTagContent('ok', 'question')).toBe(true)
expect(hasCompleteHtmlTagContent('ok', 'question')).toBe(false)
})
it('routes heavy node props by code block mode', () => {
const context = {
codeBlockProps: { showHeader: false },
mermaidProps: { renderDebounceMs: 180 },
d2Props: { themeId: 7 },
infographicProps: { showHeader: false },
events: {},
}
let node = {
type: 'code_block',
language: 'mermaid',
code: 'graph TD\nA-->B\n',
} as any
expect(resolveNodeOutletCodeMode(node, context as any)).toBe('mermaid')
expect(resolveNodeOutletCustomInputs(node, context as any)).toEqual({ renderDebounceMs: 180, estimatedPreviewHeightPx: 360 })
node = {
type: 'code_block',
language: 'd2',
code: 'a -> b',
} as any
expect(resolveNodeOutletCodeMode(node, context as any)).toBe('d2')
expect(resolveNodeOutletCustomInputs(node, context as any)).toEqual({ themeId: 7 })
node = {
type: 'code_block',
language: 'infographic',
code: 'infographic list-row-simple-horizontal-arrow',
} as any
expect(resolveNodeOutletCodeMode(node, context as any)).toBe('infographic')
expect(resolveNodeOutletCustomInputs(node, context as any)).toEqual({ showHeader: false, estimatedPreviewHeightPx: 360 })
node = {
type: 'code_block',
language: 'ts',
code: 'const value = 1',
} as any
expect(resolveNodeOutletCodeMode(node, context as any)).toBe('code')
expect(resolveNodeOutletCustomInputs(node, context as any)).toEqual({ showHeader: false })
})
it('prefers exact language custom components over code_block fallback', () => {
const context = {
customComponents: {
echarts: ExactLanguageComponent as any,
code_block: GenericCodeBlockComponent as any,
},
codeBlockProps: { showHeader: false },
events: {},
} as any
let node = {
type: 'code_block',
language: 'echarts',
code: 'option = {}',
} as any
expect(resolveNodeOutletCustomComponent(node, context)).toBe(ExactLanguageComponent)
expect(resolveNodeOutletCustomInputs(node, context)).toEqual({ showHeader: false })
node = {
type: 'code_block',
language: 'ts',
code: 'const value = 1',
} as any
expect(resolveNodeOutletCustomComponent(node, context)).toBe(GenericCodeBlockComponent)
expect(resolveNodeOutletCustomInputs(node, context)).toEqual({ showHeader: false })
})
it('keeps specialized mermaid routing ahead of code_block fallback', () => {
const context = {
customComponents: {
mermaid: MermaidComponent as any,
code_block: GenericCodeBlockComponent as any,
},
mermaidProps: { renderDebounceMs: 180 },
events: {},
} as any
const node = {
type: 'code_block',
language: 'mermaid',
code: 'graph TD\nA-->B\n',
} as any
expect(resolveNodeOutletCustomComponent(node, context)).toBe(MermaidComponent)
expect(resolveNodeOutletCustomInputs(node, context)).toEqual({ renderDebounceMs: 180, estimatedPreviewHeightPx: 360 })
})
it('injects stable preview height estimates for angular mermaid and infographic custom inputs', () => {
const context = {
events: {},
} as any
let node = {
type: 'code_block',
language: 'mermaid',
code: 'flowchart TD\nA-->B\nB-->C\nC-->D\nD-->E\nE-->F\nF-->G\nG-->H\nH-->I\nI-->J\nJ-->K\nK-->L\n',
} as any
expect(resolveNodeOutletCustomInputs(node, context)).toEqual({ estimatedPreviewHeightPx: 500 })
node = {
type: 'code_block',
language: 'infographic',
code: ['# Release progress', '- Plan: complete', '- Build: active', '- Verify: pending'].join('\n'),
} as any
expect(resolveNodeOutletCustomInputs(node, context)).toEqual({ estimatedPreviewHeightPx: 500 })
})
it('lets d2lang exact overrides beat d2 fallback while keeping d2 inputs', () => {
const context = {
customComponents: {
d2: D2Component as any,
d2lang: D2LangComponent as any,
},
d2Props: { themeId: 7 },
events: {},
} as any
const node = {
type: 'code_block',
language: 'd2lang',
code: 'a -> b',
} as any
expect(resolveNodeOutletCustomComponent(node, context)).toBe(D2LangComponent)
expect(resolveNodeOutletCustomInputs(node, context)).toEqual({ themeId: 7 })
})
})