conversationSearchDelete.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import { afterEach, describe, expect, it, vi } from 'vitest'
  2. import { flushPromises, shallowMount, type VueWrapper } from '@vue/test-utils'
  3. import { shallowRef } from 'vue'
  4. import { createMemoryHistory, createRouter, matchedRouteKey } from 'vue-router'
  5. import ConversationsView from '../../src/client/views/ConversationsView.vue'
  6. import { apiRequest } from '../../src/client/api/client'
  7. vi.mock('../../src/client/api/client', () => ({ apiRequest: vi.fn(), ApiError: class extends Error {} }))
  8. vi.mock('../../src/client/stores/auth', () => ({ useAuthStore: () => ({ user: { id: 'owner' }, profiles: [], refreshProfileAvatars: async () => {} }) }))
  9. vi.mock('../../src/client/stores/theme', () => ({ useThemeStore: () => ({}) }))
  10. let wrapper: VueWrapper | undefined
  11. afterEach(() => { wrapper?.unmount(); wrapper = undefined; vi.restoreAllMocks(); vi.mocked(apiRequest).mockReset() })
  12. async function setup(kind: 'direct' | 'group', remove: () => Promise<void> = async () => {}) {
  13. let archived = [{ id: 'archived', name: '旧聊天', kind, archived: true, memberIds: ['bot'], updatedAt: 1 }]
  14. const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/conversations/:id?', component: { template: '<div />' } }] })
  15. await router.push('/conversations'); await router.isReady()
  16. vi.mocked(apiRequest).mockImplementation(async (path, options) => {
  17. if (options?.method === 'DELETE') { await remove(); archived = []; return {} as never }
  18. if (path === '/api/app/agents') return { agents: [{ id: 'bot', name: '旧聊天', archived: true }] } as never
  19. if (path === '/api/app/conversations') return { conversations: archived, cursor: 1 } as never
  20. if (path.startsWith('/api/app/events')) return { events: [], cursor: 1 } as never
  21. return {} as never
  22. })
  23. wrapper = shallowMount(ConversationsView, { attachTo: document.body, global: {
  24. provide: { [matchedRouteKey as symbol]: shallowRef(router.currentRoute.value.matched[0]) },
  25. plugins: [router], stubs: { WorkspaceShell: { template: '<div><slot /></div>' }, FloatingResourceSearch: false, Teleport: false },
  26. } })
  27. await flushPromises()
  28. document.dispatchEvent(new CustomEvent('hermes-yaoyao:sidebar-search', { detail: { section: 'groups' } }))
  29. await flushPromises()
  30. expect(document.querySelector('.search-delete')).toBeNull()
  31. document.querySelector<HTMLButtonElement>('[role="tab"][aria-label="已归档"]')!.click()
  32. await flushPromises()
  33. return router
  34. }
  35. describe('archived chat deletion from search', () => {
  36. it('keeps the archived result and route when confirmation is cancelled', async () => {
  37. const router = await setup('direct')
  38. const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false)
  39. document.querySelector<HTMLButtonElement>('.search-delete')!.click()
  40. await flushPromises()
  41. expect(confirm).toHaveBeenCalledWith(expect.stringContaining('永久删除 Bot「旧聊天」'))
  42. expect(vi.mocked(apiRequest).mock.calls.some(([, options]) => options?.method === 'DELETE')).toBe(false)
  43. expect(router.currentRoute.value.path).toBe('/conversations')
  44. expect(document.querySelector('[role="dialog"]')?.textContent).toContain('旧聊天')
  45. })
  46. it.each([['direct', '/api/app/agents/bot'], ['group', '/api/app/conversations/archived']] as const)('deletes an archived %s through its own endpoint and updates the open search', async (kind, endpoint) => {
  47. await setup(kind)
  48. vi.spyOn(window, 'confirm').mockReturnValue(true)
  49. const button = document.querySelector<HTMLButtonElement>('.search-delete')!
  50. button.focus(); button.click()
  51. await flushPromises()
  52. expect(apiRequest).toHaveBeenCalledWith(endpoint, { method: 'DELETE' })
  53. expect(document.querySelector('[role="dialog"]')?.textContent).toContain('没有已归档聊天')
  54. expect(document.querySelector('[role="tab"][aria-label="已归档"]')?.textContent).toContain('0')
  55. expect(document.activeElement).toBe(document.querySelector('[role="dialog"] input'))
  56. })
  57. it('disables repeated deletion and keeps server errors inside search', async () => {
  58. let reject!: (error: Error) => void
  59. await setup('direct', () => new Promise<void>((_, fail) => { reject = fail }))
  60. vi.spyOn(window, 'confirm').mockReturnValue(true)
  61. const button = document.querySelector<HTMLButtonElement>('.search-delete')!
  62. button.click(); await flushPromises()
  63. expect(button.disabled).toBe(true)
  64. button.click()
  65. expect(vi.mocked(apiRequest).mock.calls.filter(([, options]) => options?.method === 'DELETE')).toHaveLength(1)
  66. reject(new Error('此 Bot 仍是群聊成员'))
  67. await flushPromises()
  68. expect(document.querySelector('[role="dialog"] [role="alert"]')?.textContent).toBe('此 Bot 仍是群聊成员')
  69. expect(button.disabled).toBe(false)
  70. expect(document.querySelector('[role="dialog"]')?.textContent).toContain('旧聊天')
  71. })
  72. })