header-action.client.spec.tsx 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. // @vitest-environment jsdom
  2. import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import { useSyncExternalStore } from 'react'
  5. import type { SessionId } from '@deepseek-ai/dsh-session/types'
  6. import { SessionLogDownloadController } from '../src/client/controller.ts'
  7. import { SessionLogDownloadHeaderAction } from '../src/client/HeaderAction.tsx'
  8. import type { SessionLogDownloadDialogProps } from '../src/client/Dialog.tsx'
  9. import { en } from '../src/client/locales.ts'
  10. const SID = 'session-export-header' as SessionId
  11. function bindSessionExport(controller: SessionLogDownloadController) {
  12. return function useSessionLogDownload<T>(selector: (state: ReturnType<typeof controller.store.getSnapshot>) => T): T {
  13. return useSyncExternalStore(
  14. listener => controller.store.subscribe(listener),
  15. () => selector(controller.store.getSnapshot()),
  16. )
  17. }
  18. }
  19. function bench() {
  20. const controller = new SessionLogDownloadController(async () => new Response('zip'), vi.fn())
  21. const request = vi.fn((sessionId: SessionId) => controller.download(sessionId))
  22. const dismiss = vi.fn((sessionId: SessionId) => { controller.dismiss(sessionId) })
  23. const useSessionLogDownload = bindSessionExport(controller)
  24. const props = {
  25. sessionId: SID,
  26. useSessionLogDownload,
  27. request,
  28. dismiss,
  29. t: (key: keyof typeof en): string => en[key],
  30. } as unknown as SessionLogDownloadDialogProps
  31. const view = render(<SessionLogDownloadHeaderAction {...props} />)
  32. return { controller, request, view }
  33. }
  34. afterEach(cleanup)
  35. describe('Session export Header action', () => {
  36. it('opens the more-actions menu and downloads through the shared controller', async () => {
  37. const b = bench()
  38. const button = b.view.getByRole('button', { name: 'More actions' })
  39. expect(button.querySelector('svg')).not.toBeNull()
  40. expect(button.getAttribute('aria-expanded')).toBe('false')
  41. fireEvent.click(button)
  42. expect(button.getAttribute('aria-expanded')).toBe('true')
  43. fireEvent.click(b.view.getByRole('menuitem', { name: 'Download session log' }))
  44. await waitFor(() => { expect(b.request).toHaveBeenCalledWith(SID) })
  45. expect(b.view.queryByRole('menuitem', { name: 'Download session log' })).toBeNull()
  46. expect(await b.view.findByRole('dialog', { name: 'Session download started' })).toBeTruthy()
  47. })
  48. it('closes the menu on Escape without downloading', () => {
  49. const b = bench()
  50. fireEvent.click(b.view.getByRole('button', { name: 'More actions' }))
  51. expect(b.view.getByRole('menuitem', { name: 'Download session log' })).toBeTruthy()
  52. fireEvent.keyDown(document, { key: 'Escape' })
  53. expect(b.view.queryByRole('menuitem', { name: 'Download session log' })).toBeNull()
  54. expect(b.request).not.toHaveBeenCalled()
  55. })
  56. it('disables the download row while either entry path downloads this Session', async () => {
  57. const b = bench()
  58. let release!: (response: Response) => void
  59. const pending = new Promise<Response>((resolve) => { release = resolve })
  60. const controller = new SessionLogDownloadController(() => pending, vi.fn())
  61. const useSessionLogDownload = bindSessionExport(controller)
  62. b.view.rerender(<SessionLogDownloadHeaderAction {...({
  63. sessionId: SID,
  64. useSessionLogDownload,
  65. request: (sessionId: SessionId) => controller.download(sessionId),
  66. dismiss: (sessionId: SessionId) => { controller.dismiss(sessionId) },
  67. t: (key: keyof typeof en): string => en[key],
  68. } as unknown as SessionLogDownloadDialogProps)} />)
  69. const download = controller.download(SID)
  70. const button = b.view.getByRole('button', { name: 'More actions' })
  71. await waitFor(() => { expect(button.getAttribute('aria-busy')).toBe('true') })
  72. fireEvent.click(button)
  73. const item = b.view.getByRole('menuitem', { name: 'Download session log' })
  74. expect((item as HTMLButtonElement).disabled).toBe(true)
  75. release(new Response('zip'))
  76. await download
  77. await waitFor(() => { expect(button.getAttribute('aria-busy')).toBe('false') })
  78. expect((item as HTMLButtonElement).disabled).toBe(false)
  79. })
  80. })