helpers.client.spec.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. // @vitest-environment jsdom
  2. import { act, cleanup, renderHook } from '@testing-library/react'
  3. import type { SessionEventEntry } from '@deepseek-ai/dsh-api-session-controller/types'
  4. import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
  5. import { EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-ui-chat/client'
  6. import { EMPTY_CONVERSATION_SNAPSHOT } from '@deepseek-ai/dsh-client-ui-conversation/client'
  7. import type { SessionId } from '@deepseek-ai/dsh-session/types'
  8. import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'
  9. import {
  10. bindSnapshotSelector,
  11. chatSnapshot,
  12. conversationSnapshot,
  13. SlotTestRuntime,
  14. usePinnedBrowserLanguages,
  15. } from '../src/index.ts'
  16. const originalLanguages = [...navigator.languages]
  17. const originalLanguage = navigator.language
  18. usePinnedBrowserLanguages('zh-CN', 'en-US')
  19. afterEach(cleanup)
  20. afterAll(() => {
  21. expect(navigator.languages).toEqual(originalLanguages)
  22. expect(navigator.language).toBe(originalLanguage)
  23. })
  24. function entry(seq: number): SessionEventEntry {
  25. return {
  26. event: {
  27. type: 'fixture/event',
  28. seq,
  29. time: seq,
  30. data: { seq },
  31. ignorable: true,
  32. },
  33. }
  34. }
  35. describe('fixture helpers', () => {
  36. it('builds independent Conversation and Chat snapshots with optional overrides', () => {
  37. const conversation = conversationSnapshot()
  38. expect(conversation).toEqual(EMPTY_CONVERSATION_SNAPSHOT)
  39. expect(conversation).not.toBe(EMPTY_CONVERSATION_SNAPSHOT)
  40. const activeTargets = new Set(['chat'])
  41. expect(conversationSnapshot({ activeTargets }).activeTargets).toBe(activeTargets)
  42. const chat = chatSnapshot()
  43. expect(chat).toEqual(EMPTY_CHAT_SNAPSHOT)
  44. expect(chat).not.toBe(EMPTY_CHAT_SNAPSHOT)
  45. const order = ['node-1']
  46. expect(chatSnapshot({ order }).order).toBe(order)
  47. })
  48. it('binds an observable snapshot through the production selector hook', () => {
  49. const source = createSnapshotStore({ value: 1 })
  50. const useValue = bindSnapshotSelector(source)
  51. const view = renderHook(() => useValue(snapshot => snapshot.value))
  52. expect(view.result.current).toBe(1)
  53. act(() => { source.update((draft) => { draft.value = 2 }) })
  54. expect(view.result.current).toBe(2)
  55. })
  56. it('pins both browser language fields for the calling suite', () => {
  57. expect(navigator.languages).toEqual(['zh-CN', 'en-US'])
  58. expect(navigator.language).toBe('zh-CN')
  59. })
  60. })
  61. describe('Session fixture lifecycle', () => {
  62. it('initializes and drives complete event windows through replace, prepend, and append', async () => {
  63. const runtime = await SlotTestRuntime.create()
  64. const first = entry(1)
  65. const older = entry(0)
  66. const live = entry(2)
  67. await runtime.sessions.add({ id: 'events', events: [first] }, { current: false })
  68. expect(runtime.sessions.behavior('events').eventSource.getSnapshot()).toMatchObject({
  69. entries: [first],
  70. hasMore: false,
  71. change: { kind: 'replace', entries: [first] },
  72. })
  73. await runtime.sessions.add({ id: 'has-more', hasMore: true }, { current: false })
  74. expect(runtime.sessions.behavior('has-more').eventSource.getSnapshot()).toMatchObject({
  75. entries: [],
  76. hasMore: true,
  77. })
  78. await runtime.sessions.replaceEvents('events', [first])
  79. await runtime.sessions.prependEvents('events', [older])
  80. await runtime.sessions.appendEvent('events', live)
  81. expect(runtime.sessions.behavior('events').eventSource.getSnapshot()).toMatchObject({
  82. entries: [older, first, live],
  83. hasMore: false,
  84. change: { kind: 'append', entries: [live] },
  85. })
  86. await runtime.dispose()
  87. })
  88. it('requires an explicit create stub and records successful create and refresh calls', async () => {
  89. const runtime = await SlotTestRuntime.create()
  90. await expect(runtime.sessions.create()).rejects.toThrow(/create is not stubbed/)
  91. await runtime.sessions.add({ id: 'created' }, { current: false })
  92. const create = vi.fn(() => Promise.resolve('created' as SessionId))
  93. runtime.sessions.stubCreate(create)
  94. await expect(runtime.sessions.create({ cwd: '/workspace' })).resolves.toBe('created')
  95. await expect(runtime.sessions.refresh()).resolves.toBeUndefined()
  96. expect(create).toHaveBeenCalledWith({ cwd: '/workspace' })
  97. expect(runtime.sessions.calls.slice(-2)).toEqual([
  98. { method: 'create', args: [{ cwd: '/workspace' }] },
  99. { method: 'refresh', args: [] },
  100. ])
  101. await runtime.dispose()
  102. })
  103. it('disposes a scope without materializing a binding', async () => {
  104. const runtime = await SlotTestRuntime.create()
  105. await runtime.sessions.add({ id: 'scope-only' }, { current: false })
  106. const scope = runtime.sessions.scope('scope-only')
  107. expect(scope).toBeDefined()
  108. const release = vi.fn()
  109. scope?.effect(() => release, 'fixture scope release')
  110. runtime.releaseWorkspaceSource()
  111. await runtime.dispose()
  112. expect(release).toHaveBeenCalledOnce()
  113. })
  114. })