queue-dock.spec.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. // @vitest-environment jsdom
  2. /**
  3. * QueueDock rendering (web input-triggers queue cut 1): empty queue renders
  4. * nothing, rows render one preview line each keyed by rpcId, and the strip
  5. * follows queue changes through the useSession selector.
  6. */
  7. import { afterEach, describe, expect, it } from 'vitest'
  8. import { act, cleanup, render } from '@testing-library/react'
  9. import { useSyncExternalStore } from 'react'
  10. import type { ConversationSnapshot, QueuedMessage, SessionId, SessionListState } from '@deepseek-ai/dsh-client-runtime/client'
  11. import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
  12. import type { InputState } from '../src/client/input/contract.ts'
  13. import { QueueDock, queueDockEntry } from '../src/client/queue/QueueDock.tsx'
  14. afterEach(cleanup)
  15. const SID = 's1' as SessionId
  16. function snapshotWith(queue: QueuedMessage[]): ConversationSnapshot {
  17. return {
  18. sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
  19. pending: [], queue, running: true, composerPhase: 'active', removed: false, openState: 'open', openError: null,
  20. hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
  21. }
  22. }
  23. /** Minimal live source backing the useSession stub (queue swaps notify subscribers). */
  24. function liveSession(initial: ConversationSnapshot) {
  25. let snapshot = initial
  26. const listeners = new Set<() => void>()
  27. const useSession: SnapshotSelectorHook<ConversationSnapshot> = sel =>
  28. useSyncExternalStore(
  29. (fn) => {
  30. listeners.add(fn)
  31. return () => listeners.delete(fn)
  32. },
  33. () => sel(snapshot),
  34. )
  35. return {
  36. useSession,
  37. push(next: ConversationSnapshot): void {
  38. snapshot = next
  39. for (const fn of [...listeners]) fn()
  40. },
  41. }
  42. }
  43. /** InputZone owner stub (the dock reads useSession only; the zone fields satisfy the owner share). */
  44. const INPUT_STATE: InputState = { draft: '', draftRev: 0, phase: 'plain', occurrences: [], queue: [] }
  45. function kitFor(snapshot: ConversationSnapshot) {
  46. return {
  47. sessionId: SID,
  48. useSessions: (() => { throw new Error('unused') }) as unknown as SnapshotSelectorHook<SessionListState>,
  49. useWorkspaces: (() => { throw new Error('unused') }) as never,
  50. useProjection: (() => undefined) as never,
  51. useInput: (() => { throw new Error('unused') }) as never,
  52. inputActions: { setDraft: () => {}, submit: () => {} } as never,
  53. session: snapshot,
  54. input: INPUT_STATE,
  55. }
  56. }
  57. describe('QueueDock', () => {
  58. it('renders null while the queue is empty', () => {
  59. const snap = snapshotWith([])
  60. const source = liveSession(snap)
  61. const { container } = render(<QueueDock {...kitFor(snap)} useSession={source.useSession} />)
  62. expect(container.innerHTML).toBe('')
  63. })
  64. it('renders one preview row per queued message with the count strip', () => {
  65. const snap = snapshotWith([
  66. { key: 'p-1', preview: '第一条排队消息' },
  67. { key: 'p-2', preview: 'second queued line' },
  68. ])
  69. const source = liveSession(snap)
  70. const { container } = render(<QueueDock {...kitFor(snap)} useSession={source.useSession} />)
  71. expect(container.textContent).toContain('已排队 2 条')
  72. const rows = [...container.querySelectorAll('li')]
  73. expect(rows.map(r => r.textContent)).toEqual(['第一条排队消息', 'second queued line'])
  74. })
  75. it('follows queue changes: retirement empties the strip back to null', () => {
  76. const snap = snapshotWith([{ key: 'p-1', preview: '在场' }])
  77. const source = liveSession(snap)
  78. const { container } = render(<QueueDock {...kitFor(snap)} useSession={source.useSession} />)
  79. expect(container.textContent).toContain('在场')
  80. act(() => { source.push(snapshotWith([])) })
  81. expect(container.innerHTML).toBe('')
  82. })
  83. it('ships the registrant plugin shape (list entry into conversation.input.dock)', () => {
  84. // Registration itself runs under T5's slot declaration; here we pin the
  85. // frozen registration surface so the wiring layer can mount it verbatim.
  86. expect(queueDockEntry.name).toBe('conversation-queue-dock')
  87. expect(queueDockEntry.inject).toEqual(['slots', 'conversation'])
  88. expect(typeof queueDockEntry.apply).toBe('function')
  89. })
  90. })