zammad/zammad

View on GitHub
app/frontend/shared/server/apollo/handler/__tests__/QueryHandler.spec.ts

Summary

Maintainability
D
1 day
Test Coverage
// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

import { NetworkStatus } from '@apollo/client/core'
import { useLazyQuery, useQuery } from '@vue/apollo-composable'

import { SampleTypedQueryDocument } from '#tests/fixtures/graphqlSampleTypes.ts'
import type {
  SampleQuery,
  SampleQueryVariables,
} from '#tests/fixtures/graphqlSampleTypes.ts'
import createMockClient from '#tests/support/mock-apollo-client.ts'
import { waitForNextTick, waitUntilSpyCalled } from '#tests/support/utils.ts'

import { useNotifications } from '#shared/components/CommonNotifications/index.ts'
import { GraphQLErrorTypes } from '#shared/types/error.ts'

import QueryHandler from '../QueryHandler.ts'

import type { ApolloError, ApolloQueryResult } from '@apollo/client/core'

const queryFunctionCallSpy = vi.fn()

const querySampleResult = {
  Sample: {
    __typename: 'Sample',
    id: 1,
    title: 'Test Title',
    text: 'Test Text',
  },
}

const querySampleErrorResult = {
  networkStatus: NetworkStatus.error,
  errors: [
    {
      message: 'GraphQL Error',
      extensions: { type: 'Exceptions::UnknownError' },
    },
  ],
}

const querySampleNetworkErrorResult = new Error('GraphQL Network Error')

const handlerCallSpy = vi.fn()

const mockClient = (error = false, errorType = 'GraphQL') => {
  handlerCallSpy.mockImplementation(() => {
    if (error) {
      return errorType === 'GraphQL'
        ? Promise.resolve(querySampleErrorResult)
        : Promise.reject(querySampleNetworkErrorResult)
    }

    return Promise.resolve({
      data: querySampleResult,
    })
  })

  createMockClient([
    {
      operationDocument: SampleTypedQueryDocument,
      handler: handlerCallSpy,
    },
  ])

  handlerCallSpy.mockClear()
  queryFunctionCallSpy.mockClear()
}

const waitFirstResult = (queryHandler: QueryHandler<any, any>) =>
  new Promise<ApolloQueryResult<any> | ApolloError>((resolve) => {
    queryHandler.onResult((res) => {
      if (res.data) {
        resolve(res)
      }
    })
    queryHandler.onError((err) => {
      resolve(err)
    })
  })

