Files
NotesAgentic/frontend/src/features/editor/VisualMarkdownEditor.spec.ts
T

127 lines
5.2 KiB
TypeScript

// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mount, type VueWrapper } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { editorViewCtx, type Editor } from '@milkdown/kit/core'
import { TextSelection } from '@milkdown/kit/prose/state'
import { getMarkdown } from '@milkdown/kit/utils'
import VisualMarkdownEditor from './VisualMarkdownEditor.vue'
import { useSettingsStore } from '@/stores/settings'
import { useThemeStore } from '@/stores/theme'
import { codeBlockConfig } from '@milkdown/kit/component/code-block'
import { EditorView as CodeMirror } from '@codemirror/view'
type EditorComponent = { getEditor: () => Editor | undefined }
const mounted: VueWrapper[] = []
async function waitForEditor(wrapper: VueWrapper): Promise<Editor> {
for (let attempt = 0; attempt < 100; attempt++) {
const editor = (wrapper.vm as unknown as EditorComponent).getEditor()
if (editor) {
try {
editor.action(getMarkdown())
return editor
} catch { /* editor is still creating */ }
}
await new Promise((resolve) => setTimeout(resolve, 10))
}
throw new Error('Milkdown editor did not become ready')
}
function selectText(editor: Editor, from: number, to: number) {
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, from, to)))
view.focus()
})
}
beforeEach(() => {
localStorage.clear()
setActivePinia(createPinia())
})
afterEach(() => {
mounted.splice(0).forEach((wrapper) => wrapper.unmount())
document.body.innerHTML = ''
})
describe('VisualMarkdownEditor formatting toolbars', () => {
it.each(['github-light', 'github-dark'] as const)('keeps Shiki %s mappings after Crepe merges its defaults', async theme => {
useThemeStore().codeBlockTheme = theme
const wrapper = mount(VisualMarkdownEditor, { props: { initialContent: '```python\nprint("Hello")\n```' }, attachTo: document.body })
mounted.push(wrapper)
const editor = await waitForEditor(wrapper)
const config = editor.action(ctx => ctx.get(codeBlockConfig.key))
const matching = config.languages.filter(item => item.alias.includes('python'))
expect(matching).toHaveLength(1)
const cm = new CodeMirror({ doc: 'print("Hello")', extensions: [...config.extensions, await matching[0]!.load()] })
try {
const string = [...cm.dom.querySelectorAll<HTMLElement>('.shiki-token')].find(el => el.textContent?.includes('Hello'))
expect(string?.style.color.toUpperCase()).toBe(theme === 'github-dark' ? '#9ECBFF' : '#032F62')
} finally { cm.destroy() }
})
it('applies bold from the top toolbar to the selected text', async () => {
const wrapper = mount(VisualMarkdownEditor, { props: { initialContent: 'alpha beta' }, attachTo: document.body })
mounted.push(wrapper)
const editor = await waitForEditor(wrapper)
selectText(editor, 1, 6)
await wrapper.get('[aria-label="加粗"]').trigger('pointerdown')
expect(editor.action(getMarkdown())).toContain('**alpha** beta')
})
it('applies italic from the floating toolbar to the selected text', async () => {
const wrapper = mount(VisualMarkdownEditor, { props: { initialContent: 'alpha beta' }, attachTo: document.body })
mounted.push(wrapper)
const editor = await waitForEditor(wrapper)
selectText(editor, 1, 6)
await new Promise((resolve) => setTimeout(resolve, 80))
const floatingItalic = document.querySelector<HTMLButtonElement>('.milkdown-toolbar [data-toolbar-item="italic"]')
expect(floatingItalic).not.toBeNull()
floatingItalic?.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
expect(editor.action(getMarkdown())).toContain('*alpha* beta')
})
it('writes a custom input font size into markdown for the selected text', async () => {
const wrapper = mount(VisualMarkdownEditor, { props: { initialContent: 'alpha beta' }, attachTo: document.body })
mounted.push(wrapper)
const editor = await waitForEditor(wrapper)
selectText(editor, 1, 6)
await wrapper.get('[aria-label="自定义字号"]').setValue(22)
await wrapper.get('[aria-label="应用自定义字号"]').trigger('pointerdown')
expect(editor.action(getMarkdown())).toContain('<span style="font-size: 22px">alpha</span> beta')
})
it('turns a heading back into a normal paragraph', async () => {
const wrapper = mount(VisualMarkdownEditor, { props: { initialContent: '# alpha' }, attachTo: document.body })
mounted.push(wrapper)
const editor = await waitForEditor(wrapper)
await wrapper.get('[aria-label="标题级别"]').setValue('paragraph')
expect(editor.action(getMarkdown()).trim()).toBe('alpha')
})
it('updates native spell checking on the ProseMirror editor', async () => {
const settings = useSettingsStore()
const wrapper = mount(VisualMarkdownEditor, { props: { initialContent: 'mispelled word' }, attachTo: document.body })
mounted.push(wrapper)
await waitForEditor(wrapper)
settings.spellCheck = true
settings.language = 'en'
await wrapper.vm.$nextTick()
const editable = wrapper.get('.ProseMirror')
expect(editable.attributes('spellcheck')).toBe('true')
expect(editable.attributes('lang')).toBe('en')
})
})