Files
BoosAPI/desktop/libs/markstream-vue/test/html-block-details-regression.test.ts
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

213 lines
8.2 KiB
TypeScript

import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
import { describe, expect, it } from 'vitest'
function expectDetailsContainer(markdown: string, name: string) {
for (const final of [false, true]) {
const md = getMarkdown(`${name}-${final}`)
const nodes = parseMarkdownToStructure(markdown, md, { final }) as any[]
expect(nodes).toHaveLength(1)
expect(nodes[0]?.type).toBe('html_block')
expect(nodes[0]?.tag).toBe('details')
expect(String(nodes[0]?.raw ?? '')).toBe(markdown)
}
}
describe('html block details regression', () => {
it('keeps details as one container node while rendering trailing blocks into html children content', () => {
const markdown = `<details style="color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;" open> <summary> Thinking... </summary>
好的,我需要帮用户生成首次病程记录。首先得仔细阅读提供的入院记录内容,确保所有信息都被正确提取。这里是详情
首先。。。。
</details>
`
expectDetailsContainer(markdown, 'html-block-details-regression')
const md = getMarkdown('html-block-details-regression-structure')
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
const details = nodes[0]
const summary = details?.children?.[0]
const introParagraph = details?.children?.[1]
expect(String(details?.content ?? '')).toContain('<p>首先。。。。</p>')
expect(details?.attrs).toEqual([
['style', 'color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;'],
['open', ''],
])
expect(details?.children).toHaveLength(3)
expect(summary?.type).toBe('html_block')
expect(summary?.tag).toBe('summary')
expect(summary?.attrs).toBeUndefined()
expect(summary?.children?.[0]?.type).toBe('paragraph')
expect(summary?.children?.[0]?.children?.[0]?.content).toContain('Thinking')
expect(introParagraph?.type).toBe('paragraph')
expect(introParagraph?.children?.[0]?.content).toContain('这里是详情')
expect(details?.children?.[2]?.type).toBe('paragraph')
expect(details?.children?.[2]?.children?.[0]?.content).toBe('首先。。。。')
})
it('keeps the blank line paragraph inside details while streaming before the closing tag arrives', () => {
const chunks = [
`<details style="color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;" open> <summary> Thinking... </summary>
好的,我需要帮用户生成首次病程记录。首先得仔细阅读提供的入院记录内容,确保所有信息都被正确提取。这里是详情
`,
`首先。。。。
`,
'</details>',
]
const md = getMarkdown('html-block-details-streaming')
let markdown = ''
for (let i = 0; i < chunks.length; i++) {
markdown += chunks[i]
const final = i === chunks.length - 1
const nodes = parseMarkdownToStructure(markdown, md, { final }) as any[]
const hasTrailingParagraph = markdown.includes('首先。。。。')
expect(nodes).toHaveLength(1)
expect(nodes[0]?.type).toBe('html_block')
expect(nodes[0]?.tag).toBe('details')
if (!final) {
expect(nodes[0]?.loading).toBe(true)
if (hasTrailingParagraph) {
expect(String(nodes[0]?.content ?? '')).toContain('<p>首先。。。。</p>')
expect(nodes[0]?.children?.[0]?.tag).toBe('summary')
expect(nodes[0]?.children?.[1]?.type).toBe('paragraph')
expect(nodes[0]?.children?.[2]?.type).toBe('paragraph')
}
else {
expect(String(nodes[0]?.content ?? '')).not.toContain('<p>首先。。。。</p>')
expect(nodes[0]?.children?.[0]?.tag).toBe('summary')
expect(nodes[0]?.children?.[1]?.type).toBe('paragraph')
expect(nodes[0]?.children ?? []).toHaveLength(2)
}
}
if (final) {
expect(nodes[0]?.loading).toBe(false)
expect(String(nodes[0]?.content ?? '')).toContain('<p>首先。。。。</p>')
expect(String(nodes[0]?.raw ?? '')).toBe(markdown)
}
}
})
it('keeps nested details blocks grouped under the outer details container', () => {
const markdown = `<details open>
<summary>outer</summary>
outer start
<details>
<summary>inner</summary>
inner body
</details>
after inner
</details>`
expectDetailsContainer(markdown, 'html-block-details-nested')
const md = getMarkdown('html-block-details-nested-structure')
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
const outer = nodes[0]
const outerSummary = outer?.children?.find((child: any) => child?.type === 'html_block' && child?.tag === 'summary')
const inner = outer?.children?.find((child: any) => child?.type === 'html_block' && child?.tag === 'details')
const trailingParagraph = outer?.children?.find((child: any) => child?.type === 'paragraph' && child?.children?.[0]?.content === 'after inner')
expect(outerSummary).toBeDefined()
expect(inner).toBeDefined()
expect(inner?.children?.[0]?.tag).toBe('summary')
expect(String(inner?.content ?? '')).toContain('<p>inner body</p>')
expect(trailingParagraph?.children?.[0]?.content).toBe('after inner')
})
it('keeps complex markdown children inside details and preserves the trailing closing newline', () => {
const markdown = `<details open>
<summary>sum</summary>
- a
- b
> q
\`\`\`js
console.log(1)
\`\`\`
tail
</details>
`
const md = getMarkdown('html-block-details-complex')
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
const details = nodes[0]
expect(nodes).toHaveLength(1)
expect(details?.tag).toBe('details')
expect(String(details?.raw ?? '')).toBe(markdown)
expect(String(details?.content ?? '')).toContain('<ul>')
expect(String(details?.content ?? '')).toContain('<blockquote>')
expect(String(details?.content ?? '')).toContain('data-lang="js"')
expect(String(details?.content ?? '')).toContain('<p>tail</p>')
expect(details?.children?.map((child: any) => child?.type === 'html_block' ? `${child?.type}:${child?.tag}` : child?.type))
.toEqual(['html_block:summary', 'list', 'blockquote', 'code_block', 'paragraph'])
})
it('parses markdown inside summary into summary children', () => {
const markdown = `<details open>
<summary>**sum** _x_</summary>
body
</details>
`
const md = getMarkdown('html-block-details-summary-markdown')
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
const details = nodes[0]
const summary = details?.children?.[0]
const summaryParagraph = summary?.children?.[0]
expect(nodes).toHaveLength(1)
expect(details?.tag).toBe('details')
expect(summary?.tag).toBe('summary')
expect(details?.attrs).toEqual([['open', '']])
expect(summaryParagraph?.type).toBe('paragraph')
expect(summaryParagraph?.children?.[0]?.type).toBe('strong')
expect(summaryParagraph?.children?.[0]?.children?.[0]?.content).toBe('sum')
expect(summaryParagraph?.children?.[2]?.type).toBe('emphasis')
expect(summaryParagraph?.children?.[2]?.children?.[0]?.content).toBe('x')
expect(details?.children?.[1]?.children?.[0]?.content).toBe('body')
})
it('keeps opener content before and after summary as structured children', () => {
const markdown = `<details open>
lead
<summary>sum</summary>
text
<div>inner</div>
para
</details>
`
const md = getMarkdown('html-block-details-opener-mixed')
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
const details = nodes[0]
expect(nodes).toHaveLength(1)
expect(details?.tag).toBe('details')
expect(details?.attrs).toEqual([['open', '']])
expect(details?.children?.map((child: any) => child?.type === 'html_block' ? `${child?.type}:${child?.tag}` : child?.type))
.toEqual(['paragraph', 'html_block:summary', 'paragraph', 'html_block:div', 'paragraph'])
expect(details?.children?.[0]?.children?.[0]?.content).toBe('lead')
expect(details?.children?.[1]?.children?.[0]?.children?.[0]?.content).toBe('sum')
expect(details?.children?.[2]?.children?.[0]?.content).toBe('text')
expect(details?.children?.[3]?.tag).toBe('div')
expect(details?.children?.[4]?.children?.[0]?.content).toBe('para')
})
})