QueryHandler.spec.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { NetworkStatus } from '@apollo/client/core'
  3. import { useLazyQuery, useQuery } from '@vue/apollo-composable'
  4. import { SampleTypedQueryDocument } from '#tests/fixtures/graphqlSampleTypes.ts'
  5. import type {
  6. SampleQuery,
  7. SampleQueryVariables,
  8. } from '#tests/fixtures/graphqlSampleTypes.ts'
  9. import createMockClient from '#tests/support/mock-apollo-client.ts'
  10. import { waitForNextTick, waitUntilSpyCalled } from '#tests/support/utils.ts'
  11. import { useNotifications } from '#shared/components/CommonNotifications/index.ts'
  12. import { GraphQLErrorTypes } from '#shared/types/error.ts'
  13. import QueryHandler from '../QueryHandler.ts'
  14. import type { ApolloError, ApolloQueryResult } from '@apollo/client/core'
  15. const queryFunctionCallSpy = vi.fn()
  16. const querySampleResult = {
  17. Sample: {
  18. __typename: 'Sample',
  19. id: 1,
  20. title: 'Test Title',
  21. text: 'Test Text',
  22. },
  23. }
  24. const querySampleErrorResult = {
  25. networkStatus: NetworkStatus.error,
  26. errors: [
  27. {
  28. message: 'GraphQL Error',
  29. extensions: { type: 'Exceptions::UnknownError' },
  30. },
  31. ],
  32. }
  33. const querySampleNetworkErrorResult = new Error('GraphQL Network Error')
  34. const handlerCallSpy = vi.fn()
  35. const mockClient = (error = false, errorType = 'GraphQL') => {
  36. handlerCallSpy.mockImplementation(() => {
  37. if (error) {
  38. return errorType === 'GraphQL'
  39. ? Promise.resolve(querySampleErrorResult)
  40. : Promise.reject(querySampleNetworkErrorResult)
  41. }
  42. return Promise.resolve({
  43. data: querySampleResult,
  44. })
  45. })
  46. createMockClient([
  47. {
  48. operationDocument: SampleTypedQueryDocument,
  49. handler: handlerCallSpy,
  50. },
  51. ])
  52. handlerCallSpy.mockClear()
  53. queryFunctionCallSpy.mockClear()
  54. }
  55. const waitFirstResult = (queryHandler: QueryHandler<any, any>) =>
  56. new Promise<ApolloQueryResult<any> | ApolloError>((resolve) => {
  57. queryHandler.onResult((res) => {
  58. if (res.data) {
  59. resolve(res)
  60. }
  61. })
  62. queryHandler.onError((err) => {
  63. resolve(err)
  64. })
  65. })
  66. describe('QueryHandler', () => {
  67. const sampleQuery = (variables: SampleQueryVariables, options = {}) => {
  68. queryFunctionCallSpy()
  69. return useQuery<SampleQuery, SampleQueryVariables>(
  70. SampleTypedQueryDocument,
  71. variables,
  72. options,
  73. )
  74. }
  75. const sampleLazyQuery = (variables: SampleQueryVariables, options = {}) => {
  76. queryFunctionCallSpy()
  77. return useLazyQuery<SampleQuery, SampleQueryVariables>(
  78. SampleTypedQueryDocument,
  79. variables,
  80. options,
  81. )
  82. }
  83. describe('constructor', () => {
  84. beforeEach(() => {
  85. mockClient()
  86. })
  87. it('instance can be created', () => {
  88. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  89. expect(queryHandlerObject).toBeInstanceOf(QueryHandler)
  90. })
  91. it('default handler options can be changed', () => {
  92. const errorNotificationMessage = 'A test message.'
  93. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }), {
  94. errorNotificationMessage,
  95. })
  96. expect(queryHandlerObject.handlerOptions.errorNotificationMessage).toBe(
  97. errorNotificationMessage,
  98. )
  99. })
  100. it('given query function was executed', () => {
  101. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  102. expect(queryFunctionCallSpy).toBeCalled()
  103. expect(queryHandlerObject.operationResult).toBeTruthy()
  104. })
  105. })
  106. describe('loading', () => {
  107. beforeEach(() => {
  108. mockClient()
  109. })
  110. it('loading state will be updated', async () => {
  111. expect.assertions(2)
  112. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  113. const loading = queryHandlerObject.loading()
  114. expect(loading.value).toBe(true)
  115. await waitFirstResult(queryHandlerObject)
  116. expect(loading.value).toBe(false)
  117. })
  118. it('supports lazy queries', async () => {
  119. expect.assertions(3)
  120. const queryHandlerObject = new QueryHandler(sampleLazyQuery({ id: 1 }))
  121. expect(queryHandlerObject.loading().value).toBe(false)
  122. queryHandlerObject.load()
  123. await waitForNextTick()
  124. expect(queryHandlerObject.loading().value).toBe(true)
  125. await queryHandlerObject.query()
  126. expect(queryHandlerObject.loading().value).toBe(false)
  127. })
  128. })
  129. describe('result', () => {
  130. beforeEach(() => {
  131. mockClient()
  132. })
  133. it('result is available', async () => {
  134. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  135. const result = await waitFirstResult(queryHandlerObject)
  136. expect(result).toMatchObject({
  137. data: querySampleResult,
  138. })
  139. })
  140. it('loaded result is also resolved after additional result call with active trigger refetch', async () => {
  141. const queryHandlerObject = new QueryHandler(sampleLazyQuery({ id: 1 }))
  142. await expect(queryHandlerObject.query()).resolves.toMatchObject({
  143. data: querySampleResult,
  144. })
  145. await expect(queryHandlerObject.query()).resolves.toMatchObject({
  146. data: querySampleResult,
  147. })
  148. expect(handlerCallSpy).toBeCalledTimes(1)
  149. })
  150. it('watch on result change', async () => {
  151. expect.assertions(1)
  152. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  153. queryHandlerObject.watchOnResult((result) => {
  154. expect(result).toEqual(querySampleResult)
  155. })
  156. await waitFirstResult(queryHandlerObject)
  157. })
  158. it('on result trigger', async () => {
  159. expect.assertions(1)
  160. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  161. queryHandlerObject.onResult((result) => {
  162. if (result.data) {
  163. expect(result.data).toEqual(querySampleResult)
  164. }
  165. })
  166. await waitFirstResult(queryHandlerObject)
  167. })
  168. it('receive value immediately in non-reactive way', async () => {
  169. const queryHandlerObject = new QueryHandler(sampleLazyQuery({ id: 1 }))
  170. await expect(queryHandlerObject.query()).resolves.toEqual(
  171. expect.objectContaining({ data: querySampleResult }),
  172. )
  173. })
  174. it('cancels previous attempt, if the new one started', async () => {
  175. const queryHandlerObject = new QueryHandler(sampleLazyQuery({ id: 1 }))
  176. const cancelSpy = vi.spyOn(queryHandlerObject, 'cancel')
  177. expect(cancelSpy).not.toHaveBeenCalled()
  178. const result1 = queryHandlerObject.query()
  179. expect(cancelSpy).toHaveBeenCalledTimes(1)
  180. const result2 = queryHandlerObject.query()
  181. expect(cancelSpy).toHaveBeenCalledTimes(2)
  182. // both resolve, because signal is not actually aborted in node
  183. await expect(result1).resolves.toEqual(
  184. expect.objectContaining({ data: querySampleResult }),
  185. )
  186. await expect(result2).resolves.toEqual(
  187. expect.objectContaining({ data: querySampleResult }),
  188. )
  189. })
  190. })
  191. describe('error handling', () => {
  192. describe('GraphQL errors', () => {
  193. beforeEach(() => {
  194. mockClient(true)
  195. })
  196. it('notification is triggerd', async () => {
  197. expect.assertions(1)
  198. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  199. await waitFirstResult(queryHandlerObject)
  200. const { notifications } = useNotifications()
  201. expect(notifications.value.length).toBe(1)
  202. })
  203. it('use error callback', async () => {
  204. expect.assertions(1)
  205. const errorCallbackSpy = vi.fn()
  206. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }), {
  207. errorCallback: (error) => {
  208. errorCallbackSpy(error)
  209. },
  210. })
  211. await waitFirstResult(queryHandlerObject)
  212. await waitUntilSpyCalled(errorCallbackSpy)
  213. expect(errorCallbackSpy).toHaveBeenCalledWith({
  214. type: 'Exceptions::UnknownError',
  215. message: 'GraphQL Error',
  216. })
  217. })
  218. it('refetch with error', async () => {
  219. expect.assertions(1)
  220. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  221. const errorCallbackSpy = vi.fn()
  222. await waitFirstResult(queryHandlerObject)
  223. // Refetch after first load again.
  224. await queryHandlerObject.refetch().catch((error) => {
  225. errorCallbackSpy(error)
  226. })
  227. expect(errorCallbackSpy).toHaveBeenCalled()
  228. })
  229. })
  230. describe('Network errors', () => {
  231. beforeEach(() => {
  232. mockClient(true, 'NetworkError')
  233. })
  234. it('use error callback', async () => {
  235. expect.assertions(1)
  236. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }), {
  237. errorCallback: (error) => {
  238. expect(error).toEqual({
  239. type: GraphQLErrorTypes.NetworkError,
  240. })
  241. },
  242. })
  243. await waitFirstResult(queryHandlerObject)
  244. })
  245. })
  246. })
  247. describe('use operation result wrapper', () => {
  248. beforeEach(() => {
  249. mockClient()
  250. })
  251. it('use returned query options', () => {
  252. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  253. expect(queryHandlerObject.options()).toBeTruthy()
  254. })
  255. it('use fetchMore query function', async () => {
  256. const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
  257. await expect(queryHandlerObject.fetchMore({})).resolves.toEqual(
  258. querySampleResult,
  259. )
  260. })
  261. })
  262. })