@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
text/typescript
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)
})
})