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

155 lines
5.6 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.
// 阶段 2 验收:标记显隐装饰、代码块 Tab、任务列表勾选 DOM、列表命令、纯文本插入、IME 守卫判定。
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 { applyCodeBlockIndent } from '../src/editor/plugins'
import { editorCommands } from '../src/editor/commands'
import { insertPlainText } from '../src/editor/paste'
import { isImeComposing } from '../src/utils/ime'
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
}
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) => {
const view = ctx.get(editorViewCtx)
view.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
}
function markers(editor: Editor): string[] {
let texts: string[] = []
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
texts = [...view.dom.querySelectorAll('.md-marker')].map((el) => el.textContent ?? '')
})
return texts
}
describe('语法标记显隐', () => {
it('光标在标题内显示 # 前缀,在粗体段内显示 **,移开后消失,且文档序列化不受影响', async () => {
const md = '# 大标题\n\n普通段落里的**粗体字**在这里。\n'
const editor = await load(md)
// 初始选区在文档开头(标题内)
expect(markers(editor).some((t) => t.includes('#'))).toBe(true)
const boldPos = findTextPos(editor, '粗体字')
expect(boldPos).toBeGreaterThan(0)
setSelection(editor, boldPos + 1)
const shown = markers(editor)
expect(shown).toContain('**')
expect(shown.some((t) => t.includes('#'))).toBe(false)
// 移到段落末尾的纯文本处,标记应全部隐藏
const endPos = findTextPos(editor, '在这里')
setSelection(editor, endPos + 2)
expect(markers(editor)).toEqual([])
// 装饰不进文档:序列化与原文一致(往返稳定)
expect(getMarkdown(editor)).toBe(md)
})
it('光标在引用内显示 > 前缀', async () => {
const editor = await load('前文。\n\n> 引用内容在此。\n')
const pos = findTextPos(editor, '引用内容')
setSelection(editor, pos + 1)
expect(markers(editor)).toContain('> ')
})
})
describe('代码块 Tab', () => {
it('代码块内 Tab 插入两空格,Shift+Tab 去掉', async () => {
const editor = await load('```ts\nconst a = 1;\n```\n')
const pos = findTextPos(editor, 'const')
setSelection(editor, pos)
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
expect(applyCodeBlockIndent(view as never, false)).toBe(true)
})
expect(getMarkdown(editor)).toContain(' const a = 1;')
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
expect(applyCodeBlockIndent(view as never, true)).toBe(true)
})
expect(getMarkdown(editor)).toContain('const a = 1;')
expect(getMarkdown(editor)).not.toContain(' const')
})
it('普通段落里不处理 Tab', async () => {
const editor = await load('普通段落。\n')
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
expect(applyCodeBlockIndent(view as never, false)).toBe(false)
})
})
})
describe('任务列表与列表命令', () => {
it('任务列表渲染出可点击 checkbox,点击切换并序列化回 [x]', async () => {
const editor = await load('- [ ] 待办事项\n- [x] 已完成\n')
let firstBox: HTMLInputElement | null = null
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
const boxes = view.dom.querySelectorAll('input[type="checkbox"]')
expect(boxes.length).toBe(2)
expect((boxes[1] as HTMLInputElement).checked).toBe(true)
firstBox = boxes[0] as HTMLInputElement
})
firstBox!.click()
expect(getMarkdown(editor)).toMatch(/-\s\[[xX]\]\s待办事项/)
expect(getMarkdown(editor)).toMatch(/-\s\[[xX]\]\s已完成/)
})
it('段落转无序列表命令生效', async () => {
const editor = await load('变成列表项\n')
setSelection(editor, 2)
editorCommands.bulletList(editor)
expect(getMarkdown(editor)).toContain('- 变成列表项')
})
})
describe('纯文本插入与 IME 守卫', () => {
it('insertPlainText 在选区处插入并可撤销序列保留', async () => {
const editor = await load('前后\n')
setSelection(editor, 2)
expect(insertPlainText(editor, '中间')).toBe(true)
expect(getMarkdown(editor)).toContain('前中间后')
})
it('isImeComposing 判定', () => {
expect(isImeComposing({ isComposing: true })).toBe(true)
expect(isImeComposing({ keyCode: 229 })).toBe(true)
expect(isImeComposing({ isComposing: false, keyCode: 13 })).toBe(false)
expect(isImeComposing({})).toBe(false)
})
})