Files
new-markdown/tests/stage3.test.ts

191 lines
7.0 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.
// 阶段 3 验收:表格操作、数学公式、Mermaid 视图、脚注/HTML 转正、图片命令、附件链接。
import { describe, it, expect, afterEach } from 'vitest'
import type { Editor } from '@milkdown/kit/core'
import { editorViewCtx } from '@milkdown/kit/core'
import { TextSelection } from '@milkdown/kit/prose/state'
import { createMarkdownEditor, getMarkdown } from '../src/editor/editor'
import { editorCommands, insertMathBlock, insertMermaidBlock } from '../src/editor/commands'
import { insertLinkText } from '../src/editor/paste'
import { selectionInTable, isLastTableCell } from '../src/editor/plugins'
let editors: Editor[] = []
afterEach(async () => {
for (const e of editors) { try { await e.destroy() } catch { /* ignore */ } }
editors = []
document.body.innerHTML = ''
})
async function load(md: string): Promise<Editor> {
const root = document.createElement('div')
document.body.appendChild(root)
const editor = createMarkdownEditor(root, md)
editors.push(editor)
await editor.create()
return editor
}
async function roundtrip(md: string): Promise<string> {
const once = getMarkdown(await load(md))
const twice = getMarkdown(await load(once))
expect(twice).toBe(once)
return once
}
function setSelection(editor: Editor, pos: number) {
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, pos)))
})
}
function findTextPos(editor: Editor, text: string): number {
let found = -1
editor.action((ctx) => {
ctx.get(editorViewCtx).state.doc.descendants((node, pos) => {
if (found < 0 && node.isText && node.text?.includes(text)) found = pos + node.text.indexOf(text)
return found < 0
})
})
return found
}
const TABLE_MD = '| 名称 | 数量 |\n| :--- | ---: |\n| 苹果 | 3 |\n| 香蕉 | 5 |\n'
describe('表格', () => {
it('插入 3×3 表格命令生成表头与数据行', async () => {
const editor = await load('正文。\n')
setSelection(editor, 2)
editorCommands.insertTable(editor, 3, 3)
const md = getMarkdown(editor)
expect(md).toContain('|')
expect(md.split('\n').filter((l) => l.includes('|')).length).toBeGreaterThanOrEqual(4)
})
it('光标在单元格内可判定表格上下文与末格', async () => {
const editor = await load(TABLE_MD)
const pos = findTextPos(editor, '香蕉')
setSelection(editor, pos)
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
expect(selectionInTable(view.state.selection as never)).toBe(true)
// “香蕉”在最后一行第一列,不是末格;其右邻“5”所在格是末格
expect(isLastTableCell(view.state as never)).toBe(false)
})
const lastPos = findTextPos(editor, '5')
setSelection(editor, lastPos)
editor.action((ctx) => {
expect(isLastTableCell(ctx.get(editorViewCtx).state as never)).toBe(true)
})
})
it('加行/加列/删列改变结构且往返稳定', async () => {
const editor = await load(TABLE_MD)
setSelection(editor, findTextPos(editor, '苹果'))
editorCommands.addRowAfter(editor)
let md = getMarkdown(editor)
expect(md.split('\n').filter((l) => l.startsWith('|')).length).toBe(5) // 表头+分隔+3 数据行
editorCommands.addColAfter(editor)
md = getMarkdown(editor)
expect(md.split('\n')[0].split('|').length).toBeGreaterThanOrEqual(5) // 3 列
editorCommands.deleteCol(editor)
const twice = getMarkdown(await load(getMarkdown(editor)))
expect(twice).toBe(getMarkdown(editor))
})
it('对齐命令改变列对齐并保留到序列化', async () => {
const editor = await load(TABLE_MD)
setSelection(editor, findTextPos(editor, '苹果'))
editorCommands.setAlign(editor, 'center')
const md = getMarkdown(editor)
expect(md).toMatch(/:-+:/)
})
})
describe('数学公式(KaTeX 插件)', () => {
it('行内公式往返保留 LaTeX', async () => {
const once = await roundtrip('质能方程 $E = mc^2$ 是物理学常识。\n')
expect(once).toContain('E = mc^2')
expect(once).toContain('$')
})
it('块级公式往返保留', async () => {
const once = await roundtrip('$$\n\\frac{a}{b} + \\sqrt{x}\n$$\n')
expect(once).toContain('\\frac{a}{b}')
expect(once).toContain('$$')
})
it('插入公式块命令产出 $$ 块', async () => {
const editor = await load('正文。\n')
setSelection(editor, 2)
insertMathBlock(editor)
expect(getMarkdown(editor)).toContain('$$')
expect(getMarkdown(editor)).toContain('E = mc^2')
})
})
describe('Mermaid 与代码块视图', () => {
it('Mermaid 块渲染出节点视图容器,源码保留', async () => {
const source = '```mermaid\ngraph TD\n A[开始] --> B[结束]\n```\n'
const editor = await load(source)
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
expect(view.dom.querySelector('.mermaid-block')).not.toBeNull()
})
expect(getMarkdown(editor)).toContain('graph TD')
})
it('普通代码块仍是 pre>code 且内容可序列化', async () => {
const editor = await load('```ts\nconst v = 42;\n```\n')
editor.action((ctx) => {
const code = ctx.get(editorViewCtx).dom.querySelector('pre code')
expect(code?.textContent).toContain('const v = 42;')
})
expect(getMarkdown(editor)).toContain('const v = 42;')
})
it('插入 Mermaid 命令产出 mermaid 代码块', async () => {
const editor = await load('正文。\n')
setSelection(editor, 2)
insertMermaidBlock(editor)
const md = getMarkdown(editor)
expect(md).toContain('```mermaid')
expect(md).toContain('graph TD')
})
})
describe('脚注与 HTML(阶段 0 探针转正)', () => {
it('脚注定义与引用往返稳定', async () => {
const once = await roundtrip('正文带脚注[^1],还有第二个[^note]。\n\n[^1]: 第一条脚注。\n\n[^note]: 第二条脚注。\n')
expect(once).toContain('[^1]')
expect(once).toContain('第一条脚注')
expect(once).toContain('[^note]')
})
it('行内与块级 HTML 往返保留', async () => {
const once = await roundtrip('一段<u>下划线</u>文本。\n\n<div class="note">原始 HTML 块</div>\n')
expect(once).toContain('<u>下划线</u>')
expect(once).toContain('原始 HTML 块')
})
})
describe('图片与附件', () => {
it('图片命令插入相对路径图片并往返保留', async () => {
const editor = await load('看图:\n')
setSelection(editor, 3)
editorCommands.image(editor, '文章.assets/image-1.png', '示意图')
const md = getMarkdown(editor)
expect(md).toContain('文章.assets/image-1.png')
const twice = getMarkdown(await load(md))
expect(twice).toBe(md)
})
it('附件以链接形式插入并往返保留', async () => {
const editor = await load('附件:\n')
setSelection(editor, 3)
expect(insertLinkText(editor, '说明.pdf', '文档.assets/说明.pdf')).toBe(true)
const md = getMarkdown(editor)
expect(md).toContain('说明.pdf')
expect(md).toContain('文档.assets/')
})
})