fix(acceptance-r2): 二轮验收 4 项回修(docs/15)

C5 粘贴:handleDOMEvents.paste 前置拦截无 HTML/无文件的多行纯文本按段落
插入(真机菜单粘贴走 execCommand 兜底、与 Ctrl+V 同中 clipboard 插件的
Markdown 解析);插入改开放 Slice,消除段中空段。
E3 相对导出:先选保存位置,再按导出目录重写图片相对路径。
E4 复制为 HTML:剪贴板变体公式只留 MathML、Mermaid 栅格化 PNG。
E6 HTML 导入:DOM 预处理剥 head/脚本/SVG、自家 KaTeX 还原 $ 源码、
GFM 插件加载失败降级。vitest 79 通过 + 2 跳过。
This commit is contained in:
Muse
2026-11-14 11:17:12 +08:00
parent 9bd3f3de97
commit b2d36b7397
8 changed files with 348 additions and 34 deletions

View File

@@ -2,7 +2,7 @@
import { describe, it, expect } from 'vitest'
import { createMarkdownEditor, cursorFromSelection } from '../src/editor/editor'
import { editorViewCtx } from '@milkdown/kit/core'
import { AllSelection, NodeSelection } from '@milkdown/kit/prose/state'
import { AllSelection, NodeSelection, TextSelection } from '@milkdown/kit/prose/state'
import { buildTextIndex, findInIndex } from '../src/editor/search'
import { insertPlainText } from '../src/editor/paste'
import { findBlockIndex } from '../src/editor/cursorSync'
@@ -123,3 +123,38 @@ describe('验收回修回归', () => {
expect(md).toContain('| 1 | 2 |')
})
})
describe('二轮验收回归', () => {
it('Ctrl+V 粘贴记事本三行纯文本分成三段(C5)', async () => {
const { host, editor } = await makeEditor('已有文字')
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, 5)))
})
const view = editor.action((ctx) => ctx.get(editorViewCtx))
const event = new Event('paste', { bubbles: true, cancelable: true })
Object.defineProperty(event, 'clipboardData', {
value: { files: [], getData: (type: string) => (type === 'text/plain' ? '第一行\r\n第二行\r\n第三行' : '') },
})
view.dom.dispatchEvent(event)
const md = getMarkdown(editor)
expect(md).toContain('第一行\n\n第二行\n\n第三行')
await editor.destroy()
host.remove()
})
it('段落中间粘贴多行不产生空段落(C5)', async () => {
const { host, editor } = await makeEditor('前后文')
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, 2)))
})
const { insertPlainText } = await import('../src/editor/paste')
insertPlainText(editor, '第一行\n第二行')
const md = getMarkdown(editor)
expect(md).toContain('前第一行\n\n第二行后文')
expect(md).not.toContain('<br />')
await editor.destroy()
host.remove()
})
})

View File

@@ -52,3 +52,38 @@ describe('导入转换', () => {
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')
})
})