use-async.test.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { renderHook, act } from '@testing-library/react-hooks'
  2. import { expect } from 'chai'
  3. import sinon from 'sinon'
  4. import useAsync from '../../../../frontend/js/shared/hooks/use-async'
  5. function deferred() {
  6. let res!: (
  7. value: Record<string, unknown> | PromiseLike<Record<string, unknown>>
  8. ) => void
  9. let rej!: (reason?: any) => void
  10. const promise = new Promise<Record<string, unknown>>((resolve, reject) => {
  11. res = resolve
  12. rej = reject
  13. })
  14. return { promise, resolve: res, reject: rej }
  15. }
  16. const defaultState = {
  17. status: 'idle',
  18. data: null,
  19. error: null,
  20. isIdle: true,
  21. isLoading: false,
  22. isError: false,
  23. isSuccess: false,
  24. }
  25. const pendingState = {
  26. ...defaultState,
  27. status: 'pending',
  28. isIdle: false,
  29. isLoading: true,
  30. }
  31. const resolvedState = {
  32. ...defaultState,
  33. status: 'resolved',
  34. isIdle: false,
  35. isSuccess: true,
  36. }
  37. const rejectedState = {
  38. ...defaultState,
  39. status: 'rejected',
  40. isIdle: false,
  41. isError: true,
  42. }
  43. describe('useAsync', function () {
  44. beforeEach(function () {
  45. global.console.error = sinon.stub()
  46. })
  47. afterEach(function () {
  48. // eslint-disable-next-line
  49. // @ts-ignore
  50. global.console.error.reset()
  51. })
  52. it('exposes the methods', function () {
  53. const { result } = renderHook(() => useAsync())
  54. expect(result.current.setData).to.be.a('function')
  55. expect(result.current.setError).to.be.a('function')
  56. expect(result.current.runAsync).to.be.a('function')
  57. })
  58. it('calling `runAsync` with a promise which resolves', async function () {
  59. const { promise, resolve } = deferred()
  60. const { result } = renderHook(() => useAsync())
  61. expect(result.current).to.include(defaultState)
  62. let p: Promise<unknown>
  63. act(() => {
  64. p = result.current.runAsync(promise)
  65. })
  66. expect(result.current).to.include(pendingState)
  67. const resolvedValue = {}
  68. await act(async () => {
  69. resolve(resolvedValue)
  70. await p
  71. })
  72. expect(result.current).to.include({
  73. ...resolvedState,
  74. data: resolvedValue,
  75. })
  76. })
  77. it('calling `runAsync` with a promise which rejects', async function () {
  78. const { promise, reject } = deferred()
  79. const { result } = renderHook(() => useAsync())
  80. expect(result.current).to.include(defaultState)
  81. let p: Promise<unknown>
  82. act(() => {
  83. p = result.current.runAsync(promise)
  84. })
  85. expect(result.current).to.include(pendingState)
  86. const rejectedValue = Symbol('rejected value')
  87. await act(async () => {
  88. reject(rejectedValue)
  89. await p
  90. })
  91. expect(result.current).to.include({
  92. ...rejectedState,
  93. error: rejectedValue,
  94. })
  95. })
  96. it('can specify an initial state', function () {
  97. const mockData = Symbol('resolved value')
  98. const customInitialState = { status: 'resolved' as const, data: mockData }
  99. const { result } = renderHook(() => useAsync(customInitialState))
  100. expect(result.current).to.include({
  101. ...resolvedState,
  102. ...customInitialState,
  103. })
  104. })
  105. it('can set the data', function () {
  106. const mockData = Symbol('resolved value')
  107. const { result } = renderHook(() => useAsync())
  108. act(() => {
  109. result.current.setData(mockData)
  110. })
  111. expect(result.current).to.include({
  112. ...resolvedState,
  113. data: mockData,
  114. })
  115. })
  116. it('can set the error', function () {
  117. const mockError = Symbol('rejected value')
  118. const { result } = renderHook(() => useAsync())
  119. act(() => {
  120. result.current.setError(mockError)
  121. })
  122. expect(result.current).to.include({
  123. ...rejectedState,
  124. error: mockError,
  125. })
  126. })
  127. it('no state updates happen if the component is unmounted while pending', async function () {
  128. const { promise, resolve } = deferred()
  129. const { result, unmount } = renderHook(() => useAsync())
  130. let p: Promise<unknown>
  131. act(() => {
  132. p = result.current.runAsync(promise)
  133. })
  134. unmount()
  135. await act(async () => {
  136. resolve({})
  137. await p
  138. })
  139. expect(global.console.error).not.to.have.been.called
  140. })
  141. })