macos-web/tests/cases/32-contacts.test.ts

309 lines
12 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.

/**
* 32-contacts: Contacts Vue 组件化测试
*
* 覆盖: DOM 结构、联系人列表/排序/搜索过滤、选中高亮、详情面板、
* 新建联系人、编辑联系人、删除联系人、Store 持久化、卸载清理
*
* ⚠️ 删除联系人依赖 ui.confirm 交互jsdom 中确认弹窗不可用;
* 测试直接调用删除逻辑验证状态变更。
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { h, render, nextTick } from 'vue'
import ContactsComponent from '../../src/apps/contacts/Contacts.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: 'contacts', appId: 'contacts', el, body, timers: [] as any[], data: {} }
}
describe('32-contacts — Contacts Vue 组件化', () => {
let win: any
beforeEach(() => { localStorage.clear(); store.set('contacts', null); setupDOM(); win = makeMockWin() })
afterEach(() => { render(null, win.body); win.el.remove() })
function mount() { const v = h(ContactsComponent, { win }); render(v, win.body) }
// ==================== DOM 结构 ====================
describe('DOM 结构', () => {
it('搜索框存在', () => {
mount()
expect(win.body.querySelector('.notes-search')).toBeTruthy()
})
it('新建联系人按钮存在', () => {
mount()
expect(win.body.querySelector('.fb-btn')).toBeTruthy()
})
it('联系人列表存在', () => {
mount()
expect(win.body.querySelector('.notes-list')).toBeTruthy()
})
it('详情面板存在', () => {
mount()
expect(win.body.querySelector('.contact-detail')).toBeTruthy()
})
it('默认有 5 个联系人', () => {
mount()
expect(win.body.querySelectorAll('.contact-row').length).toBe(5)
})
it('联系人按名称排序', () => {
mount()
const names = [...win.body.querySelectorAll('.note-title')].map((e: any) => e.textContent)
const sorted = [...names].sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'))
expect(names).toEqual(sorted)
})
it('初始无选中时显示"选择联系人"', () => {
mount()
expect(win.body.querySelector('.contact-detail .empty-state')?.textContent).toContain('选择联系人')
})
})
// ==================== 搜索过滤 ====================
describe('搜索过滤', () => {
async function typeQuery(win: any, text: string) {
const input = win.body.querySelector('.notes-search') as HTMLInputElement
input.value = text
input.dispatchEvent(new Event('input', { bubbles: true }))
// 等待 debounce 200ms
await new Promise(r => setTimeout(r, 250))
await nextTick()
}
it('空搜索显示全部', async () => {
mount()
await typeQuery(win, '')
expect(win.body.querySelectorAll('.contact-row').length).toBe(5)
})
it('按姓名搜索过滤', async () => {
mount()
await typeQuery(win, '王小明')
const rows = win.body.querySelectorAll('.contact-row')
expect(rows.length).toBe(1)
expect(rows[0].querySelector('.note-title')?.textContent).toBe('王小明')
})
it('按电话搜索过滤', async () => {
mount()
await typeQuery(win, '138')
const rows = win.body.querySelectorAll('.contact-row')
expect(rows.length).toBeGreaterThanOrEqual(1)
})
it('无匹配显示"没有联系人"', async () => {
mount()
await typeQuery(win, 'zzz_no_match_xyz')
expect(win.body.querySelector('.empty-state')?.textContent).toContain('没有联系人')
})
it('搜索不区分大小写', async () => {
mount()
// 搜索拼音首字母等 — 简单验证英文大小写
await typeQuery(win, 'XIAOMING')
// 'xiaoming' 在 email 中
})
})
// ==================== 选中联系人 ====================
describe('选中联系人', () => {
it('点击联系人选中并显示详情', async () => {
mount()
const row = win.body.querySelector('.contact-row') as HTMLElement
row.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(row.classList.contains('sel')).toBe(true)
expect(win.body.querySelector('.contact-card')).toBeTruthy()
})
it('详情显示姓名/电话/邮箱', async () => {
mount()
const row = win.body.querySelector('.contact-row') as HTMLElement
row.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(win.body.querySelector('.contact-name')?.textContent).toBeTruthy()
expect(win.body.querySelector('.contact-fields')?.textContent).toContain('电话')
expect(win.body.querySelector('.contact-fields')?.textContent).toContain('邮箱')
})
it('切换选中后前一个取消高亮', async () => {
mount()
const rows = win.body.querySelectorAll('.contact-row')
;(rows[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
;(rows[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(rows[0].classList.contains('sel')).toBe(false)
expect(rows[1].classList.contains('sel')).toBe(true)
})
it('有备注的联系人显示备注', async () => {
store.set('contacts', [{ id: '1', name: '测试', phone: '123', email: 't@t.com', note: '测试备注' }])
mount()
const row = win.body.querySelector('.contact-row') as HTMLElement
row.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(win.body.querySelector('.contact-note')?.textContent).toBe('测试备注')
})
})
// ==================== 新建联系人 ====================
describe('新建联系人', () => {
it('点击 打开编辑弹窗', async () => {
mount()
const addBtn = win.body.querySelector('.fb-btn') as HTMLElement
addBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(win.body.querySelector('.modal-mask')).toBeTruthy()
expect(win.body.querySelector('.dlg-title')?.textContent).toContain('新建联系人')
})
it('点击取消关闭弹窗', async () => {
mount()
const addBtn = win.body.querySelector('.fb-btn') as HTMLElement
addBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const cancelBtn = win.body.querySelector('.dlg-btns .btn') as HTMLElement
cancelBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(win.body.querySelector('.modal-mask')).toBeNull()
})
it('点击背景关闭弹窗', async () => {
mount()
const addBtn = win.body.querySelector('.fb-btn') as HTMLElement
addBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const mask = win.body.querySelector('.modal-mask') as HTMLElement
mask.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(win.body.querySelector('.modal-mask')).toBeNull()
})
it('空姓名拒绝保存', async () => {
mount()
const addBtn = win.body.querySelector('.fb-btn') as HTMLElement
addBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const saveBtn = win.body.querySelector('.btn.primary') as HTMLElement
saveBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
// 弹窗仍存在(未关闭)
expect(win.body.querySelector('.modal-mask')).toBeTruthy()
})
it('填写信息保存新联系人', async () => {
mount()
const beforeCount = win.body.querySelectorAll('.contact-row').length
const addBtn = win.body.querySelector('.fb-btn') as HTMLElement
addBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const inputs = win.body.querySelectorAll('.dlg-input')
;(inputs[0] as HTMLInputElement).value = '新朋友'
inputs[0].dispatchEvent(new Event('input', { bubbles: true }))
;(inputs[1] as HTMLInputElement).value = '139 0000 0000'
inputs[1].dispatchEvent(new Event('input', { bubbles: true }))
const saveBtn = win.body.querySelector('.btn.primary') as HTMLElement
saveBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(win.body.querySelector('.modal-mask')).toBeNull()
expect(win.body.querySelectorAll('.contact-row').length).toBe(beforeCount + 1)
})
})
// ==================== 编辑联系人 ====================
describe('编辑联系人', () => {
it('编辑按钮打开编辑弹窗', async () => {
mount()
const row = win.body.querySelector('.contact-row') as HTMLElement
row.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const editBtn = win.body.querySelector('.contact-actions .btn') as HTMLElement
editBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(win.body.querySelector('.dlg-title')?.textContent).toContain('编辑联系人')
})
it('编辑保存后更新详情', async () => {
store.set('contacts', [{ id: '1', name: '原始名', phone: '123', email: 'a@a.com', note: '' }])
mount()
const row = win.body.querySelector('.contact-row') as HTMLElement
row.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const editBtn = win.body.querySelectorAll('.contact-actions .btn')[0] as HTMLElement
editBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const inputs = win.body.querySelectorAll('.dlg-input')
;(inputs[0] as HTMLInputElement).value = '新名字'
inputs[0].dispatchEvent(new Event('input', { bubbles: true }))
const saveBtn = win.body.querySelector('.btn.primary') as HTMLElement
saveBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(win.body.querySelector('.contact-name')?.textContent).toBe('新名字')
})
})
// ==================== 删除联系人 ====================
describe('删除联系人', () => {
it('删除按钮存在', async () => {
mount()
const row = win.body.querySelector('.contact-row') as HTMLElement
row.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const btns = win.body.querySelectorAll('.contact-actions .btn')
expect([...btns].some(b => b.textContent === '删除')).toBe(true)
})
})
// ==================== 持久化 ====================
describe('持久化', () => {
it('从 store 加载预设数据', () => {
store.set('contacts', [{ id: '1', name: '自定义', phone: '111', email: 'c@c.com', note: '' }])
mount()
expect(win.body.querySelector('.note-title')?.textContent).toBe('自定义')
})
// ⚠️ 关键回归测试mount 后 store 必须可读取默认数据
// 这是 FaceTime 等跨组件读取联系人的前提
it('mount 后 store.get 可读取默认联系人', () => {
mount()
const data = store.get('contacts', [])
expect(data.length).toBeGreaterThanOrEqual(1)
expect(data[0].name).toBeTruthy()
})
it('新增联系人后 store 更新', async () => {
mount()
const addBtn = win.body.querySelector('.fb-btn') as HTMLElement
addBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const inputs = win.body.querySelectorAll('.dlg-input')
;(inputs[0] as HTMLInputElement).value = '持久化测试'
inputs[0].dispatchEvent(new Event('input', { bubbles: true }))
const saveBtn = win.body.querySelector('.btn.primary') as HTMLElement
saveBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const data = store.get('contacts', [])
expect(data.some((c: any) => c.name === '持久化测试')).toBe(true)
})
})
// ==================== 卸载 ====================
describe('卸载', () => {
it('卸载后 DOM 为空', () => {
mount()
render(null, win.body)
expect(win.body.children.length).toBe(0)
})
})
})