UNPKG

msw

Version:

Seamless REST/GraphQL API mocking library for browser and Node.js.

245 lines (216 loc) 7.48 kB
import { invariant } from 'outvariant' import { Emitter, type DefaultEventMap } from 'rettime' import { NetworkSource, type ExtractSourceEvents, } from './sources/network-source' import { type NetworkFrameResolutionContext } from './frames/network-frame' import { type UnhandledFrameHandle } from './on-unhandled-frame' import { HandlersController, InMemoryHandlersController, type AnyHandler, } from './handlers-controller' import { toReadonlyArray } from '../utils/internal/toReadonlyArray' import { Disposable } from '../utils/internal/Disposable' type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ( k: infer I, ) => void ? I : never type MergeEventMaps<Sources extends Array<NetworkSource<any>>> = UnionToIntersection<ExtractSourceEvents<Sources[number]>> extends infer R ? R extends Record<string, any> ? R : DefaultEventMap : DefaultEventMap type MaybePromise<T> = Extract<T, Promise<unknown>> extends never ? void : Promise<void> export interface DefineNetworkOptions< Sources extends Array<NetworkSource<any>>, > { /** * List of the network sources. * Every network source emits frames, and every frame describes how * to handle the various network scenarios, like mocking a response, * erroring the request, or performing it as-is. */ sources: Sources /** * List of handlers to describe the network. */ handlers?: Array<AnyHandler> | HandlersController context?: NetworkFrameResolutionContext onUnhandledFrame?: UnhandledFrameHandle } export interface NetworkApi< Sources extends Array<NetworkSource<any>>, > extends NetworkHandlersApi { readyState: NetworkReadyState /** * Enable the network interception and handling. */ enable: () => MaybePromise<ReturnType<Sources[number]['enable']>> /** * Disable the network interception and handling. */ disable: () => MaybePromise<ReturnType<Sources[number]['disable']>> /** * Configure the network instance with additional options. * The options provided in the `.configure()` call will override the same * options in the `defineNetwork()` call. */ configure: (options: Partial<DefineNetworkOptions<Sources>>) => void events: Emitter<MergeEventMaps<Sources>> } export interface NetworkHandlersApi { use: (...handlers: Array<AnyHandler>) => void resetHandlers: (...handlers: Array<AnyHandler>) => void restoreHandlers: () => void listHandlers: () => ReadonlyArray<AnyHandler> } function colorlessPromiseAll<T>(values: Array<T>): MaybePromise<T> function colorlessPromiseAll(values: Array<unknown>): Promise<void> | void { const promises: Array<Promise<void>> = [] for (const value of values) { if (value instanceof Promise) { promises.push(value) } } if (promises.length > 0) { return Promise.all(promises).then(() => {}) } } export enum NetworkReadyState { DISABLED, ENABLED, } /** * Define a network instance with the given configuration. * @example * import { InterceptorSource } from 'msw/experimental' * import { handlers } from './handlers' * * const network = defineNetwork({ * sources: [new InterceptorSource({ interceptors })], * handlers, * }) * await network.enable() */ export function defineNetwork<Sources extends Array<NetworkSource<any>>>( options: DefineNetworkOptions<Sources>, ): NetworkApi<Sources> { let readyState: NetworkReadyState = NetworkReadyState.DISABLED const events = new Emitter<MergeEventMaps<Sources>>() const disposable = new Disposable() const deriveHandlersController = ( handlers: DefineNetworkOptions<Sources>['handlers'], ) => { return handlers instanceof HandlersController ? handlers : new InMemoryHandlersController(handlers || []) } let resolvedOptions: DefineNetworkOptions<Sources> = { ...options, } /** * @note Create the handlers controller immediately because * certain setup APIs, like `setupServer`, don't await `.enable` (`.listen`). */ let handlersController = deriveHandlersController(resolvedOptions.handlers) return { get readyState() { return readyState }, events, configure(options) { invariant( readyState === NetworkReadyState.DISABLED, 'Failed to call "configure()" on the network: cannot configure an already enabled network.', ) if ( options.handlers && !Object.is(options.handlers, resolvedOptions.handlers) ) { handlersController = deriveHandlersController(options.handlers) } resolvedOptions = { ...resolvedOptions, ...options, } }, enable() { invariant( readyState === NetworkReadyState.DISABLED, 'Failed to call "enable" on the network: already enabled', ) readyState = NetworkReadyState.ENABLED /** * @note Use a session object scoped to the current "enable()" * to prevent "frame.events" listeners from surviving across enable/disable cycles. * @see The note about `AbortController` below. */ const session = { active: true } disposable['subscriptions'].push(() => { session.active = false }) const result = resolvedOptions.sources.map((source) => { /** * @note Preemptively disable the network source before enabling. * This intentionally calls only the prototype method that clears the * event listeners and nothing else. This prevents the "frame" listeners * from accumulating across enable/disable in case the source is a singleton. */ NetworkSource.prototype.disable.call(source) source.on('frame', async ({ frame }) => { frame.events.on('*', (event) => { /** * @note Prevent event forwarding manually and not via an AbortController * because certain runtimes, like Cloudflare, throw when referencing an * AbortController created in a different context. Bear in mind that the frame * events run in the patched request client context while the AbortController * is created outside, in the "defineNetwork" closure, which is a test context. */ if (!session.active) { return } events.emit(event) }) const handlers = frame.getHandlers(handlersController) await frame.resolve( handlers, resolvedOptions.onUnhandledFrame || 'warn', resolvedOptions.context, ) }) return source.enable() }) return colorlessPromiseAll(result) as MaybePromise< ReturnType<Sources[number]['enable']> > }, disable() { invariant( readyState === NetworkReadyState.ENABLED, 'Failed to call "disable" on the network: already disabled', ) readyState = NetworkReadyState.DISABLED disposable.dispose() return colorlessPromiseAll( resolvedOptions.sources.map((source) => source.disable()), ) as MaybePromise<ReturnType<Sources[number]['disable']>> }, use(...handlers) { handlersController.use(handlers) }, resetHandlers(...handlers) { handlersController.reset(handlers) }, restoreHandlers() { handlersController.restore() }, listHandlers() { return toReadonlyArray(handlersController.currentHandlers()) }, } }