import { mount, flushPromises } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createPinia } from 'pinia' import { createMemoryHistory, createRouter } from 'vue-router' import UnreadPanel from '@/components/app/UnreadPanel.vue' import { useAuthStore } from '@/stores/auth' import { useUnreadStore } from '@/stores/unread' import type { UnreadSnapshot } from '@shared/unread' const snapshot: UnreadSnapshot = { total: 5, bot: 2, chat: 3, conversations: [ { mode: 'bot', id: 'bot-1', name: '开发助手', preview: '任务已完成', count: 2, updatedAt: 1, messages: [{ id: 'bot-message', seq: 1, taskId: 'task-1' }] }, { mode: 'chat', id: 'chat-1', profile: 'ops', name: '运维聊天', preview: '检查结果', count: 3, updatedAt: 2, messages: [{ id: 'chat-message', seq: 2 }] }, ], } const unread = useUnreadStore() async function mountPanel() { const pinia = createPinia() const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/conversations', component: { template: '
' } }, { path: '/conversations/:id', component: { template: '
' } }, { path: '/chat/:id', component: { template: '
' } }, ], }) await router.push('/conversations') await router.isReady() const selectProfile = vi.spyOn(useAuthStore(pinia), 'selectProfile').mockResolvedValue(undefined) const wrapper = mount(UnreadPanel, { attachTo: document.body, props: { anchor: { left: 372, top: 86 } }, global: { plugins: [pinia, router] }, }) return { wrapper, router, selectProfile } } beforeEach(() => { unread.reset() unread.snapshot = structuredClone(snapshot) }) afterEach(() => { vi.restoreAllMocks() unread.reset() }) function button(selector: string) { return document.querySelector(selector)! } describe('Unread center', () => { it('starts with all modes and marks only the selected mode as read', async () => { const read = vi.spyOn(unread, 'read').mockResolvedValue(undefined) const { wrapper } = await mountPanel() expect(document.querySelector('.unread-header p')?.textContent).toBe('5 条未读 · 2 个会话') expect(document.querySelectorAll('.unread-row')).toHaveLength(2) button('.unread-filters button:nth-child(2)').click() await flushPromises() expect(document.querySelector('.unread-header p')?.textContent).toBe('2 条未读 · 1 个会话') expect(document.querySelectorAll('.unread-row')).toHaveLength(1) button('.unread-clear').click() await flushPromises() expect(read).toHaveBeenCalledOnce() expect(read).toHaveBeenCalledWith([snapshot.conversations[0]]) wrapper.unmount() }) it('preserves unread message navigation without marking unseen messages as read', async () => { const read = vi.spyOn(unread, 'read').mockResolvedValue(undefined) const { wrapper, router, selectProfile } = await mountPanel() button('.unread-row').click() await flushPromises() expect(router.currentRoute.value.path).toBe('/conversations/bot-1') expect(router.currentRoute.value.query).toEqual({ taskId: 'task-1', unread: 'bot-message' }) expect(wrapper.emitted('close')).toHaveLength(1) expect(read).not.toHaveBeenCalled() document.querySelectorAll('.unread-row')[1]!.click() await flushPromises() expect(selectProfile).toHaveBeenCalledWith('ops') expect(router.currentRoute.value.path).toBe('/chat/chat-1') expect(router.currentRoute.value.query).toEqual({ profile: 'ops', unread: 'chat-message' }) expect(read).not.toHaveBeenCalled() wrapper.unmount() }) it('keeps keyboard focus inside the dialog', async () => { const { wrapper } = await mountPanel() const panel = document.querySelector('#unread-center')! expect(document.activeElement).toBe(panel) panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true, cancelable: true })) const last = [...panel.querySelectorAll('button')].at(-1)! expect(document.activeElement).toBe(last) last.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true })) expect(document.activeElement).toBe(button('.unread-close')) wrapper.unmount() }) it('prevents duplicate bulk reads and keeps errors visible for retry', async () => { let reject!: (reason: Error) => void const read = vi.spyOn(unread, 'read').mockImplementationOnce(() => new Promise((_, fail) => { reject = fail })) const { wrapper } = await mountPanel() button('.unread-clear').click() await flushPromises() expect(button('.unread-clear').disabled).toBe(true) expect(button('.unread-clear').textContent).toContain('正在同步') button('.unread-clear').click() expect(read).toHaveBeenCalledOnce() reject(new Error('同步失败,请重试')) await flushPromises() expect(document.querySelector('[role="alert"]')?.textContent).toBe('同步失败,请重试') expect(button('.unread-clear').disabled).toBe(false) unread.reset() await flushPromises() expect(document.querySelector('.unread-empty')?.textContent).toContain('暂无未读消息') expect(button('.unread-clear').disabled).toBe(true) wrapper.unmount() }) })