@zendesk/laika
Version:
Test, mock, intercept and modify Apollo Client's operations — in both browser and unit tests!
416 lines • 19.3 kB
JavaScript
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