/** * 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('粗体渲染为 ', () => { mountComponent() expect(preview().innerHTML).toContain('') }) it('代码渲染为 ', () => { 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) }) }) })