describe('QueryHandler', () => {
  const sampleQuery = (variables: SampleQueryVariables, options = {}) => {
    queryFunctionCallSpy()
    return useQuery<SampleQuery, SampleQueryVariables>(
      SampleTypedQueryDocument,
      variables,
      options,
    )
  }

  const sampleLazyQuery = (variables: SampleQueryVariables, options = {}) => {
    queryFunctionCallSpy()
    return useLazyQuery<SampleQuery, SampleQueryVariables>(
      SampleTypedQueryDocument,
      variables,
      options,
    )
  }

  describe('constructor', () => {
    beforeEach(() => {
      mockClient()
    })

    it('instance can be created', () => {
      const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
      expect(queryHandlerObject).toBeInstanceOf(QueryHandler)
    })
    it('default handler options can be changed', () => {
      const errorNotificationMessage = 'A test message.'

      const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }), {
        errorNotificationMessage,
      })
      expect(queryHandlerObject.handlerOptions.errorNotificationMessage).toBe(
        errorNotificationMessage,
      )
    })

    it('given query function was executed', () => {
      const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
      expect(queryFunctionCallSpy).toBeCalled()
      expect(queryHandlerObject.operationResult).toBeTruthy()
    })
  })

  describe('loading', () => {
    beforeEach(() => {
      mockClient()
    })

    it('loading state will be updated', async () => {
      expect.assertions(2)

      const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))
      const loading = queryHandlerObject.loading()
      expect(loading.value).toBe(true)

      await waitFirstResult(queryHandlerObject)

      expect(loading.value).toBe(false)
    })

    it('supports lazy queries', async () => {
      expect.assertions(3)

      const queryHandlerObject = new QueryHandler(sampleLazyQuery({ id: 1 }))

      expect(queryHandlerObject.loading().value).toBe(false)

      queryHandlerObject.load()
      await waitForNextTick()

      expect(queryHandlerObject.loading().value).toBe(true)

      await queryHandlerObject.query()

      expect(queryHandlerObject.loading().value).toBe(false)
    })
  })

  describe('result', () => {
    beforeEach(() => {
      mockClient()
    })

    it('result is available', async () => {
      const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))

      const result = await waitFirstResult(queryHandlerObject)

      expect(result).toMatchObject({
        data: querySampleResult,
      })
    })

    it('loaded result is also resolved after additional result call with active trigger refetch', async () => {
      const queryHandlerObject = new QueryHandler(sampleLazyQuery({ id: 1 }))

      await expect(queryHandlerObject.query()).resolves.toMatchObject({
        data: querySampleResult,
      })

      await expect(queryHandlerObject.query()).resolves.toMatchObject({
        data: querySampleResult,
      })

      expect(handlerCallSpy).toBeCalledTimes(1)
    })

    it('watch on result change', async () => {
      expect.assertions(1)

      const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))

      queryHandlerObject.watchOnResult((result) => {
        expect(result).toEqual(querySampleResult)
      })
      await waitFirstResult(queryHandlerObject)
    })

    it('on result trigger', async () => {
      expect.assertions(1)

      const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))

      queryHandlerObject.onResult((result) => {
        if (result.data) {
          expect(result.data).toEqual(querySampleResult)
        }
      })
      await waitFirstResult(queryHandlerObject)
    })

    it('receive value immediately in non-reactive way', async () => {
      const queryHandlerObject = new QueryHandler(sampleLazyQuery({ id: 1 }))

      await expect(queryHandlerObject.query()).resolves.toEqual(
        expect.objectContaining({ data: querySampleResult }),
      )
    })

    it('cancels previous attempt, if the new one started', async () => {
      const queryHandlerObject = new QueryHandler(sampleLazyQuery({ id: 1 }))

      const cancelSpy = vi.spyOn(queryHandlerObject, 'cancel')

      expect(cancelSpy).not.toHaveBeenCalled()

      const result1 = queryHandlerObject.query()

      expect(cancelSpy).toHaveBeenCalledTimes(1)

      const result2 = queryHandlerObject.query()

      expect(cancelSpy).toHaveBeenCalledTimes(2)

      // both resolve, because signal is not actually aborted in node
      await expect(result1).resolves.toEqual(
        expect.objectContaining({ data: querySampleResult }),
      )
      await expect(result2).resolves.toEqual(
        expect.objectContaining({ data: querySampleResult }),
      )
    })
  })

  describe('error handling', () => {
    describe('GraphQL errors', () => {
      beforeEach(() => {
        mockClient(true)
      })

      it('notification is triggerd', async () => {
        expect.assertions(1)

        const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))

        await waitFirstResult(queryHandlerObject)

        const { notifications } = useNotifications()

        expect(notifications.value.length).toBe(1)
      })

      it('use error callback', async () => {
        expect.assertions(1)

        const errorCallbackSpy = vi.fn()

        const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }), {
          errorCallback: (error) => {
            errorCallbackSpy(error)
          },
        })

        await waitFirstResult(queryHandlerObject)
        await waitUntilSpyCalled(errorCallbackSpy)

        expect(errorCallbackSpy).toHaveBeenCalledWith({
          type: 'Exceptions::UnknownError',
          message: 'GraphQL Error',
        })
      })

      it('refetch with error', async () => {
        expect.assertions(1)
        const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))

        const errorCallbackSpy = vi.fn()

        await waitFirstResult(queryHandlerObject)

        // Refetch after first load again.
        await queryHandlerObject.refetch().catch((error) => {
          errorCallbackSpy(error)
        })

        expect(errorCallbackSpy).toHaveBeenCalled()
      })
    })

    describe('Network errors', () => {
      beforeEach(() => {
        mockClient(true, 'NetworkError')
      })

      it('use error callback', async () => {
        expect.assertions(1)
        const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }), {
          errorCallback: (error) => {
            expect(error).toEqual({
              type: GraphQLErrorTypes.NetworkError,
            })
          },
        })

        await waitFirstResult(queryHandlerObject)
      })
    })
  })

  describe('use operation result wrapper', () => {
    beforeEach(() => {
      mockClient()
    })

    it('use returned query options', () => {
      const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))

      expect(queryHandlerObject.options()).toBeTruthy()
    })

    it('use fetchMore query function', async () => {
      const queryHandlerObject = new QueryHandler(sampleQuery({ id: 1 }))

      await expect(queryHandlerObject.fetchMore({})).resolves.toEqual(
        querySampleResult,
      )
    })
  })
})