UNPKG

@zendesk/laika

Version:

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

416 lines 19.3 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import gql from 'graphql-tag'; import waitFor from 'wait-for-observables'; import { ApolloLink } from '@apollo/client/core'; import { DEFAULT_GLOBAL_PROPERTY_NAME } from './constants'; import { Laika } from './laika'; import { executeLink, observableError, observableOf, onNextTick, } 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' } }; const createStubLink = (stub) => new ApolloLink((operation, forward) => stub(operation, forward)); const createDeferred = () => { let settle = () => undefined; let rejectPromise = () => undefined; const promise = new Promise((resolve, reject) => { settle = resolve; rejectPromise = reject; }); return { promise, resolve: settle, reject: rejectPromise, }; }; describe('Laika', () => { it('returns passthrough data from the following link', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); const [result] = (yield waitFor(executeLink(link, { query }))); 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', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); const interceptor = laika.intercept(); interceptor.mockResultOnce({ result: mockData, }); const [result] = (yield waitFor(executeLink(link, { query }))); 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', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); 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] = (yield waitFor(executeLink(link, { query }), executeLink(link, { query }))); const { values: mockValues } = result1; const { values: remoteValues } = result2; expect(mockValues).toEqual([mockData]); expect(remoteValues).toEqual([data]); expect(backendStub).toHaveBeenCalledTimes(triedCount); } })); it('waits for async mocked query results before emitting and completing', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const deferred = createDeferred(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); const interceptor = laika.intercept(); interceptor.mockResultOnce(() => deferred.promise); const observer = { next: jest.fn(), complete: jest.fn(), error: jest.fn(), }; executeLink(link, { query }).subscribe(observer); expect(observer.next).not.toHaveBeenCalled(); expect(observer.complete).not.toHaveBeenCalled(); deferred.resolve({ result: mockData }); yield onNextTick(() => { expect(observer.next).toHaveBeenCalledTimes(1); expect(observer.next).toHaveBeenCalledWith(mockData); expect(observer.complete).toHaveBeenCalledTimes(1); expect(observer.error).not.toHaveBeenCalled(); expect(backendStub).toHaveBeenCalledTimes(0); }); })); it('forwards async mocked query rejections to observer.error', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const deferred = createDeferred(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); const interceptor = laika.intercept(); interceptor.mockResultOnce(() => deferred.promise); const observer = { next: jest.fn(), complete: jest.fn(), error: jest.fn(), }; executeLink(link, { query }).subscribe(observer); const asyncError = new Error('Async mock failed'); deferred.reject(asyncError); yield onNextTick(() => { expect(observer.error).toHaveBeenCalledTimes(1); expect(observer.error).toHaveBeenCalledWith(asyncError); expect(observer.next).not.toHaveBeenCalled(); expect(observer.complete).not.toHaveBeenCalled(); expect(backendStub).toHaveBeenCalledTimes(0); }); })); it('delays mocked query results when delay is provided', () => { jest.useFakeTimers(); try { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); const interceptor = laika.intercept(); interceptor.mockResultOnce({ result: mockData, delay: 250, }); const observer = { next: jest.fn(), complete: jest.fn(), error: jest.fn(), }; executeLink(link, { query }).subscribe(observer); expect(observer.next).not.toHaveBeenCalled(); jest.advanceTimersByTime(249); expect(observer.next).not.toHaveBeenCalled(); jest.advanceTimersByTime(1); expect(observer.next).toHaveBeenCalledTimes(1); expect(observer.next).toHaveBeenCalledWith(mockData); expect(observer.complete).toHaveBeenCalledTimes(1); expect(observer.error).not.toHaveBeenCalled(); expect(backendStub).toHaveBeenCalledTimes(0); } finally { jest.useRealTimers(); } }); it('connects to a mocked subscription without connecting to the following link and immediately fires mocked data', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const mockedResultFn = jest.fn(() => ({ result: mockDataImmediate })); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); 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 = executeLink(link, { query: subscription }).subscribe(observer); expect.assertions(7); yield 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', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); const interceptor = laika.intercept(); const observer = { next: jest.fn(), complete: jest.fn(), error: jest.fn(), }; expect.assertions(7); const sub = executeLink(link, { query: subscription }).subscribe(observer); yield 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', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); 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); yield onNextTick(() => { expect(hasSettled).not.toHaveBeenCalled(); }); const sub = executeLink(link, { query: subscription }).subscribe(observer); yield onNextTick(() => { expect(hasSettled).toHaveBeenCalled(); sub.unsubscribe(); }); })); describe('intercept with a matcher', () => { it.each([ ['MatcherObject (operationName)', { operationName: 'goodbyeQuery' }], ['MatcherObject (operation)', { operation: goodbyeQuery }], ['MatcherObject (variables)', { variables: { type: 'goodbye' } }], [ 'MatcherFn', (operation) => operation.operationName === 'goodbyeQuery', ], ])('correctly intercepts only operations matched by %s and leaves other alone', (_, matcher) => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); const interceptor = laika.intercept(matcher); interceptor.mockResultOnce({ result: mockData, }); const [result1, result2] = (yield waitFor(executeLink(link, { query }), executeLink(link, { query: goodbyeQuery, variables: { type: 'goodbye' }, }))); const { values } = result1; const { values: goodbyeValues } = result2; expect(values).toEqual([data]); expect(goodbyeValues).toEqual([mockData]); expect(backendStub).toHaveBeenCalledTimes(1); })); }); it('mockRestoreAll removes stale interceptors so the same operation can be mocked again', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const backendStub = jest.fn(() => observableOf(data)); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); const firstInterceptor = laika.intercept({ operationName: 'helloQuery' }); firstInterceptor.mockResult({ result: mockData, }); const [firstResult] = (yield waitFor(executeLink(link, { query }))); expect(firstResult.values).toEqual([mockData]); laika.mockRestoreAll(); const secondInterceptor = laika.intercept({ operationName: 'helloQuery' }); secondInterceptor.mockResultOnce({ result: mockDataImmediate, }); const [secondResult] = (yield waitFor(executeLink(link, { query }))); expect(firstInterceptor.calls).toHaveLength(0); expect(secondResult.values).toEqual([mockDataImmediate]); expect(backendStub).toHaveBeenCalledTimes(0); })); }); it('calls unsubscribe on the appropriate downstream observable', () => __awaiter(void 0, void 0, void 0, function* () { 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; const untilSubscribed = new Promise((resolve) => { underlyingObservable = { subscribe(observer) { resolve(undefined); // Release hold on test. void Promise.resolve().then(() => { var _a, _b; (_a = observer.next) === null || _a === void 0 ? void 0 : _a.call(observer, data); (_b = observer.complete) === null || _b === void 0 ? void 0 : _b.call(observer); }); return { unsubscribe: unsubscribeStub, closed: false }; }, }; }); const backendStub = jest.fn(); backendStub.mockReturnValueOnce(underlyingObservable); const link = ApolloLink.from([ interceptionLink, createStubLink(backendStub), ]); // eslint-disable-next-line @typescript-eslint/no-shadow const subscription = executeLink(link, { query }).subscribe({}); yield untilSubscribed; subscription.unsubscribe(); expect(unsubscribeStub).toHaveBeenCalledTimes(1); })); it('supports multiple subscribers to the same request', () => __awaiter(void 0, void 0, void 0, function* () { const laika = new Laika({ referenceName: DEFAULT_GLOBAL_PROPERTY_NAME, }); const interceptionLink = laika.createLink(); const stub = jest.fn(); stub.mockReturnValueOnce(observableError(standardError)); stub.mockReturnValueOnce(observableError(standardError)); stub.mockReturnValueOnce(observableOf(data)); const link = ApolloLink.from([interceptionLink, createStubLink(stub)]); const observable = executeLink(link, { query }); const [result1, result2, result3] = (yield waitFor(observable, observable, observable)); expect(result1).toEqual({ error: standardError }); expect(result2).toEqual({ error: standardError }); expect(result3.values).toEqual([data]); expect(stub).toHaveBeenCalledTimes(3); })); }); //# sourceMappingURL=laika.test.js.map