UNPKG

@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
/** * {@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. * * ![Example logging output](media://example-logging.png) */ 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()`. * * ![Example recording output](media://example-recording.png) * * @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; }