use-async.test.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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. act(() => {
  77. result.current.reset()
  78. })
  79. expect(result.current).to.include(defaultState)
  80. })
  81. it('calling `runAsync` with a promise which rejects', async function () {
  82. const { promise, reject } = deferred()
  83. const { result } = renderHook(() => useAsync())
  84. expect(result.current).to.include(defaultState)
  85. let p: Promise<unknown>
  86. act(() => {
  87. p = result.current.runAsync(promise).catch(() => {})
  88. })
  89. expect(result.current).to.include(pendingState)
  90. const rejectedValue = Symbol('rejected value')
  91. await act(async () => {
  92. reject(rejectedValue)
  93. await p
  94. })
  95. expect(result.current).to.include({
  96. ...rejectedState,
  97. error: rejectedValue,
  98. })
  99. })
  100. it('can specify an initial state', function () {
  101. const mockData = Symbol('resolved value')
  102. const customInitialState = { status: 'resolved' as const, data: mockData }
  103. const { result } = renderHook(() => useAsync(customInitialState))
  104. expect(result.current).to.include({
  105. ...resolvedState,
  106. ...customInitialState,
  107. })
  108. })
  109. it('can set the data', function () {
  110. const mockData = Symbol('resolved value')
  111. const { result } = renderHook(() => useAsync())
  112. act(() => {
  113. result.current.setData(mockData)
  114. })
  115. expect(result.current).to.include({
  116. ...resolvedState,
  117. data: mockData,
  118. })
  119. })
  120. it('can set the error', function () {
  121. const mockError = Symbol('rejected value')
  122. const { result } = renderHook(() => useAsync())
  123. act(() => {
  124. result.current.setError(mockError)
  125. })
  126. expect(result.current).to.include({
  127. ...rejectedState,
  128. error: mockError,
  129. })
  130. })
  131. it('no state updates happen if the component is unmounted while pending', async function () {
  132. const { promise, resolve } = deferred()
  133. const { result, unmount } = renderHook(() => useAsync())
  134. let p: Promise<unknown>
  135. act(() => {
  136. p = result.current.runAsync(promise)
  137. })
  138. unmount()
  139. await act(async () => {
  140. resolve({})
  141. await p
  142. })
  143. expect(global.console.error).not.to.have.been.called
  144. })
  145. })