221 lines
5.8 KiB
TypeScript
221 lines
5.8 KiB
TypeScript
/**
|
||
* 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 标题渲染为 <h1>', () => {
|
||
mountComponent()
|
||
expect(preview().innerHTML).toContain('<h1>')
|
||
})
|
||
|
||
it('粗体渲染为 <b>', () => {
|
||
mountComponent()
|
||
expect(preview().innerHTML).toContain('<b>')
|
||
})
|
||
|
||
it('代码渲染为 <code>', () => {
|
||
mountComponent()
|
||
expect(preview().innerHTML).toContain('<code>')
|
||
})
|
||
|
||
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('<h1>')
|
||
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('<h1>')
|
||
expect(html).toContain('<h2>')
|
||
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 = '<div>test</div>'
|
||
textarea().dispatchEvent(new Event('input', { bubbles: true }))
|
||
|
||
vi.advanceTimersByTime(200)
|
||
await nextTick()
|
||
|
||
expect(preview().innerHTML).not.toContain('<div>')
|
||
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)
|
||
})
|
||
})
|
||
})
|