/** * 41-typora — Typora Markdown 编辑器 深层测试 * * 使用 vitest 原汁原味的 vi.mock() + vi.useFakeTimers() 模式。 * 统一在 afterEach 中恢复 fake timers(setup.ts 全局已处理,此处兜底)。 */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { mount } from '@vue/test-utils' import { nextTick } from 'vue' import TyporaComponent from '../../src/apps/typora/Typora.vue' import { setupDOM } from '../helpers' vi.mock('../../src/composables/useFS', () => ({ fs: { HOME: '/Users/guest', exists: vi.fn(() => true), read: vi.fn(() => '# Typora 欢迎页\n\n这是 **Markdown** 实时预览编辑器。\n\n- 支持标题\n- 支持列表\n- 支持 **粗体** 和 *斜体*'), write: vi.fn(), baseName: (p: string) => p.split('/').pop() || '', }, })) vi.mock('../../src/composables/useWM', () => ({ wm: { setTitle: vi.fn() }, })) describe('41-typora — Typora', () => { let el: HTMLElement, body: HTMLElement beforeEach(() => { vi.clearAllMocks() setupDOM() el = document.createElement('div') el.className = 'window' document.body.appendChild(el) body = document.createElement('div') body.className = 'win-body' el.appendChild(body) }) afterEach(() => { el.remove() vi.useRealTimers() }) function mountComponent() { return mount(TyporaComponent, { props: { win: { el, body, timers: [], data: {} } }, attachTo: body, }) } function textarea() { return body.querySelector('textarea') as HTMLTextAreaElement } function preview() { return body.querySelector('.typora-preview') as HTMLElement } // ==================== DOM 结构 ==================== describe('DOM 结构', () => { it('textarea 存在', () => { mountComponent() expect(textarea()).toBeTruthy() }) it('预览区存在', () => { mountComponent() expect(preview()).toBeTruthy() }) it('textarea placeholder 包含 Markdown', () => { mountComponent() expect(textarea().placeholder).toContain('Markdown') }) }) // ==================== Markdown 渲染 ==================== describe('Markdown 渲染', () => { it('h1 标题渲染为
', () => {
mountComponent()
expect(preview().innerHTML).toContain('')
})
it('列表渲染为 ty-li', () => {
mountComponent()
expect(preview().innerHTML).toContain('ty-li')
})
it('空行渲染为 spacer', () => {
mountComponent()
expect(preview().innerHTML).toContain('height:10px')
})
it('h2 渲染', () => {
mountComponent()
expect(preview().innerHTML).toBeTruthy()
})
})
// ==================== 编辑 ====================
describe('编辑', () => {
it('输入后 debounce 更新预览', async () => {
vi.useFakeTimers()
mountComponent()
textarea().value = '# 新标题'
textarea().dispatchEvent(new Event('input', { bubbles: true }))
vi.advanceTimersByTime(200)
await nextTick()
expect(preview().innerHTML).toContain('')
vi.useRealTimers()
})
it('中文标题正确渲染', async () => {
vi.useFakeTimers()
mountComponent()
textarea().value = '# 中文标题'
textarea().dispatchEvent(new Event('input', { bubbles: true }))
vi.advanceTimersByTime(200)
await nextTick()
expect(preview().innerHTML).toContain('中文标题')
vi.useRealTimers()
})
it('多行 Markdown 正确渲染', async () => {
vi.useFakeTimers()
mountComponent()
textarea().value = '# A\n## B\n- C'
textarea().dispatchEvent(new Event('input', { bubbles: true }))
vi.advanceTimersByTime(200)
await nextTick()
const html = preview().innerHTML
expect(html).toContain('')
expect(html).toContain('')
expect(html).toContain('ty-li')
vi.useRealTimers()
})
})
// ==================== 边界 ====================
describe('边界', () => {
it('空输入预览不崩溃', async () => {
vi.useFakeTimers()
mountComponent()
textarea().value = ''
textarea().dispatchEvent(new Event('input', { bubbles: true }))
vi.advanceTimersByTime(200)
await nextTick()
// 不抛错即为通过
})
it('极长文本不崩溃', async () => {
vi.useFakeTimers()
mountComponent()
textarea().value = 'A'.repeat(5000)
textarea().dispatchEvent(new Event('input', { bubbles: true }))
vi.advanceTimersByTime(200)
await nextTick()
})
it('HTML 标签被转义', async () => {
vi.useFakeTimers()
mountComponent()
textarea().value = 'test'
textarea().dispatchEvent(new Event('input', { bubbles: true }))
vi.advanceTimersByTime(200)
await nextTick()
expect(preview().innerHTML).not.toContain('')
vi.useRealTimers()
})
it('连续快速输入 debounce 合并', async () => {
vi.useFakeTimers()
mountComponent()
textarea().value = 'A'
textarea().dispatchEvent(new Event('input', { bubbles: true }))
vi.advanceTimersByTime(50)
textarea().value = 'B'
textarea().dispatchEvent(new Event('input', { bubbles: true }))
vi.advanceTimersByTime(200)
await nextTick()
expect(preview().textContent).toContain('B')
vi.useRealTimers()
})
})
// ==================== 卸载 ====================
describe('卸载', () => {
it('卸载后 DOM 清理干净', () => {
const wrapper = mountComponent()
wrapper.unmount()
expect(body.children).toHaveLength(0)
})
})
})