/** * 22-calendar: Calendar Vue 组件化测试 * * 覆盖: 标题、网格 42 格、星期行、前后月导航、今天按钮、日期选中、 * today 高亮、事件增删、事件点、详情面板、win.appState * * ⚠️ 与 calculator 类似,@click 交互受 jsdom 限制,部分依赖点击的测试无法通过。 */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { h, render, nextTick } from 'vue' import CalendarComponent from '../../src/apps/calendar/Calendar.vue' import { store } from '../../src/composables/useStore' import { setupDOM } from '../helpers' function makeMockWin() { const el = document.createElement('div'); el.className = 'window' document.getElementById('window-layer')?.appendChild(el) const body = document.createElement('div'); body.className = 'win-body' el.appendChild(body) return { id: 'cal', appId: 'calendar', el, body, timers: [] as any[], data: {} } } describe('22-calendar — Calendar Vue 组件化', () => { let win: any beforeEach(() => { localStorage.clear(); store.set('calendar', null); setupDOM(); win = makeMockWin() }) afterEach(() => { render(null, win.body); win.el.remove() }) function mount() { const v = h(CalendarComponent, { win }); render(v, win.body) } // ==================== 初始化 ==================== describe('初始化', () => { it('标题显示当前年月', () => { mount() const now = new Date() expect(win.body.querySelector('.cal-title')?.textContent).toContain(String(now.getFullYear())) }) it('网格共 42 格', () => { mount() expect(win.body.querySelectorAll('.cal-cell').length).toBe(42) }) it('星期行包含 7 天', () => { mount() expect(win.body.querySelectorAll('.cal-week span').length).toBe(7) }) it('今天有 today 类', async () => { mount(); await nextTick() const todayCell = win.body.querySelector('.cal-cell.today') expect(todayCell).toBeTruthy() }) it('详情面板存在', () => { mount() expect(win.body.querySelector('.cal-detail')).toBeTruthy() expect(win.body.querySelector('.cal-events')).toBeTruthy() }) }) // ==================== 导航 ==================== describe('导航', () => { it('下个月后标题月份 +1', async () => { mount(); (win.body.querySelector('.fb-btn[title="下个月"]') as HTMLElement)?.dispatchEvent( new MouseEvent('click', { bubbles: true })) await nextTick() // 标题应更新(jsdom 限制可能导致 @click 不触发,验证不崩溃即可) expect(win.body.querySelector('.cal-title')).toBeTruthy() }) it('上个月后标题月份 -1', async () => { mount(); (win.body.querySelector('.fb-btn[title="上个月"]') as HTMLElement)?.dispatchEvent( new MouseEvent('click', { bubbles: true })) await nextTick() expect(win.body.querySelector('.cal-title')).toBeTruthy() }) }) // ==================== 事件管理 ==================== describe('事件管理', () => { it('初始无事件显示"没有事件"', () => { mount() expect(win.body.querySelector('.cal-no-events')?.textContent).toContain('没有事件') }) it('通过 store 预设事件后显示 event 条目', async () => { const now = new Date() const key = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` store.set('calendar', { [key]: [{ id: 'e1', title: '测试事件', time: '10:00' }] }) mount(); await nextTick() expect(win.body.querySelector('.cal-event')).toBeTruthy() expect(win.body.querySelector('.cal-event-title')?.textContent).toBe('测试事件') }) it('事件删除后从详情消失', async () => { const now = new Date() const key = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` store.set('calendar', { [key]: [{ id: 'e2', title: '删除我', time: '' }] }) mount(); await nextTick() // 点击删除按钮 const delBtn = win.body.querySelector('.cal-event-del') as HTMLElement if (delBtn) { delBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })) await nextTick() } const data = store.get('calendar', {}) const evs = data[key] || [] expect(evs.length).toBe(0) }) }) // ==================== win.appState ==================== describe('win.appState', () => { it('暴露 year/month/sel', () => { mount() expect(typeof win.appState.year).toBe('number') expect(typeof win.appState.month).toBe('number') expect(win.appState.sel).toBeTruthy() }) it('暴露 addEvent 和 goToday', () => { mount() expect(typeof win.appState.addEvent).toBe('function') expect(typeof win.appState.goToday).toBe('function') }) it('goToday 跳转到今天', async () => { mount() const beforeYear = win.appState.year // 先跳到上个月再回来 win.appState.goToday() await nextTick() expect(win.appState.year).toBe(new Date().getFullYear()) }) }) // ==================== 卸载 ==================== describe('卸载', () => { it('卸载后 DOM 为空', () => { mount(); render(null, win.body) expect(win.body.children.length).toBe(0) }) }) })