Files
new-markdown/tests/stage5.test.ts
Muse e8701d4eeb test(export): Mermaid 导出管线桩渲染回归(替换与 PNG 兜底)
jsdom 跑不了 mermaid.render(缺 SVG 布局 API),用 vi.mock 桩验证剪贴板版
Mermaid 被替换为渲染结果而非代码块;全量 80 通过 + 2 跳过。
2026-11-14 11:23:38 +08:00

110 lines
4.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 阶段 5 验收:主题注册表完整性、导出 HTML 管线(结构/表格/公式/代码)、HTML 导入转换。
import { describe, it, expect, vi } from 'vitest'
import { DOC_THEMES, docThemeById } from '../src/themes'
import { markdownToExportHtml, htmlToMarkdown, exportCss } from '../src/services/exportHtml'
// jsdom 里 mermaid.render 依赖真实 SVG 布局 API 会失败;用桩验证导出管线的
// 替换与兜底逻辑(真机栅格化 PNG 只能在 Windows 复验)
vi.mock('mermaid', () => ({
default: {
initialize: () => undefined,
render: async () => ({ svg: '<svg data-stub="mermaid"><text>图</text></svg>' }),
},
}))
describe('文档主题注册表', () => {
it('7 套主题 id 唯一且可查', () => {
expect(DOC_THEMES).toHaveLength(7)
expect(new Set(DOC_THEMES.map((t) => t.id)).size).toBe(7)
expect(docThemeById('paper').name).toBe('经典纸张')
expect(docThemeById('不存在').id).toBe('default')
for (const t of DOC_THEMES) expect(exportCss(t.id)).toContain('--f:')
})
})
describe('导出 HTML 管线', () => {
it('标题/表格/行内公式/代码块都转成对应 HTML(关高亮与 Mermaid 时走纯管线)', async () => {
const md = '# 导出标题\n\n正文含 $E = mc^2$ 公式。\n\n| A | B |\n| --- | --- |\n| 1 | 2 |\n\n```ts\nconst v = 1;\n```\n'
const html = await markdownToExportHtml(md, {
title: '导出标题', themeId: 'github', imageMode: 'relative', highlight: false, renderMermaid: false,
})
expect(html).toContain('<!DOCTYPE html>')
expect(html).toContain('<h1>导出标题</h1>')
expect(html).toContain('<table>')
expect(html).toContain('katex') // 公式经 rehype-katex 渲染(含 MathML)
expect(html).toContain('const v = 1;')
expect(html).toContain('<title>导出标题</title>')
})
it('Shiki 高亮开启时代码块带着色 span', async () => {
const html = await markdownToExportHtml('```ts\nconst v: number = 42;\n```\n', {
title: 't', themeId: 'default', imageMode: 'relative', renderMermaid: false,
})
expect(html).toContain('style="color:')
expect(html).toContain('42')
})
it('图片内嵌模式用 resolveImage 替换为 data URL', async () => {
const html = await markdownToExportHtml('![示意图](文章.assets/a.png)\n', {
title: 't', themeId: 'default', imageMode: 'embed', highlight: false, renderMermaid: false,
resolveImage: async (src) => (src.endsWith('a.png') ? 'data:image/png;base64,QUJD' : null),
})
expect(html).toContain('src="data:image/png;base64,QUJD"')
})
})
describe('导入转换', () => {
it('HTML 转 Markdown 保留标题与列表', async () => {
const md = await htmlToMarkdown('<h1>标题</h1><ul><li>第一项</li><li>第二项</li></ul><p>正文<strong>加粗</strong></p>')
expect(md).toContain('# 标题')
expect(md).toMatch(/-\s+第一项/)
expect(md).toContain('**加粗**')
})
})
describe('二轮验收:导出与导入', () => {
it('相对路径按导出文件位置重写(E3)', async () => {
const html = await markdownToExportHtml('# 图\n\n![示意图](doc.assets/pic%20one.png)\n', {
title: 't', themeId: 'default', imageMode: 'relative',
docDir: 'C:/ws/docs', exportDir: 'C:/out', highlight: false, renderMermaid: false,
})
expect(html).toContain('src="../ws/docs/doc.assets/pic%20one.png"')
const sameDir = await markdownToExportHtml('![a](img.png)', {
title: 't', themeId: 'default', imageMode: 'relative',
docDir: 'C:/ws', exportDir: 'C:/ws', highlight: false, renderMermaid: false,
})
expect(sameDir).toContain('src="img.png"')
})
it('剪贴板版公式只剩 MathML(E4)', async () => {
const html = await markdownToExportHtml('公式 $x=1$ 在此\n', {
title: 't', themeId: 'default', imageMode: 'embed', forClipboard: true,
highlight: false, renderMermaid: false,
})
expect(html).toContain('<math')
expect(html).not.toContain('katex-html')
})
it('导入整页 HTML 先剥 head/脚本/SVG(E6)', async () => {
const md = await htmlToMarkdown('<html><head><title>标题</title><style>.a{color:red}</style></head>'
+ '<body><h1>正文題</h1><script>alert(1)</script><p>第一段</p>'
+ '<svg><rect width="10"/></svg><p>第二段</p></body></html>')
expect(md).toContain('# 正文題')
expect(md).toContain('第一段')
expect(md).toContain('第二段')
expect(md).not.toContain('alert')
expect(md).not.toContain('color:red')
})
})
describe('二轮验收:Mermaid 导出替换', () => {
it('剪贴板版 Mermaid 经桩渲染后不再是代码块(PNG 栅格化失败退回 SVG)', async () => {
const html = await markdownToExportHtml('```mermaid\ngraph TD\n A-->B\n```\n', {
title: 't', themeId: 'default', imageMode: 'embed', forClipboard: true, mermaidPng: true,
highlight: false,
})
expect(html).toContain('data-stub="mermaid"')
expect(html).not.toContain('language-mermaid')
})
})