UNPKG

@zendesk/laika

Version:

Test, mock, intercept and modify Apollo Client's operations — in both browser and unit tests!

315 lines (270 loc) 10.3 kB
import gql from 'graphql-tag' import waitFor from 'wait-for-observables' import { ApolloLink, execute, fromError, Observable, Observer, Operation, } from '@apollo/client/core' import { DEFAULT_GLOBAL_PROPERTY_NAME } from './constants' import { Laika } from './laika' import { onNextTick, WaitForResult } from './testUtils' const query = gql` query helloQuery { sample { id } } ` const goodbyeQuery = gql` query goodbyeQuery { sample { id } } ` const subscription = gql` subscription helloSubscription { sample { id } } ` const standardError = new Error('I never work') const data = { data: { hello: 'world' } } const mockData = { data: { goodbye: 'world' } } const mockDataImmediate = { data: { so: 'fast' } } describe('Laika', () => { it('returns passthrough data from the following link', async () => { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }) const interceptionLink = laika.createLink() const backendStub = jest.fn(() => Observable.of(data), ) as unknown as ApolloLink const link = ApolloLink.from([interceptionLink, backendStub]) const [result] = (await waitFor(execute(link, { query }))) as WaitForResult< typeof data > const { values } = result! expect(values).toEqual([data]) expect(backendStub).toHaveBeenCalledTimes(1) }) describe('Intercept API', () => { it('returns mocked data and does not connect to the following link', async () => { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }) const interceptionLink = laika.createLink() const backendStub = jest.fn(() => Observable.of(data)) const link = ApolloLink.from([interceptionLink, backendStub as any]) const interceptor = laika.intercept() interceptor.mockResultOnce({ result: mockData, }) const [result] = (await waitFor( execute(link, { query }), )) as WaitForResult<unknown> const { values } = result! expect(values).toEqual([mockData]) expect(backendStub).toHaveBeenCalledTimes(0) }) it('returns mock once and then falls back to the following link - twice in a row', async () => { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }) const interceptionLink = laika.createLink() const backendStub = jest.fn(() => Observable.of(data)) const link = ApolloLink.from([interceptionLink, backendStub as any]) const interceptor = laika.intercept() let triedCount = 0 while (++triedCount <= 2) { interceptor.mockResultOnce({ result: mockData, }) // eslint-disable-next-line no-await-in-loop const [result1, result2] = (await waitFor( execute(link, { query }), execute(link, { query }), )) as WaitForResult<unknown> const { values: mockValues } = result1! const { values: remoteValues } = result2! expect(mockValues).toEqual([mockData]) expect(remoteValues).toEqual([data]) expect(backendStub).toHaveBeenCalledTimes(triedCount) } }) it('connects to a mocked subscription without connecting to the following link and immediately fires mocked data', async () => { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }) const interceptionLink = laika.createLink() const mockedResultFn = jest.fn(() => ({ result: mockDataImmediate })) const backendStub = jest.fn(() => Observable.of(data)) const link = ApolloLink.from([interceptionLink, backendStub as any]) const interceptor = laika.intercept() // testing that this will get pushed immediately interceptor.mockResultOnce(mockedResultFn) const observer = { next: jest.fn(), complete: jest.fn(), error: jest.fn(), } const sub = execute(link, { query: subscription }).subscribe(observer) expect.assertions(7) await onNextTick(() => { expect(mockedResultFn).toHaveBeenCalledTimes(1) expect(observer.next).toHaveBeenCalledTimes(1) expect(observer.next).toHaveBeenCalledWith(mockDataImmediate) expect(observer.complete).not.toHaveBeenCalled() expect(backendStub).toHaveBeenCalledTimes(0) sub.unsubscribe() expect(observer.complete).not.toHaveBeenCalled() expect(observer.error).not.toHaveBeenCalled() }) }) it('connects to a mocked subscription without connecting to the following link, then fires a mock update', async () => { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }) const interceptionLink = laika.createLink() const backendStub = jest.fn(() => Observable.of(data)) const link = ApolloLink.from([interceptionLink, backendStub as any]) const interceptor = laika.intercept() const observer = { next: jest.fn(), complete: jest.fn(), error: jest.fn(), } expect.assertions(7) const sub = execute(link, { query: subscription }).subscribe(observer) await onNextTick(() => { expect(observer.next).not.toHaveBeenCalled() interceptor.fireSubscriptionUpdate({ result: mockData }) expect(observer.next).toHaveBeenCalledTimes(1) expect(observer.next).toHaveBeenCalledWith(mockData) expect(observer.complete).not.toHaveBeenCalled() expect(backendStub).toHaveBeenCalledTimes(0) sub.unsubscribe() expect(observer.complete).not.toHaveBeenCalled() expect(observer.error).not.toHaveBeenCalled() }) }) it('waitForActiveSubscription generates a Promise when no current active subscription, which resolves once one is made', async () => { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }) const interceptionLink = laika.createLink() const backendStub = jest.fn(() => Observable.of(data)) const link = ApolloLink.from([interceptionLink, backendStub as any]) const interceptor = laika.intercept() const observer = { next: jest.fn(), complete: jest.fn(), error: jest.fn(), } expect.assertions(3) const hasSettled = jest.fn() const waitPromise = interceptor.waitForActiveSubscription() expect(waitPromise).toBeInstanceOf(Promise) void waitPromise!.then(hasSettled) await onNextTick(() => { expect(hasSettled).not.toHaveBeenCalled() }) const sub = execute(link, { query: subscription }).subscribe(observer) await onNextTick(() => { expect(hasSettled).toHaveBeenCalled() sub.unsubscribe() }) }) describe('intercept with a matcher', () => { it.each([ ['MatcherObject (operationName)', { operationName: 'goodbyeQuery' }], ['MatcherObject (variables)', { variables: { type: 'goodbye' } }], [ 'MatcherFn', (operation: Operation) => operation.operationName === 'goodbyeQuery', ], ])( 'correctly intercepts only operations matched by %s and leaves other alone', async (_, matcher) => { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }) const interceptionLink = laika.createLink() const backendStub = jest.fn(() => Observable.of(data)) const link = ApolloLink.from([interceptionLink, backendStub as any]) const interceptor = laika.intercept(matcher) interceptor.mockResultOnce({ result: mockData, }) const [result1, result2] = (await waitFor( execute(link, { query }), execute(link, { query: goodbyeQuery, variables: { type: 'goodbye' }, }), )) as WaitForResult<unknown> const { values } = result1! const { values: goodbyeValues } = result2! expect(values).toEqual([data]) expect(goodbyeValues).toEqual([mockData]) expect(backendStub).toHaveBeenCalledTimes(1) }, ) }) }) it('calls unsubscribe on the appropriate downstream observable', async () => { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }) const interceptionLink = laika.createLink() const unsubscribeStub = jest.fn() // Hold the test hostage until we're hit let underlyingObservable: any const untilSubscribed = new Promise((resolve) => { underlyingObservable = { subscribe(observer: Observer<typeof data>) { resolve(undefined) // Release hold on test. void Promise.resolve().then(() => { observer.next!(data) observer.complete!() }) return { unsubscribe: unsubscribeStub, closed: false } }, } }) const backendStub = jest.fn() backendStub.mockReturnValueOnce(underlyingObservable!) const link = ApolloLink.from([interceptionLink, backendStub as any]) // eslint-disable-next-line @typescript-eslint/no-shadow const subscription = execute(link, { query }).subscribe({}) await untilSubscribed subscription.unsubscribe() expect(unsubscribeStub).toHaveBeenCalledTimes(1) }) it('supports multiple subscribers to the same request', async () => { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }) const interceptionLink = laika.createLink() const stub = jest.fn() stub.mockReturnValueOnce(fromError(standardError)) stub.mockReturnValueOnce(fromError(standardError)) stub.mockReturnValueOnce(Observable.of(data)) const link = ApolloLink.from([interceptionLink, stub as any]) const observable = execute(link, { query }) const [result1, result2, result3] = (await waitFor( observable, observable, observable, )) as any expect(result1).toEqual({ error: standardError }) expect(result2).toEqual({ error: standardError }) expect(result3.values).toEqual([data]) expect(stub).toHaveBeenCalledTimes(3) }) })