import { describe, expect, it } from 'vitest'
import {
renderMarkdownNodeToHtml,
renderNestedMarkdownToHtml,
} from '../packages/markstream-vue2/src/utils/nestedHtml'
describe('vue2 nested html helper', () => {
it('renders nested custom nodes and markdown children into stable html', () => {
const html = renderNestedMarkdownToHtml(
{
nodes: [
{
type: 'paragraph',
raw: '',
children: [
{ type: 'text', raw: '', content: 'Before' },
],
},
{
type: 'thinking',
tag: 'thinking',
raw: '',
attrs: [['class', 'preset']],
children: [
{
type: 'paragraph',
raw: '',
children: [
{ type: 'text', raw: '', content: 'Inner ' },
{
type: 'strong',
raw: '',
children: [
{ type: 'text', raw: '', content: 'bold' },
],
},
],
},
{
type: 'list',
raw: '',
ordered: false,
items: [
{
type: 'list_item',
raw: '',
children: [
{ type: 'text', raw: '', content: 'item' },
],
},
],
},
],
},
],
},
{
customHtmlTags: ['thinking'],
customNodeClass(node) {
return node.type === 'thinking' ? 'thinking-node__nested' : ''
},
},
)
expect(html).toContain('
Before
')
expect(html).toContain('bold')
expect(html).toContain('')
expect(html).toContain('data-markstream-custom-tag="thinking"')
expect(html).toContain('class="markstream-nested-custom markstream-nested-custom--thinking thinking-node__nested preset"')
})
it('falls back to markdown rendering for streaming content fragments', () => {
const html = renderNestedMarkdownToHtml(
{
node: {
type: 'thinking',
tag: 'thinking',
raw: '',
content: 'Streamed intro\n\n- first\n- second',
} as any,
},
{
cacheKey: 'vue2-nested-html-streaming',
customHtmlTags: ['thinking'],
},
)
expect(html).toContain('Streamed intro
')
expect(html).toContain('')
expect(html).toContain('- first
')
expect(html).toContain('- second
')
})
it('sanitizes unsafe attrs while preserving merged classes for custom nodes', () => {
const html = renderMarkdownNodeToHtml(
{
type: 'hint',
tag: 'hint',
raw: '',
attrs: [
['onclick', 'alert(1)'],
['data-role', 'inner'],
['class', 'preset'],
],
children: [
{ type: 'text', raw: '', content: 'Safe body' },
],
} as any,
{
customNodeClass: 'extra-class',
},
)
expect(html).toContain('data-role="inner"')
expect(html).toContain('class="markstream-nested-custom markstream-nested-custom--hint extra-class preset"')
expect(html).not.toContain('onclick=')
expect(html).toContain('Safe body')
})
it('escapes incomplete html nodes instead of trusting partial markup', () => {
const html = renderMarkdownNodeToHtml(
{
type: 'html_inline',
raw: '',
content: 'partial',
loading: true,
autoClosed: false,
} as any,
)
expect(html).toBe('<span>partial')
})
it('renders structured html wrappers while leaving blocked tags unstructured', () => {
const structuredHtml = renderMarkdownNodeToHtml(
{
type: 'html_block',
tag: 'span',
raw: '',
content: '',
attrs: [['style', 'font-size: 12px;']],
children: [
{
type: 'list',
raw: '',
ordered: false,
items: [
{
type: 'list_item',
raw: '',
children: [{ type: 'text', raw: '', content: 'alpha' }],
},
{
type: 'list_item',
raw: '',
children: [{ type: 'text', raw: '', content: 'beta' }],
},
],
},
],
} as any,
)
expect(structuredHtml).toContain('')
expect(structuredHtml).toContain('')
const blockedHtml = renderMarkdownNodeToHtml(
{
type: 'html_block',
tag: 'script',
raw: '',
content: '',
children: [
{
type: 'list',
raw: '',
ordered: false,
items: [
{
type: 'list_item',
raw: '',
children: [{ type: 'text', raw: '', content: 'alpha' }],
},
],
},
],
} as any,
)
expect(blockedHtml).toBe('')
expect(blockedHtml).not.toContain('')
const literalHtml = renderMarkdownNodeToHtml(
{
type: 'html_block',
tag: 'pre',
raw: '
\n\n- alpha\n\n
',
content: '\n\n- alpha\n\n
',
children: [
{
type: 'list',
raw: '',
ordered: false,
items: [
{
type: 'list_item',
raw: '',
children: [{ type: 'text', raw: '', content: 'alpha' }],
},
],
},
],
} as any,
)
expect(literalHtml).toBe('\n\n- alpha\n\n
')
})
it('sanitizes dangerous attrs on structured html wrappers', () => {
const html = renderMarkdownNodeToHtml(
{
type: 'html_block',
tag: 'a',
raw: '',
content: '',
attrs: [
['href', 'javascript:alert(1)'],
['onclick', 'alert(1)'],
['data-safe', 'ok'],
],
children: [
{
type: 'paragraph',
raw: 'safe child',
children: [{ type: 'text', raw: 'safe child', content: 'safe child' }],
},
],
} as any,
)
expect(html).toContain('')
expect(html).toContain('safe child
')
expect(html).not.toContain('onclick=')
expect(html).not.toContain('javascript:')
})
})