macos-web/tests/cases/41-typora.test.ts
2026-07-23 15:16:57 +08:00

221 lines
5.8 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.

/**
* 41-typora — Typora Markdown 编辑器 深层测试
*
* 使用 vitest 原汁原味的 vi.mock() + vi.useFakeTimers() 模式。
* 统一在 afterEach 中恢复 fake timerssetup.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)
})
})
})