@zendesk/laika
Version:
Test, mock, intercept and modify Apollo Client's operations — in both browser and unit tests!
326 lines (325 loc) • 14.2 kB
TypeScript
/**
* {@link Laika | `Laika`} is the place where most of the magic happens.
* All the operations are routed through its Apollo Link, and Laika can decide what happens to them along the way.
* By default every connection is passed through and no additional action is taken.
*
* If you're using createGlobalLaikaLink, an instance of Laika is by default installed as `laika` property
* on the global object (most likely `window`), accessible as `window.laika`
* or simply as `laika`.
*
* Key functionality:
*
* - {@link Laika.intercept | `laika.intercept()`}:
*
* If you use `jest`, you can think of laika like the `jest` global,
* where the equivalent of `jest.fn()` is {@link Laika.intercept | `laika.intercept()`}
* - {@link Laika.LogApi | `laika.log`}
*
* The other thing laika is responsible for is logging.
*
* Logging functionality is behind a separate API available under {@link Laika.LogApi | `laika.log`}.
*
* @packageDocumentation
* @module Laika
*/
import { ApolloLink, FetchResult, NextLink, Observer, Operation } from '@apollo/client/core';
import type { GenerateCodeOptions } from './codeGenerator';
import type { EventFilterFn, InterceptorFn, ManInTheMiddleFn, Matcher, OnSubscribeCallback, ResultOrFn, Variables } from './typedefs';
/**
* Class responsible for managing interceptions.
* By default a singleton is installed on `globalThis` (usually `window`) under `laika`.
*
* Read more in the {@link Laika | module page} or scroll down to see it's functionality.
*
* @example
* ```js
* laika.log.startLogging();
* ```
*/
export declare class Laika {
private readonly referenceName;
constructor({ referenceName, }?: {
referenceName?: string;
});
/**
* Provides functionality to intercept, and optionally mock or modify each operation's subscription.
* The API returned is heavily inspired on jest's mocking functionality (`jest.fn()`)
* and is described in length here: {@link InterceptApi}.
*
* Every interceptor you create should be as specific as needed in a given session.
* At the very least, ensure the order of creating interceptors is from most specific, to least specific.
*
* This is because any operations that are executed by your client will end up
* being intercepted by the **first** interceptor that matches
* the constraints of the {@link Matcher}.
*
* See [*Pitfalls*](pitfalls.md) for more information.
*
* @param matcher [[include:matcher.md]]
* @param connectFutureLinksOrMitmFn If true, future links will still be called (e.g. reach the backend) and return responses. If set to a function, can serve for man-in-the-middle tinkering with the result.
* @param keepNonSubscriptionConnectionsOpen If true, queries and mutations will behave a little like subscriptions, in that you will be able to fire updates even after the initial response. Experimental.
* @example
* ```js
* const getActiveUsersInterceptor = laika.intercept({
* clientName: 'users',
* operationName: 'getActiveUsers',
* });
* ```
*/
intercept(matcher?: Matcher | undefined, connectFutureLinksOrMitmFn?: (ManInTheMiddleFn | boolean) | undefined, keepNonSubscriptionConnectionsOpen?: boolean): InterceptApi;
/**
* Modify backend (or mocked) responses before they reach subscribers.
*
* @param matcher [[include:matcher.md]]
* @param mapFn Mapping function to alter the responses.
*/
modifyRemote(matcher: Matcher | undefined, mapFn: (result: FetchResult, operation: Operation) => FetchResult): {
restore: () => void;
};
/**
* A set of functions that controls logging and recording of all (or selected) operations.
*
* Read more on the {@link Laika.LogApi | LogApi} page.
*
* @example
* ```js
* laika.log.startLogging();
* ```
*/
log: LogApi;
/**
* Use this function to create an Apollo Link that uses this Laika instance.
* Useful in unit tests.
* @param onRequest
*/
createLink(onRequest?: (operation: Operation, forward: NextLink) => void): ApolloLink;
/**
* @internal
* */
interceptor: InterceptorFn;
private readonly behaviors;
private readonly unmatchedOperationOptions;
private readonly cleanupFnPerSubscribeMeta;
/**
* @param input
*/
private getLogFunction;
/**
* @param data
*/
private logSubscribe;
private loggingMatcher;
private firstCaptureTimestamp;
private recording;
private actionName;
private isRecording;
}
export declare abstract class LogApi {
/** @ignore */
constructor();
/**
* Starts logging every matching operation and subscription to the console.
* If you did not provide a matcher, it will log everything.
* You will see queries, mutations, and subscription pushes along with their data.
*
* 
*/
startLogging(matcher?: Matcher | undefined): void;
/**
* Stops logging to the console.
*/
stopLogging(): void;
/**
* Starts the recording process. Every result will be saved until you run `log.stopRecording()`.
*
* 
*
* @param startingActionName Name what you are about to do. For example "opening a new ticket".
* @param matcher A matcher object or function to record only the events that you are interested in, for example `{operationName: 'getColors', clientName: 'backend1'}` will record only `'getColors'` operations.
*/
startRecording(startingActionName?: string | undefined, matcher?: Matcher | undefined): void;
/**
* Pauses recording without clearing what was recorded so far.
*/
stopRecording(): void;
/**
* Resets the recording in preparation of another one.
*/
resetRecording(): void;
/**
* Use this function to mark a new action if recording a sequence of events.
*
* These will show up when you generate mock code as comments,
* so you can more easily orient yourself in it.
*
* @param actionName Describe what action you will be performing, e.g. 'opening the ticket'
* @example
* ```js
* log.markAction('opening the ticket');
* // click around the site
* log.markAction('changing the assignee');
* ```
*/
markAction(actionName: string): void;
/**
* Returns a code snippet that will help you reproduce your recording without hitting actual backends.
* @param eventFilter Optionally provide a function that will only keep the events you are interested in.
* @param options Optionally provide code generation options to customize the output.
*/
generateMockCode(eventFilter?: EventFilterFn, options?: GenerateCodeOptions): string;
}
/**
* This is the mocking API that is returned after running {@link Laika.intercept | `intercept()`} on the {@link Laika | Laika}.
*
* The API is chainable, with the exception of `mockRestore()`.
*
* Inspired by `jest.fn()`.
*/
export declare abstract class InterceptApi {
/** @ignore */
constructor();
/**
* An array containing the `variables` from subsequent operations that passed through this intercept.
*
* Similar to `jest.fn().mock.calls`.
*/
readonly calls: readonly Variables[];
/**
* Sets the mock data that will be used as a default response to intercepted queries and mutations.
* If used for subscriptions, will push data immediately.
*
* Similar to `jest.fn().mockReturnValue(...)`.
*
* @param resultOrFn [[include:result-or-fn.md]]
* @param matcher [[include:mock-matcher.md]]
* @example
* Always respond with the mock to all queries/mutations intercepted
* ```js
* const intercept = laika.intercept({operationName: 'getUsers'});
* intercept.mockResult(
* {result: {data: {users: [{id: 1, name: 'Mouse'}, {id: 2, name: 'Bamboo'}]}}},
* );
* ```
* @example
* Respond with an error, but only when the operations's variables contain `{userGroup: 'elephants'}`
* ```js
* const intercept = laika.intercept({operationName: 'getUsers'});
* intercept.mockResult(
* {error: new Error(`oops, server blew up from all the elephants stomping!`)},
* {variables: {userGroup: 'elephants'}}
* );
* ```
* @example
* Respond with a customized error based on the variables:
* ```js
* const intercept = laika.intercept({operationName: 'getUsers'});
* intercept.mockResult(
* ({variables}) => ({error: new Error(`oops, server blew up from all the ${variables.userGroup} stomping!`)})
* );
* ```
*/
mockResult(resultOrFn: ResultOrFn, matcher?: Matcher | undefined): InterceptApi;
/**
* Sets the mock data that will be used as the *next* response to matching intercepted queries/mutations.
* If used for subscription operations, will immediately push provided data to the next matching request.
* Works the same as {@link InterceptApi.mockResult | `mockResult`},
* except that as soon as a matching result is found in the queue of mocks, it will not be sent again.
*
* Can be run multiple times and will send responses in order in which `mockResultOnce` was called.
*
* @param resultOrFn [[include:result-or-fn.md]]
* @param matcher [[include:mock-matcher.md]]
* @example
* Respond with the mock to the first intercepted operation with the name `getUsers`,
* then with a different mock the second time that operation is intercepted.
* ```js
* const intercept = laika.intercept({operationName: 'getUsers'});
* intercept
* .mockResultOnce(
* {result: {data: {users: [{id: 1, name: 'Mouse'}, {id: 2, name: 'Bamboo'}]}}},
* );
* .mockResultOnce(
* {result: {data: {users: [{id: 9, name: 'Ox'}, {id: 10, name: 'Fox'}]}}},
* );
* ```
*/
mockResultOnce(resultOrFn: ResultOrFn, matcher?: Matcher | undefined): InterceptApi;
/**
* In case of GraphQL subscriptions, will return synchronously if at least
* one intercepted subscription is already active.
* In other cases returns a `Promise` and behaves the same way as {@link InterceptApi.waitForNextSubscription | `waitForNextSubscription()`}.
*/
waitForActiveSubscription(): Promise<void> | undefined;
/**
* Returns a Promise that will resolve when the *next* operation is run.
* This translates to whenever a query/mutation is run, or whenever the *next* subscription is made.
*/
waitForNextSubscription(): Promise<{
operation: Operation;
observer: Observer<FetchResult>;
}>;
/**
* Push data to an already active `subscription`-type operation.
* Will throw if there are no subscribers (e.g. active `useQuery` hooks).
*
* Works similarly to {@link InterceptApi.mockResult | `mockResult(...)`}, but the listener
* is being fed the new data upon execution.
*
* Combine with {@link InterceptApi.waitForActiveSubscription | `waitForActiveSubscription()`}
* to ensure a subscription is active before calling.
*
* @param resultOrFn [[include:result-or-fn.md]]
* @param fireMatcher [[include:mock-matcher.md]]
* @example
* Push new information to a live feed:
* ```js
* const intercept = laika.intercept({operationName: 'getActiveUsersCount'});
* await intercept.waitForActiveSubscription();
* intercept.fireSubscriptionUpdate(
* {result: {data: {count: 10}}},
* );
* // e.g. assert the count displayed on the page is in fact 10
* intercept.fireSubscriptionUpdate(
* {result: {data: {count: 0}}},
* );
* // e.g. assert the page shows "there are no active users currently on the page"
* ```
*/
fireSubscriptionUpdate(resultOrFn: ResultOrFn, fireMatcher?: Matcher): InterceptApi;
/**
* Add a callback that will fire every time a component connects to the query (i.e. mounts).
* You may return a clean-up function which will be run when the query disconnects.
*/
onSubscribe(callback: OnSubscribeCallback): (() => void) | void;
/**
* If you invoke this and do not setup any mocked results, your intercepted queries will not respond,
* i.e. hang in a "loading" state, until you fire the data event manually
* (e.g. in a custom callback defined in {@link InterceptApi.onSubscribe `onSubscribe(callback)`}.
*
* Does not affect `subscription` operations which will not reach the backend regardless of this setting (unless the `connectFutureLinksOrMitmFn` argument was set).
*
* Opposite of {@link InterceptApi.allowNetworkFallback `allowNetworkFallback()`}.
*/
disableNetworkFallback(): void;
/**
* This restores the default behavior: both queries and mutations
* will be passed to future links (e.g. your backend) and back to the components.
*
* Does not affect `subscription` operations which will not reach the backend regardless of this setting (unless the `connectFutureLinksOrMitmFn` argument was set).
*
* Opposite of {@link InterceptApi.disableNetworkFallback `disableNetworkFallback()`}.
*/
allowNetworkFallback(): void;
/**
* Resets the mock configuration to its initial state and reenables the intercept if disabled by {@link InterceptApi.mockRestore `mockRestore()`}.
*/
mockReset(): InterceptApi;
/**
* Removes the intercept completely and re-establishes connectivity in current and _future_ intercepted operations.
* Note the word _future_. Any connections that were established prior to running this command,
* will not automatically switch over to other mocks. This will mostly affect subscriptions.
* Ideally, keep a reference to the original intercept throughout the duration of your session
* and simply `intercept.reset()` if you need to restore connectivity or setup a different scenario.
*/
mockRestore(): void;
}