QueryHandler.spec.ts 10 KB

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