200 lines
5.7 KiB
TypeScript
200 lines
5.7 KiB
TypeScript
/**
|
||
* 35-stickies — 便笺 深层测试
|
||
*
|
||
* 使用 vitest 原汁原味的 vi.mock() 模式 mock store/WM。
|
||
* 使用 @vue/test-utils mount() 渲染组件。
|
||
* 统一在 afterEach 中恢复 fake timers。
|
||
*/
|
||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||
import { mount } from '@vue/test-utils'
|
||
import { nextTick } from 'vue'
|
||
import StickiesComponent from '../../src/apps/stickies/Stickies.vue'
|
||
|
||
// ---- mock 数据 ----
|
||
const mockNotes = [
|
||
{ id: 's1', text: '双击便笺即可编辑。\n通过菜单可以更换颜色。', color: 'yellow', x: null, y: null },
|
||
]
|
||
|
||
const { mockStoreSet, mockStoreGet } = vi.hoisted(() => ({
|
||
mockStoreSet: vi.fn(),
|
||
mockStoreGet: vi.fn((_key: string, fallback: any) => structuredClone(fallback)),
|
||
}))
|
||
|
||
vi.mock('../../src/composables/useStore', () => ({
|
||
store: {
|
||
PREFIX: 'macos-web:',
|
||
get: mockStoreGet,
|
||
set: mockStoreSet,
|
||
remove: vi.fn(),
|
||
clearAll: vi.fn(),
|
||
},
|
||
}))
|
||
|
||
vi.mock('../../src/composables/useWM', () => ({
|
||
wm: { close: vi.fn(), setTitle: vi.fn() },
|
||
}))
|
||
|
||
describe('35-stickies — 便笺', () => {
|
||
let el: HTMLElement, body: HTMLElement
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks()
|
||
localStorage.clear()
|
||
|
||
el = document.createElement('div')
|
||
el.className = 'window'
|
||
document.body.appendChild(el)
|
||
body = document.createElement('div')
|
||
body.className = 'win-body'
|
||
el.appendChild(body)
|
||
|
||
// 默认返回预设便笺
|
||
mockStoreGet.mockReturnValue(structuredClone(mockNotes))
|
||
})
|
||
|
||
afterEach(() => {
|
||
el.remove()
|
||
// 确保恢复真实 timers(已在 setup.ts 全局 afterEach 中处理,此处兜底)
|
||
vi.useRealTimers()
|
||
})
|
||
|
||
function mountComponent() {
|
||
return mount(StickiesComponent, {
|
||
props: {
|
||
win: { id: 'st1', appId: 'stickies', el, body, timers: [], data: { noteId: 's1' } },
|
||
},
|
||
attachTo: body,
|
||
})
|
||
}
|
||
|
||
function editableDiv() {
|
||
return body.querySelector('[contenteditable]') as HTMLElement
|
||
}
|
||
|
||
// ==================== 渲染 ====================
|
||
describe('渲染', () => {
|
||
it('contenteditable div 存在', () => {
|
||
mountComponent()
|
||
expect(editableDiv()).toBeTruthy()
|
||
})
|
||
|
||
it('有 no-chrome 类', () => {
|
||
mountComponent()
|
||
expect(body.querySelector('.no-chrome')).toBeTruthy()
|
||
})
|
||
|
||
it('显示预设文本', () => {
|
||
mountComponent()
|
||
expect(editableDiv().textContent).toContain('双击便笺')
|
||
})
|
||
|
||
it('setColor 函数存在且不抛错', () => {
|
||
const wrapper = mountComponent()
|
||
const appState = (wrapper.vm as any).$props.win.appState
|
||
expect(typeof appState.setColor).toBe('function')
|
||
expect(() => appState.setColor('yellow')).not.toThrow()
|
||
})
|
||
})
|
||
|
||
// ==================== 编辑 ====================
|
||
describe('编辑', () => {
|
||
it('输入更新 DOM 文本', async () => {
|
||
mountComponent()
|
||
const div = editableDiv()
|
||
div.textContent = '新内容'
|
||
div.dispatchEvent(new Event('input', { bubbles: true }))
|
||
await nextTick()
|
||
expect(div.textContent).toContain('新内容')
|
||
})
|
||
|
||
it('输入后触发持久化 debounce', async () => {
|
||
vi.useFakeTimers()
|
||
mountComponent()
|
||
const div = editableDiv()
|
||
div.textContent = '持久化文本'
|
||
div.dispatchEvent(new Event('input', { bubbles: true }))
|
||
|
||
vi.advanceTimersByTime(500)
|
||
|
||
expect(mockStoreSet).toHaveBeenCalled()
|
||
const call = mockStoreSet.mock.calls.find((c: any[]) => c[0] === 'stickies')
|
||
expect(call).toBeTruthy()
|
||
expect(call[1][0].text).toBe('持久化文本')
|
||
|
||
vi.useRealTimers()
|
||
})
|
||
})
|
||
|
||
// ==================== 颜色 ====================
|
||
describe('颜色', () => {
|
||
function getAppState() {
|
||
const wrapper = mountComponent()
|
||
return (wrapper.vm as any).$props.win.appState
|
||
}
|
||
|
||
it('setColor 蓝色不抛错', () => {
|
||
expect(() => getAppState().setColor('blue')).not.toThrow()
|
||
})
|
||
|
||
it('换色后 store 更新', () => {
|
||
const appState = getAppState()
|
||
appState.setColor('blue')
|
||
expect(mockStoreSet).toHaveBeenCalled()
|
||
const call = mockStoreSet.mock.calls.find((c: any[]) => c[0] === 'stickies')
|
||
expect(call[1][0].color).toBe('blue')
|
||
})
|
||
|
||
it('全部 5 色可切换', () => {
|
||
const appState = getAppState()
|
||
for (const c of ['yellow', 'blue', 'green', 'pink', 'purple']) {
|
||
expect(() => appState.setColor(c)).not.toThrow()
|
||
}
|
||
})
|
||
})
|
||
|
||
// ==================== 删除 ====================
|
||
describe('删除', () => {
|
||
it('removeSelf 函数存在', () => {
|
||
mountComponent()
|
||
const wrapper = mountComponent()
|
||
const appState = (wrapper.vm as any).$props.win.appState
|
||
expect(typeof appState.removeSelf).toBe('function')
|
||
})
|
||
|
||
it('removeSelf 从 store 移除', () => {
|
||
const wrapper = mountComponent()
|
||
const appState = (wrapper.vm as any).$props.win.appState
|
||
appState.removeSelf()
|
||
expect(mockStoreSet).toHaveBeenCalledWith('stickies', [])
|
||
})
|
||
})
|
||
|
||
// ==================== appState ====================
|
||
describe('appState', () => {
|
||
it('暴露 note 对象', () => {
|
||
mountComponent()
|
||
const wrapper = mountComponent()
|
||
const appState = (wrapper.vm as any).$props.win.appState
|
||
expect(appState.note).toBeTruthy()
|
||
expect(appState.note.id).toBe('s1')
|
||
})
|
||
|
||
it('暴露 setColor', () => {
|
||
mountComponent()
|
||
const wrapper = mountComponent()
|
||
const appState = (wrapper.vm as any).$props.win.appState
|
||
expect(typeof appState.setColor).toBe('function')
|
||
})
|
||
})
|
||
|
||
// ==================== 卸载 ====================
|
||
describe('卸载', () => {
|
||
it('卸载后 DOM 为空', () => {
|
||
const wrapper = mountComponent()
|
||
wrapper.unmount()
|
||
expect(body.children).toHaveLength(0)
|
||
})
|
||
})
|
||
})
|
||
|