diff --git a/tests/cases/37-mail.test.ts b/tests/cases/37-mail.test.ts index 4ef1554..8a9c2a0 100644 --- a/tests/cases/37-mail.test.ts +++ b/tests/cases/37-mail.test.ts @@ -1,27 +1,79 @@ /** - * 37-mail: Mail Vue 组件化测试 + * 37-mail: Mail Vue 组件化 — 深层测试 + * 覆盖: 三个邮箱渲染、未读计数、邮件列表/排序、搜索过滤、选中/已读标记、 + * 星标切换、删除/移入废纸篓、写邮件弹窗/关闭、发送/存储、 + * 回复引用、compose appState、卸载 */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { h, render, nextTick } from 'vue' import MailComponent from '../../src/apps/mail/Mail.vue' import { store } from '../../src/composables/useStore' +import { setSysForNotify } from '../../src/composables/useNotify' 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 { el, body, timers: [] as any[], data: {} } } +function mkWin() { 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 { el, body, timers: [] as any[], data: {} } } -describe('37-mail — Mail Vue 组件化', () => { +describe('37-mail — Mail Vue 深层测试', () => { let win: any - beforeEach(() => { localStorage.clear(); store.set('mail', null); setupDOM(); win = makeMockWin() }) + beforeEach(() => { localStorage.clear(); store.set('mail', null); setupDOM(); win = mkWin(); + setSysForNotify({ settings: { notificationsEnabled: true, notifAllow: {} as any, focus: false } }) }) afterEach(() => { render(null, win.body); win.el.remove() }) function mount() { const v = h(MailComponent, { win }); render(v, win.body) } - it('显示 3 个邮箱', () => { mount(); expect(win.body.querySelectorAll('.mail-side .fb-side-item').length).toBe(3) }) - it('收件箱有邮件列表', () => { mount(); expect(win.body.querySelectorAll('.mail-row').length).toBeGreaterThanOrEqual(1) }) - it('默认选中收件箱', () => { mount(); expect(win.body.querySelector('.mail-side .fb-side-item.sel')?.textContent).toContain('收件箱') }) - it('搜索框存在', () => { mount(); expect(win.body.querySelector('.notes-search')).toBeTruthy() }) - it('新邮件按钮存在', () => { mount(); expect(win.body.querySelector('.fb-btn')).toBeTruthy() }) - it('点击邮件显示详情', async () => { mount(); const row = win.body.querySelector('.mail-row') as HTMLElement; row.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-detail-subject')).toBeTruthy() }) - it('点击新邮件打开弹窗', async () => { mount(); (win.body.querySelector('.fb-btn') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-compose')).toBeTruthy() }) - it('切换邮箱清除选中', async () => { mount(); const row = win.body.querySelector('.mail-row') as HTMLElement; row.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); const side = win.body.querySelectorAll('.mail-side .fb-side-item')[1] as HTMLElement; side.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-detail .empty-state')).toBeTruthy() }) - it('卸载', () => { mount(); render(null, win.body); expect(win.body.children.length).toBe(0) }) + // ==================== 邮箱导航 ==================== + describe('邮箱导航', () => { + it('3 个邮箱:收件箱/已发送/废纸篓', () => { mount(); const items = win.body.querySelectorAll('.mail-side .fb-side-item'); expect(items.length).toBe(3) }) + it('默认选中收件箱', () => { mount(); expect(win.body.querySelector('.mail-side .fb-side-item.sel')?.textContent).toContain('收件箱') }) + it('收件箱有未读计数 badge', () => { mount(); expect(win.body.querySelector('.mail-side .badge')).toBeTruthy() }) + it('切换邮箱清除当前选中邮件', async () => { mount(); const row = win.body.querySelector('.mail-row') as HTMLElement; row.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); (win.body.querySelectorAll('.mail-side .fb-side-item')[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-detail .empty-state')).toBeTruthy() }) + it('已发送无邮件显示提示', async () => { mount(); (win.body.querySelectorAll('.mail-side .fb-side-item')[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.empty-state')?.textContent).toContain('没有邮件') }) + }) + + // ==================== 邮件列表 ==================== + describe('邮件列表', () => { + it('收件箱有 4 封预设邮件', () => { mount(); expect(win.body.querySelectorAll('.mail-row').length).toBe(4) }) + it('邮件显示发件人和主题', () => { mount(); const first = win.body.querySelector('.mail-row') as HTMLElement; expect(first.querySelector('b')?.textContent).toBeTruthy(); expect(first.querySelector('.mail-subject')?.textContent).toBeTruthy() }) + it('未读邮件有 unread 类', () => { mount(); expect(win.body.querySelector('.mail-row.unread')).toBeTruthy() }) + it('已读邮件无 unread 类', () => { mount(); expect(win.body.querySelectorAll('.mail-row:not(.unread)').length).toBeGreaterThanOrEqual(1) }) + it('星标邮件显示 ★', () => { mount(); expect(win.body.querySelector('.mail-star')).toBeTruthy() }) + it('按时间倒序排列', () => { mount(); const subjects = [...win.body.querySelectorAll('.mail-subject')].map((e: any) => e.textContent?.trim()); expect(subjects[0]).toContain('欢迎使用') }) // 最新 + }) + + // ==================== 选中邮件 ==================== + describe('选中邮件', () => { + it('点击未读邮件→标记已读+显示详情', async () => { mount(); const unread = win.body.querySelector('.mail-row.unread') as HTMLElement; unread.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(unread.classList.contains('unread')).toBe(false); expect(win.body.querySelector('.mail-detail-body')).toBeTruthy() }) + it('详情显示主题/发件人/时间', async () => { mount(); (win.body.querySelector('.mail-row') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-detail-subject')).toBeTruthy(); expect(win.body.querySelector('.mail-detail-meta')?.textContent).toContain('<') }) + it('详情有星标/删除/回复按钮', async () => { mount(); (win.body.querySelector('.mail-row') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); const btns = win.body.querySelectorAll('.mail-detail-actions .fb-btn, .mail-detail-actions .btn'); expect(btns.length).toBeGreaterThanOrEqual(2) }) + }) + + // ==================== 星标 ==================== + describe('星标', () => { + it('toggle 星标切换', async () => { mount(); (win.body.querySelector('.mail-row') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); const starBtn = win.body.querySelector('.mail-detail-actions .fb-btn') as HTMLElement; const before = starBtn.textContent; starBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(starBtn.textContent).not.toBe(before) }) + }) + + // ==================== 删除 ==================== + describe('删除', () => { + it('删除后从列表消失', async () => { mount(); (win.body.querySelector('.mail-row') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); const delBtn = win.body.querySelectorAll('.mail-detail-actions .fb-btn')[1] as HTMLElement; delBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-detail .empty-state')).toBeTruthy() }) + }) + + // ==================== 搜索 ==================== + describe('搜索', () => { + async function search(text: string) { const input = win.body.querySelector('.notes-search') as HTMLInputElement; input.value = text; input.dispatchEvent(new Event('input', { bubbles: true })); await new Promise(r => setTimeout(r, 250)); await nextTick() } + it('搜索"Apple"过滤出一封', async () => { mount(); await search('Apple'); expect(win.body.querySelectorAll('.mail-row').length).toBe(1) }) + it('搜索无匹配显示"没有邮件"', async () => { mount(); await search('zzz_no_match'); expect(win.body.querySelector('.empty-state')?.textContent).toContain('没有邮件') }) + }) + + // ==================== 写邮件 ==================== + describe('写邮件', () => { + it('✏️ 按钮打开弹窗', async () => { mount(); (win.body.querySelector('.fb-btn') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-compose')).toBeTruthy() }) + it('弹窗有收件人/主题/正文', async () => { mount(); (win.body.querySelector('.fb-btn') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelectorAll('.dlg-input').length).toBeGreaterThanOrEqual(2) }) + it('点击取消关闭弹窗', async () => { mount(); (win.body.querySelector('.fb-btn') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); const cancel = win.body.querySelector('.dlg-btns .btn') as HTMLElement; cancel.dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-compose')).toBeNull() }) + it('点击背景关闭弹窗', async () => { mount(); (win.body.querySelector('.fb-btn') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); (win.body.querySelector('.modal-mask') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-compose')).toBeNull() }) + it('空收件人发送被拒', async () => { mount(); (win.body.querySelector('.fb-btn') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); (win.body.querySelector('.btn.primary') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-compose')).toBeTruthy() }) // 弹窗仍在 + it('填写后发送→已发送+1', async () => { mount(); (win.body.querySelector('.fb-btn') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); const inputs = win.body.querySelectorAll('.dlg-input'); (inputs[0] as HTMLInputElement).value = 'test@test.com'; inputs[0].dispatchEvent(new Event('input', { bubbles: true })); (win.body.querySelector('.btn.primary') as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick(); expect(win.body.querySelector('.mail-compose')).toBeNull(); const data = store.get('mail', {}); expect(data.sent.length).toBe(1) }) + it('compose 函数通过 appState 暴露', () => { mount(); expect(typeof win.appState.compose).toBe('function') }) + }) + + // ==================== 卸载 ==================== + it('卸载后 DOM 为空', () => { mount(); render(null, win.body); expect(win.body.children.length).toBe(0) }) })