diff --git a/src/node/createSetupServer.ts b/src/node/createSetupServer.ts index 3ac38b1c5..56b501ace 100644 --- a/src/node/createSetupServer.ts +++ b/src/node/createSetupServer.ts @@ -17,6 +17,7 @@ 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', @@ -137,8 +138,14 @@ export function createSetupServer( ) }, + listHandlers() { + return toReadonlyArray(currentHandlers) + }, + printHandlers() { - currentHandlers.forEach((handler) => { + const handlers = this.listHandlers() + + handlers.forEach((handler) => { const { header, callFrame } = handler.info const pragma = handler.info.hasOwnProperty('operationType') diff --git a/src/node/glossary.ts b/src/node/glossary.ts index ad4ea99cb..4bc051da6 100644 --- a/src/node/glossary.ts +++ b/src/node/glossary.ts @@ -1,11 +1,16 @@ import type { PartialDeep } from 'type-fest' import type { IsomorphicResponse } from '@mswjs/interceptors' -import { RequestHandler } from '../handlers/RequestHandler' +import { + DefaultBodyType, + RequestHandler, + RequestHandlerDefaultInfo, +} from '../handlers/RequestHandler' import { LifeCycleEventEmitter, LifeCycleEventsMap, SharedOptions, } from '../sharedOptions' +import { MockedRequest } from '../utils/request/MockedRequest' export type ServerLifecycleEventsMap = LifeCycleEventsMap @@ -40,6 +45,19 @@ export interface SetupServerApi { */ resetHandlers(...nextHandlers: RequestHandler[]): void + /** + * Returns a readonly list of cyurrently active request handlers. + * @see {@link https://mswjs.io/docs/api/setup-server/list-handlers `server.listHandlers()`} + */ + listHandlers(): ReadonlyArray< + RequestHandler< + RequestHandlerDefaultInfo, + MockedRequest, + any, + MockedRequest + > + > + /** * Lists all active request handlers. * @see {@link https://mswjs.io/docs/api/setup-server/print-handlers `server.print-handlers()`} diff --git a/src/setupWorker/glossary.ts b/src/setupWorker/glossary.ts index 417f1dbed..ef48f5cc5 100644 --- a/src/setupWorker/glossary.ts +++ b/src/setupWorker/glossary.ts @@ -6,10 +6,15 @@ import { SharedOptions, } from '../sharedOptions' import { ServiceWorkerMessage } from './start/utils/createMessageChannel' -import { DefaultBodyType, RequestHandler } from '../handlers/RequestHandler' +import { + DefaultBodyType, + RequestHandler, + RequestHandlerDefaultInfo, +} from '../handlers/RequestHandler' import type { HttpRequestEventMap, Interceptor } from '@mswjs/interceptors' import { Path } from '../utils/matching/matchRequestUrl' import { RequiredDeep } from '../typeUtils' +import { MockedRequest } from '../utils/request/MockedRequest' export type ResolvedPath = Path | URL @@ -237,6 +242,19 @@ export interface SetupWorkerApi { */ resetHandlers: (...nextHandlers: RequestHandler[]) => void + /** + * Returns a readonly list of currently active request handlers. + * @see {@link https://mswjs.io/docs/api/setup-worker/list-handlers `worker.listHandlers()`} + */ + listHandlers(): ReadonlyArray< + RequestHandler< + RequestHandlerDefaultInfo, + MockedRequest, + any, + MockedRequest + > + > + /** * Lists all active request handlers. * @see {@link https://mswjs.io/docs/api/setup-worker/print-handlers `worker.printHandlers()`} diff --git a/src/setupWorker/setupWorker.ts b/src/setupWorker/setupWorker.ts index ec171a9eb..4770ea60f 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/setupWorker/setupWorker.ts @@ -17,6 +17,7 @@ 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' interface Listener { target: EventTarget @@ -190,8 +191,14 @@ export function setupWorker( ) }, + listHandlers() { + return toReadonlyArray(context.requestHandlers) + }, + printHandlers() { - context.requestHandlers.forEach((handler) => { + const handlers = this.listHandlers() + + handlers.forEach((handler) => { const { header, callFrame } = handler.info const pragma = handler.info.hasOwnProperty('operationType') ? '[graphql]' diff --git a/src/utils/internal/toReadonlyArray.test.ts b/src/utils/internal/toReadonlyArray.test.ts new file mode 100644 index 000000000..1314aebb4 --- /dev/null +++ b/src/utils/internal/toReadonlyArray.test.ts @@ -0,0 +1,30 @@ +import { toReadonlyArray } from './toReadonlyArray' + +it('creates a copy of an array', () => { + expect(toReadonlyArray([1, 2, 3])).toEqual([1, 2, 3]) +}) + +it('does not affect the source array', () => { + const source = ['a', 'b', 'c'] + toReadonlyArray(source) + + expect(source.push('d')).toBe(4) + expect(source).toEqual(['a', 'b', 'c', 'd']) +}) + +it('forbids modifying the array copy', () => { + const source = [1, 2, 3] + const copy = toReadonlyArray(source) + + expect(() => { + // @ts-expect-error Intentional runtime misusage. + copy[2] = 1 + }).toThrow(/Cannot assign to read only property '\d+' of object/) + + expect(() => { + // @ts-expect-error Intentional runtime misusage. + copy.push(4) + }).toThrow(/Cannot add property \d+, object is not extensible/) + + expect(source).toEqual([1, 2, 3]) +}) diff --git a/src/utils/internal/toReadonlyArray.ts b/src/utils/internal/toReadonlyArray.ts new file mode 100644 index 000000000..b3d99cd9b --- /dev/null +++ b/src/utils/internal/toReadonlyArray.ts @@ -0,0 +1,8 @@ +/** + * Creates an immutable copy of the given array. + */ +export function toReadonlyArray(source: Array): ReadonlyArray { + const clone = [...source] as Array + Object.freeze(clone) + return clone +} diff --git a/test/msw-api/setup-server/listHandlers.test.ts b/test/msw-api/setup-server/listHandlers.test.ts new file mode 100644 index 000000000..2abe6f41f --- /dev/null +++ b/test/msw-api/setup-server/listHandlers.test.ts @@ -0,0 +1,78 @@ +/** + * @jest-environment node + */ +import { rest, graphql } from 'msw' +import { setupServer } from 'msw/node' + +const resolver = () => null +const github = graphql.link('https://api.github.com') + +const server = setupServer( + rest.get('https://test.mswjs.io/book/:bookId', resolver), + graphql.query('GetUser', resolver), + graphql.mutation('UpdatePost', resolver), + graphql.operation(resolver), + github.query('GetRepo', resolver), + github.operation(resolver), +) + +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +test('lists all current request handlers', () => { + const handlers = server.listHandlers() + const handlerHeaders = handlers.map((handler) => handler.info.header) + + expect(handlerHeaders).toEqual([ + 'GET https://test.mswjs.io/book/:bookId', + 'query GetUser (origin: *)', + 'mutation UpdatePost (origin: *)', + 'all (origin: *)', + 'query GetRepo (origin: https://api.github.com)', + 'all (origin: https://api.github.com)', + ]) +}) + +test('forbids from modifying the list of handlers', () => { + const handlers = server.listHandlers() + + expect(() => { + // @ts-expect-error Intentional runtime misusage. + handlers[0] = 1 + }).toThrow(/Cannot assign to read only property '\d+' of object/) + + expect(() => { + // @ts-expect-error Intentional runtime misusage. + handlers.push(1) + }).toThrow(/Cannot add property \d+, object is not extensible/) +}) + +test('includes runtime request handlers when listing handlers', () => { + server.use( + rest.get('https://test.mswjs.io/book/:bookId', resolver), + graphql.query('GetRandomNumber', resolver), + ) + + const handlers = server.listHandlers() + const handlerHeaders = handlers.map((handler) => handler.info.header) + + expect(handlerHeaders).toEqual([ + 'GET https://test.mswjs.io/book/:bookId', + 'query GetRandomNumber (origin: *)', + 'GET https://test.mswjs.io/book/:bookId', + 'query GetUser (origin: *)', + 'mutation UpdatePost (origin: *)', + 'all (origin: *)', + 'query GetRepo (origin: https://api.github.com)', + 'all (origin: https://api.github.com)', + ]) +}) diff --git a/test/msw-api/setup-worker/listHandlers.mocks.ts b/test/msw-api/setup-worker/listHandlers.mocks.ts new file mode 100644 index 000000000..e35b6214e --- /dev/null +++ b/test/msw-api/setup-worker/listHandlers.mocks.ts @@ -0,0 +1,21 @@ +import { setupWorker, rest, graphql } from 'msw' + +const resolver = () => null + +const github = graphql.link('https://api.github.com') + +const worker = setupWorker( + rest.get('https://test.mswjs.io/book/:bookId', resolver), + graphql.query('GetUser', resolver), + graphql.mutation('UpdatePost', resolver), + graphql.operation(resolver), + github.query('GetRepo', resolver), + github.operation(resolver), +) + +// @ts-ignore +window.msw = { + worker, + rest, + graphql, +} diff --git a/test/msw-api/setup-worker/listHandlers.test.ts b/test/msw-api/setup-worker/listHandlers.test.ts new file mode 100644 index 000000000..1cff4c002 --- /dev/null +++ b/test/msw-api/setup-worker/listHandlers.test.ts @@ -0,0 +1,76 @@ +import * as path from 'path' +import { pageWith } from 'page-with' +import { SetupWorkerApi, rest, graphql } from 'msw' + +declare namespace window { + export const msw: { + worker: SetupWorkerApi + rest: typeof rest + graphql: typeof graphql + } +} + +function createRuntime() { + return pageWith({ + example: path.resolve(__dirname, 'printHandlers.mocks.ts'), + }) +} + +test('lists all current request handlers', async () => { + const runtime = await createRuntime() + + const handlerHeaders = await runtime.page.evaluate(() => { + const handlers = window.msw.worker.listHandlers() + return handlers.map((handler) => handler.info.header) + }) + + expect(handlerHeaders).toEqual([ + 'GET https://test.mswjs.io/book/:bookId', + 'query GetUser (origin: *)', + 'mutation UpdatePost (origin: *)', + 'all (origin: *)', + 'query GetRepo (origin: https://api.github.com)', + 'all (origin: https://api.github.com)', + ]) +}) + +test('forbids from modifying the list of handlers', async () => { + const runtime = await createRuntime() + + /** + * @note For some reason, property assignment on frozen object + * does not throw an error: handlers[0] = 1 + */ + await expect( + runtime.page.evaluate(() => { + const handlers = window.msw.worker.listHandlers() + // @ts-expect-error Intentional runtime misusage. + handlers.push(1) + }), + ).rejects.toThrow(/Cannot add property \d+, object is not extensible/) +}) + +test('includes runtime request handlers when listing handlers', async () => { + const runtime = await createRuntime() + + const handlerHeaders = await runtime.page.evaluate(() => { + const { worker, rest, graphql } = window.msw + worker.use( + rest.get('https://test.mswjs.io/book/:bookId', () => void 0), + graphql.query('GetRandomNumber', () => void 0), + ) + const handlers = worker.listHandlers() + return handlers.map((handler) => handler.info.header) + }) + + expect(handlerHeaders).toEqual([ + 'GET https://test.mswjs.io/book/:bookId', + 'query GetRandomNumber (origin: *)', + 'GET https://test.mswjs.io/book/:bookId', + 'query GetUser (origin: *)', + 'mutation UpdatePost (origin: *)', + 'all (origin: *)', + 'query GetRepo (origin: https://api.github.com)', + 'all (origin: https://api.github.com)', + ]) +}) diff --git a/test/msw-api/setup-worker/printHandlers.test.ts b/test/msw-api/setup-worker/printHandlers.test.ts index f75e75d76..ae01288e5 100644 --- a/test/msw-api/setup-worker/printHandlers.test.ts +++ b/test/msw-api/setup-worker/printHandlers.test.ts @@ -49,7 +49,7 @@ test('lists rest request handlers', async () => { ]) }) -test('respects runtime request handlers', async () => { +test('includes runtime request handlers', async () => { const { page, consoleSpy } = await createRuntime() await page.evaluate(() => {