From f6318b26d91107992ad8b1a5caf895cdcabf85a5 Mon Sep 17 00:00:00 2001 From: Alexei Darmin Date: Wed, 24 Aug 2022 13:13:46 -0400 Subject: [PATCH 1/4] feat(createsetupserver.ts): add listHandlers method that returns active handlers --- src/node/createSetupServer.ts | 4 ++ src/node/glossary.ts | 12 ++++- .../msw-api/setup-server/listHandlers.test.ts | 45 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 test/msw-api/setup-server/listHandlers.test.ts diff --git a/src/node/createSetupServer.ts b/src/node/createSetupServer.ts index 3ac38b1c5..3ea3d6c23 100644 --- a/src/node/createSetupServer.ts +++ b/src/node/createSetupServer.ts @@ -137,6 +137,10 @@ export function createSetupServer( ) }, + listHandlers() { + return currentHandlers + }, + printHandlers() { currentHandlers.forEach((handler) => { const { header, callFrame } = handler.info diff --git a/src/node/glossary.ts b/src/node/glossary.ts index ad4ea99cb..04b743df1 100644 --- a/src/node/glossary.ts +++ b/src/node/glossary.ts @@ -1,11 +1,13 @@ + 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 +42,12 @@ export interface SetupServerApi { */ resetHandlers(...nextHandlers: RequestHandler[]): void + /** + * Returns lists of all active request handlers. + * @see {@link https://mswjs.io/docs/api/setup-server/list-handlers `server.list-handlers()`} + */ + listHandlers(): RequestHandler, any, MockedRequest>[] + /** * Lists all active request handlers. * @see {@link https://mswjs.io/docs/api/setup-server/print-handlers `server.print-handlers()`} @@ -47,4 +55,4 @@ export interface SetupServerApi { printHandlers(): void events: LifeCycleEventEmitter -} +} \ No newline at end of file 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..b4a714c83 --- /dev/null +++ b/test/msw-api/setup-server/listHandlers.test.ts @@ -0,0 +1,45 @@ +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() + + expect(handlers.length).toEqual(6) +}) + +test('respects 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() + + // Runtime handlers are prepended to the list of handlers + // and they DON'T remove the handlers they may override. + expect(handlers.length).toEqual(8) +}) From f29c2a9818293ee5633ef4ed491c9b0e760c03a0 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 30 Aug 2022 13:03:20 +0200 Subject: [PATCH 2/4] fix(setupServer): return readonly array in "listHandlers()" --- src/node/createSetupServer.ts | 7 +++- src/node/glossary.ts | 24 +++++++---- src/utils/internal/toReadonlyArray.test.ts | 22 ++++++++++ src/utils/internal/toReadonlyArray.ts | 8 ++++ .../msw-api/setup-server/listHandlers.test.ts | 40 ++++++++++++++++--- 5 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 src/utils/internal/toReadonlyArray.test.ts create mode 100644 src/utils/internal/toReadonlyArray.ts diff --git a/src/node/createSetupServer.ts b/src/node/createSetupServer.ts index 3ea3d6c23..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', @@ -138,11 +139,13 @@ export function createSetupServer( }, listHandlers() { - return currentHandlers + 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 04b743df1..4bc051da6 100644 --- a/src/node/glossary.ts +++ b/src/node/glossary.ts @@ -1,7 +1,10 @@ - import type { PartialDeep } from 'type-fest' import type { IsomorphicResponse } from '@mswjs/interceptors' -import { DefaultBodyType, RequestHandler, RequestHandlerDefaultInfo } from '../handlers/RequestHandler' +import { + DefaultBodyType, + RequestHandler, + RequestHandlerDefaultInfo, +} from '../handlers/RequestHandler' import { LifeCycleEventEmitter, LifeCycleEventsMap, @@ -43,10 +46,17 @@ export interface SetupServerApi { resetHandlers(...nextHandlers: RequestHandler[]): void /** - * Returns lists of all active request handlers. - * @see {@link https://mswjs.io/docs/api/setup-server/list-handlers `server.list-handlers()`} - */ - listHandlers(): RequestHandler, any, MockedRequest>[] + * 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. @@ -55,4 +65,4 @@ export interface SetupServerApi { printHandlers(): void events: LifeCycleEventEmitter -} \ No newline at end of file +} diff --git a/src/utils/internal/toReadonlyArray.test.ts b/src/utils/internal/toReadonlyArray.test.ts new file mode 100644 index 000000000..29e7d36d9 --- /dev/null +++ b/src/utils/internal/toReadonlyArray.test.ts @@ -0,0 +1,22 @@ +import { toReadonlyArray } from './toReadonlyArray' + +it('creates a copy of an array', () => { + expect(toReadonlyArray([1, 2, 3])).toEqual([1, 2, 3]) +}) + +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 index b4a714c83..1c7b00106 100644 --- a/test/msw-api/setup-server/listHandlers.test.ts +++ b/test/msw-api/setup-server/listHandlers.test.ts @@ -27,19 +27,49 @@ afterAll(() => { test('lists all current request handlers', () => { const handlers = server.listHandlers() + const handlerHeaders = handlers.map((handler) => handler.info.header) - expect(handlers.length).toEqual(6) + 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('respects runtime request handlers when listing handlers', () => { +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) - // Runtime handlers are prepended to the list of handlers - // and they DON'T remove the handlers they may override. - expect(handlers.length).toEqual(8) + 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)', + ]) }) From be9a07645999f1d924446cb7c1687ed5a0c44a81 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 30 Aug 2022 13:11:42 +0200 Subject: [PATCH 3/4] feat(setupWorker): add "listHandlers()" method --- src/setupWorker/glossary.ts | 20 ++++- src/setupWorker/setupWorker.ts | 9 ++- .../msw-api/setup-server/listHandlers.test.ts | 3 + .../setup-worker/listHandlers.mocks.ts | 21 +++++ .../msw-api/setup-worker/listHandlers.test.ts | 76 +++++++++++++++++++ .../setup-worker/printHandlers.test.ts | 2 +- 6 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 test/msw-api/setup-worker/listHandlers.mocks.ts create mode 100644 test/msw-api/setup-worker/listHandlers.test.ts 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/test/msw-api/setup-server/listHandlers.test.ts b/test/msw-api/setup-server/listHandlers.test.ts index 1c7b00106..2abe6f41f 100644 --- a/test/msw-api/setup-server/listHandlers.test.ts +++ b/test/msw-api/setup-server/listHandlers.test.ts @@ -1,3 +1,6 @@ +/** + * @jest-environment node + */ import { rest, graphql } from 'msw' import { setupServer } from 'msw/node' 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(() => { From 50f6b9b1b08e0601acbb6ff55fcc93c3e0fd23f7 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 30 Aug 2022 13:15:47 +0200 Subject: [PATCH 4/4] test(toReadonlyArray): ensure source is intact --- src/utils/internal/toReadonlyArray.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils/internal/toReadonlyArray.test.ts b/src/utils/internal/toReadonlyArray.test.ts index 29e7d36d9..1314aebb4 100644 --- a/src/utils/internal/toReadonlyArray.test.ts +++ b/src/utils/internal/toReadonlyArray.test.ts @@ -4,6 +4,14 @@ 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)