use-invoke.spec.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. // @vitest-environment jsdom
  2. import { describe, expect, it, vi } from 'vitest'
  3. import { act, render } from '@testing-library/react'
  4. import { useInvoke } from '@deepseek-ai/dsh-client-web-react'
  5. function deferred() {
  6. let resolve!: () => void
  7. let reject!: (e: unknown) => void
  8. const promise = new Promise<void>((res, rej) => { resolve = res; reject = rej })
  9. return { promise, resolve, reject }
  10. }
  11. interface Probe {
  12. invoke: () => void
  13. pending: boolean
  14. renders: number
  15. }
  16. function Harness({ fn, probe }: { fn: () => Promise<unknown>; probe: Probe }) {
  17. const [invoke, pending] = useInvoke(fn)
  18. probe.invoke = invoke
  19. probe.pending = pending
  20. probe.renders += 1
  21. return null
  22. }
  23. const newProbe = (): Probe => ({ invoke: () => {}, pending: false, renders: 0 })
  24. describe('useInvoke', () => {
  25. it('tracks pending across the action lifecycle', async () => {
  26. const d = deferred()
  27. const probe = newProbe()
  28. render(<Harness fn={() => d.promise} probe={probe} />)
  29. expect(probe.pending).toBe(false)
  30. act(() => { probe.invoke() })
  31. expect(probe.pending).toBe(true)
  32. await act(async () => { d.resolve(); await d.promise })
  33. expect(probe.pending).toBe(false)
  34. })
  35. it('keeps pending true until the last concurrent call settles', async () => {
  36. const d1 = deferred()
  37. const d2 = deferred()
  38. const queue = [d1, d2]
  39. const probe = newProbe()
  40. render(<Harness fn={() => queue.shift()!.promise} probe={probe} />)
  41. act(() => { probe.invoke() })
  42. act(() => { probe.invoke() })
  43. expect(probe.pending).toBe(true)
  44. await act(async () => { d1.resolve(); await d1.promise })
  45. expect(probe.pending).toBe(true)
  46. await act(async () => { d2.resolve(); await d2.promise })
  47. expect(probe.pending).toBe(false)
  48. })
  49. it('keeps the invoke reference stable while fn changes, and calls the latest fn', async () => {
  50. const first = vi.fn(() => Promise.resolve())
  51. const second = vi.fn(() => Promise.resolve())
  52. const probe = newProbe()
  53. const { rerender } = render(<Harness fn={first} probe={probe} />)
  54. const invokeBefore = probe.invoke
  55. rerender(<Harness fn={second} probe={probe} />)
  56. expect(probe.invoke).toBe(invokeBefore)
  57. await act(async () => { probe.invoke() })
  58. expect(first).not.toHaveBeenCalled()
  59. expect(second).toHaveBeenCalledTimes(1)
  60. })
  61. it('resets pending and logs when the action rejects', async () => {
  62. const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
  63. const d = deferred()
  64. const probe = newProbe()
  65. render(<Harness fn={() => d.promise} probe={probe} />)
  66. act(() => { probe.invoke() })
  67. expect(probe.pending).toBe(true)
  68. await act(async () => {
  69. d.reject(new Error('boom'))
  70. await d.promise.catch(() => {})
  71. })
  72. expect(probe.pending).toBe(false)
  73. expect(consoleError).toHaveBeenCalledWith('useInvoke action failed:', expect.any(Error))
  74. consoleError.mockRestore()
  75. })
  76. })