feat: 重构 contacts — Vue 组件化(列表/搜索/详情/新建编辑删除弹窗)+ 27 测试
This commit is contained in:
parent
9b233a0c86
commit
6611877c31
@ -1 +1,157 @@
|
||||
<template><div></div></template>
|
||||
<template>
|
||||
<div class="contacts-body">
|
||||
<div class="notes-side">
|
||||
<div class="notes-side-bar">
|
||||
<input class="text-input notes-search" type="search" placeholder="搜索" v-model="query" @input="onSearch">
|
||||
<button class="fb-btn" title="新建联系人" @click="editContact(null)">+</button>
|
||||
</div>
|
||||
<div class="notes-list">
|
||||
<div v-if="!filtered.length" class="empty-state" style="height:120px">没有联系人</div>
|
||||
<div
|
||||
v-for="c in filtered" :key="c.id"
|
||||
class="note-row contact-row" :class="{ sel: cur === c }"
|
||||
@click="selectContact(c)"
|
||||
>
|
||||
<span class="contact-avatar">{{ c.name.slice(0, 1) }}</span>
|
||||
<div>
|
||||
<div class="note-title">{{ c.name }}</div>
|
||||
<div class="note-sub">{{ c.phone }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-detail">
|
||||
<template v-if="!cur">
|
||||
<div class="empty-state">
|
||||
<div class="es-icon">👤</div>
|
||||
<div>选择联系人</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="contact-card">
|
||||
<div class="contact-big-avatar">{{ cur.name.slice(0, 1) }}</div>
|
||||
<div class="contact-name">{{ cur.name }}</div>
|
||||
<div v-if="cur.note" class="contact-note">{{ cur.note }}</div>
|
||||
<div class="contact-fields">
|
||||
<div class="contact-field">
|
||||
<span class="cf-label">电话</span>
|
||||
<span>{{ cur.phone }}</span>
|
||||
</div>
|
||||
<div class="contact-field">
|
||||
<span class="cf-label">邮箱</span>
|
||||
<span>{{ cur.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
<button class="btn" @click="editContact(cur)">编辑</button>
|
||||
<button class="btn danger" @click="deleteContact(cur)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<div v-if="editing !== undefined" class="modal-mask" @click.self="closeEdit">
|
||||
<div class="dialog" style="width:300px">
|
||||
<div class="dlg-title">{{ editing ? '编辑联系人' : '新建联系人' }}</div>
|
||||
<input ref="editNameRef" class="text-input dlg-input" placeholder="姓名" v-model="editData.name">
|
||||
<input class="text-input dlg-input" placeholder="电话" v-model="editData.phone">
|
||||
<input class="text-input dlg-input" placeholder="邮箱" v-model="editData.email">
|
||||
<input class="text-input dlg-input" placeholder="备注" v-model="editData.note">
|
||||
<div class="dlg-btns row">
|
||||
<button class="btn" @click="closeEdit">取消</button>
|
||||
<button class="btn primary" @click="saveContact">存储</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick } from 'vue'
|
||||
import { uid } from '../../utils'
|
||||
import { store } from '../../composables/useStore'
|
||||
|
||||
const props = defineProps<{ win: any }>()
|
||||
|
||||
const DEFAULT_CONTACTS = [
|
||||
{ id: uid(), name: '王小明', phone: '138 0000 1234', email: 'xiaoming@example.com', note: '大学同学' },
|
||||
{ id: uid(), name: '李思颖', phone: '139 1111 5678', email: 'siying.li@example.com', note: '产品经理' },
|
||||
{ id: uid(), name: '张伟', phone: '137 2222 9012', email: 'zhangwei@example.com', note: '' },
|
||||
{ id: uid(), name: '陈静', phone: '136 3333 3456', email: 'chenjing@example.com', note: '设计团队' },
|
||||
{ id: uid(), name: '刘洋', phone: '135 4444 7890', email: 'liuyang@example.com', note: '周末球友' },
|
||||
]
|
||||
|
||||
const data = reactive(store.get('contacts', DEFAULT_CONTACTS))
|
||||
const cur = ref<any>(null)
|
||||
const query = ref('')
|
||||
const queryDebounced = ref('')
|
||||
let debounceTimer: any = null
|
||||
|
||||
const editing = ref<any>(undefined)
|
||||
const editData = reactive({ id: '', name: '', phone: '', email: '', note: '' })
|
||||
const editNameRef = ref<HTMLInputElement>()
|
||||
|
||||
function persist() { store.set('contacts', data) }
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = queryDebounced.value.toLowerCase()
|
||||
return data
|
||||
.filter((c: any) => !q || (c.name + c.phone + c.email).toLowerCase().includes(q))
|
||||
.sort((a: any, b: any) => a.name.localeCompare(b.name, 'zh-Hans-CN'))
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => { queryDebounced.value = query.value.trim() }, 200)
|
||||
}
|
||||
|
||||
function selectContact(c: any) { cur.value = c }
|
||||
|
||||
function editContact(c: any | null) {
|
||||
if (c) {
|
||||
Object.assign(editData, { id: c.id, name: c.name, phone: c.phone, email: c.email, note: c.note })
|
||||
editing.value = c
|
||||
} else {
|
||||
Object.assign(editData, { id: uid(), name: '', phone: '', email: '', note: '' })
|
||||
editing.value = null
|
||||
}
|
||||
nextTick(() => editNameRef.value?.focus())
|
||||
}
|
||||
|
||||
function closeEdit() { editing.value = undefined }
|
||||
|
||||
function saveContact() {
|
||||
if (!editData.name.trim()) { editNameRef.value?.focus(); return }
|
||||
const payload = {
|
||||
id: editData.id,
|
||||
name: editData.name.trim(),
|
||||
phone: editData.phone.trim(),
|
||||
email: editData.email.trim(),
|
||||
note: editData.note.trim(),
|
||||
}
|
||||
if (editing.value) {
|
||||
const idx = data.findIndex((c: any) => c === editing.value)
|
||||
if (idx !== -1) Object.assign(data[idx], payload)
|
||||
cur.value = data[idx]
|
||||
} else {
|
||||
data.push(payload)
|
||||
cur.value = payload
|
||||
}
|
||||
persist()
|
||||
closeEdit()
|
||||
}
|
||||
|
||||
async function deleteContact(c: any) {
|
||||
const ui = (window as any).ui
|
||||
if (ui) {
|
||||
const confirmed = await ui.confirm('删除联系人?', `将删除"${c.name}"。`, { ok: '删除', danger: true })
|
||||
if (!confirmed) return
|
||||
}
|
||||
data.splice(data.indexOf(c), 1)
|
||||
if (cur.value === c) cur.value = null
|
||||
persist()
|
||||
}
|
||||
|
||||
defineExpose({ editContact })
|
||||
</script>
|
||||
|
||||
@ -1,26 +1,20 @@
|
||||
// 通讯录应用 — Vue 组件化
|
||||
import { h, render } from 'vue'
|
||||
// 通讯录应用
|
||||
import { el, uid, debounce } from '../../utils'
|
||||
import { store as Store } from '../../composables/useStore'
|
||||
import { ui as UI } from '../../composables/useUI'
|
||||
import { Apps, stdMenus } from '../../composables/useApps'
|
||||
import ContactsComponent from './Contacts.vue'
|
||||
|
||||
export const ContactsApp = {
|
||||
id: 'contacts', name: '通讯录', icon: '/assets/icons/contacts.png',
|
||||
w: 760, h: 520, minW: 560, minH: 360,
|
||||
menus(win: any) { return stdMenus(this, { file: [{ label: '新建联系人', key: '⌘N', action: () => win?.appState?.edit(null) }] }) },
|
||||
store: { get() { return Store.get('contacts', [{ id:uid(),name:'王小明',phone:'138 0000 1234',email:'xiaoming@example.com',note:'大学同学' },{ id:uid(),name:'李思颖',phone:'139 1111 5678',email:'siying.li@example.com',note:'产品经理' },{ id:uid(),name:'张伟',phone:'137 2222 9012',email:'zhangwei@example.com',note:'' },{ id:uid(),name:'陈静',phone:'136 3333 3456',email:'chenjing@example.com',note:'设计团队' },{ id:uid(),name:'刘洋',phone:'135 4444 7890',email:'liuyang@example.com',note:'周末球友' }]) }, set(v:any) { Store.set('contacts',v) } },
|
||||
menus(win: any) {
|
||||
return stdMenus(this, {
|
||||
file: [{ label: '新建联系人', key: '⌘N', action: () => win?.appState?.editContact?.(null) }]
|
||||
})
|
||||
},
|
||||
render(win: any) {
|
||||
const st = win.appState = { data: this.store.get(), cur: null as any, q: '' }; win.body.classList.add('contacts-body')
|
||||
const search = el('input',{class:'text-input notes-search',type:'search',placeholder:'搜索'}), addBtn = el('button',{class:'fb-btn',html:'+',title:'新建联系人'}), listEl = el('div',{class:'notes-list'}), detail = el('div',{class:'contact-detail'})
|
||||
win.body.append(el('div',{class:'notes-side'},el('div',{class:'notes-side-bar'},search,addBtn),listEl),detail)
|
||||
const save = () => this.store.set(st.data)
|
||||
const renderList = () => { listEl.innerHTML=''; const items = st.data.filter((c:any)=>!st.q||(c.name+c.phone+c.email).toLowerCase().includes(st.q.toLowerCase())).sort((a:any,b:any)=>a.name.localeCompare(b.name,'zh-Hans-CN')); if(!items.length)listEl.append(el('div',{class:'empty-state',style:{height:'120px'},text:'没有联系人'})); items.forEach((c:any)=>{const row=el('div',{class:'note-row contact-row'+(c===st.cur?' sel':'')},el('span',{class:'contact-avatar',text:c.name.slice(0,1)}),el('div',null,el('div',{class:'note-title',text:c.name}),el('div',{class:'note-sub',text:c.phone})));row.addEventListener('click',()=>{st.cur=c;renderDetail()});listEl.append(row)}) }
|
||||
const renderDetail = () => { renderList(); detail.innerHTML=''; if(!st.cur){detail.append(el('div',{class:'empty-state'},el('div',{class:'es-icon',text:'👤'}),el('div',{text:'选择联系人'})));return}; const c=st.cur; detail.append(el('div',{class:'contact-card'},el('div',{class:'contact-big-avatar',text:c.name.slice(0,1)}),el('div',{class:'contact-name',text:c.name}),c.note?el('div',{class:'contact-note',text:c.note}):null,el('div',{class:'contact-fields'},el('div',{class:'contact-field'},el('span',{class:'cf-label',text:'电话'}),el('span',{text:c.phone})),el('div',{class:'contact-field'},el('span',{class:'cf-label',text:'邮箱'}),el('span',{text:c.email}))),el('div',{class:'contact-actions'},el('button',{class:'btn',text:'编辑',onclick:()=>st.edit(c)}),el('button',{class:'btn danger',text:'删除',onclick:async()=>{if(!await UI.confirm('删除联系人?',`将删除"${c.name}"。`,{ok:'删除',danger:true}))return;st.data=st.data.filter((x:any)=>x!==c);st.cur=null;save();renderDetail()}})))) }
|
||||
st.edit = (c:any) => { const isNew=!c; const data=c?{...c}:{id:uid(),name:'',phone:'',email:'',note:''}; const mask=el('div',{class:'modal-mask'}); const nameIn=el('input',{class:'text-input dlg-input',placeholder:'姓名',value:data.name}),phoneIn=el('input',{class:'text-input dlg-input',placeholder:'电话',value:data.phone}),emailIn=el('input',{class:'text-input dlg-input',placeholder:'邮箱',value:data.email}),noteIn=el('input',{class:'text-input dlg-input',placeholder:'备注',value:data.note}); const dlg=el('div',{class:'dialog',style:{width:'300px'}},el('div',{class:'dlg-title',text:isNew?'新建联系人':'编辑联系人'}),nameIn,phoneIn,emailIn,noteIn,el('div',{class:'dlg-btns row'},el('button',{class:'btn',text:'取消',onclick:()=>mask.remove()}),el('button',{class:'btn primary',text:'存储',onclick:()=>{if(!(nameIn as HTMLInputElement).value.trim()){(nameIn as HTMLInputElement).focus();return};Object.assign(data,{name:(nameIn as HTMLInputElement).value.trim(),phone:(phoneIn as HTMLInputElement).value.trim(),email:(emailIn as HTMLInputElement).value.trim(),note:(noteIn as HTMLInputElement).value.trim()});if(isNew)st.data.push(data);else Object.assign(c,data);st.cur=data;save();mask.remove();renderDetail()}})));mask.append(dlg);document.body.append(mask);(nameIn as HTMLInputElement).focus() }
|
||||
search.addEventListener('input',debounce(()=>{st.q=(search as HTMLInputElement).value.trim();renderList()},200)); addBtn.addEventListener('click',()=>st.edit(null)); renderDetail()
|
||||
}
|
||||
const vnode = h(ContactsComponent, { win })
|
||||
render(vnode, win.body)
|
||||
},
|
||||
}
|
||||
Apps.register(ContactsApp);
|
||||
(window as any).ContactsApp = ContactsApp
|
||||
Apps.register(ContactsApp)
|
||||
;(window as any).ContactsApp = ContactsApp
|
||||
|
||||
299
tests/cases/32-contacts.test.ts
Normal file
299
tests/cases/32-contacts.test.ts
Normal file
@ -0,0 +1,299 @@
|
||||
/**
|
||||
* 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('自定义')
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user