diff --git a/src/SetupApi.ts b/src/SetupApi.ts new file mode 100644 index 000000000..e5415b31a --- /dev/null +++ b/src/SetupApi.ts @@ -0,0 +1,97 @@ +import { invariant } from 'outvariant' +import { EventMapType, StrictEventEmitter } from 'strict-event-emitter' +import { + DefaultBodyType, + RequestHandler, + RequestHandlerDefaultInfo, +} from './handlers/RequestHandler' +import { LifeCycleEventEmitter } from './sharedOptions' +import { devUtils } from './utils/internal/devUtils' +import { pipeEvents } from './utils/internal/pipeEvents' +import { toReadonlyArray } from './utils/internal/toReadonlyArray' +import { MockedRequest } from './utils/request/MockedRequest' + +/** + * Generic class for the mock API setup. + */ +export abstract class SetupApi { + protected initialHandlers: ReadonlyArray + protected currentHandlers: Array + protected readonly emitter: StrictEventEmitter + protected readonly publicEmitter: StrictEventEmitter + + public readonly events: LifeCycleEventEmitter + + constructor(initialHandlers: Array) { + this.validateHandlers(initialHandlers) + + this.initialHandlers = toReadonlyArray(initialHandlers) + this.currentHandlers = [...initialHandlers] + + this.emitter = new StrictEventEmitter() + this.publicEmitter = new StrictEventEmitter() + pipeEvents(this.emitter, this.publicEmitter) + + this.events = this.createLifeCycleEvents() + } + + private validateHandlers(handlers: ReadonlyArray): void { + // Guard against incorrect call signature of the setup API. + for (const handler of handlers) { + invariant( + !Array.isArray(handler), + devUtils.formatMessage( + 'Failed to construct "%s" given an Array of request handlers. Make sure you spread the request handlers when calling the respective setup function.', + ), + this.constructor.name, + ) + } + } + + protected dispose(): void { + this.emitter.removeAllListeners() + this.publicEmitter.removeAllListeners() + } + + public use(...runtimeHandlers: Array): void { + this.currentHandlers.unshift(...runtimeHandlers) + } + + public restoreHandlers(): void { + this.currentHandlers.forEach((handler) => { + handler.markAsSkipped(false) + }) + } + + public resetHandlers(...nextHandlers: Array): void { + this.currentHandlers = + nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers] + } + + public listHandlers(): ReadonlyArray< + RequestHandler< + RequestHandlerDefaultInfo, + MockedRequest, + any, + MockedRequest + > + > { + return toReadonlyArray(this.currentHandlers) + } + + private createLifeCycleEvents(): LifeCycleEventEmitter { + return { + on: (...args) => { + return this.publicEmitter.on(...args) + }, + removeListener: (...args) => { + return this.publicEmitter.removeListener(...args) + }, + removeAllListeners: (...args: any) => { + return this.publicEmitter.removeAllListeners(...args) + }, + } + } + + abstract printHandlers(): void +} diff --git a/src/index.ts b/src/index.ts index b126e0f15..f4cc2e26c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,9 @@ import * as context from './context' export { context } export { setupWorker } from './setupWorker/setupWorker' + +export { SetupApi } from './SetupApi' + export { response, defaultResponse, diff --git a/src/native/index.ts b/src/native/index.ts index 5f0b13776..26d2d364e 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -1,6 +1,16 @@ import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest' -import { createSetupServer } from '../node/createSetupServer' +import { RequestHandler } from '../handlers/RequestHandler' +import { SetupServerApi } from '../node/SetupServerApi' -// Provision request interception via patching the `XMLHttpRequest` class only -// in React Native. There is no `http`/`https` modules in that environment. -export const setupServer = createSetupServer(XMLHttpRequestInterceptor) +/** + * Sets up a requests interception in React Native with the given request handlers. + * @param {RequestHandler[]} handlers List of request handlers. + * @see {@link https://mswjs.io/docs/api/setup-server `setupServer`} + */ +export function setupServer( + ...handlers: Array +): SetupServerApi { + // Provision request interception via patching the `XMLHttpRequest` class only + // in React Native. There is no `http`/`https` modules in that environment. + return new SetupServerApi([XMLHttpRequestInterceptor], handlers) +} diff --git a/src/node/SetupServerApi.ts b/src/node/SetupServerApi.ts new file mode 100644 index 000000000..c64cd2b2e --- /dev/null +++ b/src/node/SetupServerApi.ts @@ -0,0 +1,158 @@ +import chalk from 'chalk' +import { invariant } from 'outvariant' +import { + BatchInterceptor, + HttpRequestEventMap, + Interceptor, + InterceptorReadyState, + IsomorphicResponse, + MockedResponse as MockedInterceptedResponse, +} from '@mswjs/interceptors' +import { SetupApi } from '../SetupApi' +import { RequestHandler } from '../handlers/RequestHandler' +import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' +import { RequiredDeep } from '../typeUtils' +import { mergeRight } from '../utils/internal/mergeRight' +import { MockedRequest } from '../utils/request/MockedRequest' +import { handleRequest } from '../utils/handleRequest' +import { devUtils } from '../utils/internal/devUtils' + +/** + * @see https://github.com/mswjs/msw/pull/1399 + */ +const { bold } = chalk + +export type ServerLifecycleEventsMap = LifeCycleEventsMap + +const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { + onUnhandledRequest: 'warn', +} + +export class SetupServerApi extends SetupApi { + protected readonly interceptor: BatchInterceptor< + Array>, + HttpRequestEventMap + > + private resolvedOptions: RequiredDeep + + constructor( + interceptors: Array<{ + new (): Interceptor + }>, + handlers: Array, + ) { + super(handlers) + + this.interceptor = new BatchInterceptor({ + name: 'setup-server', + interceptors: interceptors.map((Interceptor) => new Interceptor()), + }) + this.resolvedOptions = {} as RequiredDeep + + this.init() + } + + /** + * Subscribe to all requests that are using the interceptor object + */ + private init(): void { + this.interceptor.on('request', async (request) => { + const mockedRequest = new MockedRequest(request.url, { + ...request, + body: await request.arrayBuffer(), + }) + + const response = await handleRequest< + MockedInterceptedResponse & { delay?: number } + >( + mockedRequest, + this.currentHandlers, + this.resolvedOptions, + this.emitter, + { + transformResponse(response) { + return { + status: response.status, + statusText: response.statusText, + headers: response.headers.all(), + body: response.body, + delay: response.delay, + } + }, + }, + ) + + if (response) { + // Delay Node.js responses in the listener so that + // the response lookup logic is not concerned with responding + // in any way. The same delay is implemented in the worker. + if (response.delay) { + await new Promise((resolve) => { + setTimeout(resolve, response.delay) + }) + } + + request.respondWith(response) + } + + return + }) + + this.interceptor.on('response', (request, response) => { + if (!request.id) { + return + } + + if (response.headers.get('x-powered-by') === 'msw') { + this.emitter.emit('response:mocked', response, request.id) + } else { + this.emitter.emit('response:bypass', response, request.id) + } + }) + } + + public listen(options: Partial = {}): void { + this.resolvedOptions = mergeRight( + DEFAULT_LISTEN_OPTIONS, + options, + ) as RequiredDeep + + // Apply the interceptor when starting the server. + this.interceptor.apply() + + // Assert that the interceptor has been applied successfully. + // Also guards us from forgetting to call "interceptor.apply()" + // as a part of the "listen" method. + invariant( + [InterceptorReadyState.APPLYING, InterceptorReadyState.APPLIED].includes( + this.interceptor.readyState, + ), + devUtils.formatMessage( + 'Failed to start "setupServer": the interceptor failed to apply. This is likely an issue with the library and you should report it at "%s".', + ), + 'https://github.com/mswjs/msw/issues/new/choose', + ) + } + + public printHandlers(): void { + const handlers = this.listHandlers() + + handlers.forEach((handler) => { + const { header, callFrame } = handler.info + + const pragma = handler.info.hasOwnProperty('operationType') + ? '[graphql]' + : '[rest]' + + console.log(`\ +${bold(`${pragma} ${header}`)} + Declaration: ${callFrame} +`) + }) + } + + public close(): void { + super.dispose() + this.interceptor.dispose() + } +} diff --git a/src/node/createSetupServer.ts b/src/node/createSetupServer.ts deleted file mode 100644 index dc3ee55d7..000000000 --- a/src/node/createSetupServer.ts +++ /dev/null @@ -1,182 +0,0 @@ -import chalk from 'chalk' -const { bold } = chalk -import { isNodeProcess } from 'is-node-process' -import { StrictEventEmitter } from 'strict-event-emitter' -import { - BatchInterceptor, - MockedResponse as MockedInterceptedResponse, - Interceptor, - HttpRequestEventMap, -} from '@mswjs/interceptors' -import * as requestHandlerUtils from '../utils/internal/requestHandlerUtils' -import { ServerLifecycleEventsMap, SetupServerApi } from './glossary' -import { SharedOptions } from '../sharedOptions' -import { RequestHandler } from '../handlers/RequestHandler' -import { handleRequest } from '../utils/handleRequest' -import { mergeRight } from '../utils/internal/mergeRight' -import { devUtils } from '../utils/internal/devUtils' -import { pipeEvents } from '../utils/internal/pipeEvents' -import { RequiredDeep } from '../typeUtils' -import { MockedRequest } from '../utils/request/MockedRequest' -import { toReadonlyArray } from '../utils/internal/toReadonlyArray' - -const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { - onUnhandledRequest: 'warn', -} - -/** - * Creates a `setupServer` API using given request interceptors. - * Useful to generate identical API using different patches to request issuing modules. - */ -export function createSetupServer( - ...interceptors: { new (): Interceptor }[] -) { - const emitter = new StrictEventEmitter() - const publicEmitter = new StrictEventEmitter() - pipeEvents(emitter, publicEmitter) - - return function setupServer( - ...requestHandlers: RequestHandler[] - ): SetupServerApi { - requestHandlers.forEach((handler) => { - if (Array.isArray(handler)) - throw new Error( - devUtils.formatMessage( - 'Failed to call "setupServer" given an Array of request handlers (setupServer([a, b])), expected to receive each handler individually: setupServer(a, b).', - ), - ) - }) - - // Store the list of request handlers for the current server instance, - // so it could be modified at a runtime. - let currentHandlers: RequestHandler[] = [...requestHandlers] - - // Error when attempting to run this function in a browser environment. - if (!isNodeProcess()) { - throw new Error( - devUtils.formatMessage( - 'Failed to execute `setupServer` in the environment that is not Node.js (i.e. a browser). Consider using `setupWorker` instead.', - ), - ) - } - - let resolvedOptions = {} as RequiredDeep - - const interceptor = new BatchInterceptor({ - name: 'setup-server', - interceptors: interceptors.map((Interceptor) => new Interceptor()), - }) - - interceptor.on('request', async function setupServerListener(request) { - const mockedRequest = new MockedRequest(request.url, { - ...request, - body: await request.arrayBuffer(), - }) - - const response = await handleRequest< - MockedInterceptedResponse & { delay?: number } - >(mockedRequest, currentHandlers, resolvedOptions, emitter, { - transformResponse(response) { - return { - status: response.status, - statusText: response.statusText, - headers: response.headers.all(), - body: response.body, - delay: response.delay, - } - }, - }) - - if (response) { - // Delay Node.js responses in the listener so that - // the response lookup logic is not concerned with responding - // in any way. The same delay is implemented in the worker. - if (response.delay) { - await new Promise((resolve) => { - setTimeout(resolve, response.delay) - }) - } - - request.respondWith(response) - } - - return - }) - - interceptor.on('response', (request, response) => { - if (!request.id) { - return - } - - if (response.headers.get('x-powered-by') === 'msw') { - emitter.emit('response:mocked', response, request.id) - } else { - emitter.emit('response:bypass', response, request.id) - } - }) - - return { - listen(options) { - resolvedOptions = mergeRight( - DEFAULT_LISTEN_OPTIONS, - options || {}, - ) as RequiredDeep - interceptor.apply() - }, - - use(...handlers) { - requestHandlerUtils.use(currentHandlers, ...handlers) - }, - - restoreHandlers() { - requestHandlerUtils.restoreHandlers(currentHandlers) - }, - - resetHandlers(...nextHandlers) { - currentHandlers = requestHandlerUtils.resetHandlers( - requestHandlers, - ...nextHandlers, - ) - }, - - listHandlers() { - return toReadonlyArray(currentHandlers) - }, - - printHandlers() { - const handlers = this.listHandlers() - - handlers.forEach((handler) => { - const { header, callFrame } = handler.info - - const pragma = handler.info.hasOwnProperty('operationType') - ? '[graphql]' - : '[rest]' - - console.log(`\ -${bold(`${pragma} ${header}`)} - Declaration: ${callFrame} -`) - }) - }, - - events: { - on(...args) { - return publicEmitter.on(...args) - }, - removeListener(...args) { - return publicEmitter.removeListener(...args) - }, - removeAllListeners(...args) { - return publicEmitter.removeAllListeners(...args) - }, - }, - - close() { - emitter.removeAllListeners() - publicEmitter.removeAllListeners() - interceptor.dispose() - }, - } - } -} diff --git a/src/node/index.ts b/src/node/index.ts index 45a8cde32..12f6ac48a 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -1,2 +1,3 @@ +export { ServerLifecycleEventsMap } from './SetupServerApi' export { setupServer } from './setupServer' export type { SetupServerApi } from './glossary' diff --git a/src/node/setupServer.ts b/src/node/setupServer.ts index 3b3426528..bb3067b99 100644 --- a/src/node/setupServer.ts +++ b/src/node/setupServer.ts @@ -1,15 +1,18 @@ import { ClientRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/ClientRequest/index.js' import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest/index.js' -import { createSetupServer } from './createSetupServer' +import { RequestHandler } from '../handlers/RequestHandler' +import { SetupServerApi } from './SetupServerApi' /** * Sets up a requests interception in Node.js with the given request handlers. - * @param {RequestHandler[]} requestHandlers List of request handlers. + * @param {RequestHandler[]} handlers List of request handlers. * @see {@link https://mswjs.io/docs/api/setup-server `setupServer`} */ -export const setupServer = createSetupServer( - // List each interceptor separately instead of using the "node" preset - // so that MSW wouldn't bundle the unnecessary classes (i.e. "SocketPolyfill"). - ClientRequestInterceptor, - XMLHttpRequestInterceptor, -) +export const setupServer = ( + ...handlers: Array +): SetupServerApi => { + return new SetupServerApi( + [ClientRequestInterceptor, XMLHttpRequestInterceptor], + handlers, + ) +} diff --git a/src/setupWorker/glossary.ts b/src/setupWorker/glossary.ts index 6734242d5..bcfa5b202 100644 --- a/src/setupWorker/glossary.ts +++ b/src/setupWorker/glossary.ts @@ -99,7 +99,7 @@ export type WorkerLifecycleEventsMap = LifeCycleEventsMap export interface SetupWorkerInternalContext { isMockingEnabled: boolean - startOptions?: RequiredDeep + startOptions: RequiredDeep worker: ServiceWorker | null registration: ServiceWorkerRegistration | null requestHandlers: RequestHandler[] diff --git a/src/setupWorker/setupWorker.ts b/src/setupWorker/setupWorker.ts index 4770ea60f..39ce50b23 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/setupWorker/setupWorker.ts @@ -1,23 +1,24 @@ +import { invariant } from 'outvariant' import { isNodeProcess } from 'is-node-process' -import { StrictEventEmitter } from 'strict-event-emitter' import { SetupWorkerInternalContext, - SetupWorkerApi, ServiceWorkerIncomingEventsMap, WorkerLifecycleEventsMap, + StartReturnType, + StopHandler, + StartHandler, + StartOptions, } from './glossary' import { createStartHandler } from './start/createStartHandler' import { createStop } from './stop/createStop' -import * as requestHandlerUtils from '../utils/internal/requestHandlerUtils' import { ServiceWorkerMessage } from './start/utils/createMessageChannel' import { RequestHandler } from '../handlers/RequestHandler' -import { RestHandler } from '../handlers/RestHandler' -import { prepareStartHandler } from './start/utils/prepareStartHandler' +import { DEFAULT_START_OPTIONS } from './start/utils/prepareStartHandler' import { createFallbackStart } from './start/createFallbackStart' import { createFallbackStop } from './stop/createFallbackStop' import { devUtils } from '../utils/internal/devUtils' -import { pipeEvents } from '../utils/internal/pipeEvents' -import { toReadonlyArray } from '../utils/internal/toReadonlyArray' +import { SetupApi } from '../SetupApi' +import { mergeRight } from '../utils/internal/mergeRight' interface Listener { target: EventTarget @@ -25,214 +26,203 @@ interface Listener { callback: EventListener } -// Declare the list of event handlers on the module's scope -// so it persists between Fash refreshes of the application's code. -let listeners: Listener[] = [] +export class SetupWorkerApi extends SetupApi { + private context: SetupWorkerInternalContext + private startHandler: StartHandler = null as any + private stopHandler: StopHandler = null as any + private listeners: Array -/** - * Creates a new mock Service Worker registration - * with the given request handlers. - * @param {RequestHandler[]} requestHandlers List of request handlers - * @see {@link https://mswjs.io/docs/api/setup-worker `setupWorker`} - */ -export function setupWorker( - ...requestHandlers: RequestHandler[] -): SetupWorkerApi { - requestHandlers.forEach((handler) => { - if (Array.isArray(handler)) - throw new Error( - devUtils.formatMessage( - 'Failed to call "setupWorker" given an Array of request handlers (setupWorker([a, b])), expected to receive each handler individually: setupWorker(a, b).', - ), - ) - }) - - // Error when attempting to run this function in a Node.js environment. - if (isNodeProcess()) { - throw new Error( + constructor(handlers: Array) { + super(handlers) + + invariant( + !isNodeProcess(), devUtils.formatMessage( 'Failed to execute `setupWorker` in a non-browser environment. Consider using `setupServer` for Node.js environment instead.', ), ) + + this.listeners = [] + this.context = this.createWorkerContext() } - const emitter = new StrictEventEmitter() - const publicEmitter = new StrictEventEmitter() - pipeEvents(emitter, publicEmitter) - - const context: SetupWorkerInternalContext = { - // Mocking is not considered enabled until the worker - // signals back the successful activation event. - isMockingEnabled: false, - startOptions: undefined, - worker: null, - registration: null, - requestHandlers: [...requestHandlers], - emitter, - workerChannel: { - on(eventType, callback) { - context.events.addListener( - navigator.serviceWorker, - 'message', - (event: MessageEvent) => { - // Avoid messages broadcasted from unrelated workers. - if (event.source !== context.worker) { - return - } + private createWorkerContext(): SetupWorkerInternalContext { + const context = { + // Mocking is not considered enabled until the worker + // signals back the successful activation event. + isMockingEnabled: false, + startOptions: null as any, + worker: null, + registration: null, + requestHandlers: this.currentHandlers, + emitter: this.emitter, + workerChannel: { + on: ( + eventType: EventType, + callback: ( + event: MessageEvent, + message: ServiceWorkerMessage< + EventType, + ServiceWorkerIncomingEventsMap[EventType] + >, + ) => void, + ) => { + this.context.events.addListener( + navigator.serviceWorker, + 'message', + (event: MessageEvent) => { + // Avoid messages broadcasted from unrelated workers. + if (event.source !== this.context.worker) { + return + } + + const message = event.data as ServiceWorkerMessage< + typeof eventType, + any + > + + if (!message) { + return + } - const message = event.data as ServiceWorkerMessage< + if (message.type === eventType) { + callback(event, message) + } + }, + ) + }, + send: (type: any) => { + this.context.worker?.postMessage(type) + }, + }, + events: { + addListener: ( + target: EventTarget, + eventType: string, + callback: EventListener, + ) => { + target.addEventListener(eventType, callback) + this.listeners.push({ eventType, target, callback }) + + return () => { + target.removeEventListener(eventType, callback) + } + }, + removeAllListeners: () => { + for (const { target, eventType, callback } of this.listeners) { + target.removeEventListener(eventType, callback) + } + this.listeners = [] + }, + once: ( + eventType: EventType, + ) => { + const bindings: Array<() => void> = [] + + return new Promise< + ServiceWorkerMessage< typeof eventType, - any + ServiceWorkerIncomingEventsMap[typeof eventType] > - - if (!message) { - return + >((resolve, reject) => { + const handleIncomingMessage = (event: MessageEvent) => { + try { + const message = event.data + + if (message.type === eventType) { + resolve(message) + } + } catch (error) { + reject(error) + } } - if (message.type === eventType) { - callback(event, message) - } - }, - ) + bindings.push( + this.context.events.addListener( + navigator.serviceWorker, + 'message', + handleIncomingMessage, + ), + this.context.events.addListener( + navigator.serviceWorker, + 'messageerror', + reject, + ), + ) + }).finally(() => { + bindings.forEach((unbind) => unbind()) + }) + }, }, - send(type) { - context.worker?.postMessage(type) + useFallbackMode: + !('serviceWorker' in navigator) || location.protocol === 'file:', + } + + /** + * @todo Not sure I like this but "this.currentHandlers" + * updates never bubble to "this.context.requestHandlers". + */ + Object.defineProperties(context, { + requestHandlers: { + get: () => this.currentHandlers, }, - }, - events: { - addListener( - target: EventTarget, - eventType: string, - callback: EventListener, - ) { - target.addEventListener(eventType, callback) - listeners.push({ eventType, target, callback }) - - return () => { - target.removeEventListener(eventType, callback) - } - }, - removeAllListeners() { - for (const { target, eventType, callback } of listeners) { - target.removeEventListener(eventType, callback) - } - listeners = [] - }, - once(eventType) { - const bindings: Array<() => void> = [] - - return new Promise< - ServiceWorkerMessage< - typeof eventType, - ServiceWorkerIncomingEventsMap[typeof eventType] - > - >((resolve, reject) => { - const handleIncomingMessage = (event: MessageEvent) => { - try { - const message = event.data + }) - if (message.type === eventType) { - resolve(message) - } - } catch (error) { - reject(error) - } - } + this.startHandler = context.useFallbackMode + ? createFallbackStart(context) + : createStartHandler(context) - bindings.push( - context.events.addListener( - navigator.serviceWorker, - 'message', - handleIncomingMessage, - ), - context.events.addListener( - navigator.serviceWorker, - 'messageerror', - reject, - ), - ) - }).finally(() => { - bindings.forEach((unbind) => unbind()) - }) - }, - }, - useFallbackMode: - !('serviceWorker' in navigator) || location.protocol === 'file:', + this.stopHandler = context.useFallbackMode + ? createFallbackStop(context) + : createStop(context) + + return context } - const startHandler = context.useFallbackMode - ? createFallbackStart(context) - : createStartHandler(context) - const stopHandler = context.useFallbackMode - ? createFallbackStop(context) - : createStop(context) - - return { - start: prepareStartHandler(startHandler, context), - stop() { - context.events.removeAllListeners() - context.emitter.removeAllListeners() - publicEmitter.removeAllListeners() - stopHandler() - }, - - use(...handlers) { - requestHandlerUtils.use(context.requestHandlers, ...handlers) - }, - - restoreHandlers() { - requestHandlerUtils.restoreHandlers(context.requestHandlers) - }, - - resetHandlers(...nextHandlers) { - context.requestHandlers = requestHandlerUtils.resetHandlers( - requestHandlers, - ...nextHandlers, - ) - }, - - listHandlers() { - return toReadonlyArray(context.requestHandlers) - }, - - printHandlers() { - const handlers = this.listHandlers() - - handlers.forEach((handler) => { - const { header, callFrame } = handler.info - const pragma = handler.info.hasOwnProperty('operationType') - ? '[graphql]' - : '[rest]' - - console.groupCollapsed(`${pragma} ${header}`) - - if (callFrame) { - console.log(`Declaration: ${callFrame}`) - } - - console.log('Handler:', handler) - - if (handler instanceof RestHandler) { - console.log( - 'Match:', - `https://mswjs.io/repl?path=${handler.info.path}`, - ) - } + public async start(options: StartOptions = {}): StartReturnType { + this.context.startOptions = mergeRight( + DEFAULT_START_OPTIONS, + options, + ) as SetupWorkerInternalContext['startOptions'] - console.groupEnd() - }) - }, + return await this.startHandler(this.context.startOptions, options) + } - events: { - on(...args) { - return publicEmitter.on(...args) - }, - removeListener(...args) { - return publicEmitter.removeListener(...args) - }, - removeAllListeners(...args) { - return publicEmitter.removeAllListeners(...args) - }, - }, + public printHandlers(): void { + const handlers = this.listHandlers() + + handlers.forEach((handler) => { + const { header, callFrame } = handler.info + const pragma = handler.info.hasOwnProperty('operationType') + ? '[graphql]' + : '[rest]' + + console.groupCollapsed(`${pragma} ${header}`) + + if (callFrame) { + console.log(`Declaration: ${callFrame}`) + } + + console.log('Handler:', handler) + console.groupEnd() + }) } + + public stop(): void { + super.dispose() + this.context.events.removeAllListeners() + this.context.emitter.removeAllListeners() + this.stopHandler() + } +} + +/** + * Sets up a requests interception in the browser with the given request handlers. + * @param {RequestHandler[]} handlers List of request handlers. + * @see {@link https://mswjs.io/docs/api/setup-worker `setupWorker`} + */ +export function setupWorker( + ...handlers: Array +): SetupWorkerApi { + return new SetupWorkerApi(handlers) } diff --git a/test/msw-api/setup-server/input-validation.test.ts b/test/msw-api/setup-server/input-validation.test.ts index a5223f986..4c9d0cb16 100644 --- a/test/msw-api/setup-server/input-validation.test.ts +++ b/test/msw-api/setup-server/input-validation.test.ts @@ -14,6 +14,6 @@ test('throws an error given an Array of request handlers to setupServer', async } expect(createServer).toThrow( - `[MSW] Failed to call "setupServer" given an Array of request handlers (setupServer([a, b])), expected to receive each handler individually: setupServer(a, b).`, + `[MSW] Failed to construct "SetupServerApi" given an Array of request handlers. Make sure you spread the request handlers when calling the respective setup function.`, ) }) diff --git a/test/msw-api/setup-server/printHandlers.test.ts b/test/msw-api/setup-server/printHandlers.test.ts index c4e5836ff..855a8b5c2 100644 --- a/test/msw-api/setup-server/printHandlers.test.ts +++ b/test/msw-api/setup-server/printHandlers.test.ts @@ -36,6 +36,7 @@ afterAll(() => { test('lists all current request handlers', () => { server.printHandlers() + // Test failed here, commenting so it shows up in the PR expect(console.log).toBeCalledTimes(6) expect(console.log).toBeCalledWith(`\ @@ -83,11 +84,11 @@ test('respects runtime request handlers when listing handlers', () => { expect(console.log).toBeCalledWith(`\ ${bold('[rest] GET https://test.mswjs.io/book/:bookId')} - Declaration: ${__filename}:74:10 + Declaration: ${__filename}:75:10 `) expect(console.log).toBeCalledWith(`\ ${bold('[graphql] query GetRandomNumber (origin: *)')} - Declaration: ${__filename}:75:13 + Declaration: ${__filename}:76:13 `) }) diff --git a/test/msw-api/setup-worker/input-validation.test.ts b/test/msw-api/setup-worker/input-validation.test.ts index 2106d7799..d2a02661b 100644 --- a/test/msw-api/setup-worker/input-validation.test.ts +++ b/test/msw-api/setup-worker/input-validation.test.ts @@ -16,7 +16,7 @@ test('throws an error given an Array of request handlers to "setupWorker"', asyn expect(exceptions).toEqual( expect.arrayContaining([ expect.stringContaining( - '[MSW] Failed to call "setupWorker" given an Array of request handlers (setupWorker([a, b])), expected to receive each handler individually: setupWorker(a, b).', + '[MSW] Failed to construct "SetupWorkerApi" given an Array of request handlers. Make sure you spread the request handlers when calling the respective setup function.', ), ]), ) diff --git a/test/msw-api/setup-worker/printHandlers.test.ts b/test/msw-api/setup-worker/printHandlers.test.ts index ae01288e5..016968f06 100644 --- a/test/msw-api/setup-worker/printHandlers.test.ts +++ b/test/msw-api/setup-worker/printHandlers.test.ts @@ -24,7 +24,6 @@ test('lists rest request handlers', async () => { }) const startGroupCollapsed = consoleSpy.get('startGroupCollapsed') - const log = consoleSpy.get('log') expect(startGroupCollapsed).toHaveLength(6) expect(startGroupCollapsed).toContain( @@ -41,12 +40,6 @@ test('lists rest request handlers', async () => { expect(startGroupCollapsed).toContain( '[graphql] all (origin: https://api.github.com)', ) - - const matchSuggestions = log.filter((message) => message.startsWith('Match:')) - expect(matchSuggestions).toHaveLength(1) - expect(matchSuggestions).toEqual([ - 'Match: https://mswjs.io/repl?path=https://test.mswjs.io/book/:bookId', - ]) }) test('includes runtime request handlers', async () => { @@ -63,7 +56,6 @@ test('includes runtime request handlers', async () => { }) const startGroupCollapsed = consoleSpy.get('startGroupCollapsed') - const log = consoleSpy.get('log') expect(startGroupCollapsed).toHaveLength(8) @@ -71,10 +63,4 @@ test('includes runtime request handlers', async () => { expect(startGroupCollapsed).toContain( '[graphql] query SubmitTransaction (origin: *)', ) - - const matchSuggestions = log.filter((message) => message.startsWith('Match:')) - expect(matchSuggestions).toHaveLength(2) - expect(matchSuggestions).toContain( - 'Match: https://mswjs.io/repl?path=/profile', - ) })