Repository: mswjs/msw Branch: main Commit: 002f3e7d86dd Files: 584 Total size: 1021.2 KB Directory structure: gitextract_krzcz2ns/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 01-issue-browser.yml │ │ ├── 02-issue-nodejs.yml │ │ └── 03-feature.yml │ └── workflows/ │ ├── auto.yml │ ├── ci.yml │ ├── compat.yml │ ├── lock-closed-issues.yml │ ├── release-preview.yml │ ├── release.yml │ ├── smoke-test.yml │ └── typescript-nightly.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── browser/ │ └── package.json ├── cli/ │ ├── index.js │ ├── init.js │ ├── invariant.js │ └── package.json ├── commitlint.config.js ├── config/ │ ├── constants.js │ ├── copyServiceWorker.ts │ ├── package.json │ ├── plugins/ │ │ └── esbuild/ │ │ ├── copyWorkerPlugin.ts │ │ ├── forceEsmExtensionsPlugin.ts │ │ ├── graphQLImportPlugin.ts │ │ └── resolveCoreImportsPlugin.ts │ ├── polyfills-node.ts │ ├── replaceCoreImports.js │ └── scripts/ │ ├── patch-ts.js │ ├── postinstall.js │ └── smoke-test.sh ├── decisions/ │ ├── jest-support.md │ ├── linting-worker-script.md │ ├── releases.md │ └── typescript-versioning.md ├── eslint.config.mjs ├── global.d.ts ├── knip.json ├── native/ │ └── package.json ├── node/ │ └── package.json ├── package.json ├── release.config.json ├── src/ │ ├── browser/ │ │ ├── global.browser.d.ts │ │ ├── index.ts │ │ ├── setupWorker/ │ │ │ ├── glossary.ts │ │ │ ├── setupWorker.node.test.ts │ │ │ ├── setupWorker.ts │ │ │ ├── start/ │ │ │ │ ├── createFallbackRequestListener.ts │ │ │ │ ├── createRequestListener.ts │ │ │ │ ├── createResponseListener.ts │ │ │ │ ├── createStartHandler.ts │ │ │ │ └── utils/ │ │ │ │ ├── enableMocking.ts │ │ │ │ ├── getWorkerByRegistration.ts │ │ │ │ ├── getWorkerInstance.ts │ │ │ │ ├── prepareStartHandler.test.ts │ │ │ │ ├── prepareStartHandler.ts │ │ │ │ ├── printStartMessage.test.ts │ │ │ │ ├── printStartMessage.ts │ │ │ │ └── validateWorkerScope.ts │ │ │ └── stop/ │ │ │ └── utils/ │ │ │ ├── printStopMessage.test.ts │ │ │ └── printStopMessage.ts │ │ ├── tsconfig.browser.build.json │ │ ├── tsconfig.browser.json │ │ └── utils/ │ │ ├── checkWorkerIntegrity.ts │ │ ├── deserializeRequest.ts │ │ ├── getAbsoluteWorkerUrl.test.ts │ │ ├── getAbsoluteWorkerUrl.ts │ │ ├── pruneGetRequestBody.test.ts │ │ ├── pruneGetRequestBody.ts │ │ ├── supports.ts │ │ └── workerChannel.ts │ ├── core/ │ │ ├── HttpResponse.test.ts │ │ ├── HttpResponse.ts │ │ ├── SetupApi.ts │ │ ├── bypass.test.ts │ │ ├── bypass.ts │ │ ├── delay.ts │ │ ├── getResponse.test.ts │ │ ├── getResponse.ts │ │ ├── graphql.test.ts │ │ ├── graphql.ts │ │ ├── handlers/ │ │ │ ├── GraphQLHandler.test.ts │ │ │ ├── GraphQLHandler.ts │ │ │ ├── HttpHandler.test.ts │ │ │ ├── HttpHandler.ts │ │ │ ├── RequestHandler.ts │ │ │ ├── WebSocketHandler.test.ts │ │ │ ├── WebSocketHandler.ts │ │ │ └── common.ts │ │ ├── http.test.ts │ │ ├── http.ts │ │ ├── index.ts │ │ ├── isCommonAssetRequest.ts │ │ ├── passthrough.test.ts │ │ ├── passthrough.ts │ │ ├── sharedOptions.ts │ │ ├── sse.ts │ │ ├── typeUtils.ts │ │ ├── utils/ │ │ │ ├── HttpResponse/ │ │ │ │ └── decorators.ts │ │ │ ├── cookieStore.ts │ │ │ ├── executeHandlers.ts │ │ │ ├── handleRequest.test.ts │ │ │ ├── handleRequest.ts │ │ │ ├── internal/ │ │ │ │ ├── Disposable.ts │ │ │ │ ├── checkGlobals.ts │ │ │ │ ├── devUtils.test.ts │ │ │ │ ├── devUtils.ts │ │ │ │ ├── getCallFrame.test.ts │ │ │ │ ├── getCallFrame.ts │ │ │ │ ├── hasRefCounted.test.ts │ │ │ │ ├── hasRefCounted.ts │ │ │ │ ├── isHandlerKind.test.ts │ │ │ │ ├── isHandlerKind.ts │ │ │ │ ├── isIterable.test.ts │ │ │ │ ├── isIterable.ts │ │ │ │ ├── isObject.test.ts │ │ │ │ ├── isObject.ts │ │ │ │ ├── isStringEqual.test.ts │ │ │ │ ├── isStringEqual.ts │ │ │ │ ├── jsonParse.test.ts │ │ │ │ ├── jsonParse.ts │ │ │ │ ├── mergeRight.test.ts │ │ │ │ ├── mergeRight.ts │ │ │ │ ├── parseGraphQLRequest.test.ts │ │ │ │ ├── parseGraphQLRequest.ts │ │ │ │ ├── parseMultipartData.test.ts │ │ │ │ ├── parseMultipartData.ts │ │ │ │ ├── pipeEvents.test.ts │ │ │ │ ├── pipeEvents.ts │ │ │ │ ├── requestHandlerUtils.ts │ │ │ │ ├── toReadonlyArray.test.ts │ │ │ │ ├── toReadonlyArray.ts │ │ │ │ ├── tryCatch.test.ts │ │ │ │ └── tryCatch.ts │ │ │ ├── logging/ │ │ │ │ ├── getStatusCodeColor.test.ts │ │ │ │ ├── getStatusCodeColor.ts │ │ │ │ ├── getTimestamp.test.ts │ │ │ │ ├── getTimestamp.ts │ │ │ │ ├── serializeRequest.test.ts │ │ │ │ ├── serializeRequest.ts │ │ │ │ ├── serializeResponse.test.ts │ │ │ │ └── serializeResponse.ts │ │ │ ├── matching/ │ │ │ │ ├── matchRequestUrl.test.ts │ │ │ │ ├── matchRequestUrl.ts │ │ │ │ ├── normalizePath.node.test.ts │ │ │ │ ├── normalizePath.test.ts │ │ │ │ └── normalizePath.ts │ │ │ ├── request/ │ │ │ │ ├── getAllAcceptedMimeTypes.test.ts │ │ │ │ ├── getAllAcceptedMimeTypes.ts │ │ │ │ ├── getRequestCookies.ts │ │ │ │ ├── onUnhandledRequest.node.test.ts │ │ │ │ ├── onUnhandledRequest.test.ts │ │ │ │ ├── onUnhandledRequest.ts │ │ │ │ ├── storeResponseCookies.ts │ │ │ │ ├── toPublicUrl.node.test.ts │ │ │ │ ├── toPublicUrl.test.ts │ │ │ │ └── toPublicUrl.ts │ │ │ ├── toResponseInit.ts │ │ │ └── url/ │ │ │ ├── cleanUrl.test.ts │ │ │ ├── cleanUrl.ts │ │ │ ├── getAbsoluteUrl.node.test.ts │ │ │ ├── getAbsoluteUrl.test.ts │ │ │ ├── getAbsoluteUrl.ts │ │ │ ├── isAbsoluteUrl.test.ts │ │ │ └── isAbsoluteUrl.ts │ │ ├── ws/ │ │ │ ├── WebSocketClientManager.test.ts │ │ │ ├── WebSocketClientManager.ts │ │ │ ├── WebSocketClientStore.ts │ │ │ ├── WebSocketIndexedDBClientStore.ts │ │ │ ├── WebSocketMemoryClientStore.ts │ │ │ ├── handleWebSocketEvent.ts │ │ │ ├── utils/ │ │ │ │ ├── attachWebSocketLogger.ts │ │ │ │ ├── getMessageLength.test.ts │ │ │ │ ├── getMessageLength.ts │ │ │ │ ├── getPublicData.test.ts │ │ │ │ ├── getPublicData.ts │ │ │ │ ├── truncateMessage.test.ts │ │ │ │ └── truncateMessage.ts │ │ │ └── webSocketInterceptor.ts │ │ ├── ws.test.ts │ │ └── ws.ts │ ├── iife/ │ │ └── index.ts │ ├── mockServiceWorker.js │ ├── native/ │ │ └── index.ts │ ├── node/ │ │ ├── SetupServerApi.ts │ │ ├── SetupServerCommonApi.ts │ │ ├── glossary.ts │ │ ├── index.ts │ │ └── setupServer.ts │ ├── package.json │ ├── shims/ │ │ ├── cookie.ts │ │ └── statuses.ts │ ├── tsconfig.core.build.json │ ├── tsconfig.node.build.json │ ├── tsconfig.node.json │ ├── tsconfig.src.json │ └── tsconfig.worker.json ├── test/ │ ├── README.md │ ├── browser/ │ │ ├── graphql-api/ │ │ │ ├── anonymous-operation.mocks.ts │ │ │ ├── anonymous-operation.test.ts │ │ │ ├── cookies.mocks.ts │ │ │ ├── cookies.test.ts │ │ │ ├── custom-predicate.mocks.ts │ │ │ ├── custom-predicate.test.ts │ │ │ ├── document-node.mocks.ts │ │ │ ├── errors.mocks.ts │ │ │ ├── errors.test.ts │ │ │ ├── extensions.mocks.ts │ │ │ ├── extensions.test.ts │ │ │ ├── link.mocks.ts │ │ │ ├── link.test.ts │ │ │ ├── logging.mocks.ts │ │ │ ├── logging.test.ts │ │ │ ├── multipart-data.mocks.ts │ │ │ ├── multipart-data.test.ts │ │ │ ├── mutation.mocks.ts │ │ │ ├── mutation.test.ts │ │ │ ├── operation-reference.mocks.ts │ │ │ ├── operation-reference.test.ts │ │ │ ├── operation.mocks.ts │ │ │ ├── operation.test.ts │ │ │ ├── query.mocks.ts │ │ │ ├── query.test.ts │ │ │ ├── response-patching.mocks.ts │ │ │ ├── response-patching.test.ts │ │ │ ├── variables.mocks.ts │ │ │ └── variables.test.ts │ │ ├── msw-api/ │ │ │ ├── context/ │ │ │ │ ├── delay.mocks.ts │ │ │ │ └── delay.test.ts │ │ │ ├── distribution/ │ │ │ │ ├── iife.mocks.js │ │ │ │ └── iife.test.ts │ │ │ ├── exception-handling.mocks.ts │ │ │ ├── exception-handling.test.ts │ │ │ ├── hard-reload.mocks.ts │ │ │ ├── hard-reload.test.ts │ │ │ ├── integrity-check-invalid.mocks.ts │ │ │ ├── integrity-check-valid.mocks.ts │ │ │ ├── integrity-check.test.ts │ │ │ ├── regression/ │ │ │ │ ├── 2129-worker-use.mocks.ts │ │ │ │ ├── 2129-worker-use.test.ts │ │ │ │ ├── handle-stream.mocks.ts │ │ │ │ ├── handle-stream.test.ts │ │ │ │ ├── null-body.mocks.ts │ │ │ │ └── null-body.test.ts │ │ │ ├── req/ │ │ │ │ ├── passthrough.mocks.ts │ │ │ │ └── passthrough.test.ts │ │ │ ├── res/ │ │ │ │ ├── network-error.mocks.ts │ │ │ │ └── network-error.test.ts │ │ │ ├── setup-worker/ │ │ │ │ ├── fallback-mode/ │ │ │ │ │ ├── fallback-mode.mocks.ts │ │ │ │ │ └── fallback-mode.test.ts │ │ │ │ ├── input-validation.mocks.ts │ │ │ │ ├── input-validation.test.ts │ │ │ │ ├── life-cycle-events/ │ │ │ │ │ ├── on.mocks.ts │ │ │ │ │ ├── on.test.ts │ │ │ │ │ ├── removeAllListeners.test.ts │ │ │ │ │ └── removeListener.test.ts │ │ │ │ ├── listHandlers.mocks.ts │ │ │ │ ├── listHandlers.test.ts │ │ │ │ ├── resetHandlers.test.ts │ │ │ │ ├── response-logging.test.ts │ │ │ │ ├── restoreHandlers.test.ts │ │ │ │ ├── scenarios/ │ │ │ │ │ ├── custom-transformers.mocks.ts │ │ │ │ │ ├── custom-transformers.test.ts │ │ │ │ │ ├── errors/ │ │ │ │ │ │ ├── internal-error.mocks.ts │ │ │ │ │ │ ├── internal-error.test.ts │ │ │ │ │ │ ├── network-error.mocks.ts │ │ │ │ │ │ └── network-error.test.ts │ │ │ │ │ ├── fall-through.mocks.ts │ │ │ │ │ ├── fall-through.test.ts │ │ │ │ │ ├── iframe/ │ │ │ │ │ │ ├── app.html │ │ │ │ │ │ ├── iframe.mocks.ts │ │ │ │ │ │ ├── iframe.test.ts │ │ │ │ │ │ ├── multiple-workers/ │ │ │ │ │ │ │ ├── child.mocks.ts │ │ │ │ │ │ │ ├── iframe-multiple-workers.test.ts │ │ │ │ │ │ │ └── parent.mocks.ts │ │ │ │ │ │ ├── page-in-iframe.html │ │ │ │ │ │ └── page-in-nested-iframe.html │ │ │ │ │ ├── iframe-isolated-response/ │ │ │ │ │ │ ├── app.html │ │ │ │ │ │ ├── iframe-isolated-response.mocks.ts │ │ │ │ │ │ ├── iframe-isolated-response.test.ts │ │ │ │ │ │ ├── one.html │ │ │ │ │ │ └── two.html │ │ │ │ │ ├── scope/ │ │ │ │ │ │ ├── scope-nested-quiet.mocks.ts │ │ │ │ │ │ ├── scope-nested.mocks.ts │ │ │ │ │ │ ├── scope-root.mocks.ts │ │ │ │ │ │ └── scope-validation.test.ts │ │ │ │ │ ├── shared-worker/ │ │ │ │ │ │ ├── shared-worker.mocks.ts │ │ │ │ │ │ ├── shared-worker.test.ts │ │ │ │ │ │ └── worker.js │ │ │ │ │ ├── text-event-stream.mocks.ts │ │ │ │ │ └── text-event-stream.test.ts │ │ │ │ ├── start/ │ │ │ │ │ ├── error.mocks.ts │ │ │ │ │ ├── error.test.ts │ │ │ │ │ ├── find-worker.error.mocks.ts │ │ │ │ │ ├── find-worker.mocks.ts │ │ │ │ │ ├── find-worker.test.ts │ │ │ │ │ ├── on-unhandled-request/ │ │ │ │ │ │ ├── bypass.mocks.ts │ │ │ │ │ │ ├── bypass.test.ts │ │ │ │ │ │ ├── callback-print.mocks.ts │ │ │ │ │ │ ├── callback-print.test.ts │ │ │ │ │ │ ├── callback-throws.mocks.ts │ │ │ │ │ │ ├── callback.mocks.ts │ │ │ │ │ │ ├── callback.test.ts │ │ │ │ │ │ ├── default.mocks.ts │ │ │ │ │ │ ├── default.test.ts │ │ │ │ │ │ ├── warn.mocks.ts │ │ │ │ │ │ └── warn.test.ts │ │ │ │ │ ├── options-sw-scope.mocks.ts │ │ │ │ │ ├── options-sw-scope.test.ts │ │ │ │ │ ├── quiet.mocks.ts │ │ │ │ │ ├── quiet.test.ts │ │ │ │ │ ├── start.mocks.ts │ │ │ │ │ ├── start.test.ts │ │ │ │ │ ├── warn-on-wait-until-ready.mocks.ts │ │ │ │ │ ├── warn-on-wait-until-ready.test.ts │ │ │ │ │ └── worker.delayed.js │ │ │ │ ├── stop/ │ │ │ │ │ ├── in-flight-request.mocks.ts │ │ │ │ │ ├── in-flight-request.test.ts │ │ │ │ │ ├── quiet.mocks.ts │ │ │ │ │ ├── quiet.test.ts │ │ │ │ │ ├── removes-all-listeners.mocks.ts │ │ │ │ │ └── removes-all-listeners.test.ts │ │ │ │ ├── stop.mocks.ts │ │ │ │ ├── stop.test.ts │ │ │ │ ├── use.mocks.ts │ │ │ │ ├── use.test.ts │ │ │ │ ├── worker-passthrough-header.mocks.ts │ │ │ │ └── worker-passthrough-header.test.ts │ │ │ ├── unregister.mocks.ts │ │ │ └── unregister.test.ts │ │ ├── playwright.config.ts │ │ ├── playwright.extend.ts │ │ ├── rest-api/ │ │ │ ├── 204-response.test.ts │ │ │ ├── 206-response.mocks.ts │ │ │ ├── 206-response.test.ts │ │ │ ├── basic.mocks.ts │ │ │ ├── basic.test.ts │ │ │ ├── body.mocks.ts │ │ │ ├── body.test.ts │ │ │ ├── context.mocks.ts │ │ │ ├── context.test.ts │ │ │ ├── cors.mocks.ts │ │ │ ├── cors.test.ts │ │ │ ├── generator.mocks.ts │ │ │ ├── generator.test.ts │ │ │ ├── headers-multiple.mocks.ts │ │ │ ├── headers-multiple.test.ts │ │ │ ├── logging.test.ts │ │ │ ├── params.mocks.ts │ │ │ ├── params.test.ts │ │ │ ├── plain-response.mocks.ts │ │ │ ├── plain-response.test.ts │ │ │ ├── query-params-warning.mocks.ts │ │ │ ├── query-params-warning.test.ts │ │ │ ├── query.mocks.ts │ │ │ ├── query.test.ts │ │ │ ├── redirect.mocks.ts │ │ │ ├── redirect.test.ts │ │ │ ├── request/ │ │ │ │ ├── body/ │ │ │ │ │ ├── body-arraybuffer-range.mocks.ts │ │ │ │ │ ├── body-arraybuffer-range.test.ts │ │ │ │ │ ├── body-arraybuffer.test.ts │ │ │ │ │ ├── body-form-data.page.html │ │ │ │ │ ├── body-form-data.test.ts │ │ │ │ │ ├── body-json.test.ts │ │ │ │ │ ├── body-text.test.ts │ │ │ │ │ └── body.mocks.ts │ │ │ │ ├── matching/ │ │ │ │ │ ├── all.mocks.ts │ │ │ │ │ ├── all.test.ts │ │ │ │ │ ├── custom-predicate.mocks.ts │ │ │ │ │ ├── custom-predicate.test.ts │ │ │ │ │ ├── method.mocks.ts │ │ │ │ │ ├── method.test.ts │ │ │ │ │ ├── path-params-decode.mocks.ts │ │ │ │ │ ├── path-params-decode.test.ts │ │ │ │ │ ├── uri.mocks.ts │ │ │ │ │ └── uri.test.ts │ │ │ │ ├── request-cookies.mocks.ts │ │ │ │ └── request-cookies.test.ts │ │ │ ├── response/ │ │ │ │ ├── body/ │ │ │ │ │ ├── body-binary.mocks.ts │ │ │ │ │ ├── body-binary.test.ts │ │ │ │ │ ├── body-blob.mocks.ts │ │ │ │ │ ├── body-blob.test.ts │ │ │ │ │ ├── body-formdata.mocks.ts │ │ │ │ │ ├── body-formdata.test.ts │ │ │ │ │ ├── body-html.mocks.ts │ │ │ │ │ ├── body-html.test.ts │ │ │ │ │ ├── body-json.mocks.ts │ │ │ │ │ ├── body-json.test.ts │ │ │ │ │ ├── body-stream.mocks.ts │ │ │ │ │ ├── body-stream.test.ts │ │ │ │ │ ├── body-text.mocks.ts │ │ │ │ │ ├── body-text.test.ts │ │ │ │ │ ├── body-xml.mocks.ts │ │ │ │ │ └── body-xml.test.ts │ │ │ │ ├── response-cookies.mocks.ts │ │ │ │ ├── response-cookies.test.ts │ │ │ │ ├── response-error.mocks.ts │ │ │ │ ├── response-error.test.ts │ │ │ │ ├── throw-response.mocks.ts │ │ │ │ └── throw-response.test.ts │ │ │ ├── response-patching.mocks.ts │ │ │ ├── response-patching.test.ts │ │ │ ├── send-beacon.mocks.ts │ │ │ ├── send-beacon.test.ts │ │ │ ├── status.mocks.ts │ │ │ ├── status.test.ts │ │ │ ├── xhr.mocks.ts │ │ │ └── xhr.test.ts │ │ ├── setup/ │ │ │ ├── webpackHttpServer.ts │ │ │ └── workerConsole.ts │ │ ├── sse-api/ │ │ │ ├── sse.client.send.extraneous.test.ts │ │ │ ├── sse.client.send.multiline.test.ts │ │ │ ├── sse.client.send.test.ts │ │ │ ├── sse.mocks.ts │ │ │ ├── sse.quiet.test.ts │ │ │ ├── sse.retry.test.ts │ │ │ ├── sse.server.connect.test.ts │ │ │ ├── sse.use.test.ts │ │ │ └── sse.with-credentials.test.ts │ │ ├── third-party/ │ │ │ ├── axios-upload.browser.test.ts │ │ │ └── axios-upload.runtime.js │ │ └── ws-api/ │ │ ├── ws.apply.browser.test.ts │ │ ├── ws.client.send.test.ts │ │ ├── ws.clients.browser.test.ts │ │ ├── ws.intercept.client.browser.test.ts │ │ ├── ws.intercept.server.browser.test.ts │ │ ├── ws.logging.browser.test.ts │ │ ├── ws.runtime.js │ │ ├── ws.server.connect.browser.test.ts │ │ └── ws.use.browser.test.ts │ ├── e2e/ │ │ ├── auto-update-worker.node.test.ts │ │ ├── cli-init.node.test.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ ├── vitest.d.ts │ │ └── vitest.global.setup.ts │ ├── global.d.ts │ ├── modules/ │ │ ├── browser/ │ │ │ ├── esm-browser.test.ts │ │ │ └── playwright.config.ts │ │ ├── module-utils.ts │ │ └── node/ │ │ ├── esm-node.test.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── native/ │ │ ├── node-events.native.test.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ └── vitest.setup.ts │ ├── node/ │ │ ├── graphql-api/ │ │ │ ├── anonymous-operations.test.ts │ │ │ ├── batched-queries.apollo.test.ts │ │ │ ├── batched-queries.batched-execute.test.ts │ │ │ ├── compatibility.node.test.ts │ │ │ ├── content-type.test.ts │ │ │ ├── cookies.test.ts │ │ │ ├── custom-predicate.node.test.ts │ │ │ ├── extensions.node.test.ts │ │ │ ├── response-patching.node.test.ts │ │ │ ├── typed-document-node.test.ts │ │ │ └── typed-document-string.test.ts │ │ ├── msw-api/ │ │ │ ├── context/ │ │ │ │ ├── delay-infinite.fixture.js │ │ │ │ └── delay.node.test.ts │ │ │ ├── req/ │ │ │ │ └── passthrough.node.test.ts │ │ │ ├── res/ │ │ │ │ └── network-error.node.test.ts │ │ │ └── setup-server/ │ │ │ ├── boundary/ │ │ │ │ ├── boundary.args.test.ts │ │ │ │ ├── boundary.concurrency.test.ts │ │ │ │ └── boundary.handlers.test.ts │ │ │ ├── input-validation.node.test.ts │ │ │ ├── life-cycle-events/ │ │ │ │ ├── on.node.test.ts │ │ │ │ ├── removeAllListeners.node.test.ts │ │ │ │ └── removeListener.node.test.ts │ │ │ ├── listHandlers.node.test.ts │ │ │ ├── resetHandlers.node.test.ts │ │ │ ├── restoreHandlers.node.test.ts │ │ │ ├── scenarios/ │ │ │ │ ├── cookies-request.node.test.ts │ │ │ │ ├── custom-interceptors.node.test.ts │ │ │ │ ├── custom-transformers.node.test.ts │ │ │ │ ├── fake-timers.node.test.ts │ │ │ │ ├── fall-through.node.test.ts │ │ │ │ ├── fetch.node.test.ts │ │ │ │ ├── generator.node.test.ts │ │ │ │ ├── graphql.node.test.ts │ │ │ │ ├── http.node.test.ts │ │ │ │ ├── https.node.test.ts │ │ │ │ ├── on-unhandled-request/ │ │ │ │ │ ├── bypass.node.test.ts │ │ │ │ │ ├── callback-throws.node.test.ts │ │ │ │ │ ├── callback.node.test.ts │ │ │ │ │ ├── default.node.test.ts │ │ │ │ │ ├── error.node.test.ts │ │ │ │ │ └── warn.node.test.ts │ │ │ │ ├── relative-url.node.test.ts │ │ │ │ ├── response-patching.node.test.ts │ │ │ │ └── xhr.node.test.ts │ │ │ └── use.node.test.ts │ │ ├── regressions/ │ │ │ ├── 2129-server-use.test.ts │ │ │ ├── 2370-listen-after-close.test.ts │ │ │ ├── many-request-handlers-jsdom.test.ts │ │ │ ├── many-request-handlers.test.ts │ │ │ ├── miniflare.node.test.ts │ │ │ └── mixed-graphql-http-with-query-in-body.node.test.ts │ │ ├── rest-api/ │ │ │ ├── cookies-inheritance.node.test.ts │ │ │ ├── https.node.test.ts │ │ │ ├── request/ │ │ │ │ ├── body/ │ │ │ │ │ ├── body-arraybuffer.node.test.ts │ │ │ │ │ ├── body-blob.node.test.ts │ │ │ │ │ ├── body-form-data.node.test.ts │ │ │ │ │ ├── body-json.node.test.ts │ │ │ │ │ ├── body-protobuf.node.test.ts │ │ │ │ │ ├── body-text.node.test.ts │ │ │ │ │ └── body-used.node.test.ts │ │ │ │ └── matching/ │ │ │ │ ├── all.node.test.ts │ │ │ │ ├── custom-predicate.node.test.ts │ │ │ │ ├── path-params-decode.node.test.ts │ │ │ │ ├── path-params-optional.node.test.ts │ │ │ │ └── relative-url.node.test.ts │ │ │ ├── response/ │ │ │ │ ├── body-binary.node.test.ts │ │ │ │ ├── body-html.node.test.ts │ │ │ │ ├── body-json.node.test.ts │ │ │ │ ├── body-stream.node.test.ts │ │ │ │ ├── body-text.node.test.ts │ │ │ │ ├── body-xml.node.test.ts │ │ │ │ ├── generator.test.ts │ │ │ │ ├── response-cookies.test.ts │ │ │ │ ├── response-error.test.ts │ │ │ │ └── throw-response.node.test.ts │ │ │ └── response-patching.node.test.ts │ │ ├── third-party/ │ │ │ ├── axios-error-response.test.ts │ │ │ ├── axios-timeout.node.test.ts │ │ │ └── axios-upload.node.test.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ └── ws-api/ │ │ ├── on-unhandled-request/ │ │ │ ├── callback.test.ts │ │ │ ├── error.test.ts │ │ │ └── warn.test.ts │ │ ├── ws.apply.test.ts │ │ ├── ws.event-patching.test.ts │ │ ├── ws.intercept.client.test.ts │ │ ├── ws.intercept.server.test.ts │ │ ├── ws.server.connect.test.ts │ │ ├── ws.stop-propagation.test.ts │ │ ├── ws.unhandled-exception.test.ts │ │ └── ws.use.test.ts │ ├── package.json │ ├── support/ │ │ ├── WebSocketServer.ts │ │ ├── alias.ts │ │ ├── environments/ │ │ │ └── vitest-environment-node-websocket.ts │ │ ├── graphql.ts │ │ ├── msw-esm/ │ │ │ └── package.json │ │ ├── utils.ts │ │ └── waitFor.ts │ ├── tsconfig.json │ └── typings/ │ ├── custom-handler.test-d.ts │ ├── custom-resolver.test-d.ts │ ├── graphql-custom-predicate.test-d.ts │ ├── graphql-typed-document-node.test-d.ts │ ├── graphql-typed-document-string.test-d.ts │ ├── graphql.test-d.ts │ ├── http-custom-predicate.test-d.ts │ ├── http.test-d.ts │ ├── regressions/ │ │ ├── default-resolver-type.test-d.ts │ │ ├── request-handler-type.test-d.ts │ │ └── response-body-type.test-d.ts │ ├── resolver-generator.test-d.ts │ ├── server.boundary.test-d.ts │ ├── setup-server.test-d.ts │ ├── setup-worker.test-d.ts │ ├── sse.test-d.ts │ ├── tsconfig.5.0.json │ ├── tsconfig.5.1.json │ ├── tsconfig.5.2.json │ ├── tsconfig.5.3.json │ ├── tsconfig.5.4.json │ ├── tsconfig.5.5.json │ ├── tsconfig.json │ ├── vitest.config.ts │ └── ws.test-d.ts ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.test.unit.json ├── tsup.config.ts └── vitest.config.mts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: mswjs open_collective: mswjs ================================================ FILE: .github/ISSUE_TEMPLATE/01-issue-browser.yml ================================================ name: 'Bug report (Browser)' description: I experience unexpected behavior using the library in a browser. labels: ['bug', 'scope:browser', 'needs:triage'] body: - type: markdown attributes: value: Thank you for reporting an issue to Mock Service Worker! Please fill in the template below to help our team tackle it in the most efficient way. - type: checkboxes attributes: label: Prerequisites description: Before we begin, let's make sure your issue hasn't been solved already. options: - label: I confirm my issue is not in the [opened issues](https://github.com/mswjs/msw/issues) required: true - label: I confirm the [Frequently Asked Questions](https://mswjs.io/docs/faq) didn't contain the answer to my issue required: true - type: checkboxes attributes: label: Environment check options: - label: I'm using the [latest](https://github.com/mswjs/msw/releases/latest) `msw` version required: true - label: I'm using Node.js version 20 or higher required: true - type: dropdown attributes: label: Browsers description: Select in which browser(s) you're experiencing the issue. multiple: true options: - 'Chromium (Chrome, Brave, etc.)' - 'Firefox' - 'Safari' - type: input attributes: label: Reproduction repository description: A link to the repository where your issue can be reproduced. You can clone one of [our examples](https://github.com/mswjs/examples) to create a reproduction repository much faster. **Issues without a reproduction repository will be closed**. placeholder: i.e. https://github.com/you/msw-issue validations: required: true - type: textarea attributes: label: Reproduction steps description: Include any steps necessary to reproduce the issue in the repository above. placeholder: i.e. "npm test" validations: required: true - type: textarea attributes: label: Current behavior description: Share any details on what behavior you're experiencing (error messages, logs, context). validations: required: true - type: textarea attributes: label: Expected behavior description: What do you expect to happen instead? validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/02-issue-nodejs.yml ================================================ name: 'Bug report (Node.js)' description: I experience unexpected behavior using the library in Node.js (Vitest/React Native/Express/etc.). labels: ['bug', 'scope:node', 'needs:triage'] body: - type: markdown attributes: value: Thank you for reporting an issue to Mock Service Worker! Please fill in the template below to help our team tackle it in the most efficient way. - type: checkboxes attributes: label: Prerequisites description: Before we begin, let's make sure your issue hasn't been solved already. options: - label: I confirm my issue is not in the [opened issues](https://github.com/mswjs/msw/issues) required: true - label: I confirm the [Frequently Asked Questions](https://mswjs.io/docs/faq) didn't contain the answer to my issue required: true - type: checkboxes attributes: label: Environment check description: Next, let's make sure you're using the library in the officially supported environments. options: - label: I'm using the [latest](https://github.com/mswjs/msw/releases/latest) `msw` version required: true - label: I'm using Node.js version 20 or higher required: true - type: input attributes: label: Node.js version description: Specify which Node.js version you're using (`node -v`). placeholder: i.e. v20.18.0 validations: required: true - type: input attributes: label: Reproduction repository description: A link to the repository where your issue can be reproduced. You can clone one of [our examples](https://github.com/mswjs/examples) to create a reproduction repository much faster. **Issues without a reproduction repository will be closed**. placeholder: i.e. https://github.com/you/msw-issue validations: required: true - type: textarea attributes: label: Reproduction steps description: Include any steps necessary to reproduce the issue in the repository above. placeholder: i.e. "npm test" validations: required: true - type: textarea attributes: label: Current behavior description: Share any details on what behavior you're experiencing (error messages, logs, context). validations: required: true - type: textarea attributes: label: Expected behavior description: What do you expect to happen instead? validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/03-feature.yml ================================================ name: 'Feature request' description: Propose a feature or improvement. labels: [feature] body: - type: markdown attributes: value: Thank you for deciding to make Mock Service Worker better! Please fill in the form below so our team could understand your feature better. - type: dropdown attributes: label: Scope multiple: false options: - 'Adds a new behavior' - 'Improves an existing behavior' validations: required: true - type: checkboxes attributes: label: Compatibility description: Check if your proposal is a breaking change. Leave this field unchecked if not sure. options: - label: This is a breaking change required: false - type: textarea attributes: label: Feature description description: "Describe the feature you're suggesting. Be as detailed as possible: include behavior description, pseudo-code snippets, comparison to similar features." validations: required: true ================================================ FILE: .github/workflows/auto.yml ================================================ name: auto on: [push] jobs: cancel-previous-workflows: runs-on: macos-latest timeout-minutes: 3 steps: - uses: styfle/cancel-workflow-action@0.9.1 with: workflow_id: ci.yml access_token: ${{ github.token }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: ci on: push: branches: [main] pull_request: workflow_dispatch: jobs: build-20: name: build (20) runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Install dependencies run: pnpm install - name: Lint run: pnpm lint - name: Build run: pnpm build - name: Cache build output uses: actions/cache@v4 with: path: ./lib key: ${{ runner.os }}-node-20-build-${{ github.sha }} restore-keys: | ${{ runner.os }}-build-20 build-22: name: build (22) runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: 'pnpm' - name: Install dependencies run: pnpm install - name: Lint run: pnpm lint - name: Build run: pnpm build - name: Cache build output uses: actions/cache@v4 with: path: ./lib key: ${{ runner.os }}-node-22-build-${{ github.sha }} restore-keys: | ${{ runner.os }}-build-22 test-unit: name: test (unit) needs: build-20 runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Restore build cache uses: actions/cache@v4 with: path: ./lib key: ${{ runner.os }}-node-20-build-${{ github.sha }} restore-keys: | ${{ runner.os }}-node-20-build- - name: Install dependencies run: pnpm install - name: Install Playwright browsers run: pnpm exec playwright install chromium --with-deps --only-shell - name: Unit tests run: pnpm test:unit test-node-20: name: test (node.js) (20) needs: build-20 runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Restore build cache uses: actions/cache@v4 with: path: ./lib key: ${{ runner.os }}-node-20-build-${{ github.sha }} restore-keys: | ${{ runner.os }}-node-20-build- - name: Install dependencies run: pnpm install - name: Node.js tests run: pnpm test:node test-node-22: name: test (node.js) (22) needs: build-22 runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: 'pnpm' - name: Restore build cache uses: actions/cache@v4 with: path: ./lib key: ${{ runner.os }}-node-22-build-${{ github.sha }} restore-keys: | ${{ runner.os }}-node-22-build- - name: Install dependencies run: pnpm install - name: Node.js tests run: pnpm test:node test-e2e: name: test (e2e) (20) needs: build-20 runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Install dependencies run: pnpm install - name: E2E tests run: pnpm test:e2e test-browser: name: test (browser) needs: build-20 runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Restore build cache uses: actions/cache@v4 with: path: ./lib key: ${{ runner.os }}-node-20-build-${{ github.sha }} restore-keys: | ${{ runner.os }}-node-20-build- - name: Install dependencies run: pnpm install - name: Playwright install run: pnpm exec playwright install chromium --with-deps --only-shell - name: Browser tests run: pnpm test:browser - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: playwright-report path: test/browser/test-results test-native: name: test (react-native) needs: build-20 runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Restore build cache uses: actions/cache@v4 with: path: ./lib key: ${{ runner.os }}-node-20-build-${{ github.sha }} restore-keys: | ${{ runner.os }}-node-20-build- - name: Install dependencies run: pnpm install - name: React Native tests run: pnpm test:native ================================================ FILE: .github/workflows/compat.yml ================================================ name: compat on: push: branches: [main] pull_request: workflow_dispatch: jobs: # Validate the package.json exports and emitted CJS/ESM bundles. exports: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Install dependencies run: pnpm install - name: Install Playwright browsers run: pnpm exec playwright install chromium --with-deps --only-shell - name: Build run: pnpm build - name: Lint package run: pnpm publint - name: Test modules (Node.js) run: pnpm test:modules:node - name: Test modules (browser) run: pnpm test:modules:browser # Checks the library's compatibility with different # TypeScript versions to discover type regressions. typescript: runs-on: macos-latest # Skip TypeScript compatibility check on "main". # A merged pull request implies passing "typescript" job. if: github.ref != 'refs/heads/main' strategy: fail-fast: false matrix: # As a general rule, when adding a new version, remove the oldest version # Check if the oldest version is EOL or not first. ts: ['5.0', '5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '5.7', '5.8'] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Install dependencies run: pnpm install - name: Build run: pnpm build - name: Install TypeScript ${{ matrix.ts }} run: pnpm add typescript@${{ matrix.ts }} - name: Typings tests run: | pnpm tsc --version pnpm test:ts ================================================ FILE: .github/workflows/lock-closed-issues.yml ================================================ name: locked-closed-issues on: schedule: - cron: '0 0 * * *' permissions: issues: write jobs: action: runs-on: macos-latest steps: - uses: dessant/lock-threads@v5 with: github-token: ${{ secrets.GITHUB_TOKEN }} issue-inactive-days: '14' issue-lock-reason: '' process-only: 'issues' ================================================ FILE: .github/workflows/release-preview.yml ================================================ name: release-preview on: push: branches-ignore: - 'main' tags: - '!**' workflow_dispatch: jobs: publish: runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Install dependencies run: pnpm install - name: Install Playwright browsers run: pnpm exec playwright install chromium --with-deps --only-shell - name: Lint run: pnpm lint - name: Build run: pnpm build - name: Tests run: pnpm test - name: Publish preview run: pnpm dlx pkg-pr-new@0.0 publish --compact --pnpm --comment=update ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: schedule: - cron: '0 1 * * *' workflow_dispatch: jobs: build: runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 with: token: ${{ secrets.GH_ADMIN_TOKEN }} - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: 'pnpm' - name: Install dependencies run: pnpm install - name: Install Playwright browsers run: pnpm exec playwright install chromium --with-deps --only-shell - name: Lint run: pnpm lint - name: Build run: pnpm build - name: Tests run: pnpm test - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-artifacts path: ./lib release: needs: [build] runs-on: macos-latest permissions: contents: read id-token: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GH_ADMIN_TOKEN }} - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Set up Node.js uses: actions/setup-node@v4 with: # Trusted Publishing only works on Node.js v24. node-version: 24 cache: 'pnpm' - name: Setup Git run: | git config --local user.name "Artem Zakharchenko" git config --local user.email "kettanaito@gmail.com" - name: Install dependencies run: pnpm install - name: Download build artifacts uses: actions/download-artifact@v4 with: name: build-artifacts path: ./lib - name: Release run: pnpm release env: GITHUB_TOKEN: ${{ secrets.GH_ADMIN_TOKEN }} ================================================ FILE: .github/workflows/smoke-test.yml ================================================ name: smoke-test on: # Always run smoke tests upon a successful # "ci" job completion on "main". workflow_run: workflows: ['ci'] branches: [main] types: [completed] workflow_dispatch: jobs: examples: if: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || github.event_name == 'workflow_dispatch' }} runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Install dependencies run: pnpm install - name: Test examples run: ./config/scripts/smoke-test.sh ================================================ FILE: .github/workflows/typescript-nightly.yml ================================================ name: typescript-nightly on: schedule: # Schedule to run nightly at midnight - cron: '0 0 * * *' workflow_dispatch: jobs: compare: runs-on: macos-latest steps: - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Get latest TypeScript version id: get-versions run: | # Fetch the latest published version of TypeScript on npm latest_version=$(npm show typescript dist-tags.latest) # Get the currently installed version rc_version=$(npm show typescript dist-tags.rc) latest_major_minor=$(echo "$latest_version" | cut -d '.' -f 1,2) rc_major_minor=$(echo "$rc_version" | cut -d '.' -f 1,2) echo "latest_version=$latest_major_minor" >> $GITHUB_OUTPUT echo "rc_version=$rc_major_minor" >> $GITHUB_OUTPUT test: runs-on: macos-latest # Skip TypeScript compatibility check on "main". # A merged pull request implies passing "typescript" job. if: ${{ needs.compare.outputs.latest_version != needs.compare.outputs.rc_version }} steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Set up pnpm uses: pnpm/action-setup@v4 with: version: 9.15.0 - name: Install dependencies run: pnpm install - name: Build run: pnpm build - name: Install TypeScript RC run: pnpm add typescript@rc - name: Write tsconfig run: | cp test/typings/tsconfig.5.2.json test/typings/tsconfig.${needs.compare.outputs.rc_version}.json - name: Typings tests run: | pnpm tsc --version pnpm test:ts ================================================ FILE: .gitignore ================================================ __* .DS_* node_modules /lib tmp *-error.log /package-lock.json /yarn.lock stats.html .vscode .idea msw-*.tgz .husky/_ .env **/test-results /test/modules/node/node-esm-tests # Smoke test temporary files. /package.json.copy /examples /test/modules/node/node-esm-tests *.vitest-temp.json ================================================ FILE: .nvmrc ================================================ v20 ================================================ FILE: .prettierrc ================================================ { "arrowParens": "always", "bracketSpacing": true, "semi": false, "useTabs": false, "trailingComma": "all", "singleQuote": true } ================================================ FILE: CHANGELOG.md ================================================ # Change Log This project adheres to [Semantic Versioning](https://semver.org/). Every release, along with the migration instructions, is documented on the Github [Releases](https://github.com/mswjs/msw/releases) page. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Hey! Thank you for deciding to contribute to Mock Service Worker! This page will help you land your first contribution by giving you a better understanding about the project's structure, dependencies, and development workflow. ## Tools Getting yourself familiar with the tools below will substantially ease your contribution experience. - [TypeScript](https://www.typescriptlang.org/) - [Vitest](https://vitest.dev/) - [Playwright](https://playwright.dev/) ## Dependencies Mock Service Worker depends on multiple other libraries. | Library name | Purpose | | ------------------------------------------------------- | ------------------------------------------------------------------------ | | [cookies](https://github.com/mswjs/cookies) | Enables cookies persistence and inference between environments. | | [headers-utils](https://github.com/mswjs/headers-utils) | `Headers` polyfill to manage request/response headers. | | [interceptors](https://github.com/mswjs/interceptors) | Provisions request interception in Node.js (internals of `setupServer`). | There are cases when an issue originates from one of the said dependencies. Don't hesitate to address it in the respective repository, as they all are governed by the same team. ## Getting started ### Fork the repository Please use the GitHub UI to [fork this repository](https://github.com/mswjs/msw) (_read more about [Forking a repository](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo)_). Mock Service Worker has forked builds enabled in the CI, so you will see the build status of your fork's branch. ### Install ```bash $ cd msw $ pnpm install $ pnpm start ``` > Please use [PNPM][pnpm-url] version 8.15 while working on this project. > Guide on how to install a specific PNPM version can be [found here][pnpm-install-guide-url]. ## Git workflow ```bash # Checkout the default branch and ensure it's up-to-date $ git checkout main $ git pull --rebase # Create a feature branch $ git checkout -b feature/graphql-subscriptions # Commit the changes $ git add . $ git commit # Follow the interactive prompt to compose a commit message # Push $ git push -u origin feature/graphql-subscriptions ``` We are using [Conventional Commits](https://conventionalcommits.org/) naming convention. It helps us automate library releases and ensure clean and standardized commit tree. Please take a moment to read about the said convention before you name your commits. > **Tip:** running `git commit` will open an interactive prompt in your terminal. Follow the prompt to compose a valid commit message. Once you have pushed the changes to your remote feature branch, [create a pull request](https://github.com/open-draft/msw/compare) on GitHub. Undergo the process of code review, where the maintainers of the library will help you get the changes from good to great, and enjoy your implementation merged to the default branch. > Please be respectful when requesting and going through the code review. Everyone on the team is interested in merging quality and well tested code, and we're hopeful that you have the same goal. It may take some time and iterations to get it right, and we will assist you throughout the process. ## Build Build the library with the following command: ```bash $ pnpm build ``` ## Tests ### Testing levels There are two levels of tests on the project: - [Unit tests](#unit-tests), cover small independent functions. - [Integration tests](#integration-tests), test in-browser usage scenarios. **Always begin your implementation from tests**. When tackling an issue, a test for it must be missing, or is incomplete. When working on a feature, starting with a test allows you to model the feature's usage before diving into implementation. ### Unit tests #### Writing a unit test Unit tests are placed next to the tested code. For example, if you're testing a newly added `multiply` function, create a `multiply.test.ts` file next to where the function is located: ```bash $ touch src/utils/multiply.test.ts ``` Proceed by writing a unit test that resembles the usage of the function. Make sure to cover all the scenarios ```ts // src/utils/multiply.test.ts import { multiply } from './multiply' test('multiplies two given numbers', () => { expect(multiply(2, 3)).toEqual(6) }) ``` > Please [avoid nesting](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing/) while you're testing. #### Running a single unit test Once your test is written, run it in isolation. ```bash $ pnpm test:unit src/utils/multiply.test.ts ``` At this point, the actual implementation is not ready yet, so you can expect your test to fail. **That's perfect**. Add the necessary modules and logic, and gradually see your test cases pass. #### Running all unit tests ```bash $ pnpm test:unit ``` ### Integration tests We follow an example-driven testing paradigm, meaning that each integration test represents a _usage example_. Mock Service Worker can be used in different environments (browser, Node.js), making such usage examples different. > **Make sure that you [build the library](#build) before running the integration tests**. It's a good idea to keep the build running (`pnpm start`) while working on the tests. Keeping both compiler and test runner in watch mode boosts your productivity. #### Browser integration tests You can find all the browser integration tests under `./test/browser`. Those tests are run with Playwright and usually consist of two parts: - `[test-name].mocks.ts`, the usage example of MSW; - `[test-name].test.ts`, the test suite that loads the usage example, does actions and performs assertions. It's also a great idea to get familiar with our Playwright configuration and extensions: - [**Playwright configuration file**](./test/browser/playwright.config.ts) - [Playwright extensions](./test/browser/playwright.extend.ts) Let's write an example integration test that asserts the interception of a GET request. First, start with the `*.mocks.ts` file: ```js // test/browser/example.mocks.ts import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/books', () => { return HttpResponse.json([ { id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da', title: 'The Lord of the Rings', publishedAt: -486867600, }, ]) }), ) worker.start() ``` > Notice how there's nothing test-specific in the example? The `example.mocks.ts` file is a copy-paste example of intercepting the `GET /books` request. This allows to share these mocks with the users as a legitimate example, because it is! Once the `*.mocks.ts` file is written, proceed by creating a test file: ```ts // test/browser/example.test.ts import * as path from 'path' import { test, expect } from './playwright.extend' test('returns a mocked response', async ({ loadExample, fetch }) => { // Compile the given usage example on runtime. await loadExample(new URL('./example.mocks.ts', import.meta.url)) // Perform the "GET /books" request in the browser. const res = await fetch('/books') // Assert the returned response body. expect(await res.json()).toEqual([ { id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da', title: 'The Lord of the Rings', publishedAt: -486867600, }, ]) }) ``` ##### Running all browser tests Make sure Playwright chromium has been installed before running browser tests. ```sh pnpm test:browser ``` #### Running a single browser test ```sh pnpm test:browser ./test/browser/example.test.ts ``` #### Node.js integration test Integration tests showcase a usage example in Node.js and are often placed next to the in-browser tests. Node.js integration tests reside in the `./test/node` directory. Similar to the browser tests, these are going to contain a usage example and the assertions over it. However, for Node.js tests there is no need to create a separate `*.mocks.ts` file. Instead, keep the usage example in the test file directly. Let's replicate the same `GET /books` integration test in Node.js. ```ts // test/node/example.test.ts import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('/books', () => { return HttpResponse.json([ { id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da', title: 'The Lord of the Rings', publishedAt: -486867600, }, ]) }), ) beforeAll(() => server.listen()) afterAll(() => server.close()) test('returns a mocked response', async () => { const res = await fetch('/books') expect(await res.json()).toEqual([ { id: 'ea42ffcb-e729-4dd5-bfac-7a5b645cb1da', title: 'The Lord of the Rings', publishedAt: -486867600, }, ]) }) ``` ##### Running all Node.js tests ```sh pnpm test:node ``` ##### Running a single Node.js test ```sh pnpm test:node ./test/node/example.test.ts ``` ## Build Build the library with the following command: ```bash $ pnpm build ``` [pnpm-url]: https://pnpm.io/ [page-with-url]: https://github.com/kettanaito/page-with [pnpm-install-guide-url]: https://pnpm.io/8.x/installation#installing-a-specific-version ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2018–present Artem Zakharchenko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

The Mock Service Worker logo

Mock Service Worker

Industry standard API mocking for JavaScript.

Join our Discord server



## Features - **Seamless**. A dedicated layer of requests interception at your disposal. Keep your application's code and tests unaware of whether something is mocked or not. - **Deviation-free**. Request the same production resources and test the actual behavior of your app. Augment an existing API, or design it as you go when there is none. - **Familiar & Powerful**. Use [Express](https://github.com/expressjs/express)-like routing syntax to intercept requests. Use parameters, wildcards, and regular expressions to match requests, and respond with necessary status codes, headers, cookies, delays, or completely custom resolvers. --- > "_I found MSW and was thrilled that not only could I still see the mocked responses in my DevTools, but that the mocks didn't have to be written in a Service Worker and could instead live alongside the rest of my app. This made it silly easy to adopt. The fact that I can use it for testing as well makes MSW a huge productivity booster._" > > — [Kent C. Dodds](https://twitter.com/kentcdodds) ## Documentation This README will give you a brief overview of the library, but there's no better place to start with Mock Service Worker than its official documentation. - [Documentation](https://mswjs.io/docs) - [**Quick start**](https://mswjs.io/docs/quick-start) - [FAQ](https://mswjs.io/docs/faq) ## Examples - See the list of [**Usage examples**](https://github.com/mswjs/examples) ## Courses We've partnered with Egghead to bring you quality paid materials to learn the best practices of API mocking on the web. Please give them a shot! The royalties earned from them help sustain the project's development. Thank you. - 🚀 [**Mocking REST and GraphQL APIs with Mock Service Worker**](https://egghead.io/courses/mock-rest-and-graphql-apis-with-mock-service-worker-8d471ece?af=8mci9b) - 🔌 [Mocking (and testing) WebSocket APIs with Mock Service Worker](https://egghead.io/courses/mocking-websocket-apis-with-mock-service-worker-9933b7f5) ## Browser - [Learn more about using MSW in a browser](https://mswjs.io/docs/integrations/browser) - [`setupWorker` API](https://mswjs.io/docs/api/setup-worker) ### How does it work? In-browser usage is what sets Mock Service Worker apart from other tools. Utilizing the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), which can intercept requests for the purpose of caching, Mock Service Worker responds to intercepted requests with your mock definition on the network level. This way your application knows nothing about the mocking. **Take a look at this quick presentation on how Mock Service Worker functions in a browser:** [![What is Mock Service Worker?](https://raw.githubusercontent.com/mswjs/msw/main/media/msw-video-thumbnail.jpg)](https://youtu.be/HcQCqboatZk) ### How is it different? - This library intercepts requests on the network level, which means _after_ they have been performed and "left" your application. As a result, the entirety of your code runs, giving you more confidence when mocking; - Imagine your application as a box. Every API mocking library out there opens your box and removes the part that does the request, placing a blackbox in its stead. Mock Service Worker leaves your box intact, 1-1 as it is in production. Instead, MSW lives in a separate box next to yours; - No more stubbing of `fetch`, `axios`, `react-query`, you-name-it; - You can reuse the same mock definition for the unit, integration, and E2E testing. Did we mention local development and debugging? Yep. All running against the same network description without the need for adapters or bloated configurations. ### Usage example ```js // 1. Import the library. import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' // 2. Describe network behavior with request handlers. const worker = setupWorker( http.get('https://github.com/octocat', ({ request, params, cookies }) => { return HttpResponse.json( { message: 'Mocked response', }, { status: 202, statusText: 'Mocked status', }, ) }), ) // 3. Start mocking by starting the Service Worker. await worker.start() ``` Performing a `GET https://github.com/octocat` request in your application will result into a mocked response that you can inspect in your browser's "Network" tab: ![Chrome DevTools Network screenshot with the request mocked](https://github.com/mswjs/msw/blob/main/media/msw-quick-look-network.png?raw=true) > **Tip:** Did you know that although Service Worker runs in a separate thread, your request handlers execute entirely on the client? This way you can use the same languages, like TypeScript, third-party libraries, and internal logic to create the mocks you need. ## Node.js - [Learn more about using MSW in Node.js](https://mswjs.io/docs/integrations/node) - [`setupServer` API](https://mswjs.io/docs/api/setup-server) ### How does it work? There's no such thing as Service Workers in Node.js. Instead, MSW implements a [low-level interception algorithm](https://github.com/mswjs/interceptors) that can utilize the very same request handlers you have for the browser. This blends the boundary between environments, allowing you to focus on your network behaviors. ### How is it different? - Does not stub `fetch`, `axios`, etc. As a result, your tests know _nothing_ about mocking; - You can reuse the same request handlers for local development and debugging, as well as for testing. Truly a single source of truth for your network behavior across all environments and all tools. ### Usage example Here's an example of using Mock Service Worker while developing your Express server: ```js import express from 'express' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const app = express() const server = setupServer() app.get( '/checkout/session', server.boundary((req, res) => { // Describe the network for this Express route. server.use( http.get( 'https://api.stripe.com/v1/checkout/sessions/:id', ({ params }) => { return HttpResponse.json({ id: params.id, mode: 'payment', status: 'open', }) }, ), ) // Continue with processing the checkout session. handleSession(req, res) }), ) ``` > This example showcases [`server.boundary()`](https://mswjs.io/docs/api/setup-server/boundary) to scope request interception to a particular closure, which is extremely handy! ## Sponsors Mock Service Worker is trusted by hundreds of thousands of engineers around the globe. It's used by companies like Google, Microsoft, Spotify, Amazon, Netflix, and countless others. Despite that, it remains a hobby project maintained in a spare time and has no opportunity to financially support even a single full-time contributor. **You can change that!** Consider [sponsoring the effort](https://github.com/sponsors/mswjs) behind one of the most innovative approaches around API mocking. Raise a topic of open source sponsorships with your boss and colleagues. Let's build sustainable open source together! ### Golden sponsors > Become our _golden sponsor_ and get featured right here, enjoying other perks like issue prioritization and a personal consulting session with us. > > **Learn more on our [GitHub Sponsors profile](https://github.com/sponsors/mswjs)**.
GitHub Codacy Workleap Chromatic
StackBlitz
### Silver sponsors > Become our _silver sponsor_ and get your profile image and link featured right here. > > **Learn more on our [GitHub Sponsors profile](https://github.com/sponsors/mswjs)**.
Replay Codemod Ryan Magoon
### Bronze sponsors > Become our _bronze sponsor_ and get your profile image and link featured in this section. > > **Learn more on our [GitHub Sponsors profile](https://github.com/sponsors/mswjs)**.
Materialize Trigger.dev Vital
## Awards & mentions We've been extremely humbled to receive awards and mentions from the community for all the innovation and reach Mock Service Worker brings to the JavaScript ecosystem.
Technology Radar

Solution Worth Pursuing

Technology Radar (2020–2021)

Open Source Awards 2020

The Most Exciting Use of Technology

Open Source Awards (2020)

================================================ FILE: browser/package.json ================================================ { "type": "module", "main": "../lib/browser/index.js", "module": "../lib/browser/index.mjs", "types": "../lib/browser/index.d.ts", "exports": { ".": { "module-sync": { "types": "./../lib/browser/index.d.mts", "default": "./../lib/browser/index.mjs" }, "module": { "types": "./../lib/browser/index.d.mts", "default": "./../lib/browser/index.mjs" }, "browser": { "types": "./../lib/browser/index.d.mts", "default": "./../lib/browser/index.mjs" }, "import": { "types": "./../lib/browser/index.d.mts", "default": "./../lib/browser/index.mjs" }, "default": { "types": "./../lib/browser/index.d.ts", "default": "./../lib/browser/index.js" } }, "./package.json": "./package.json" } } ================================================ FILE: cli/index.js ================================================ #!/usr/bin/env node import yargs from 'yargs' import { init } from './init.js' // eslint-disable-next-line @typescript-eslint/no-unused-expressions yargs(process.argv.slice(2)) .usage('$0 [args]') .command( 'init', 'Initializes Mock Service Worker at the specified directory', (yargs) => { yargs .positional('publicDir', { type: 'string', description: 'Relative path to the public directory', demandOption: false, normalize: true, }) .option('save', { type: 'boolean', description: 'Save the worker directory in your package.json', }) .option('cwd', { type: 'string', description: 'Custom current worker directory', normalize: true, }) .example('msw init') .example('msw init ./public') .example('msw init ./static --save') }, init, ) .demandCommand() .help().argv ================================================ FILE: cli/init.js ================================================ import fs from 'node:fs' import path from 'node:path' import colors from 'picocolors' import confirm from '@inquirer/confirm' import { invariant } from './invariant.js' import { SERVICE_WORKER_BUILD_PATH } from '../config/constants.js' export async function init(args) { const CWD = args.cwd || process.cwd() const publicDir = args._[1] ? normalizePath(args._[1]) : undefined const packageJsonPath = path.resolve(CWD, 'package.json') const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) const savedWorkerDirectories = Array.prototype .concat((packageJson.msw && packageJson.msw.workerDirectory) || []) .map(normalizePath) if (publicDir) { // If the public directory was provided, copy the worker script // to that directory only. Even if there are paths stored in "msw.workerDirectory", // those will not be touched. await copyWorkerScript(publicDir, CWD) const relativePublicDir = path.relative(CWD, publicDir) printSuccessMessage([publicDir]) if (args.save) { // Only save the public path if it's not already saved in "package.json". if (!savedWorkerDirectories.includes(relativePublicDir)) { saveWorkerDirectory(packageJsonPath, relativePublicDir) } } // Explicitly check if "save" was not provided (was null). // You can also provide the "--no-save" option, and then "args.save" // will equal to false. else if (args.save == null) { // eslint-disable-next-line no-console console.log(`\ ${colors.cyan( 'INFO', )} In order to ease the future updates to the worker script, we recommend saving the path to the worker directory in your package.json.`) // If the "--save" flag was not provided, prompt to save // the public path. promptWorkerDirectoryUpdate( `Do you wish to save "${relativePublicDir}" as the worker directory?`, packageJsonPath, relativePublicDir, ) } return } // Calling "init" without a public directory but with the "--save" flag is a no-op. invariant( args.save == null, 'Failed to copy the worker script: cannot call the "init" command without a public directory but with the "--save" flag. Either drop the "--save" flag to copy the worker script to all paths listed in "msw.workerDirectory", or add an explicit public directory to the command, like "npx msw init ./public".', ) // If the public directory was not provided, check any existing // paths in "msw.workerDirectory". When called without the public // directory, the "init" command must copy the worker script // to all the paths stored in "msw.workerDirectory". if (savedWorkerDirectories.length > 0) { const copyResults = await Promise.allSettled( savedWorkerDirectories.map((destination) => { return copyWorkerScript(destination, CWD).catch((error) => { // Inject the absolute destination path onto the copy function rejections // so it's available in the failed paths array below. throw [toAbsolutePath(destination, CWD), error] }) }), ) const successfulPaths = copyResults .filter((result) => result.status === 'fulfilled') .map((result) => result.value) const failedPathsWithErrors = copyResults .filter((result) => result.status === 'rejected') .map((result) => result.reason) // Notify about failed copies, if any. if (failedPathsWithErrors.length > 0) { printFailureMessage(failedPathsWithErrors) } // Notify about successful copies, if any. if (successfulPaths.length > 0) { printSuccessMessage(successfulPaths) } } } /** * @param {string} maybeAbsolutePath * @param {string} cwd * @returns {string} */ function toAbsolutePath(maybeAbsolutePath, cwd) { return path.isAbsolute(maybeAbsolutePath) ? maybeAbsolutePath : path.resolve(cwd, maybeAbsolutePath) } /** * @param {string} destination * @param {string} cwd * @returns {Promise} */ async function copyWorkerScript(destination, cwd) { // When running as a part of "postinstall" script, "cwd" equals the library's directory. // The "postinstall" script resolves the right absolute public directory path. const absolutePublicDir = toAbsolutePath(destination, cwd) if (!fs.existsSync(absolutePublicDir)) { await fs.promises .mkdir(absolutePublicDir, { recursive: true }) .catch((error) => { throw new Error( invariant( false, 'Failed to copy the worker script at "%s": directory does not exist and could not be created.\nMake sure to include a relative path to the public directory of your application.\n\nSee the original error below:\n\n%s', absolutePublicDir, error, ), ) }) } // eslint-disable-next-line no-console console.log('Copying the worker script at "%s"...', absolutePublicDir) const workerFilename = path.basename(SERVICE_WORKER_BUILD_PATH) const workerDestinationPath = path.resolve(absolutePublicDir, workerFilename) fs.copyFileSync(SERVICE_WORKER_BUILD_PATH, workerDestinationPath) return workerDestinationPath } /** * @param {Array} paths */ function printSuccessMessage(paths) { // eslint-disable-next-line no-console console.log(` ${colors.green('Worker script successfully copied!')} ${paths.map((path) => colors.gray(` - ${path}\n`))} Continue by describing the network in your application: ${colors.red(colors.bold('https://mswjs.io/docs/quick-start'))} `) } function printFailureMessage(pathsWithErrors) { // eslint-disable-next-line no-console console.error(`\ ${colors.red('Copying the worker script failed at following paths:')} ${pathsWithErrors .map(([path, error]) => colors.gray(` - ${path}`) + '\n' + ` ${error}`) .join('\n\n')} `) } /** * @param {string} packageJsonPath * @param {string} publicDir */ function saveWorkerDirectory(packageJsonPath, publicDir) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) // eslint-disable-next-line no-console console.log( colors.gray('Updating "msw.workerDirectory" at "%s"...'), packageJsonPath, ) const prevWorkerDirectory = Array.prototype.concat( (packageJson.msw && packageJson.msw.workerDirectory) || [], ) const nextWorkerDirectory = Array.from( new Set(prevWorkerDirectory).add(publicDir), ) const nextPackageJson = Object.assign({}, packageJson, { msw: { workerDirectory: nextWorkerDirectory, }, }) fs.writeFileSync( packageJsonPath, JSON.stringify(nextPackageJson, null, 2), 'utf8', ) } /** * @param {string} message * @param {string} packageJsonPath * @param {string} publicDir * @returns {void} */ function promptWorkerDirectoryUpdate(message, packageJsonPath, publicDir) { return confirm({ theme: { prefix: colors.yellowBright('?'), }, message, }).then((answer) => { if (answer) { saveWorkerDirectory(packageJsonPath, publicDir) } }) } /** * Normalizes the given path, replacing ambiguous path separators * with the platform-specific path separator. * @param {string} input Path to normalize. * @returns {string} */ function normalizePath(input) { return input.replace(/[\\|\/]+/g, path.sep) } ================================================ FILE: cli/invariant.js ================================================ import colors from 'picocolors' export function invariant(predicate, message, ...args) { if (!predicate) { // eslint-disable-next-line no-console console.error(colors.red(message), ...args) process.exit(1) } } ================================================ FILE: cli/package.json ================================================ { "type": "module" } ================================================ FILE: commitlint.config.js ================================================ module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'body-max-line-length': [0, 'always', Infinity], 'footer-max-line-length': [1, 'always'], }, } ================================================ FILE: config/constants.js ================================================ import url from 'node:url' import path from 'node:path' export const SERVICE_WORKER_SOURCE_PATH = url.fileURLToPath( new URL('../src/mockServiceWorker.js', import.meta.url), ) export const SERVICE_WORKER_BUILD_PATH = url.fileURLToPath( new URL( path.join('../lib', path.basename(SERVICE_WORKER_SOURCE_PATH)), import.meta.url, ), ) ================================================ FILE: config/copyServiceWorker.ts ================================================ import fs from 'node:fs' import path from 'node:path' import { until } from 'until-async' /** * Copies the given Service Worker source file into the destination. * Injects the integrity checksum into the destination file. */ export default async function copyServiceWorker( sourceFilePath: string, destFilePath: string, checksum: string, ): Promise { // eslint-disable-next-line no-console console.log('Compiling Service Worker...') const [readFileError, readFileResult] = await until(() => fs.promises.readFile(sourceFilePath, 'utf8'), ) if (readFileError) { throw new Error('Failed to read file.\n${readError.message}') } const destFileDirectory = path.dirname(destFilePath) // eslint-disable-next-line no-console console.log('Checking if "%s" path exists...', destFileDirectory) if (!fs.existsSync(destFileDirectory)) { // eslint-disable-next-line no-console console.log('Destination directory does not exist, creating...') await fs.promises.mkdir(destFileDirectory, { recursive: true }) } const packageJson = JSON.parse( fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'), ) const nextFileContent = readFileResult .replace('', checksum) .replace('', packageJson.version) const [writeFileError] = await until(() => fs.promises.writeFile(destFilePath, nextFileContent), ) if (writeFileError) { throw new Error(`Failed to write file.\n${writeFileError.message}`) } // eslint-disable-next-line no-console console.log('Service Worker copied to: %s', destFilePath) } ================================================ FILE: config/package.json ================================================ { "type": "module" } ================================================ FILE: config/plugins/esbuild/copyWorkerPlugin.ts ================================================ import fs from 'node:fs' import path from 'node:path' import crypto from 'crypto' import minify from 'babel-minify' import { invariant } from 'outvariant' import type { Plugin } from 'esbuild' import copyServiceWorker from '../../copyServiceWorker.js' const SERVICE_WORKER_ENTRY_PATH = path.resolve( process.cwd(), './src/mockServiceWorker.js', ) const SERVICE_WORKER_OUTPUT_PATH = path.resolve( process.cwd(), './lib/mockServiceWorker.js', ) function getChecksum(contents: string): string { const { code } = minify(contents, {}, { comments: false }) return crypto.createHash('md5').update(code, 'utf8').digest('hex') } export function getWorkerChecksum(): string { const workerContents = fs.readFileSync(SERVICE_WORKER_ENTRY_PATH, 'utf8') return getChecksum(workerContents) } export function copyWorkerPlugin(checksum: string): Plugin { return { name: 'copyWorkerPlugin', async setup(build) { invariant( SERVICE_WORKER_ENTRY_PATH, 'Failed to locate the worker script source file', ) if (fs.existsSync(SERVICE_WORKER_OUTPUT_PATH)) { console.warn( 'Skipped copying the worker script to "%s": already exists', SERVICE_WORKER_OUTPUT_PATH, ) return } // Generate the checksum from the worker script's contents. // const workerContents = await fs.readFile(workerSourcePath, 'utf8') // const checksum = getChecksum(workerContents) build.onLoad({ filter: /mockServiceWorker\.js$/ }, async () => { return { // Prevent the worker script from being transpiled. // But, generally, the worker script is not in the entrypoints. contents: '', } }) build.onEnd(() => { // eslint-disable-next-line no-console console.log('worker script checksum:', checksum) // Copy the worker script on the next tick. process.nextTick(async () => { await copyServiceWorker( SERVICE_WORKER_ENTRY_PATH, SERVICE_WORKER_OUTPUT_PATH, checksum, ) }) }) }, } } ================================================ FILE: config/plugins/esbuild/forceEsmExtensionsPlugin.ts ================================================ import { type Plugin } from 'esbuild' export const ESM_EXTENSION = '.mjs' export const CJS_EXTENSION = '.js' export function forceEsmExtensionsPlugin(): Plugin { return { name: 'forceEsmExtensionsPlugin', setup(build) { const isEsm = build.initialOptions.format === 'esm' build.onEnd(async (result) => { if (result.errors.length > 0) { return } for (const outputFile of result.outputFiles || []) { // Only target CJS/ESM files. // This ignores additional files emitted, like sourcemaps ("*.js.map"). if ( !( outputFile.path.endsWith(ESM_EXTENSION) || outputFile.path.endsWith('.mjs') ) ) { continue } const fileContents = outputFile.text const nextFileContents = modifyRelativeImports(fileContents, isEsm) outputFile.contents = Buffer.from(nextFileContents) } }) }, } } const CJS_RELATIVE_IMPORT_EXP = /require\(["'](\..+)["']\)(;)?/gm const ESM_RELATIVE_IMPORT_EXP = /from ["'](\..+)["'](;)?/gm function modifyRelativeImports(contents: string, isEsm: boolean): string { const extension = isEsm ? ESM_EXTENSION : CJS_EXTENSION const importExpression = isEsm ? ESM_RELATIVE_IMPORT_EXP : CJS_RELATIVE_IMPORT_EXP return contents.replace( importExpression, (_, importPath, maybeSemicolon = '') => { if (importPath.endsWith('.') || importPath.endsWith('/')) { return isEsm ? `from '${importPath}/index${extension}'${maybeSemicolon}` : `require("${importPath}/index${extension}")${maybeSemicolon}` } if (importPath.endsWith(extension)) { return isEsm ? `from '${importPath}'${maybeSemicolon}` : `require("${importPath}")${maybeSemicolon}` } return isEsm ? `from '${importPath}${extension}'${maybeSemicolon}` : `require("${importPath}${extension}")${maybeSemicolon}` }, ) } ================================================ FILE: config/plugins/esbuild/graphQLImportPlugin.ts ================================================ import fs from 'node:fs' import type { Plugin } from 'esbuild' /** * A plugin to replace `require('graphql')` statements with `await import('graphql')` * only for ESM bundles. This makes the GraphQL module to be imported lazily * while maintaining the CommonJS compatibility. * @see https://github.com/mswjs/msw/issues/2254 */ export function graphqlImportPlugin(): Plugin { return { name: 'graphql-import-plugin', setup(build) { if (build.initialOptions.format !== 'esm') { return } build.onLoad({ filter: /\.ts$/ }, async (args) => { const contents = await fs.promises.readFile(args.path, 'utf-8') const match = /require\(['"]graphql['"]\)/g.exec(contents) if (match) { return { loader: 'ts', contents: contents.slice(0, match.index - 1) + `await import('graphql').catch((error) => {console.error('[MSW] Failed to parse a GraphQL query: cannot import the "graphql" module. Please make sure you install it if you wish to intercept GraphQL requests. See the original import error below.'); throw error})` + contents.slice(match.index + match[0].length), } } }) }, } } ================================================ FILE: config/plugins/esbuild/resolveCoreImportsPlugin.ts ================================================ import { Plugin } from 'esbuild' import { replaceCoreImports } from '../../replaceCoreImports.js' import { ESM_EXTENSION } from './forceEsmExtensionsPlugin.js' export function resolveCoreImportsPlugin(): Plugin { return { name: 'resolveCoreImportsPlugin', setup(build) { build.onEnd(async (result) => { if (result.errors.length > 0) { return } for (const outputFile of result.outputFiles || []) { const isEsm = outputFile.path.endsWith(ESM_EXTENSION) const fileContents = outputFile.text const nextFileContents = replaceCoreImports( outputFile.path, fileContents, isEsm, ) outputFile.contents = Buffer.from(nextFileContents) } }) }, } } ================================================ FILE: config/polyfills-node.ts ================================================ import { setTimeout as nodeSetTimeout } from 'timers' // Polyfill the global "setTimeout" so MSW could be used // with "jest.useFakeTimers()". MSW response handling // is wrapped in "setTimeout", and without this polyfill // you'd have to manually advance the timers for the response // to finally resolve. export const setTimeout = nodeSetTimeout ================================================ FILE: config/replaceCoreImports.js ================================================ const CORE_ESM_IMPORT_PATTERN = /from ["'](~\/core(.*))["'](;)?/gm const CORE_CJS_IMPORT_PATTERN = /require\(["'](~\/core(.*))["']\)(;)?/gm function getCoreImportPattern(isEsm) { return isEsm ? CORE_ESM_IMPORT_PATTERN : CORE_CJS_IMPORT_PATTERN } export function hasCoreImports(fileContents, isEsm) { return getCoreImportPattern(isEsm).test(fileContents) } export function replaceCoreImports(moduleFilePath, fileContents, isEsm) { return fileContents.replace( getCoreImportPattern(isEsm), (_, __, maybeSubmodulePath, maybeSemicolon) => { const submodulePath = maybeSubmodulePath || '/index' /** * @note Although all .d.ts are considered ESM, append different * file extension for d.mts files. */ const extension = moduleFilePath.endsWith('.d.mts') ? '.mjs' : '' const semicolon = maybeSemicolon || '' return isEsm ? `from "../core${submodulePath}${extension}"${semicolon}` : `require("../core${submodulePath}")${semicolon}` }, ) } ================================================ FILE: config/scripts/patch-ts.js ================================================ import fs from 'node:fs' import { exec } from 'node:child_process' import { promisify } from 'node:util' import { invariant } from 'outvariant' import * as glob from 'glob' import { hasCoreImports, replaceCoreImports } from '../replaceCoreImports.js' const execAsync = promisify(exec) const BUILD_DIR = new URL('../../lib/', import.meta.url) async function patchTypeDefs() { const typeDefsPaths = glob.sync('**/*.d.{ts,mts}', { cwd: BUILD_DIR, absolute: true, }) const typeDefsWithCoreImports = typeDefsPaths .map((modulePath) => { const fileContents = fs.readFileSync(modulePath, 'utf8') /** * @note Treat all type definition files as ESM because even * CJS .d.ts use `import` statements. */ if (hasCoreImports(fileContents, true)) { return [modulePath, fileContents] } }) .filter(Boolean) if (typeDefsWithCoreImports.length === 0) { console.log( 'Found no .d.ts modules containing the "~/core" import, skipping...', ) return process.exit(0) } console.log( 'Found %d module(s) with the "~/core" import, resolving...', typeDefsWithCoreImports.length, ) for (const [typeDefsPath, fileContents] of typeDefsWithCoreImports) { // Treat ".d.ts" files as ESM to replace "import" statements. // Force no extension on the ".d.ts" imports. const nextFileContents = replaceCoreImports( typeDefsPath, fileContents, true, ) fs.writeFileSync(typeDefsPath, nextFileContents, 'utf8') console.log('Successfully patched "%s"!', typeDefsPath) } console.log( 'Imports resolved in %d file(s), verifying...', typeDefsWithCoreImports.length, ) // Next, validate that we left no "~/core" imports unresolved. const result = await execAsync( `grep "~/core" ./**/*.d.{ts,mts} -R -l || exit 0`, { cwd: BUILD_DIR, shell: '/bin/bash', }, ) invariant( result.stderr === '', 'Failed to validate the .d.ts modules for the presence of the "~/core" import. See the original error below.', result.stderr, ) if (result.stdout !== '') { const modulesWithUnresolvedImports = result.stdout .split('\n') .filter(Boolean) console.error( `Found .d.ts modules containing unresolved "~/core" import after the patching: ${modulesWithUnresolvedImports.map((path) => ` - ${new URL(path, BUILD_DIR).pathname}`).join('\n')} `, ) return process.exit(1) } // Ensure that the .d.ts files compile without errors after resolving the "~/core" imports. console.log('Compiling the .d.ts modules with tsc...') const tscCompilation = await execAsync( `tsc --noEmit --skipLibCheck ${typeDefsPaths.join(' ')}`, { cwd: BUILD_DIR, }, ) if (tscCompilation.stderr !== '') { console.error( 'Failed to compile the .d.ts modules with tsc. See the original error below.', tscCompilation.stderr, ) return process.exit(1) } // Ensure that CJS .d.ts file never reference .mjs files. const mjsInCjsResult = await execAsync( `grep ".mjs" ./**/*.d.ts -R -l || exit 0`, { cwd: BUILD_DIR, shell: '/bin/bash', }, ) invariant( mjsInCjsResult.stderr === '', 'Failed to validate the .d.ts modules not referencing ".mjs" files. See the original error below.', mjsInCjsResult.stderr, ) if (mjsInCjsResult.stdout !== '') { const modulesWithUnresolvedImports = mjsInCjsResult.stdout .split('\n') .filter(Boolean) console.error( `Found .d.ts modules referencing ".mjs" files after patching: ${modulesWithUnresolvedImports.map((path) => ` - ${new URL(path, BUILD_DIR).pathname}`).join('\n')} `, ) return process.exit(1) } console.log( 'The "~/core" imports resolved successfully in %d .d.ts modules! 🎉', typeDefsWithCoreImports.length, ) } patchTypeDefs() ================================================ FILE: config/scripts/postinstall.js ================================================ import fs from 'node:fs' import path from 'node:path' import { execFileSync } from 'node:child_process' // When executing the "postinstall" script, the "process.cwd" equals // the package directory, not the parent project where the package is installed. // NPM stores the parent project directory in the "INIT_CWD" env variable. const parentPackageCwd = process.env.INIT_CWD function postInstall() { const packageJson = JSON.parse( fs.readFileSync(path.resolve(parentPackageCwd, 'package.json'), 'utf8'), ) if (!packageJson.msw || !packageJson.msw.workerDirectory) { return } const cliExecutable = path.resolve(process.cwd(), 'cli/index.js') try { /** * @note Call the "init" command directly. It will now copy the worker script * to all saved paths in "msw.workerDirectory" */ execFileSync(process.execPath, [cliExecutable, 'init'], { cwd: parentPackageCwd, }) } catch (error) { console.error( `[MSW] Failed to automatically update the worker script.\n\n${error}`, ) } } postInstall() ================================================ FILE: config/scripts/smoke-test.sh ================================================ #!/bin/bash set -e COMMIT_HASH=$(git rev-parse HEAD) MSW_VERSION="0.0.0-$COMMIT_HASH" echo "Latest commit: $COMMIT_HASH" echo "In-progress MSW version: $MSW_VERSION" PKG_JSON_COPY="package.json.copy" cp package.json $PKG_JSON_COPY pnpm version $MSW_VERSION --no-git-tag-version --allow-same-version echo "" echo "Packing MSW..." pnpm pack EXAMPLES_REPO=https://github.com/mswjs/examples.git EXAMPLES_DIR=./examples echo "" echo "Cloning the examples from "$EXAMPLES_REPO"..." if [[ -d "$EXAMPLES_DIR" ]]; then echo "Examples already cloned, skipping..." else git clone $EXAMPLES_REPO $EXAMPLES_DIR fi echo "" echo "Installing dependencies..." cd $EXAMPLES_DIR pnpm install echo "" echo "Linking MSW..." pnpm add msw --filter="with-*" file:../../../msw-$MSW_VERSION.tgz pnpm ls msw echo "" echo "Running tests..." CI=1 pnpm test ; (cd ../ && mv $PKG_JSON_COPY ./package.json) ================================================ FILE: decisions/jest-support.md ================================================ # Jest support With the introduction of [Mock Service Worker 2.0](https://mswjs.io/blog/introducing-msw-2.0), the library has made a significant step forward in the effort of embracing and promoting web standards. Since that release, its contributors have reported multiple issues with Node.js simply because MSW exposed developers to using standard Node.js APIs. Betting on the web standards is one of the goals behind this project. One of such standards is ESM. It's the present and the future of JavaScript, and we are planning on switching to ESM-only in the years to come. For that transition to happen, we need to prioritize and, at times, make hard decisions. **MSW offers no official support for Jest.** It doesn't mean MSW cannot be used in Jest. We maintain usage examples of both [Jest](https://github.com/mswjs/examples/tree/main/examples/with-jest) and [Jest+JSDOM](https://github.com/mswjs/examples/tree/main/examples/with-jest-jsdom) to attest to that. Although it's necessary to mention that those examples require additional setup to tackle underlying Jest or JSDOM issues. What this means is that **we are not going to address any issues specific to Jest or JSDOM**. Those pose a significant time investment just to uncover another inconsistency between the browser and JSDOM, or the lacking features in Jest, like proper ESM support. That is not a reasonable use of the limited contributors' time. You will have a far better chance of getting your issue solved by reporting it to the Jest or JSDOM repo for the respective teams to address it. ## What's next? > [!IMPORTANT] > If you are experiencing issues with using MSW in Jest, **please verify them outside of Jest before reporting them**. You can verify the issue in a plain Node.js script, or by copying your problematic test to [Vitest](https://vitest.dev/). Please note that we do not support issue reports from non-standard environments, like Deno or Bun either. You can use one of our existing [Usage examples](https://github.com/mswjs/examples) as a template project to reproduce your issue. ================================================ FILE: decisions/linting-worker-script.md ================================================ # Linting the worker script When linting your application, you may encounter warnings or errors originating from the `mockServiceWorker.js` script. Please refrain from opening pull requests to add the `ignore` pragma comments to the script itself. ## Solution **Make sure that the worker script is ignored by your linting tools**. The worker script isn't a part of your application's code but a static asset. It must be ignored during linting and prettifying in the same way all your static assets in `/public` are ignored. Please configure your tools respectively. If there are warnings/errors originating from the worker script, it's likely your public directory is not ignored by your linting tools. You may consider ignoring the entire public directory if that suits your project's conventions. ================================================ FILE: decisions/releases.md ================================================ # Releases MSW uses the [Release](https://github.com/ossjs/release) library to automate its releases to NPM. ## Release schedule The next version of **the library releases automatically every day**. We do our best to choose the optimal release window to accumulate multiple changes under a single release. We do deviate from the release schedule in emergency cases, like when a critical issue has been fixed and needs an immediate release. > [!IMPORTANT] > Please do not ping the library maintainers to release a new version of MSW. Be patient and wait for the automated release to happen. Subscribe to any issue or pull request you are interested in, and you will be notified whenever it gets released. ## Preview releases This repository is configured to **release work-in-progress pull requests** using [okg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). Once the pull request in question is approved, it will automatically be published to a temporary registry. Follow the instructions in the automated comment to install the work-in-progress version of the package. ================================================ FILE: decisions/typescript-versioning.md ================================================ # TypeScript versioning ## Strict TypeScript peer dependency version In the past, MSW set the explicit upper version range for the `typescript` peer dependency (`>= 4.4.x <= 5.1.x`). While that provides better stability and predictability, we acknowledge that it negatively impacts our users, forcing them to wait for MSW to update the TypeScript version range in order to migrate to newest versions of TypeScript in their projects. > Context: TypeScript is [not distributed according to semver](https://github.com/microsoft/TypeScript/issues/14116). Instead, it's `{marketing}.{major}.{minor}` versioning pattern. This means that it may and does contain breaking changes across what consumers perceive as _minor_ versions. I've had numerous fights with this as it's not uncommon for TypeScript to exhibit different behavior across minor versions (the same types compile on 4.6, break on 4.7, then compile without issue on 4.8). With this in mind, _we are removing the upper range_ of the `typescript` peer dependency. We have an [automated job](../.github/workflows/typescript-nightly.yml) in place that validates the latest build of MSW against the nightly releases of TypeScript, which should ensure early issue detection and help us resolve those issues before they happen. ## TypeScript version compliance Every version within the `typescript` peer dependency version range must have a corresponding TypeScript compliance test to guarantee the library's operatbility on that version. A compliance test is represented as a regular _typings test_ compiled using a specific version of TypeScript. You can learn more in the [typings tests](/test/typings). The typings test will automatically attempt to look up `tsconfig.{major}.{minor}.json` file corresponding to the currently installed version of TypeScript. The compliance is then achieved by installing different TypeScript versions from the supported range and running the existing typings tests on that version. ================================================ FILE: eslint.config.mjs ================================================ // @ts-check import eslint from '@eslint/js' import tseslint from 'typescript-eslint' import eslintConfigPrettier from 'eslint-config-prettier' import eslintPluginPrettier from 'eslint-plugin-prettier/recommended' export default tseslint.config( eslint.configs.recommended, eslintConfigPrettier, ...tseslint.configs.recommended, { languageOptions: { parserOptions: { ecmaVersion: 2020, sourceType: 'module', }, }, ignores: ['/lib', '/node', '/native', '/config', '/test'], rules: { 'no-console': [ 'error', { allow: ['warn', 'error', 'group', 'groupCollapsed', 'groupEnd'], }, ], 'no-async-promise-executor': 'off', 'require-yield': 'off', 'no-empty-pattern': 'off', 'no-control-regex': 'off', '@typescript-eslint/no-empty-object-type': 'off', '@typescript-eslint/prefer-ts-expect-error': 'error', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-namespace': [ 'error', { allowDeclarations: true, }, ], '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_', }, ], }, }, // Unused variables are useful in test files, and type test files { files: [ '**/*.test.ts', '**/*.test-d.ts', '**/*.mocks.ts', '**/*.setup.ts', '**/*.config.ts', ], rules: { 'no-console': 'off', '@typescript-eslint/prefer-ts-expect-error': 'off', '@typescript-eslint/no-unused-vars': 'off', }, }, { files: ['**/*.test-d.ts'], rules: { '@typescript-eslint/no-unused-expressions': 'off', }, }, eslintPluginPrettier, ) ================================================ FILE: global.d.ts ================================================ declare module 'babel-minify' { export default function babelMinify( code: string, opts: Record, babelOpts: Record, ): { code: string } } ================================================ FILE: knip.json ================================================ { "$schema": "https://unpkg.com/knip@5/schema.json", "entry": [ "src/{core,browser,node,native}/index.ts!", "src/mockServiceWorker.js!", "cli/index.js!", "config/**/*.{js,ts}!", "test/**/*ts" ], "project": ["src/**/*.ts!", "cli/**/*.js!", "config/**/*.{js,ts}!"] } ================================================ FILE: native/package.json ================================================ { "type": "commonjs", "browser": null, "main": "../lib/native/index.js", "module": "../lib/native/index.mjs", "types": "../lib/native/index.d.ts", "exports": { ".": { "react-native": { "import": { "types": "./../lib/native/index.d.mts", "default": "./../lib/native/index.mjs" }, "default": { "types": "./../lib/native/index.d.ts", "default": "./../lib/native/index.js" } }, "browser": null, "import": { "types": "./../lib/native/index.d.mts", "default": "./../lib/native/index.mjs" }, "default": { "types": "./../lib/native/index.d.ts", "default": "./../lib/native/index.js" } } } } ================================================ FILE: node/package.json ================================================ { "type": "commonjs", "browser": null, "main": "../lib/node/index.js", "module": "../lib/node/index.mjs", "types": "../lib/node/index.d.ts", "exports": { ".": { "module-sync": { "types": "./../lib/node/index.d.mts", "default": "./../lib/node/index.mjs" }, "module": { "types": "./../lib/node/index.d.mts", "default": "./../lib/node/index.mjs" }, "node": { "require": "./../lib/node/index.js", "import": "./../lib/node/index.mjs" }, "import": { "types": "./../lib/node/index.d.mts", "default": "./../lib/node/index.mjs" }, "browser": null, "react-native": null, "default": { "types": "./../lib/node/index.d.ts", "default": "./../lib/node/index.js" } } } } ================================================ FILE: package.json ================================================ { "name": "msw", "version": "2.12.13", "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.", "type": "commonjs", "main": "./lib/core/index.js", "module": "./lib/core/index.mjs", "types": "./lib/core/index.d.ts", "packageManager": "pnpm@9.15.0", "exports": { ".": { "module-sync": { "types": "./lib/core/index.d.mts", "default": "./lib/core/index.mjs" }, "module": { "types": "./lib/core/index.d.mts", "default": "./lib/core/index.mjs" }, "react-native": { "import": { "types": "./lib/core/index.d.mts", "default": "./lib/core/index.mjs" }, "default": { "types": "./lib/core/index.d.ts", "default": "./lib/core/index.js" } }, "import": { "types": "./lib/core/index.d.mts", "default": "./lib/core/index.mjs" }, "default": { "types": "./lib/core/index.d.ts", "default": "./lib/core/index.js" } }, "./browser": { "module-sync": { "types": "./lib/browser/index.d.mts", "default": "./lib/browser/index.mjs" }, "module": { "types": "./lib/browser/index.d.mts", "default": "./lib/browser/index.mjs" }, "browser": { "types": "./lib/browser/index.d.mts", "default": "./lib/browser/index.mjs" }, "import": { "types": "./lib/browser/index.d.mts", "default": "./lib/browser/index.mjs" }, "node": null, "react-native": null, "default": { "types": "./lib/browser/index.d.ts", "default": "./lib/browser/index.js" } }, "./node": { "module-sync": { "types": "./lib/node/index.d.mts", "default": "./lib/node/index.mjs" }, "module": { "types": "./lib/node/index.d.mts", "default": "./lib/node/index.mjs" }, "node": { "require": "./lib/node/index.js", "import": "./lib/node/index.mjs" }, "import": { "types": "./lib/node/index.d.mts", "default": "./lib/node/index.mjs" }, "browser": null, "react-native": null, "default": { "types": "./lib/node/index.d.ts", "default": "./lib/node/index.js" } }, "./native": { "browser": null, "react-native": { "import": { "types": "./lib/native/index.d.mts", "default": "./lib/native/index.mjs" }, "default": { "types": "./lib/native/index.d.ts", "default": "./lib/native/index.js" } }, "import": { "types": "./lib/native/index.d.mts", "default": "./lib/native/index.mjs" }, "default": { "types": "./lib/native/index.d.ts", "default": "./lib/native/index.js" } }, "./core/http": { "module-sync": { "types": "./lib/core/http.d.mts", "default": "./lib/core/http.mjs" }, "module": { "types": "./lib/core/http.d.mts", "default": "./lib/core/http.mjs" }, "import": { "types": "./lib/core/http.d.mts", "default": "./lib/core/http.mjs" }, "default": { "types": "./lib/core/http.d.ts", "default": "./lib/core/http.js" } }, "./core/graphql": { "module-sync": { "types": "./lib/core/graphql.d.mts", "default": "./lib/core/graphql.mjs" }, "module": { "types": "./lib/core/graphql.d.mts", "default": "./lib/core/graphql.mjs" }, "import": { "types": "./lib/core/graphql.d.mts", "default": "./lib/core/graphql.mjs" }, "default": { "types": "./lib/core/graphql.d.ts", "default": "./lib/core/graphql.js" } }, "./core/ws": { "module-sync": { "types": "./lib/core/ws.d.mts", "default": "./lib/core/ws.mjs" }, "module": { "types": "./lib/core/ws.d.mts", "default": "./lib/core/ws.mjs" }, "import": { "types": "./lib/core/ws.d.mts", "default": "./lib/core/ws.mjs" }, "default": { "types": "./lib/core/ws.d.ts", "default": "./lib/core/ws.js" } }, "./mockServiceWorker.js": "./lib/mockServiceWorker.js", "./package.json": "./package.json" }, "bin": { "msw": "cli/index.js" }, "engines": { "node": ">=18" }, "scripts": { "start": "tsup --watch", "clean": "rimraf ./lib", "lint": "eslint \"{cli,src,test}/**/*.ts\"", "build": "pnpm clean && cross-env NODE_ENV=production tsup && pnpm patch:dts", "patch:dts": "node \"./config/scripts/patch-ts.js\"", "publint": "publint", "test": "pnpm test:unit && pnpm test:node && pnpm test:browser && pnpm test:native", "test:unit": "vitest", "test:node": "vitest --config=./test/node/vitest.config.ts", "test:native": "vitest --config=./test/native/vitest.config.ts", "test:browser": "playwright test -c ./test/browser/playwright.config.ts", "test:modules:node": "vitest run --config=./test/modules/node/vitest.config.ts", "test:modules:browser": "playwright test -c ./test/modules/browser/playwright.config.ts", "test:e2e": "vitest run --config=./test/e2e/vitest.config.ts", "test:ts": "vitest --config=./test/typings/vitest.config.ts", "prepare": "pnpm simple-git-hooks init", "prepack": "pnpm build", "release": "release publish", "postinstall": "node -e \"import('./config/scripts/postinstall.js').catch(() => void 0)\"", "knip": "knip" }, "lint-staged": { "**/*.ts": [ "eslint --fix" ], "**/*.{ts,json}": [ "prettier --write" ] }, "homepage": "https://mswjs.io", "repository": { "type": "git", "url": "git+https://github.com/mswjs/msw.git" }, "author": { "name": "Artem Zakharchenko", "url": "https://github.com/kettanaito" }, "license": "MIT", "funding": "https://github.com/sponsors/mswjs", "files": [ "config/package.json", "config/constants.js", "config/scripts/postinstall.js", "cli", "lib", "src", "browser", "node", "native", "LICENSE.md", "README.md" ], "keywords": [ "api", "mock", "mocking", "worker", "prototype", "server", "service", "handler", "testing", "front-end", "back-end" ], "sideEffects": false, "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "devDependencies": { "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", "@epic-web/test-server": "^0.1.6", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.39.1", "@fastify/websocket": "^11.2.0", "@graphql-typed-document-node/core": "^3.2.0", "@open-draft/test-server": "^0.4.2", "@ossjs/release": "^0.10.1", "@playwright/test": "^1.50.1", "@types/express": "^5.0.5", "@types/json-bigint": "^1.0.4", "@types/node": "~20.19.25", "@types/serviceworker": "^0.0.167", "@typescript-eslint/eslint-plugin": "^8.47.0", "@typescript-eslint/parser": "^8.47.0", "@web/dev-server": "^0.4.6", "axios": "^1.13.5", "babel-minify": "^0.5.1", "commitizen": "^4.3.1", "cross-env": "^10.1.0", "cross-fetch": "^4.1.0", "cz-conventional-changelog": "3.3.0", "esbuild": "^0.27.0", "esbuild-loader": "^4.4.0", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "express": "^5.1.0", "fastify": "^5.6.2", "fs-teardown": "^0.3.0", "glob": "^13.0.0", "jsdom": "^25.0.1", "json-bigint": "^1.0.0", "knip": "^5.70.1", "lint-staged": "^15.2.10", "msw": "workspace:*", "page-with": "^0.6.1", "prettier": "^3.6.2", "publint": "^0.3.15", "regenerator-runtime": "^0.14.1", "rimraf": "^6.1.2", "simple-git-hooks": "^2.13.1", "tsup": "^8.5.1", "typescript": "^5.9.3", "typescript-eslint": "^8.47.0", "undici": "^7.16.0", "url-loader": "^4.1.1", "vitest": "^4.0.13", "vitest-environment-miniflare": "^2.14.4", "webpack": "^5.95.0", "webpack-http-server": "^0.5.0" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "peerDependenciesMeta": { "typescript": { "optional": true } }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } }, "simple-git-hooks": { "pre-commit": "pnpm lint-staged", "prepare-commit-msg": "grep -qE '^[^#]' .git/COMMIT_EDITMSG || (exec < /dev/tty && pnpm cz --hook || true)", "commit-msg": "pnpm commitlint --edit $1" } } ================================================ FILE: release.config.json ================================================ { "$schema": "./node_modules/@ossjs/release/schema.json", "profiles": [ { "name": "latest", "use": "NPM_CONFIG_PROVENANCE=true pnpm publish --no-git-checks" } ] } ================================================ FILE: src/browser/global.browser.d.ts ================================================ declare const SERVICE_WORKER_CHECKSUM: string ================================================ FILE: src/browser/index.ts ================================================ export { setupWorker } from './setupWorker/setupWorker' export type { SetupWorker, StartOptions } from './setupWorker/glossary' export { SetupWorkerApi } from './setupWorker/setupWorker' ================================================ FILE: src/browser/setupWorker/glossary.ts ================================================ import { Emitter } from 'strict-event-emitter' import type { HttpRequestEventMap, Interceptor } from '@mswjs/interceptors' import type { DeferredPromise } from '@open-draft/deferred-promise' import { LifeCycleEventEmitter, LifeCycleEventsMap, SharedOptions, } from '~/core/sharedOptions' import { RequestHandler } from '~/core/handlers/RequestHandler' import type { RequiredDeep } from '~/core/typeUtils' import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler' import type { WorkerChannel } from '../utils/workerChannel' export interface StringifiedResponse extends ResponseInit { body: string | ArrayBuffer | ReadableStream | null } export type SetupWorkerInternalContext = { isMockingEnabled: boolean workerStoppedAt?: number startOptions: RequiredDeep workerPromise: DeferredPromise registration: ServiceWorkerRegistration | undefined getRequestHandlers: () => Array emitter: Emitter keepAliveInterval?: number workerChannel: WorkerChannel fallbackInterceptor?: Interceptor } export type ServiceWorkerInstanceTuple = [ ServiceWorker | null, ServiceWorkerRegistration, ] export type FindWorker = ( scriptUrl: string, mockServiceWorkerUrl: string, ) => boolean export interface StartOptions extends SharedOptions { /** * Service Worker registration options. */ serviceWorker?: { /** * Custom url to the worker script. * @default "/mockServiceWorker.js" */ url?: string options?: RegistrationOptions } /** * Disables the logging of the intercepted requests * into browser's console. * @default false */ quiet?: boolean /** * Defers any network requests until the Service Worker * instance is activated. * @default true */ waitUntilReady?: boolean /** * A custom lookup function to find a Mock Service Worker in the list * of all registered Service Workers on the page. */ findWorker?: FindWorker } export type StartReturnType = Promise export type StartHandler = ( options: RequiredDeep, initialOptions: StartOptions, ) => StartReturnType export type StopHandler = () => void export interface SetupWorker { /** * Registers and activates the mock Service Worker. * * @see {@link https://mswjs.io/docs/api/setup-worker/start `worker.start()` API reference} */ start: (options?: StartOptions) => StartReturnType /** * Stops requests interception for the current client. * * @see {@link https://mswjs.io/docs/api/setup-worker/stop `worker.stop()` API reference} */ stop: StopHandler /** * Prepends given request handlers to the list of existing handlers. * @param {RequestHandler[]} handlers List of runtime request handlers. * * @see {@link https://mswjs.io/docs/api/setup-worker/use `worker.use()` API reference} */ use: (...handlers: Array) => void /** * Marks all request handlers that respond using `res.once()` as unused. * * @see {@link https://mswjs.io/docs/api/setup-worker/restore-handlers `worker.restoreHandlers()` API reference} */ restoreHandlers: () => void /** * Resets request handlers to the initial list given to the `setupWorker` call, or to the explicit next request handlers list, if given. * @param {RequestHandler[]} nextHandlers List of the new initial request handlers. * * @see {@link https://mswjs.io/docs/api/setup-worker/reset-handlers `worker.resetHandlers()` API reference} */ resetHandlers: ( ...nextHandlers: Array ) => void /** * Returns a readonly list of currently active request handlers. * * @see {@link https://mswjs.io/docs/api/setup-worker/list-handlers `worker.listHandlers()` API reference} */ listHandlers(): ReadonlyArray /** * Life-cycle events. * Life-cycle events allow you to subscribe to the internal library events occurring during the request/response handling. * * @see {@link https://mswjs.io/docs/api/life-cycle-events Life-cycle Events API reference} */ events: LifeCycleEventEmitter } ================================================ FILE: src/browser/setupWorker/setupWorker.node.test.ts ================================================ /** * @vitest-environment node */ import { setupWorker } from './setupWorker' test('returns an error when run in a Node.js environment', () => { expect(setupWorker).toThrow( '[MSW] Failed to execute `setupWorker` in a non-browser environment', ) }) ================================================ FILE: src/browser/setupWorker/setupWorker.ts ================================================ import { invariant } from 'outvariant' import { isNodeProcess } from 'is-node-process' import { DeferredPromise } from '@open-draft/deferred-promise' import type { SetupWorkerInternalContext, StartReturnType, StartOptions, SetupWorker, } from './glossary' import { RequestHandler } from '~/core/handlers/RequestHandler' import { DEFAULT_START_OPTIONS } from './start/utils/prepareStartHandler' import { createStartHandler } from './start/createStartHandler' import { devUtils } from '~/core/utils/internal/devUtils' import { SetupApi } from '~/core/SetupApi' import { mergeRight } from '~/core/utils/internal/mergeRight' import type { LifeCycleEventsMap } from '~/core/sharedOptions' import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler' import { webSocketInterceptor } from '~/core/ws/webSocketInterceptor' import { handleWebSocketEvent } from '~/core/ws/handleWebSocketEvent' import { attachWebSocketLogger } from '~/core/ws/utils/attachWebSocketLogger' import { WorkerChannel } from '../utils/workerChannel' import { createFallbackRequestListener } from './start/createFallbackRequestListener' import { printStartMessage } from './start/utils/printStartMessage' import { printStopMessage } from './stop/utils/printStopMessage' import { supportsServiceWorker } from '../utils/supports' export class SetupWorkerApi extends SetupApi implements SetupWorker { private context: SetupWorkerInternalContext 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.context = this.createWorkerContext() } private createWorkerContext(): SetupWorkerInternalContext { const workerPromise = new DeferredPromise() return { // Mocking is not considered enabled until the worker // signals back the successful activation event. isMockingEnabled: false, startOptions: null as any, workerPromise, registration: undefined, getRequestHandlers: () => { return this.handlersController.currentHandlers() }, emitter: this.emitter, workerChannel: new WorkerChannel({ worker: workerPromise, }), } } public async start(options: StartOptions = {}): StartReturnType { if ('waitUntilReady' in options) { devUtils.warn( 'The "waitUntilReady" option has been deprecated. Please remove it from this "worker.start()" call. Follow the recommended Browser integration (https://mswjs.io/docs/integrations/browser) to eliminate any race conditions between the Service Worker registration and any requests made by your application on initial render.', ) } // Warn the developer on multiple "worker.start()" calls. // While this will not affect the worker in any way, // it likely indicates an issue with the developer's code. if (this.context.isMockingEnabled) { devUtils.warn( `Found a redundant "worker.start()" call. Note that starting the worker while mocking is already enabled will have no effect. Consider removing this "worker.start()" call.`, ) return this.context.registration } this.context.workerStoppedAt = undefined this.context.startOptions = mergeRight( DEFAULT_START_OPTIONS, options, ) as SetupWorkerInternalContext['startOptions'] // Enable the WebSocket interception. handleWebSocketEvent({ getUnhandledRequestStrategy: () => { return this.context.startOptions.onUnhandledRequest }, getHandlers: () => { return this.handlersController.currentHandlers() }, onMockedConnection: (connection) => { if (!this.context.startOptions.quiet) { // Attach the logger for mocked connections since // those won't be visible in the browser's devtools. attachWebSocketLogger(connection) } }, onPassthroughConnection() {}, }) webSocketInterceptor.apply() this.subscriptions.push(() => { webSocketInterceptor.dispose() }) // Use a fallback interception algorithm in the environments // where the Service Worker API isn't supported. if (!supportsServiceWorker()) { const fallbackInterceptor = createFallbackRequestListener( this.context, this.context.startOptions, ) this.subscriptions.push(() => { fallbackInterceptor.dispose() }) this.context.isMockingEnabled = true printStartMessage({ message: 'Mocking enabled (fallback mode).', quiet: this.context.startOptions.quiet, }) return undefined } const startHandler = createStartHandler(this.context) const registration = await startHandler(this.context.startOptions, options) this.context.isMockingEnabled = true return registration } public stop(): void { super.dispose() if (!this.context.isMockingEnabled) { devUtils.warn( 'Found a redundant "worker.stop()" call. Notice that stopping the worker after it has already been stopped has no effect. Consider removing this "worker.stop()" call.', ) return } this.context.isMockingEnabled = false this.context.workerStoppedAt = Date.now() this.context.emitter.removeAllListeners() if (supportsServiceWorker()) { this.context.workerChannel.removeAllListeners('RESPONSE') window.clearInterval(this.context.keepAliveInterval) } // Post the internal stop message on the window // to let any logic know when the worker has stopped. // E.g. the WebSocket client manager needs this to know // when to clear its in-memory clients list. window.postMessage({ type: 'msw/worker:stop' }) printStopMessage({ quiet: this.context.startOptions?.quiet, }) } } /** * 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()` API reference} */ export function setupWorker( ...handlers: Array ): SetupWorker { return new SetupWorkerApi(...handlers) } ================================================ FILE: src/browser/setupWorker/start/createFallbackRequestListener.ts ================================================ import { Interceptor, BatchInterceptor, HttpRequestEventMap, } from '@mswjs/interceptors' import { FetchInterceptor } from '@mswjs/interceptors/fetch' import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest' import { SetupWorkerInternalContext, StartOptions } from '../glossary' import type { RequiredDeep } from '~/core/typeUtils' import { handleRequest } from '~/core/utils/handleRequest' import { isHandlerKind } from '~/core/utils/internal/isHandlerKind' export function createFallbackRequestListener( context: SetupWorkerInternalContext, options: RequiredDeep, ): Interceptor { const interceptor = new BatchInterceptor({ name: 'fallback', interceptors: [new FetchInterceptor(), new XMLHttpRequestInterceptor()], }) interceptor.on('request', async ({ request, requestId, controller }) => { const requestCloneForLogs = request.clone() const response = await handleRequest( request, requestId, context.getRequestHandlers().filter(isHandlerKind('RequestHandler')), options, context.emitter, { resolutionContext: { quiet: options.quiet, }, onMockedResponse(_, { handler, parsedResult }) { if (!options.quiet) { context.emitter.once('response:mocked', ({ response }) => { handler.log({ request: requestCloneForLogs, response, parsedResult, }) }) } }, }, ) if (response) { controller.respondWith(response) } }) interceptor.on( 'response', ({ response, isMockedResponse, request, requestId }) => { context.emitter.emit( isMockedResponse ? 'response:mocked' : 'response:bypass', { response, request, requestId, }, ) }, ) interceptor.apply() return interceptor } ================================================ FILE: src/browser/setupWorker/start/createRequestListener.ts ================================================ import { Emitter } from 'rettime' import { StartOptions, SetupWorkerInternalContext } from '../glossary' import { deserializeRequest } from '../../utils/deserializeRequest' import { supportsReadableStreamTransfer } from '../../utils/supports' import { RequestHandler } from '~/core/handlers/RequestHandler' import { handleRequest } from '~/core/utils/handleRequest' import { RequiredDeep } from '~/core/typeUtils' import { devUtils } from '~/core/utils/internal/devUtils' import { toResponseInit } from '~/core/utils/toResponseInit' import { isHandlerKind } from '~/core/utils/internal/isHandlerKind' const SUPPORTS_READABLE_STREAM_TRANSFER = supportsReadableStreamTransfer() export const createRequestListener = ( context: SetupWorkerInternalContext, options: RequiredDeep, ): Emitter.ListenerType => { return async (event) => { // Treat any incoming requests from the worker as passthrough // if `worker.stop()` has been called for this client. if ( !context.isMockingEnabled && context.workerStoppedAt && event.data.interceptedAt > context.workerStoppedAt ) { event.postMessage('PASSTHROUGH') return } const requestId = event.data.id const request = deserializeRequest(event.data) const requestCloneForLogs = request.clone() // Make this the first request clone before the // request resolution pipeline even starts. // Store the clone in cache so the first matching // request handler would skip the cloning phase. const requestClone = request.clone() RequestHandler.cache.set(request, requestClone) try { await handleRequest( request, requestId, context.getRequestHandlers().filter(isHandlerKind('RequestHandler')), options, context.emitter, { resolutionContext: { quiet: options.quiet, }, onPassthroughResponse() { event.postMessage('PASSTHROUGH') }, async onMockedResponse(response, { handler, parsedResult }) { // Clone the mocked response so its body could be read // to buffer to be sent to the worker and also in the // ".log()" method of the request handler. const responseClone = response.clone() const responseCloneForLogs = response.clone() const responseInit = toResponseInit(response) /** * @note Safari doesn't support transferring a "ReadableStream". * Check that the browser supports that before sending it to the worker. */ if (SUPPORTS_READABLE_STREAM_TRANSFER) { const responseStreamOrNull = response.body event.postMessage( 'MOCK_RESPONSE', { ...responseInit, body: responseStreamOrNull, }, responseStreamOrNull ? [responseStreamOrNull] : undefined, ) } else { /** * @note If we are here, this means the current environment doesn't * support "ReadableStream" as transferable. In that case, * attempt to read the non-empty response body as ArrayBuffer, if it's not empty. * @see https://github.com/mswjs/msw/issues/1827 */ const responseBufferOrNull = response.body === null ? null : await responseClone.arrayBuffer() event.postMessage('MOCK_RESPONSE', { ...responseInit, body: responseBufferOrNull, }) } if (!options.quiet) { context.emitter.once('response:mocked', () => { handler.log({ request: requestCloneForLogs, response: responseCloneForLogs, parsedResult, }) }) } }, }, ) } catch (error) { if (error instanceof Error) { devUtils.error( `Uncaught exception in the request handler for "%s %s": %s This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/http/mocking-responses/error-responses`, request.method, request.url, error.stack ?? error, ) // Treat all other exceptions in a request handler as unintended, // alerting that there is a problem that needs fixing. event.postMessage('MOCK_RESPONSE', { status: 500, statusText: 'Request Handler Error', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: error.name, message: error.message, stack: error.stack, }), }) } } } } ================================================ FILE: src/browser/setupWorker/start/createResponseListener.ts ================================================ import { FetchResponse } from '@mswjs/interceptors' import type { Emitter } from 'rettime' import type { SetupWorkerInternalContext } from '../glossary' import { deserializeRequest } from '../../utils/deserializeRequest' export function createResponseListener( context: SetupWorkerInternalContext, ): Emitter.ListenerType { return (event) => { const responseMessage = event.data const request = deserializeRequest(responseMessage.request) /** * CORS requests with `mode: "no-cors"` result in "opaque" responses. * That kind of responses cannot be manipulated in JavaScript due * to the security considerations. * @see https://fetch.spec.whatwg.org/#concept-filtered-response-opaque * @see https://github.com/mswjs/msw/issues/529 */ if (responseMessage.response.type?.includes('opaque')) { return } const response = responseMessage.response.status === 0 ? Response.error() : new FetchResponse( /** * Responses may be streams here, but when we create a response object * with null-body status codes, like 204, 205, 304 Response will * throw when passed a non-null body, so ensure it's null here * for those codes */ FetchResponse.isResponseWithBody(responseMessage.response.status) ? responseMessage.response.body : null, { ...responseMessage.response, /** * Set response URL if it's not set already. * @see https://github.com/mswjs/msw/issues/2030 * @see https://developer.mozilla.org/en-US/docs/Web/API/Response/url */ url: request.url, }, ) context.emitter.emit( responseMessage.isMockedResponse ? 'response:mocked' : 'response:bypass', { requestId: responseMessage.request.id, request, response, }, ) } } ================================================ FILE: src/browser/setupWorker/start/createStartHandler.ts ================================================ import { devUtils } from '~/core/utils/internal/devUtils' import { getWorkerInstance } from './utils/getWorkerInstance' import { enableMocking } from './utils/enableMocking' import type { SetupWorkerInternalContext, StartHandler } from '../glossary' import { createRequestListener } from './createRequestListener' import { checkWorkerIntegrity } from '../../utils/checkWorkerIntegrity' import { createResponseListener } from './createResponseListener' import { validateWorkerScope } from './utils/validateWorkerScope' import { DeferredPromise } from '@open-draft/deferred-promise' export const createStartHandler = ( context: SetupWorkerInternalContext, ): StartHandler => { return function start(options, customOptions) { const startWorkerInstance = async () => { // Remove all previously existing event listeners. // This way none of the listeners persists between Fast refresh // of the application's code. context.workerChannel.removeAllListeners() // Handle requests signaled by the worker. context.workerChannel.on( 'REQUEST', createRequestListener(context, options), ) // Handle responses signaled by the worker. context.workerChannel.on('RESPONSE', createResponseListener(context)) const instance = await getWorkerInstance( options.serviceWorker.url, options.serviceWorker.options, options.findWorker, ) const [worker, registration] = instance if (!worker) { const missingWorkerMessage = customOptions?.findWorker ? devUtils.formatMessage( `Failed to locate the Service Worker registration using a custom "findWorker" predicate. Please ensure that the custom predicate properly locates the Service Worker registration at "%s". More details: https://mswjs.io/docs/api/setup-worker/start#findworker `, options.serviceWorker.url, ) : devUtils.formatMessage( `Failed to locate the Service Worker registration. This most likely means that the worker script URL "%s" cannot resolve against the actual public hostname (%s). This may happen if your application runs behind a proxy, or has a dynamic hostname. Please consider using a custom "serviceWorker.url" option to point to the actual worker script location, or a custom "findWorker" option to resolve the Service Worker registration manually. More details: https://mswjs.io/docs/api/setup-worker/start`, options.serviceWorker.url, location.host, ) throw new Error(missingWorkerMessage) } context.workerPromise.resolve(worker) context.registration = registration window.addEventListener('beforeunload', () => { if (worker.state !== 'redundant') { // Notify the Service Worker that this client has closed. // Internally, it's similar to disabling the mocking, only // client close event has a handler that self-terminates // the Service Worker when there are no open clients. context.workerChannel.postMessage('CLIENT_CLOSED') } // Make sure we're always clearing the interval - there are reports that not doing this can // cause memory leaks in headless browser environments. window.clearInterval(context.keepAliveInterval) // Notify others about this client disconnecting. // E.g. this will purge the in-memory WebSocket clients since // starting the worker again will assign them new IDs. window.postMessage({ type: 'msw/worker:stop' }) }) // Check if the active Service Worker has been generated // by the currently installed version of MSW. await checkWorkerIntegrity(context).catch((error) => { devUtils.error( 'Error while checking the worker script integrity. Please report this on GitHub (https://github.com/mswjs/msw/issues) and include the original error below.', ) console.error(error) }) context.keepAliveInterval = window.setInterval( () => context.workerChannel.postMessage('KEEPALIVE_REQUEST'), 5000, ) // Warn the user when loading the page that lies outside // of the worker's scope. validateWorkerScope(registration, context.startOptions) return registration } const workerRegistration = startWorkerInstance().then( async (registration) => { const pendingInstance = registration.installing || registration.waiting if (pendingInstance) { const activationPromise = new DeferredPromise() pendingInstance.addEventListener('statechange', () => { if (pendingInstance.state === 'activated') { activationPromise.resolve() } }) // Wait until the worker is activated. // Assume the worker is already activated if there's no pending registration // (i.e. when reloading the page after a successful activation). await activationPromise } // Print the activation message only after the worker has been activated. await enableMocking(context, options).catch((error) => { devUtils.error( 'Failed to enable mocking. Please report this on GitHub (https://github.com/mswjs/msw/issues) and include the original error below.', ) throw error }) return registration }, ) return workerRegistration } } ================================================ FILE: src/browser/setupWorker/start/utils/enableMocking.ts ================================================ import { DeferredPromise } from '@open-draft/deferred-promise' import type { StartOptions, SetupWorkerInternalContext } from '../../glossary' import { printStartMessage } from './printStartMessage' /** * Signals the worker to enable the interception of requests. */ export function enableMocking( context: SetupWorkerInternalContext, options: StartOptions, ): Promise { const mockingEnabledPromise = new DeferredPromise() context.workerChannel.postMessage('MOCK_ACTIVATE') context.workerChannel.once('MOCKING_ENABLED', async (event) => { context.isMockingEnabled = true const worker = await context.workerPromise printStartMessage({ quiet: options.quiet, workerScope: context.registration?.scope, workerUrl: worker.scriptURL, client: event.data.client, }) mockingEnabledPromise.resolve(true) }) return mockingEnabledPromise } ================================================ FILE: src/browser/setupWorker/start/utils/getWorkerByRegistration.ts ================================================ import { FindWorker } from '../../glossary' /** * Attempts to resolve a Service Worker instance from a given registration, * regardless of its state (active, installing, waiting). */ export function getWorkerByRegistration( registration: ServiceWorkerRegistration, absoluteWorkerUrl: string, findWorker: FindWorker, ): ServiceWorker | null { const allStates = [ registration.active, registration.installing, registration.waiting, ] const relevantStates = allStates.filter((state): state is ServiceWorker => { return state != null }) const worker = relevantStates.find((worker) => { return findWorker(worker.scriptURL, absoluteWorkerUrl) }) return worker || null } ================================================ FILE: src/browser/setupWorker/start/utils/getWorkerInstance.ts ================================================ import { until } from 'until-async' import { devUtils } from '~/core/utils/internal/devUtils' import { getAbsoluteWorkerUrl } from '../../../utils/getAbsoluteWorkerUrl' import { getWorkerByRegistration } from './getWorkerByRegistration' import { ServiceWorkerInstanceTuple, FindWorker } from '../../glossary' /** * Returns an active Service Worker instance. * When not found, registers a new Service Worker. */ export const getWorkerInstance = async ( url: string, options: RegistrationOptions = {}, findWorker: FindWorker, ): Promise => { // Resolve the absolute Service Worker URL. const absoluteWorkerUrl = getAbsoluteWorkerUrl(url) const mockRegistrations = await navigator.serviceWorker .getRegistrations() .then((registrations) => registrations.filter((registration) => getWorkerByRegistration(registration, absoluteWorkerUrl, findWorker), ), ) if (!navigator.serviceWorker.controller && mockRegistrations.length > 0) { // Reload the page when it has associated workers, but no active controller. // The absence of a controller can mean either: // - page has no Service Worker associated with it // - page has been hard-reloaded and its workers won't be used until the next reload. // Since we've checked that there are registrations associated with this page, // at this point we are sure it's hard reload that falls into this clause. location.reload() } const [existingRegistration] = mockRegistrations if (existingRegistration) { // Schedule the worker update in the background. // Update ensures the existing worker is up-to-date. existingRegistration.update() // Return the worker reference immediately. return [ getWorkerByRegistration( existingRegistration, absoluteWorkerUrl, findWorker, ), existingRegistration, ] } // When the Service Worker wasn't found, register it anew and return the reference. const [registrationError, registrationResult] = await until< Error, ServiceWorkerInstanceTuple >(async () => { const registration = await navigator.serviceWorker.register(url, options) return [ // Compare existing worker registration by its worker URL, // to prevent irrelevant workers to resolve here (such as Codesandbox worker). getWorkerByRegistration(registration, absoluteWorkerUrl, findWorker), registration, ] }) // Handle Service Worker registration errors. if (registrationError) { const isWorkerMissing = registrationError.message.includes('(404)') // Produce a custom error message when given a non-existing Service Worker url. // Suggest developers to check their setup. if (isWorkerMissing) { const scopeUrl = new URL(options?.scope || '/', location.href) throw new Error( devUtils.formatMessage(`\ Failed to register a Service Worker for scope ('${scopeUrl.href}') with script ('${absoluteWorkerUrl}'): Service Worker script does not exist at the given path. Did you forget to run "npx msw init "? Learn more about creating the Service Worker script: https://mswjs.io/docs/cli/init`), ) } // Fallback error message for any other registration errors. throw new Error( devUtils.formatMessage( 'Failed to register the Service Worker:\n\n%s', registrationError.message, ), ) } return registrationResult } ================================================ FILE: src/browser/setupWorker/start/utils/prepareStartHandler.test.ts ================================================ import { SetupWorkerInternalContext, StartOptions } from '../../glossary' import { DEFAULT_START_OPTIONS, resolveStartOptions, prepareStartHandler, } from './prepareStartHandler' describe('resolveStartOptions', () => { test('returns default options given no custom start options', () => { expect(resolveStartOptions()).toEqual(DEFAULT_START_OPTIONS) expect(resolveStartOptions(undefined)).toEqual(DEFAULT_START_OPTIONS) expect(resolveStartOptions({})).toEqual(DEFAULT_START_OPTIONS) }) test('deeply merges the default and custom start options', () => { expect( resolveStartOptions({ quiet: true, serviceWorker: { url: './custom.js', }, }), ).toEqual({ ...DEFAULT_START_OPTIONS, quiet: true, serviceWorker: { url: './custom.js', options: null, }, }) }) }) describe('prepareStartHandler', () => { test('exposes resolved start options to the generated star handler', () => { const createStartHandler = vi.fn() const context: SetupWorkerInternalContext = {} as any const startHandler = prepareStartHandler(createStartHandler, context) expect(startHandler).toBeInstanceOf(Function) const initialOptions: StartOptions = { quiet: true, serviceWorker: { url: './custom.js', }, } const resolvedOptions = resolveStartOptions(initialOptions) startHandler(initialOptions) // Calls the handler creator with both resolved and initial options. expect(createStartHandler).toHaveBeenCalledWith( resolvedOptions, initialOptions, ) // Sets the resolved options on the internal context. expect(context).toHaveProperty('startOptions', resolvedOptions) }) }) ================================================ FILE: src/browser/setupWorker/start/utils/prepareStartHandler.ts ================================================ import { RequiredDeep } from '~/core/typeUtils' import { mergeRight } from '~/core/utils/internal/mergeRight' import { SetupWorker, SetupWorkerInternalContext, StartHandler, StartOptions, } from '../../glossary' export const DEFAULT_START_OPTIONS: RequiredDeep = { serviceWorker: { url: '/mockServiceWorker.js', options: null as any, }, quiet: false, waitUntilReady: true, onUnhandledRequest: 'warn', findWorker(scriptURL, mockServiceWorkerUrl) { return scriptURL === mockServiceWorkerUrl }, } /** * Returns resolved worker start options, merging the default options * with the given custom options. */ export function resolveStartOptions( initialOptions?: StartOptions, ): RequiredDeep { return mergeRight( DEFAULT_START_OPTIONS, initialOptions || {}, ) as RequiredDeep } export function prepareStartHandler( handler: StartHandler, context: SetupWorkerInternalContext, ): SetupWorker['start'] { return (initialOptions) => { context.startOptions = resolveStartOptions(initialOptions) return handler(context.startOptions, initialOptions || {}) } } ================================================ FILE: src/browser/setupWorker/start/utils/printStartMessage.test.ts ================================================ import { printStartMessage } from './printStartMessage' beforeEach(() => { vi.spyOn(console, 'groupCollapsed').mockImplementation(() => void 0) vi.spyOn(console, 'log').mockImplementation(() => void 0) }) afterEach(() => { vi.restoreAllMocks() }) test('prints out a default start message into console', () => { printStartMessage({ workerScope: 'http://localhost:3000/', workerUrl: 'http://localhost:3000/worker.js', }) expect(console.groupCollapsed).toHaveBeenCalledWith( '%c[MSW] Mocking enabled.', expect.anything(), ) // Includes a link to the documentation. expect(console.log).toHaveBeenCalledWith( '%cDocumentation: %chttps://mswjs.io/docs', expect.anything(), expect.anything(), ) // Includes a link to the GitHub issues page. expect(console.log).toHaveBeenCalledWith( 'Found an issue? https://github.com/mswjs/msw/issues', ) // Includes service worker scope. expect(console.log).toHaveBeenCalledWith( 'Worker scope:', 'http://localhost:3000/', ) // Includes service worker script location. expect(console.log).toHaveBeenCalledWith( 'Worker script URL:', 'http://localhost:3000/worker.js', ) }) test('supports printing a custom start message', () => { printStartMessage({ message: 'Custom start message' }) expect(console.groupCollapsed).toHaveBeenCalledWith( '%c[MSW] Custom start message', expect.anything(), ) }) test('does not print any messages when log level is quiet', () => { printStartMessage({ quiet: true }) expect(console.groupCollapsed).not.toHaveBeenCalled() expect(console.log).not.toHaveBeenCalled() }) test('prints a worker scope in the start message', () => { printStartMessage({ workerScope: 'http://localhost:3000/user', }) expect(console.log).toHaveBeenCalledWith( 'Worker scope:', 'http://localhost:3000/user', ) }) test('prints a worker script url in the start message', () => { printStartMessage({ workerUrl: 'http://localhost:3000/mockServiceWorker.js', }) expect(console.log).toHaveBeenCalledWith( 'Worker script URL:', 'http://localhost:3000/mockServiceWorker.js', ) }) ================================================ FILE: src/browser/setupWorker/start/utils/printStartMessage.ts ================================================ import type { ServiceWorkerIncomingEventsMap } from '../../glossary' import { devUtils } from '~/core/utils/internal/devUtils' interface PrintStartMessageArgs { quiet?: boolean message?: string workerUrl?: string workerScope?: string client?: ServiceWorkerIncomingEventsMap['MOCKING_ENABLED']['client'] } /** * Prints a worker activation message in the browser's console. */ export function printStartMessage(args: PrintStartMessageArgs = {}) { if (args.quiet) { return } const message = args.message || 'Mocking enabled.' console.groupCollapsed( `%c${devUtils.formatMessage(message)}`, 'color:orangered;font-weight:bold;', ) // eslint-disable-next-line no-console console.log( '%cDocumentation: %chttps://mswjs.io/docs', 'font-weight:bold', 'font-weight:normal', ) // eslint-disable-next-line no-console console.log('Found an issue? https://github.com/mswjs/msw/issues') if (args.workerUrl) { // eslint-disable-next-line no-console console.log('Worker script URL:', args.workerUrl) } if (args.workerScope) { // eslint-disable-next-line no-console console.log('Worker scope:', args.workerScope) } if (args.client) { // eslint-disable-next-line no-console console.log('Client ID: %s (%s)', args.client.id, args.client.frameType) } console.groupEnd() } ================================================ FILE: src/browser/setupWorker/start/utils/validateWorkerScope.ts ================================================ import { devUtils } from '~/core/utils/internal/devUtils' import { StartOptions } from '../../glossary' export function validateWorkerScope( registration: ServiceWorkerRegistration, options?: StartOptions, ): void { if (!options?.quiet && !location.href.startsWith(registration.scope)) { devUtils.warn( `\ Cannot intercept requests on this page because it's outside of the worker's scope ("${registration.scope}"). If you wish to mock API requests on this page, you must resolve this scope issue. - (Recommended) Register the worker at the root level ("/") of your application. - Set the "Service-Worker-Allowed" response header to allow out-of-scope workers.\ `, ) } } ================================================ FILE: src/browser/setupWorker/stop/utils/printStopMessage.test.ts ================================================ import { printStopMessage } from './printStopMessage' beforeAll(() => { vi.spyOn(global.console, 'log').mockImplementation(() => void 0) }) afterEach(() => { vi.resetAllMocks() }) afterAll(() => { vi.restoreAllMocks() }) test('prints a stop message to the console', () => { printStopMessage() expect(console.log).toHaveBeenCalledWith( '%c[MSW] Mocking disabled.', 'color:orangered;font-weight:bold;', ) }) test('does not print any message when log level is quiet', () => { printStopMessage({ quiet: true }) expect(console.log).not.toHaveBeenCalled() }) ================================================ FILE: src/browser/setupWorker/stop/utils/printStopMessage.ts ================================================ import { devUtils } from '~/core/utils/internal/devUtils' export function printStopMessage(args: { quiet?: boolean } = {}): void { if (args.quiet) { return } // eslint-disable-next-line no-console console.log( `%c${devUtils.formatMessage('Mocking disabled.')}`, 'color:orangered;font-weight:bold;', ) } ================================================ FILE: src/browser/tsconfig.browser.build.json ================================================ { "extends": "./tsconfig.browser.json", "compilerOptions": { "composite": false } } ================================================ FILE: src/browser/tsconfig.browser.json ================================================ { "extends": "../tsconfig.src.json", "compilerOptions": { // Expose browser-specific libraries only for the // source code under the "src/browser" directory. "lib": ["DOM", "WebWorker", "DOM.Iterable"] }, "include": ["../../global.d.ts", "./global.browser.d.ts", "./**/*.ts"] } ================================================ FILE: src/browser/utils/checkWorkerIntegrity.ts ================================================ import { devUtils } from '~/core/utils/internal/devUtils' import type { SetupWorkerInternalContext } from '../setupWorker/glossary' import { DeferredPromise } from '@open-draft/deferred-promise' /** * Check whether the registered Service Worker has been * generated by the installed version of the library. * Prints a warning message if the worker scripts mismatch. */ export function checkWorkerIntegrity( context: SetupWorkerInternalContext, ): Promise { const integrityCheckPromise = new DeferredPromise() // Request the integrity checksum from the registered worker. context.workerChannel.postMessage('INTEGRITY_CHECK_REQUEST') context.workerChannel.once('INTEGRITY_CHECK_RESPONSE', (event) => { const { checksum, packageVersion } = event.data // Compare the response from the Service Worker and the // global variable set during the build. // The integrity is validated based on the worker script's checksum // that's derived from its minified content during the build. // The "SERVICE_WORKER_CHECKSUM" global variable is injected by the build. if (checksum !== SERVICE_WORKER_CHECKSUM) { devUtils.warn( `The currently registered Service Worker has been generated by a different version of MSW (${packageVersion}) and may not be fully compatible with the installed version. It's recommended you update your worker script by running this command: \u2022 npx msw init You can also automate this process and make the worker script update automatically upon the library installations. Read more: https://mswjs.io/docs/cli/init.`, ) } integrityCheckPromise.resolve() }) return integrityCheckPromise } ================================================ FILE: src/browser/utils/deserializeRequest.ts ================================================ import { pruneGetRequestBody } from './pruneGetRequestBody' import type { ServiceWorkerIncomingRequest } from '../setupWorker/glossary' /** * Converts a given request received from the Service Worker * into a Fetch `Request` instance. */ export function deserializeRequest( serializedRequest: ServiceWorkerIncomingRequest, ): Request { return new Request(serializedRequest.url, { ...serializedRequest, body: pruneGetRequestBody(serializedRequest), }) } ================================================ FILE: src/browser/utils/getAbsoluteWorkerUrl.test.ts ================================================ // @vitest-environment jsdom import { getAbsoluteWorkerUrl } from './getAbsoluteWorkerUrl' const rawLocation = window.location afterAll(() => { Object.defineProperty(window, 'location', { value: rawLocation, }) }) it('returns absolute worker url relatively to the root', () => { expect(getAbsoluteWorkerUrl('./worker.js')).toBe('http://localhost/worker.js') }) it('returns an absolute worker url relatively to the current path', () => { Object.defineProperty(window, 'location', { value: { href: 'http://localhost/path/to/page', }, }) expect(getAbsoluteWorkerUrl('./worker.js')).toBe( 'http://localhost/path/to/worker.js', ) // Leading slash must still resolve to the root. expect(getAbsoluteWorkerUrl('/worker.js')).toBe('http://localhost/worker.js') }) ================================================ FILE: src/browser/utils/getAbsoluteWorkerUrl.ts ================================================ /** * Returns an absolute Service Worker URL based on the given * relative URL (known during the registration). */ export function getAbsoluteWorkerUrl(workerUrl: string): string { return new URL(workerUrl, location.href).href } ================================================ FILE: src/browser/utils/pruneGetRequestBody.test.ts ================================================ /** * @vitest-environment jsdom */ import { TextEncoder } from 'util' import { pruneGetRequestBody } from './pruneGetRequestBody' test('sets empty GET request body to undefined', () => { expect( pruneGetRequestBody({ method: 'GET', }), ).toBeUndefined() expect( pruneGetRequestBody({ method: 'GET', // There's no such thing as a GET request with a body. body: new ArrayBuffer(5), }), ).toBeUndefined() }) test('sets HEAD request body to undefined', () => { expect( pruneGetRequestBody({ method: 'HEAD', }), ).toBeUndefined() expect( pruneGetRequestBody({ method: 'HEAD', body: new ArrayBuffer(5), }), ).toBeUndefined() }) test('ignores requests of the other methods than GET', () => { const body = new TextEncoder().encode('hello world') expect( pruneGetRequestBody({ method: 'POST', body, }), ).toEqual(body) expect( pruneGetRequestBody({ method: 'PUT', body, }), ).toEqual(body) }) ================================================ FILE: src/browser/utils/pruneGetRequestBody.ts ================================================ import type { ServiceWorkerIncomingRequest } from '../setupWorker/glossary' type Input = Pick /** * Ensures that an empty GET request body is always represented as `undefined`. */ export function pruneGetRequestBody( request: Input, ): ServiceWorkerIncomingRequest['body'] { // Force HEAD/GET request body to always be empty. // The worker reads any request's body as ArrayBuffer, // and you cannot re-construct a GET/HEAD Request // with an ArrayBuffer, even if empty. Also note that // "request.body" is always undefined in the worker. if (['HEAD', 'GET'].includes(request.method)) { return undefined } return request.body } ================================================ FILE: src/browser/utils/supports.ts ================================================ /** * Checks if the Service Worker API is supproted and available * in the current browsing context. */ export function supportsServiceWorker(): boolean { return ( typeof navigator !== 'undefined' && 'serviceWorker' in navigator && typeof location !== 'undefined' && location.protocol !== 'file:' ) } /** * Returns a boolean indicating whether the current browser * supports `ReadableStream` as a `Transferable` when posting * messages. */ export function supportsReadableStreamTransfer() { try { const stream = new ReadableStream({ start: (controller) => controller.close(), }) const message = new MessageChannel() message.port1.postMessage(stream, [stream]) return true } catch { return false } } ================================================ FILE: src/browser/utils/workerChannel.ts ================================================ import { invariant } from 'outvariant' import { Emitter, TypedEvent } from 'rettime' import { isObject } from '~/core/utils/internal/isObject' import type { StringifiedResponse } from '../setupWorker/glossary' import { supportsServiceWorker } from '../utils/supports' export interface WorkerChannelOptions { worker: Promise } export type WorkerChannelEventMap = { REQUEST: WorkerEvent RESPONSE: WorkerEvent MOCKING_ENABLED: WorkerEvent<{ client: { id: string frameType: string } }> INTEGRITY_CHECK_RESPONSE: WorkerEvent<{ packageVersion: string checksum: string }> KEEPALIVE_RESPONSE: TypedEvent } /** * Request representation received from the worker message event. */ export interface IncomingWorkerRequest extends Omit< Request, | 'text' | 'body' | 'json' | 'blob' | 'arrayBuffer' | 'formData' | 'clone' | 'signal' | 'isHistoryNavigation' | 'isReloadNavigation' > { /** * Unique ID of the request generated once the request is * intercepted by the "fetch" event in the Service Worker. */ id: string interceptedAt: number body?: ArrayBuffer | null } type IncomingWorkerResponse = { isMockedResponse: boolean request: IncomingWorkerRequest response: Pick< Response, 'type' | 'ok' | 'status' | 'statusText' | 'body' | 'headers' | 'redirected' > } export type WorkerEventResponse = { MOCK_RESPONSE: [ data: StringifiedResponse, transfer?: [ReadableStream], ] PASSTHROUGH: [] } const SUPPORTS_SERVICE_WORKER = supportsServiceWorker() export class WorkerEvent< DataType, ReturnType = any, EventType extends string = string, > extends TypedEvent { #workerEvent: MessageEvent constructor(workerEvent: MessageEvent) { const type = workerEvent.data.type as EventType const data = workerEvent.data.payload as DataType /** * @note This is the only place we're mapping { type, payload } * message structure of the worker. The client references the * payload via `event.data`. */ super( // @ts-expect-error Troublesome `TypedEvent` extension. type, { data }, ) this.#workerEvent = workerEvent } get ports() { return this.#workerEvent.ports } /** * Reply directly to this event using its `MessagePort`. */ public postMessage( type: Type, ...rest: WorkerEventResponse[Type] ): void { this.#workerEvent.ports[0].postMessage( { type, data: rest[0] }, { transfer: rest[1] }, ) } } /** * Map of the events that can be sent to the Service Worker * from any execution context. */ type OutgoingWorkerEvents = | 'MOCK_ACTIVATE' | 'INTEGRITY_CHECK_REQUEST' | 'KEEPALIVE_REQUEST' | 'CLIENT_CLOSED' export class WorkerChannel extends Emitter { constructor(protected readonly options: WorkerChannelOptions) { super() if (!SUPPORTS_SERVICE_WORKER) { return } navigator.serviceWorker.addEventListener('message', async (event) => { const worker = await this.options.worker if (event.source != null && event.source !== worker) { return } if (event.data && isObject(event.data) && 'type' in event.data) { this.emit(new WorkerEvent(event)) } }) } /** * Send data to the Service Worker controlling this client. * This triggers the `message` event listener on ServiceWorkerGlobalScope. */ public postMessage(type: OutgoingWorkerEvents): void { invariant( SUPPORTS_SERVICE_WORKER, 'Failed to post message on a WorkerChannel: the Service Worker API is unavailable in this context. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues', ) this.options.worker.then((worker) => { worker.postMessage(type) }) } } ================================================ FILE: src/core/HttpResponse.test.ts ================================================ /** * @vitest-environment node */ import { TextEncoder } from 'util' import { HttpResponse, kDefaultContentType } from './HttpResponse' it('creates a plain response', async () => { const response = new HttpResponse(null, { status: 301 }) expect(response.status).toBe(301) expect(response.statusText).toBe('Moved Permanently') expect(response.body).toBe(null) await expect(response.text()).resolves.toBe('') expect(Object.fromEntries(response.headers.entries())).toEqual({}) }) it('supports non-configurable status codes', () => { expect(new HttpResponse(null, { status: 101 })).toHaveProperty('status', 101) }) describe('HttpResponse.text()', () => { it('creates a text response', async () => { const response = HttpResponse.text('hello world', { status: 201 }) expect(response.status).toBe(201) expect(response.statusText).toBe('Created') expect(response.body).toBeInstanceOf(ReadableStream) await expect(response.text()).resolves.toBe('hello world') expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '11', 'content-type': 'text/plain', }) expect(kDefaultContentType in response).toBe(true) }) it('creates a text response with special characters', async () => { const response = HttpResponse.text('안녕 세상', { status: 201 }) expect(response.status).toBe(201) expect(response.statusText).toBe('Created') expect(response.body).toBeInstanceOf(ReadableStream) await expect(response.text()).resolves.toBe('안녕 세상') expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '13', 'content-type': 'text/plain', }) }) it('allows overriding the "Content-Type" response header', async () => { const response = HttpResponse.text('hello world', { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) await expect(response.text()).resolves.toBe('hello world') expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '11', 'content-type': 'text/plain; charset=utf-8', }) expect(kDefaultContentType in response).toBe(false) }) it('allows overriding the "Content-Length" response header', async () => { const response = HttpResponse.text('hello world', { headers: { 'Content-Length': '32' }, }) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '32', 'content-type': 'text/plain', }) }) }) describe('HttpResponse.json()', () => { it('creates a json response given an object', async () => { const response = HttpResponse.json({ firstName: 'John' }) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) expect(await response.json()).toEqual({ firstName: 'John' }) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '20', 'content-type': 'application/json', }) expect(kDefaultContentType in response).toBe(true) }) it('creates a json response given an object with special characters', async () => { const response = HttpResponse.json({ firstName: '제로' }) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) expect(await response.json()).toEqual({ firstName: '제로' }) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '22', 'content-type': 'application/json', }) }) it('creates a json response given an array', async () => { const response = HttpResponse.json([1, 2, 3]) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) expect(await response.json()).toEqual([1, 2, 3]) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '7', 'content-type': 'application/json', }) }) it('creates a json response given a plain string', async () => { const response = HttpResponse.json(`"hello"`) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) expect(await response.json()).toBe(`"hello"`) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '11', 'content-type': 'application/json', }) }) it('creates a json response given a number', async () => { const response = HttpResponse.json(123) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) expect(await response.json()).toBe(123) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '3', 'content-type': 'application/json', }) }) it('creates a json response given a json ReadableStream', async () => { const encoder = new TextEncoder() const stream = new ReadableStream({ start(controller) { controller.enqueue(encoder.encode(`{"firstName`)) controller.enqueue(encoder.encode(`":"John`)) controller.enqueue(encoder.encode(`"}`)) controller.close() }, }) const response = HttpResponse.json(stream) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) // A ReadableStream instance is not a valid body init // for the "Response.json()" static method. It gets serialized // into a plain object. expect(await response.json()).toEqual({}) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '2', 'content-type': 'application/json', }) }) it('allows overriding the "Content-Type" response header', async () => { const response = HttpResponse.json( { a: 1 }, { headers: { 'Content-Type': 'application/hal+json', }, }, ) expect(kDefaultContentType in response).toBe(false) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) expect(await response.json()).toEqual({ a: 1 }) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '7', 'content-type': 'application/hal+json', }) }) it('allows overriding the "Content-Length" response header', async () => { const response = HttpResponse.json( { a: 1 }, { headers: { 'Content-Length': '32' }, }, ) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '32', 'content-type': 'application/json', }) }) }) describe('HttpResponse.xml()', () => { it('creates an xml response', async () => { const response = HttpResponse.xml('') expect(kDefaultContentType in response).toBe(true) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) await expect(response.text()).resolves.toBe('') expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-type': 'text/xml', }) expect(kDefaultContentType in response).toBe(true) }) it('allows overriding the "Content-Type" response header', async () => { const response = HttpResponse.xml('', { headers: { 'Content-Type': 'text/xml; charset=utf-8', }, }) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) await expect(response.text()).resolves.toBe('') expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-type': 'text/xml; charset=utf-8', }) expect(kDefaultContentType in response).toBe(false) }) }) describe('HttpResponse.html()', () => { it('creates an html response', async () => { const response = HttpResponse.html('

Jane Doe

') expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) await expect(response.text()).resolves.toBe( '

Jane Doe

', ) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-type': 'text/html', }) expect(kDefaultContentType in response).toBe(true) }) it('allows overriding the "Content-Type" response header', async () => { const response = HttpResponse.html('

Jane Doe

', { headers: { 'Content-Type': 'text/html; charset=utf-8', }, }) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) await expect(response.text()).resolves.toBe( '

Jane Doe

', ) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-type': 'text/html; charset=utf-8', }) expect(kDefaultContentType in response).toBe(false) }) }) describe('HttpResponse.arrayBuffer()', () => { it('creates an array buffer response', async () => { const buffer = new TextEncoder().encode('hello world') const response = HttpResponse.arrayBuffer(buffer) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) const responseData = await response.arrayBuffer() expect(responseData).toEqual(buffer.buffer) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '11', 'content-type': 'application/octet-stream', }) expect(kDefaultContentType in response).toBe(true) }) it('allows overriding the "Content-Type" response header', async () => { const buffer = new TextEncoder().encode('hello world') const response = HttpResponse.arrayBuffer(buffer, { headers: { 'Content-Type': 'text/plain; charset=utf-8', }, }) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) const responseData = await response.arrayBuffer() expect(responseData).toEqual(buffer.buffer) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '11', 'content-type': 'text/plain; charset=utf-8', }) expect(kDefaultContentType in response).toBe(false) }) it('creates an array buffer response from a shared array buffer', async () => { const arrayBuffer = new TextEncoder().encode('hello world') // Copy the data from the array buffer to a shared array buffer const sharedBuffer = new SharedArrayBuffer(11) const sharedView = new Uint8Array(sharedBuffer) sharedView.set(arrayBuffer) const response = HttpResponse.arrayBuffer(sharedBuffer) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) const responseData = await response.arrayBuffer() expect(responseData).toEqual(arrayBuffer.buffer) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '11', 'content-type': 'application/octet-stream', }) }) it('allows overriding the "Content-Type" response header for shared array buffers', async () => { const arrayBuffer = new TextEncoder().encode('hello world') // Copy the data from the array buffer to a shared array buffer const sharedBuffer = new SharedArrayBuffer(11) const sharedView = new Uint8Array(sharedBuffer) sharedView.set(arrayBuffer) const response = HttpResponse.arrayBuffer(sharedBuffer, { headers: { 'Content-Type': 'text/plain; charset=utf-8', }, }) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) const responseData = await response.arrayBuffer() expect(responseData).toEqual(arrayBuffer.buffer) expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-length': '11', 'content-type': 'text/plain; charset=utf-8', }) expect(kDefaultContentType in response).toBe(false) }) }) it('creates a form data response', async () => { const formData = new FormData() formData.append('firstName', 'John') const response = HttpResponse.formData(formData) expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) const responseData = await response.formData() expect(responseData.get('firstName')).toBe('John') expect(Object.fromEntries(response.headers.entries())).toEqual({ 'content-type': expect.stringContaining( 'multipart/form-data; boundary=----', ), }) }) ================================================ FILE: src/core/HttpResponse.ts ================================================ import { FetchResponse } from '@mswjs/interceptors' import type { DefaultBodyType, JsonBodyType } from './handlers/RequestHandler' import type { NoInfer } from './typeUtils' import { decorateResponse, normalizeResponseInit, } from './utils/HttpResponse/decorators' export interface HttpResponseInit extends ResponseInit { type?: ResponseType } export const bodyType: unique symbol = Symbol('bodyType') export type DefaultUnsafeFetchResponse = Response & { [bodyType]?: never } export interface StrictRequest extends Request { json(): Promise clone(): StrictRequest } /** * Opaque `Response` type that supports strict body type. * * @deprecated Please use {@link HttpResponse} instead. */ export type StrictResponse = HttpResponse export const kDefaultContentType = Symbol.for('kDefaultContentType') /** * A drop-in replacement for the standard `Response` class * to allow additional features, like mocking the response `Set-Cookie` header. * * @example * new HttpResponse('Hello world', { status: 201 }) * HttpResponse.json({ name: 'John' }) * HttpResponse.formData(form) * * @see {@link https://mswjs.io/docs/api/http-response `HttpResponse` API reference} */ export class HttpResponse< BodyType extends DefaultBodyType, > extends FetchResponse { readonly [bodyType]: BodyType = null as any constructor(body?: NoInfer | null, init?: HttpResponseInit) { const responseInit = normalizeResponseInit(init) super(body as BodyInit, responseInit) decorateResponse(this, responseInit) } static error(): HttpResponse { return super.error() as HttpResponse } /** * Create a `Response` with a `Content-Type: "text/plain"` body. * @example * HttpResponse.text('hello world') * HttpResponse.text('Error', { status: 500 }) */ static text( body?: NoInfer | null, init?: HttpResponseInit, ): HttpResponse { const responseInit = normalizeResponseInit(init) const hasExplicitContentType = responseInit.headers.has('Content-Type') if (!hasExplicitContentType) { responseInit.headers.set('Content-Type', 'text/plain') } // Automatically set the "Content-Length" response header // for non-empty text responses. This enforces consistency and // brings mocked responses closer to production. if (!responseInit.headers.has('Content-Length')) { responseInit.headers.set( 'Content-Length', body ? new Blob([body]).size.toString() : '0', ) } const response = new HttpResponse(body, responseInit) if (!hasExplicitContentType) { Object.defineProperty(response, kDefaultContentType, { value: true, enumerable: false, }) } return response } /** * Create a `Response` with a `Content-Type: "application/json"` body. * @example * HttpResponse.json({ firstName: 'John' }) * HttpResponse.json({ error: 'Not Authorized' }, { status: 401 }) */ static json( body?: NoInfer | null | undefined, init?: HttpResponseInit, ): HttpResponse { const responseInit = normalizeResponseInit(init) const hasExplicitContentType = responseInit.headers.has('Content-Type') if (!hasExplicitContentType) { responseInit.headers.set('Content-Type', 'application/json') } /** * @note TypeScript is incorrect here. * Stringifying undefined will return undefined. */ const responseText = JSON.stringify(body) as string | undefined if (!responseInit.headers.has('Content-Length')) { responseInit.headers.set( 'Content-Length', responseText ? new Blob([responseText]).size.toString() : '0', ) } const response = new HttpResponse(responseText, responseInit) if (!hasExplicitContentType) { Object.defineProperty(response, kDefaultContentType, { value: true, enumerable: false, }) } return response as HttpResponse } /** * Create a `Response` with a `Content-Type: "application/xml"` body. * @example * HttpResponse.xml(``) * HttpResponse.xml(`
`, { status: 201 }) */ static xml( body?: BodyType | null, init?: HttpResponseInit, ): HttpResponse { const responseInit = normalizeResponseInit(init) const hasExplicitContentType = responseInit.headers.has('Content-Type') if (!hasExplicitContentType) { responseInit.headers.set('Content-Type', 'text/xml') } const response = new HttpResponse(body, responseInit) if (!hasExplicitContentType) { Object.defineProperty(response, kDefaultContentType, { value: true, enumerable: false, }) } return response as HttpResponse } /** * Create a `Response` with a `Content-Type: "text/html"` body. * @example * HttpResponse.html(`

Jane Doe

`) * HttpResponse.html(`
Main text
`, { status: 201 }) */ static html( body?: BodyType | null, init?: HttpResponseInit, ): HttpResponse { const responseInit = normalizeResponseInit(init) const hasExplicitContentType = responseInit.headers.has('Content-Type') if (!hasExplicitContentType) { responseInit.headers.set('Content-Type', 'text/html') } const response = new HttpResponse(body, responseInit) if (!hasExplicitContentType) { Object.defineProperty(response, kDefaultContentType, { value: true, enumerable: false, }) } return response as HttpResponse } /** * Create a `Response` with an `ArrayBuffer` body. * @example * const buffer = new ArrayBuffer(3) * const view = new Uint8Array(buffer) * view.set([1, 2, 3]) * * HttpResponse.arrayBuffer(buffer) */ static arrayBuffer( body?: BodyType, init?: HttpResponseInit, ): HttpResponse { const responseInit = normalizeResponseInit(init) const hasExplicitContentType = responseInit.headers.has('Content-Type') if (!hasExplicitContentType) { responseInit.headers.set('Content-Type', 'application/octet-stream') } if (body && !responseInit.headers.has('Content-Length')) { responseInit.headers.set('Content-Length', body.byteLength.toString()) } const response = new HttpResponse(body, responseInit) if (!hasExplicitContentType) { Object.defineProperty(response, kDefaultContentType, { value: true, enumerable: false, }) } return response as HttpResponse } /** * Create a `Response` with a `FormData` body. * @example * const data = new FormData() * data.set('name', 'Alice') * * HttpResponse.formData(data) */ static formData( body?: FormData, init?: HttpResponseInit, ): HttpResponse { return new HttpResponse(body, normalizeResponseInit(init)) } } ================================================ FILE: src/core/SetupApi.ts ================================================ import { invariant } from 'outvariant' import { EventMap, Emitter } from 'strict-event-emitter' import { RequestHandler } 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 { Disposable } from './utils/internal/Disposable' import type { WebSocketHandler } from './handlers/WebSocketHandler' export abstract class HandlersController { abstract prepend( runtimeHandlers: Array, ): void abstract reset(nextHandles: Array): void abstract currentHandlers(): Array } export class InMemoryHandlersController implements HandlersController { private handlers: Array constructor( private initialHandlers: Array, ) { this.handlers = [...initialHandlers] } public prepend( runtimeHandles: Array, ): void { this.handlers.unshift(...runtimeHandles) } public reset(nextHandlers: Array): void { this.handlers = nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers] } public currentHandlers(): Array { return this.handlers } } /** * Generic class for the mock API setup. */ export abstract class SetupApi extends Disposable { protected handlersController: HandlersController protected readonly emitter: Emitter protected readonly publicEmitter: Emitter public readonly events: LifeCycleEventEmitter constructor(...initialHandlers: Array) { super() invariant( this.validateHandlers(initialHandlers), devUtils.formatMessage( `Failed to apply given request handlers: invalid input. Did you forget to spread the request handlers Array?`, ), ) this.handlersController = new InMemoryHandlersController(initialHandlers) this.emitter = new Emitter() this.publicEmitter = new Emitter() pipeEvents(this.emitter, this.publicEmitter) this.events = this.createLifeCycleEvents() this.subscriptions.push(() => { this.emitter.removeAllListeners() this.publicEmitter.removeAllListeners() }) } private validateHandlers(handlers: ReadonlyArray): boolean { // Guard against incorrect call signature of the setup API. return handlers.every((handler) => !Array.isArray(handler)) } public use( ...runtimeHandlers: Array ): void { invariant( this.validateHandlers(runtimeHandlers), devUtils.formatMessage( `Failed to call "use()" with the given request handlers: invalid input. Did you forget to spread the array of request handlers?`, ), ) this.handlersController.prepend(runtimeHandlers) } public restoreHandlers(): void { this.handlersController.currentHandlers().forEach((handler) => { if ('isUsed' in handler) { handler.isUsed = false } }) } public resetHandlers( ...nextHandlers: Array ): void { this.handlersController.reset(nextHandlers) } public listHandlers(): ReadonlyArray { return toReadonlyArray(this.handlersController.currentHandlers()) } private createLifeCycleEvents(): LifeCycleEventEmitter { return { on: (...args: any[]) => { return (this.publicEmitter.on as any)(...args) }, removeListener: (...args: any[]) => { return (this.publicEmitter.removeListener as any)(...args) }, removeAllListeners: (...args: any[]) => { return this.publicEmitter.removeAllListeners(...args) }, } } } ================================================ FILE: src/core/bypass.test.ts ================================================ /** * @vitest-environment jsdom */ import { bypass } from './bypass' it('returns bypassed request given a request url string', async () => { const request = bypass('https://api.example.com/resource') // Relative URLs are rebased against the current location. expect(request.method).toBe('GET') expect(request.url).toBe('https://api.example.com/resource') expect(Array.from(request.headers)).toEqual([['accept', 'msw/passthrough']]) }) it('returns bypassed request given a request url', async () => { const request = bypass(new URL('/resource', 'https://api.example.com')) expect(request.url).toBe('https://api.example.com/resource') expect(Array.from(request.headers)).toEqual([['accept', 'msw/passthrough']]) }) it('returns bypassed request given request instance', async () => { const original = new Request('http://localhost/resource', { method: 'POST', headers: { accept: '*/*', 'X-My-Header': 'value', }, body: 'hello world', }) const request = bypass(original) expect(request.method).toBe('POST') expect(request.url).toBe('http://localhost/resource') const bypassedRequestBody = await request.text() expect(original.bodyUsed).toBe(false) expect(bypassedRequestBody).toEqual(await original.text()) expect(Array.from(request.headers)).toEqual([ ['accept', '*/*, msw/passthrough'], ['content-type', 'text/plain;charset=UTF-8'], ['x-my-header', 'value'], ]) }) it('allows modifying the bypassed request instance', async () => { const original = new Request('http://localhost/resource', { method: 'POST', body: 'hello world', }) const request = bypass(original, { method: 'PUT', headers: { 'x-modified-header': 'yes' }, }) expect(request.method).toBe('PUT') expect(Array.from(request.headers)).toEqual([ ['accept', 'msw/passthrough'], ['x-modified-header', 'yes'], ]) expect(original.bodyUsed).toBe(false) expect(request.bodyUsed).toBe(false) expect(await request.text()).toBe('hello world') expect(original.bodyUsed).toBe(false) }) it('supports bypassing "keepalive: true" requests', async () => { const original = new Request('http://localhost/resource', { method: 'POST', keepalive: true, }) const request = bypass(original) expect(request.method).toBe('POST') expect(request.url).toBe('http://localhost/resource') expect(request.body).toBeNull() expect(Array.from(request.headers)).toEqual([['accept', 'msw/passthrough']]) }) ================================================ FILE: src/core/bypass.ts ================================================ import { invariant } from 'outvariant' export type BypassRequestInput = string | URL | Request /** * Creates a `Request` instance that will always be ignored by MSW. * * @example * import { bypass } from 'msw' * * fetch(bypass('/resource')) * fetch(bypass(new URL('/resource', 'https://example.com))) * fetch(bypass(new Request('https://example.com/resource'))) * * @see {@link https://mswjs.io/docs/api/bypass `bypass()` API reference} */ export function bypass(input: BypassRequestInput, init?: RequestInit): Request { // Always create a new Request instance. // This way, the "init" modifications will propagate // to the bypass request instance automatically. const request = new Request( // If given a Request instance, clone it not to exhaust // the original request's body. input instanceof Request ? input.clone() : input, init, ) invariant( !request.bodyUsed, 'Failed to create a bypassed request to "%s %s": given request instance already has its body read. Make sure to clone the intercepted request if you wish to read its body before bypassing it.', request.method, request.url, ) const requestClone = request.clone() /** * Send the internal request header that would instruct MSW * to perform this request as-is, ignoring any matching handlers. * @note Use the `accept` header to support scenarios when the * request cannot have headers (e.g. `sendBeacon` requests). */ requestClone.headers.append('accept', 'msw/passthrough') return requestClone } ================================================ FILE: src/core/delay.ts ================================================ import { isNodeProcess } from 'is-node-process' import { hasRefCounted } from './utils/internal/hasRefCounted' export const SET_TIMEOUT_MAX_ALLOWED_INT = 2147483647 export const MIN_SERVER_RESPONSE_TIME = 100 export const MAX_SERVER_RESPONSE_TIME = 400 export const NODE_SERVER_RESPONSE_TIME = 5 function getRealisticResponseTime(): number { if (isNodeProcess()) { return NODE_SERVER_RESPONSE_TIME } return Math.floor( Math.random() * (MAX_SERVER_RESPONSE_TIME - MIN_SERVER_RESPONSE_TIME) + MIN_SERVER_RESPONSE_TIME, ) } export type DelayMode = 'real' | 'infinite' /** * Delays the response by the given duration (ms). * * @example * await delay() // emulate realistic server response time * await delay(1200) // delay response by 1200ms * await delay('infinite') // delay response infinitely * * @see {@link https://mswjs.io/docs/api/delay `delay()` API reference} */ export async function delay( durationOrMode?: DelayMode | number, ): Promise { let delayTime: number if (typeof durationOrMode === 'string') { switch (durationOrMode) { case 'infinite': { // Using `Infinity` as a delay value executes the response timeout immediately. // Instead, use the maximum allowed integer for `setTimeout`. delayTime = SET_TIMEOUT_MAX_ALLOWED_INT break } case 'real': { delayTime = getRealisticResponseTime() break } default: { throw new Error( `Failed to delay a response: unknown delay mode "${durationOrMode}". Please make sure you provide one of the supported modes ("real", "infinite") or a number.`, ) } } } else if (typeof durationOrMode === 'undefined') { // Use random realistic server response time when no explicit delay duration was provided. delayTime = getRealisticResponseTime() } else { // Guard against passing values like `Infinity` or `Number.MAX_VALUE` // as the response delay duration. They don't produce the result you may expect. if (durationOrMode > SET_TIMEOUT_MAX_ALLOWED_INT) { throw new Error( `Failed to delay a response: provided delay duration (${durationOrMode}) exceeds the maximum allowed duration for "setTimeout" (${SET_TIMEOUT_MAX_ALLOWED_INT}). This will cause the response to be returned immediately. Please use a number within the allowed range to delay the response by exact duration, or consider the "infinite" delay mode to delay the response indefinitely.`, ) } delayTime = durationOrMode } return new Promise((resolve) => { const timeoutId = setTimeout(resolve, delayTime) if ( delayTime === SET_TIMEOUT_MAX_ALLOWED_INT && isNodeProcess() && hasRefCounted(timeoutId) ) { // Prevent the process from hanging if this is the only active ref. timeoutId.unref() } }) } ================================================ FILE: src/core/getResponse.test.ts ================================================ // @vitest-environment node import { http } from './http' import { getResponse } from './getResponse' it('returns undefined given empty headers array', async () => { await expect( getResponse([], new Request('http://localhost/')), ).resolves.toBeUndefined() }) it('returns undefined given no matching handlers', async () => { await expect( getResponse( [http.get('/product', () => void 0)], new Request('http://localhost/user'), ), ).resolves.toBeUndefined() }) it('returns undefined given a matching handler that returned no response', async () => { await expect( getResponse( [http.get('*/user', () => void 0)], new Request('http://localhost/user'), ), ).resolves.toBeUndefined() }) it('returns undefined given a matching handler that returned explicit undefined', async () => { await expect( getResponse( [http.get('*/user', () => undefined)], new Request('http://localhost/user'), ), ).resolves.toBeUndefined() }) it('returns the response returned from a matching handler', async () => { const response = await getResponse( [http.get('*/user', () => Response.json({ name: 'John' }))], new Request('http://localhost/user'), ) expect(response?.status).toBe(200) expect(response?.headers.get('Content-Type')).toBe('application/json') await expect(response?.json()).resolves.toEqual({ name: 'John' }) }) it('returns the response from the first matching handler if multiple match', async () => { const response = await getResponse( [ http.get('*/user', () => Response.json({ name: 'John' })), http.get('*/user', () => Response.json({ name: 'Kate' })), ], new Request('http://localhost/user'), ) expect(response?.status).toBe(200) expect(response?.headers.get('Content-Type')).toBe('application/json') await expect(response?.json()).resolves.toEqual({ name: 'John' }) }) it('supports custom base url', async () => { const response = await getResponse( [http.get('/resource', () => new Response('hello world'))], new Request('https://localhost:3000/resource'), { baseUrl: 'https://localhost:3000/', }, ) expect(response?.status).toBe(200) await expect(response?.text()).resolves.toBe('hello world') }) ================================================ FILE: src/core/getResponse.ts ================================================ import { createRequestId } from '@mswjs/interceptors' import type { RequestHandler } from './handlers/RequestHandler' import { executeHandlers, type ResponseResolutionContext, } from './utils/executeHandlers' /** * Finds a response for the given request instance * in the array of request handlers. * @param handlers The array of request handlers. * @param request The `Request` instance. * @param resolutionContext Request resolution options. * @returns {Response} A mocked response, if any. */ export const getResponse = async ( handlers: Array, request: Request, resolutionContext?: ResponseResolutionContext, ): Promise => { const result = await executeHandlers({ request, requestId: createRequestId(), handlers, resolutionContext, }) return result?.response } ================================================ FILE: src/core/graphql.test.ts ================================================ import { graphql } from './graphql' test('exports supported GraphQL operation types', () => { expect(graphql).toBeDefined() expect(Object.keys(graphql)).toEqual([ 'query', 'mutation', 'operation', 'link', ]) }) ================================================ FILE: src/core/graphql.ts ================================================ import type { OperationTypeNode } from 'graphql' import { ResponseResolver, RequestHandlerOptions, } from './handlers/RequestHandler' import { GraphQLHandler, GraphQLVariables, GraphQLOperationType, GraphQLResolverExtras, GraphQLResponseBody, GraphQLQuery, GraphQLPredicate, } from './handlers/GraphQLHandler' import type { Path } from './utils/matching/matchRequestUrl' export type GraphQLRequestHandler = < Query extends GraphQLQuery = GraphQLQuery, Variables extends GraphQLVariables = GraphQLVariables, >( predicate: GraphQLPredicate, resolver: GraphQLResponseResolver< [Query] extends [never] ? GraphQLQuery : Query, Variables >, options?: RequestHandlerOptions, ) => GraphQLHandler export type GraphQLOperationHandler = < Query extends GraphQLQuery = GraphQLQuery, Variables extends GraphQLVariables = GraphQLVariables, >( resolver: GraphQLResponseResolver< [Query] extends [never] ? GraphQLQuery : Query, Variables >, options?: RequestHandlerOptions, ) => GraphQLHandler export type GraphQLResponseResolver< Query extends GraphQLQuery = GraphQLQuery, Variables extends GraphQLVariables = GraphQLVariables, > = ResponseResolver< GraphQLResolverExtras, null, GraphQLResponseBody<[Query] extends [never] ? GraphQLQuery : Query> > function createScopedGraphQLHandler( operationType: GraphQLOperationType, url: Path, ): GraphQLRequestHandler { return (predicate, resolver, options = {}) => { return new GraphQLHandler(operationType, predicate, url, resolver, options) } } function createGraphQLOperationHandler(url: Path): GraphQLOperationHandler { return (resolver, options) => { return new GraphQLHandler('all', new RegExp('.*'), url, resolver, options) } } export interface GraphQLLinkHandlers { query: GraphQLRequestHandler mutation: GraphQLRequestHandler operation: GraphQLOperationHandler } /** * A namespace to intercept and mock GraphQL operations * * @example * graphql.query('GetUser', resolver) * graphql.mutation('DeletePost', resolver) * * @see {@link https://mswjs.io/docs/api/graphql `graphql` API reference} */ export const graphql = { /** * Intercepts a GraphQL query by a given name. * * @example * graphql.query('GetUser', () => { * return HttpResponse.json({ data: { user: { name: 'John' } } }) * }) * * @see {@link https://mswjs.io/docs/api/graphql#graphqlqueryqueryname-resolver `graphql.query()` API reference} */ query: createScopedGraphQLHandler('query' as OperationTypeNode, '*'), /** * Intercepts a GraphQL mutation by its name. * * @example * graphql.mutation('SavePost', () => { * return HttpResponse.json({ data: { post: { id: 'abc-123 } } }) * }) * * @see {@link https://mswjs.io/docs/api/graphql#graphqlmutationmutationname-resolver `graphql.query()` API reference} * */ mutation: createScopedGraphQLHandler('mutation' as OperationTypeNode, '*'), /** * Intercepts any GraphQL operation, regardless of its type or name. * * @example * graphql.operation(() => { * return HttpResponse.json({ data: { name: 'John' } }) * }) * * @see {@link https://mswjs.io/docs/api/graphql#graphqloperationresolver `graphql.operation()` API reference} */ operation: createGraphQLOperationHandler('*'), /** * Intercepts GraphQL operations scoped by the given URL. * * @example * const github = graphql.link('https://api.github.com/graphql') * github.query('GetRepo', resolver) * * @see {@link https://mswjs.io/docs/api/graphql#graphqllinkurl `graphql.link()` API reference} */ link(url: Path): GraphQLLinkHandlers { return { operation: createGraphQLOperationHandler(url), query: createScopedGraphQLHandler('query' as OperationTypeNode, url), mutation: createScopedGraphQLHandler( 'mutation' as OperationTypeNode, url, ), } }, } ================================================ FILE: src/core/handlers/GraphQLHandler.test.ts ================================================ // @vitest-environment jsdom import { createRequestId, encodeBuffer } from '@mswjs/interceptors' import { OperationTypeNode, parse } from 'graphql' import { GraphQLHandler, GraphQLRequestBody, GraphQLResolverExtras, isDocumentNode, } from './GraphQLHandler' import { HttpResponse } from '../HttpResponse' import { ResponseResolver } from './RequestHandler' const resolver: ResponseResolver> = ({ variables, }) => { return HttpResponse.json({ data: { user: { id: variables.userId, }, }, }) } function createGetGraphQLRequest( body: GraphQLRequestBody, graphqlEndpoint = 'https://example.com', ) { const requestUrl = new URL(graphqlEndpoint) requestUrl.searchParams.set('query', body?.query) requestUrl.searchParams.set('variables', JSON.stringify(body?.variables)) return new Request(requestUrl) } function createPostGraphQLRequest( body: GraphQLRequestBody, graphqlEndpoint = 'https://example.com', ) { return new Request(new URL(graphqlEndpoint), { method: 'POST', headers: new Headers({ 'Content-Type': 'application/json' }), body: encodeBuffer(JSON.stringify(body)), }) } const GET_USER = ` query GetUser { user { id } } ` const LOGIN = ` mutation Login { user { id } } ` describe('info', () => { it('exposes request handler information for query', () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) expect(handler.info.header).toEqual('query GetUser (origin: *)') expect(handler.info.operationType).toEqual('query') expect(handler.info.operationName).toEqual('GetUser') }) it('exposes request handler information for mutation', () => { const handler = new GraphQLHandler( OperationTypeNode.MUTATION, 'Login', '*', resolver, ) expect(handler.info.header).toEqual('mutation Login (origin: *)') expect(handler.info.operationType).toEqual('mutation') expect(handler.info.operationName).toEqual('Login') }) it('parses a query operation name from a given DocumentNode', () => { const node = parse(` query GetUser { user { firstName } } `) const handler = new GraphQLHandler( OperationTypeNode.QUERY, node, '*', resolver, ) expect(handler.info).toHaveProperty('header', 'query GetUser (origin: *)') expect(handler.info).toHaveProperty('operationType', 'query') expect(handler.info).toHaveProperty('operationName', 'GetUser') }) it('parses a mutation operation name from a given DocumentNode', () => { const node = parse(` mutation Login { user { id } } `) const handler = new GraphQLHandler( OperationTypeNode.MUTATION, node, '*', resolver, ) expect(handler.info).toHaveProperty('header', 'mutation Login (origin: *)') expect(handler.info).toHaveProperty('operationType', 'mutation') expect(handler.info).toHaveProperty('operationName', 'Login') }) it('throws an exception given a DocumentNode with a mismatched operation type', () => { const node = parse(` mutation CreateUser { user { firstName } } `) expect( () => new GraphQLHandler(OperationTypeNode.QUERY, node, '*', resolver), ).toThrow( 'Failed to create a GraphQL handler: provided a DocumentNode with a mismatched operation type (expected "query" but got "mutation").', ) }) }) describe('parse', () => { describe('query', () => { it('parses a query without variables (GET)', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createGetGraphQLRequest({ query: GET_USER, }) expect(await handler.parse({ request })).toEqual({ cookies: {}, match: { matches: true, params: { '0': 'https://example.com/', }, }, operationType: 'query', operationName: 'GetUser', query: GET_USER, variables: undefined, }) }) it('parses a query with variables (GET)', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createGetGraphQLRequest({ query: GET_USER, variables: { userId: 'abc-123', }, }) expect(await handler.parse({ request })).toEqual({ cookies: {}, match: { matches: true, params: { '0': 'https://example.com/', }, }, operationType: 'query', operationName: 'GetUser', query: GET_USER, variables: { userId: 'abc-123', }, }) }) it('parses a query without variables (POST)', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: GET_USER, }) expect(await handler.parse({ request })).toEqual({ cookies: {}, match: { matches: true, params: { '0': 'https://example.com/', }, }, operationType: 'query', operationName: 'GetUser', query: GET_USER, variables: undefined, }) }) it('parses a query with variables (POST)', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: GET_USER, variables: { userId: 'abc-123', }, }) expect(await handler.parse({ request })).toEqual({ cookies: {}, match: { matches: true, params: { '0': 'https://example.com/', }, }, operationType: 'query', operationName: 'GetUser', query: GET_USER, variables: { userId: 'abc-123', }, }) }) }) describe('mutation', () => { it('parses a mutation without variables (GET)', async () => { const handler = new GraphQLHandler( OperationTypeNode.MUTATION, 'GetUser', '*', resolver, ) const request = createGetGraphQLRequest({ query: LOGIN, }) expect(await handler.parse({ request })).toEqual({ cookies: {}, match: { matches: true, params: { '0': 'https://example.com/', }, }, operationType: 'mutation', operationName: 'Login', query: LOGIN, variables: undefined, }) }) it('parses a mutation with variables (GET)', async () => { const handler = new GraphQLHandler( OperationTypeNode.MUTATION, 'GetUser', '*', resolver, ) const request = createGetGraphQLRequest({ query: LOGIN, variables: { userId: 'abc-123', }, }) expect(await handler.parse({ request })).toEqual({ cookies: {}, match: { matches: true, params: { '0': 'https://example.com/', }, }, operationType: 'mutation', operationName: 'Login', query: LOGIN, variables: { userId: 'abc-123', }, }) }) it('parses a mutation without variables (POST)', async () => { const handler = new GraphQLHandler( OperationTypeNode.MUTATION, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: LOGIN, }) expect(await handler.parse({ request })).toEqual({ cookies: {}, match: { matches: true, params: { '0': 'https://example.com/', }, }, operationType: 'mutation', operationName: 'Login', query: LOGIN, variables: undefined, }) }) it('parses a mutation with variables (POST)', async () => { const handler = new GraphQLHandler( OperationTypeNode.MUTATION, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: LOGIN, variables: { userId: 'abc-123', }, }) expect(await handler.parse({ request })).toEqual({ cookies: {}, match: { matches: true, params: { '0': 'https://example.com/', }, }, operationType: 'mutation', operationName: 'Login', query: LOGIN, variables: { userId: 'abc-123', }, }) }) }) describe('with endpoint configuration', () => { it('parses the request and parses grapqhl properties from it when the graphql.link endpoint matches', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', 'https://mswjs.com/graphql', resolver, ) await expect( handler.parse({ request: createGetGraphQLRequest( { query: GET_USER, variables: { userId: 'abc-123', }, }, 'https://mswjs.com/graphql', ), }), ).resolves.toEqual({ cookies: {}, match: { matches: true, params: {}, }, operationType: 'query', operationName: 'GetUser', query: GET_USER, variables: { userId: 'abc-123', }, }) await expect( handler.parse({ request: createPostGraphQLRequest( { query: GET_USER, variables: { userId: 'abc-123', }, }, 'https://mswjs.com/graphql', ), }), ).resolves.toEqual({ cookies: {}, match: { matches: true, params: {}, }, operationType: 'query', operationName: 'GetUser', query: GET_USER, variables: { userId: 'abc-123', }, }) }) it('parses a request but does not parse graphql properties from it graphql.link hostname does not match', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', 'https://mswjs.com/graphql', resolver, ) await expect( handler.parse({ request: createGetGraphQLRequest( { query: GET_USER, variables: { userId: 'abc-123', }, }, 'https://example.com/graphql', ), }), ).resolves.toEqual({ cookies: {}, match: { matches: false, params: {}, }, }) await expect( handler.parse({ request: createPostGraphQLRequest( { query: GET_USER, variables: { userId: 'abc-123', }, }, 'https://example.com/graphql', ), }), ).resolves.toEqual({ cookies: {}, match: { matches: false, params: {}, }, }) }) it('parses a request but does not parse graphql properties from it graphql.link pathname does not match', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', 'https://mswjs.com/graphql', resolver, ) await expect( handler.parse({ request: createGetGraphQLRequest( { query: GET_USER, variables: { userId: 'abc-123', }, }, 'https://mswjs.com/some/other/endpoint', ), }), ).resolves.toEqual({ cookies: {}, match: { matches: false, params: {}, }, }) await expect( handler.parse({ request: createPostGraphQLRequest( { query: GET_USER, variables: { userId: 'abc-123', }, }, 'https://mswjs.com/some/other/endpoint', ), }), ).resolves.toEqual({ cookies: {}, match: { matches: false, params: {}, }, }) }) }) }) describe('predicate', () => { it('respects operation type', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: GET_USER, }) const alienRequest = createPostGraphQLRequest({ query: LOGIN, }) expect( await handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).toBe(true) expect( await handler.predicate({ request: alienRequest, parsedResult: await handler.parse({ request: alienRequest }), }), ).toBe(false) }) it('respects operation name', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: GET_USER, }) const alienRequest = createPostGraphQLRequest({ query: ` query GetAllUsers { user { id } } `, }) await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(true) await expect( handler.predicate({ request: alienRequest, parsedResult: await handler.parse({ request: alienRequest }), }), ).resolves.toBe(false) }) it('allows anonymous GraphQL operations when using "all" expected operation type', async () => { const handler = new GraphQLHandler('all', new RegExp('.*'), '*', resolver) const request = createPostGraphQLRequest({ query: ` query { anonymousQuery { query variables } } `, }) await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(true) }) it('respects custom endpoint', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', 'https://api.github.com/graphql', resolver, ) const request = createPostGraphQLRequest( { query: GET_USER, }, 'https://api.github.com/graphql', ) const alienRequest = createPostGraphQLRequest({ query: GET_USER, }) await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(true) await expect( handler.predicate({ request: alienRequest, parsedResult: await handler.parse({ request: alienRequest }), }), ).resolves.toBe(false) }) it('supports custom predicate function', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, ({ query }) => { return query.includes('password') }, /.+/, resolver, ) { const request = createPostGraphQLRequest({ query: `query GetUser { user { password } }`, }) await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(true) } { const request = createPostGraphQLRequest({ query: `query GetUser { user { nonMatching } }`, }) await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(false) } }) }) describe('test', () => { it('respects operation type', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: GET_USER, }) const alienRequest = createPostGraphQLRequest({ query: LOGIN, }) expect(await handler.test({ request })).toBe(true) expect(await handler.test({ request: alienRequest })).toBe(false) }) it('respects operation name', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: GET_USER, }) const alienRequest = createPostGraphQLRequest({ query: ` query GetAllUsers { user { id } } `, }) await expect(handler.test({ request })).resolves.toBe(true) await expect(handler.test({ request: alienRequest })).resolves.toBe(false) }) it('respects custom endpoint', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', 'https://api.github.com/graphql', resolver, ) const request = createPostGraphQLRequest( { query: GET_USER, }, 'https://api.github.com/graphql', ) const alienRequest = createPostGraphQLRequest({ query: GET_USER, }) await expect(handler.test({ request })).resolves.toBe(true) await expect(handler.test({ request: alienRequest })).resolves.toBe(false) }) }) describe('run', () => { it('returns a mocked response given a matching query', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: GET_USER, variables: { userId: 'abc-123', }, }) const requestId = createRequestId() const result = await handler.run({ request, requestId }) expect(result!.handler).toEqual(handler) expect(result!.parsedResult).toEqual({ cookies: {}, match: { matches: true, params: { '0': 'https://example.com/', }, }, operationType: 'query', operationName: 'GetUser', query: GET_USER, variables: { userId: 'abc-123', }, }) expect(result!.request.method).toBe('POST') expect(result!.request.url).toBe('https://example.com/') await expect(result!.request.json()).resolves.toEqual({ query: GET_USER, variables: { userId: 'abc-123' }, }) expect(result!.response?.status).toBe(200) expect(result!.response?.statusText).toBe('OK') await expect(result!.response?.json()).resolves.toEqual({ data: { user: { id: 'abc-123' } }, }) }) it('returns null given a non-matching query', async () => { const handler = new GraphQLHandler( OperationTypeNode.QUERY, 'GetUser', '*', resolver, ) const request = createPostGraphQLRequest({ query: LOGIN, }) const requestId = createRequestId() const result = await handler.run({ request, requestId }) expect(result).toBeNull() }) }) describe('isDocumentNode', () => { it('returns true given a valid DocumentNode', () => { const node = parse(` query GetUser { user { login } } `) expect(isDocumentNode(node)).toEqual(true) }) it('returns false given an arbitrary input', () => { expect(isDocumentNode(null)).toEqual(false) expect(isDocumentNode(undefined)).toEqual(false) expect(isDocumentNode('')).toEqual(false) expect(isDocumentNode('value')).toEqual(false) expect(isDocumentNode(/value/)).toEqual(false) }) }) describe('request', () => { it('has parsed operationName', async () => { const matchAllResolver = vi.fn() const handler = new GraphQLHandler( OperationTypeNode.QUERY, /.*/, '*', matchAllResolver, ) const request = createPostGraphQLRequest({ query: ` query GetAllUsers { user { id } } `, }) const requestId = createRequestId() await handler.run({ request, requestId }) expect(matchAllResolver).toHaveBeenCalledTimes(1) expect(matchAllResolver.mock.calls[0][0]).toHaveProperty( 'operationName', 'GetAllUsers', ) }) }) ================================================ FILE: src/core/handlers/GraphQLHandler.ts ================================================ import { invariant } from 'outvariant' import { parse, type DocumentNode, type GraphQLError, type OperationTypeNode, } from 'graphql' import { DefaultBodyType, RequestHandler, RequestHandlerDefaultInfo, RequestHandlerExecutionResult, RequestHandlerOptions, ResponseResolver, } from './RequestHandler' import { getTimestamp } from '../utils/logging/getTimestamp' import { getStatusCodeColor } from '../utils/logging/getStatusCodeColor' import { serializeRequest } from '../utils/logging/serializeRequest' import { serializeResponse } from '../utils/logging/serializeResponse' import { Match, matchRequestUrl, Path } from '../utils/matching/matchRequestUrl' import { ParsedGraphQLRequest, GraphQLMultipartRequestBody, parseGraphQLRequest, parseDocumentNode, ParsedGraphQLQuery, } from '../utils/internal/parseGraphQLRequest' import { toPublicUrl } from '../utils/request/toPublicUrl' import { devUtils } from '../utils/internal/devUtils' import { getAllRequestCookies } from '../utils/request/getRequestCookies' import { ResponseResolutionContext } from 'src/iife' import { kDefaultContentType, StrictRequest } from '../HttpResponse' import { getAllAcceptedMimeTypes } from '../utils/request/getAllAcceptedMimeTypes' export interface DocumentTypeDecoration< Result = { [key: string]: any }, Variables = { [key: string]: any }, > { __apiType?: (variables: Variables) => Result __resultType?: Result __variablesType?: Variables } export type GraphQLOperationType = OperationTypeNode | 'all' export type GraphQLHandlerNameSelector = DocumentNode | RegExp | string export type GraphQLQuery = Record | null export type GraphQLVariables = Record export interface GraphQLHandlerInfo extends RequestHandlerDefaultInfo { operationType: GraphQLOperationType operationName: GraphQLHandlerNameSelector | GraphQLCustomPredicate } export type GraphQLRequestParsedResult = { match: Match cookies: Record } & ( | ParsedGraphQLRequest /** * An empty version of the ParsedGraphQLRequest * which simplifies the return type of the resolver * when the request is to a non-matching endpoint */ | { operationType?: undefined operationName?: undefined query?: undefined variables?: undefined } ) export type GraphQLResolverExtras = { query: string operationName: string variables: Variables cookies: Record } export type GraphQLRequestBody = | GraphQLJsonRequestBody | GraphQLMultipartRequestBody | Record | undefined export interface GraphQLJsonRequestBody { query: string variables?: Variables } export type GraphQLResponseBody = | { data?: BodyType | null errors?: readonly Partial[] | null extensions?: Record } | null | undefined export type GraphQLCustomPredicate = (args: { request: Request query: string operationType: GraphQLOperationType operationName: string variables: GraphQLVariables cookies: Record }) => GraphQLCustomPredicateResult | Promise export type GraphQLCustomPredicateResult = boolean | { matches: boolean } export type GraphQLPredicate = | GraphQLHandlerNameSelector | DocumentTypeDecoration | GraphQLCustomPredicate export function isDocumentNode( value: DocumentNode | any, ): value is DocumentNode { if (value == null) { return false } return typeof value === 'object' && 'kind' in value && 'definitions' in value } function isDocumentTypeDecoration( value: unknown, ): value is DocumentTypeDecoration { return value instanceof String } export class GraphQLHandler extends RequestHandler< GraphQLHandlerInfo, GraphQLRequestParsedResult, GraphQLResolverExtras > { private endpoint: Path static parsedRequestCache = new WeakMap< Request, ParsedGraphQLRequest >() static #parseOperationName( predicate: GraphQLPredicate, operationType: GraphQLOperationType, ): GraphQLHandlerInfo['operationName'] { const getOperationName = (node: ParsedGraphQLQuery): string => { invariant( node.operationType === operationType, 'Failed to create a GraphQL handler: provided a DocumentNode with a mismatched operation type (expected "%s" but got "%s").', operationType, node.operationType, ) invariant( node.operationName, 'Failed to create a GraphQL handler: provided a DocumentNode without operation name', ) return node.operationName } if (isDocumentNode(predicate)) { return getOperationName(parseDocumentNode(predicate)) } if (isDocumentTypeDecoration(predicate)) { const documentNode = parse(predicate.toString()) invariant( isDocumentNode(documentNode), 'Failed to create a GraphQL handler: given TypedDocumentString (%s) does not produce a valid DocumentNode', predicate, ) return getOperationName(parseDocumentNode(documentNode)) } return predicate } constructor( operationType: GraphQLOperationType, predicate: GraphQLPredicate, endpoint: Path, resolver: ResponseResolver, any, any>, options?: RequestHandlerOptions, ) { const operationName = GraphQLHandler.#parseOperationName( predicate, operationType, ) const displayOperationName = typeof operationName === 'function' ? '[custom predicate]' : operationName const header = operationType === 'all' ? `${operationType} (origin: ${endpoint.toString()})` : `${operationType}${displayOperationName ? ` ${displayOperationName}` : ''} (origin: ${endpoint.toString()})` super({ info: { header, operationType, operationName: GraphQLHandler.#parseOperationName( predicate, operationType, ), }, resolver, options, }) this.endpoint = endpoint } /** * Parses the request body, once per request, cached across all * GraphQL handlers. This is done to avoid multiple parsing of the * request body, which each requires a clone of the request. */ async parseGraphQLRequestOrGetFromCache( request: Request, ): Promise> { if (!GraphQLHandler.parsedRequestCache.has(request)) { GraphQLHandler.parsedRequestCache.set( request, await parseGraphQLRequest(request).catch((error) => { console.error(error) return undefined }), ) } return GraphQLHandler.parsedRequestCache.get(request) } async parse(args: { request: Request }): Promise { /** * If the request doesn't match a specified endpoint, there's no * need to parse it since there's no case where we would handle this */ const match = matchRequestUrl(new URL(args.request.url), this.endpoint) const cookies = getAllRequestCookies(args.request) if (!match.matches) { return { match, cookies, } } const parsedResult = await this.parseGraphQLRequestOrGetFromCache( args.request, ) if (typeof parsedResult === 'undefined') { return { match, cookies, } } return { match, cookies, query: parsedResult.query, operationType: parsedResult.operationType, operationName: parsedResult.operationName, variables: parsedResult.variables, } } async predicate(args: { request: Request parsedResult: GraphQLRequestParsedResult }): Promise { if (args.parsedResult.operationType === undefined) { return false } if (!args.parsedResult.operationName && this.info.operationType !== 'all') { const publicUrl = toPublicUrl(args.request.url) devUtils.warn(`\ Failed to intercept a GraphQL request at "${args.request.method} ${publicUrl}": anonymous GraphQL operations are not supported. Consider naming this operation or using "graphql.operation()" request handler to intercept GraphQL requests regardless of their operation name/type. Read more: https://mswjs.io/docs/api/graphql/#graphqloperationresolver`) return false } const hasMatchingOperationType = this.info.operationType === 'all' || args.parsedResult.operationType === this.info.operationType /** * Check if the operation name matches the outgoing GraphQL request. * @note Unlike the HTTP handler, the custom predicate functions are invoked * during predicate, not parsing, because GraphQL request parsing happens first, * and non-GraphQL requests are filtered out automatically. */ const hasMatchingOperationName = await this.matchOperationName({ request: args.request, parsedResult: args.parsedResult, }) return ( args.parsedResult.match.matches && hasMatchingOperationType && hasMatchingOperationName ) } public async run(args: { request: StrictRequest requestId: string resolutionContext?: ResponseResolutionContext }): Promise | null> { const result = await super.run(args) if (result?.response == null) { return result } if (!(kDefaultContentType in result.response)) { return result } const acceptedMimeTypes = getAllAcceptedMimeTypes( args.request.headers.get('accept'), ) if (acceptedMimeTypes.length === 0) { return result } const graphqlResponseIndex = acceptedMimeTypes.indexOf( 'application/graphql-response+json', ) const jsonIndex = acceptedMimeTypes.indexOf('application/json') /** * Use the "application/graphql-response+json" response content type * only when the client accepts it AND prefers it over "application/json" * (i.e. it appears earlier in the precedence-sorted list, or "application/json" * is not listed at all). * @see https://github.com/graphql/graphql-over-http/blob/4d1df1fb829ec2dd3ecbf3c6aa4025bd356c270d/spec/GraphQLOverHTTP.md#accept */ if ( graphqlResponseIndex !== -1 && (jsonIndex === -1 || graphqlResponseIndex <= jsonIndex) ) { result.response.headers.set( 'content-type', 'application/graphql-response+json', ) } return result } private async matchOperationName(args: { request: Request parsedResult: GraphQLRequestParsedResult }): Promise { if (typeof this.info.operationName === 'function') { const customPredicateResult = await this.info.operationName({ request: args.request, ...this.extendResolverArgs({ request: args.request, parsedResult: args.parsedResult, }), }) /** * @note Keep the { matches } signature in case we decide to support path parameters * in GraphQL handlers. If that happens, the custom predicate would have to be moved * to the parsing phase, the same as we have for the HttpHandler, and the user will * have a possibility to return parsed path parameters from the custom predicate. */ return typeof customPredicateResult === 'boolean' ? customPredicateResult : customPredicateResult.matches } if (this.info.operationName instanceof RegExp) { return this.info.operationName.test(args.parsedResult.operationName || '') } return args.parsedResult.operationName === this.info.operationName } protected extendResolverArgs(args: { request: Request parsedResult: GraphQLRequestParsedResult }) { return { query: args.parsedResult.query || '', operationType: args.parsedResult.operationType!, operationName: args.parsedResult.operationName || '', variables: args.parsedResult.variables || {}, cookies: args.parsedResult.cookies, } } async log(args: { request: Request response: Response parsedResult: GraphQLRequestParsedResult }) { const loggedRequest = await serializeRequest(args.request) const loggedResponse = await serializeResponse(args.response) const statusColor = getStatusCodeColor(loggedResponse.status) const requestInfo = args.parsedResult.operationName ? `${args.parsedResult.operationType} ${args.parsedResult.operationName}` : `anonymous ${args.parsedResult.operationType}` console.groupCollapsed( devUtils.formatMessage( `${getTimestamp()} ${requestInfo} (%c${loggedResponse.status} ${ loggedResponse.statusText }%c)`, ), `color:${statusColor}`, 'color:inherit', ) // eslint-disable-next-line no-console console.log('Request:', loggedRequest) // eslint-disable-next-line no-console console.log('Handler:', this) // eslint-disable-next-line no-console console.log('Response:', loggedResponse) console.groupEnd() } } ================================================ FILE: src/core/handlers/HttpHandler.test.ts ================================================ // @vitest-environment jsdom import { createRequestId } from '@mswjs/interceptors' import { HttpHandler, HttpRequestResolverExtras } from './HttpHandler' import { HttpResponse } from '..' import { ResponseResolver } from './RequestHandler' const resolver: ResponseResolver< HttpRequestResolverExtras<{ userId: string }> > = ({ params }) => { return HttpResponse.json({ userId: params.userId }) } describe('info', () => { it('exposes request handler information', () => { const handler = new HttpHandler('GET', '/user/:userId', resolver) expect(handler.info.header).toEqual('GET /user/:userId') expect(handler.info.method).toEqual('GET') expect(handler.info.path).toEqual('/user/:userId') expect(handler.isUsed).toBe(false) }) }) describe('parse', () => { it('parses a URL given a matching request', async () => { const handler = new HttpHandler('GET', '/user/:userId', resolver) const request = new Request(new URL('/user/abc-123', location.href)) expect(await handler.parse({ request })).toEqual({ match: { matches: true, params: { userId: 'abc-123', }, }, cookies: {}, }) }) it('parses a URL and ignores the request method', async () => { const handler = new HttpHandler('GET', '/user/:userId', resolver) const request = new Request(new URL('/user/def-456', location.href), { method: 'POST', }) expect(await handler.parse({ request })).toEqual({ match: { matches: true, params: { userId: 'def-456', }, }, cookies: {}, }) }) it('returns negative match result given a non-matching request', async () => { const handler = new HttpHandler('GET', '/user/:userId', resolver) const request = new Request(new URL('/login', location.href)) expect(await handler.parse({ request })).toEqual({ match: { matches: false, params: {}, }, cookies: {}, }) }) }) describe('predicate', () => { it('returns true given a matching request', async () => { const handler = new HttpHandler('POST', '/login', resolver) const request = new Request(new URL('/login', location.href), { method: 'POST', }) await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(true) }) it('supports RegExp as the request method', async () => { const handler = new HttpHandler(/.+/, '/login', resolver) const requests = [ new Request(new URL('/login', location.href)), new Request(new URL('/login', location.href), { method: 'POST' }), new Request(new URL('/login', location.href), { method: 'DELETE' }), ] for (const request of requests) { await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(true) } }) it('returns false given a non-matching request', async () => { const handler = new HttpHandler('POST', '/login', resolver) const request = new Request(new URL('/user/abc-123', location.href)) await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(false) }) it('supports custom predicate function', async () => { const handler = new HttpHandler( 'GET', ({ request }) => { return new URL(request.url).searchParams.get('a') === '1' }, resolver, ) { const request = new Request(new URL('/login?a=1', location.href)) await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(true) } { const request = new Request(new URL('/login', location.href)) await expect( handler.predicate({ request, parsedResult: await handler.parse({ request }), }), ).resolves.toBe(false) } }) }) describe('test', () => { it('returns true given a matching request', async () => { const handler = new HttpHandler('GET', '/user/:userId', resolver) const firstTest = await handler.test({ request: new Request(new URL('/user/abc-123', location.href)), }) const secondTest = await handler.test({ request: new Request(new URL('/user/def-456', location.href)), }) expect(firstTest).toBe(true) expect(secondTest).toBe(true) }) it('returns false given a non-matching request', async () => { const handler = new HttpHandler('GET', '/user/:userId', resolver) const firstTest = await handler.test({ request: new Request(new URL('/login', location.href)), }) const secondTest = await handler.test({ request: new Request(new URL('/user/', location.href)), }) const thirdTest = await handler.test({ request: new Request(new URL('/user/abc-123/extra', location.href)), }) expect(firstTest).toBe(false) expect(secondTest).toBe(false) expect(thirdTest).toBe(false) }) }) describe('run', () => { it('returns a mocked response given a matching request', async () => { const handler = new HttpHandler('GET', '/user/:userId', resolver) const request = new Request(new URL('/user/abc-123', location.href)) const requestId = createRequestId() const result = await handler.run({ request, requestId }) expect(result!.handler).toEqual(handler) expect(result!.parsedResult).toEqual({ match: { matches: true, params: { userId: 'abc-123', }, }, cookies: {}, }) expect(result!.request.method).toBe('GET') expect(result!.request.url).toBe('http://localhost/user/abc-123') expect(result!.response?.status).toBe(200) expect(result!.response?.statusText).toBe('OK') await expect(result?.response?.json()).resolves.toEqual({ userId: 'abc-123', }) }) it('returns null given a non-matching request', async () => { const handler = new HttpHandler('POST', '/login', resolver) const result = await handler.run({ request: new Request(new URL('/users', location.href)), requestId: createRequestId(), }) expect(result).toBeNull() }) it('returns an empty "params" object given request with no URL parameters', async () => { const handler = new HttpHandler('GET', '/users', resolver) const result = await handler.run({ request: new Request(new URL('/users', location.href)), requestId: createRequestId(), }) expect(result?.parsedResult?.match?.params).toEqual({}) }) it('exhausts resolver until its generator completes', async () => { const handler = new HttpHandler('GET', '/users', function* () { let count = 0 while (count < 5) { count += 1 yield HttpResponse.text('pending') } return HttpResponse.text('complete') }) const run = async () => { const result = await handler.run({ request: new Request(new URL('/users', location.href)), requestId: createRequestId(), }) return result?.response?.text() } await expect(run()).resolves.toBe('pending') await expect(run()).resolves.toBe('pending') await expect(run()).resolves.toBe('pending') await expect(run()).resolves.toBe('pending') await expect(run()).resolves.toBe('pending') await expect(run()).resolves.toBe('complete') await expect(run()).resolves.toBe('complete') }) }) ================================================ FILE: src/core/handlers/HttpHandler.ts ================================================ import { ResponseResolutionContext } from '../utils/executeHandlers' import { devUtils } from '../utils/internal/devUtils' import { isStringEqual } from '../utils/internal/isStringEqual' import { getStatusCodeColor } from '../utils/logging/getStatusCodeColor' import { getTimestamp } from '../utils/logging/getTimestamp' import { serializeRequest } from '../utils/logging/serializeRequest' import { serializeResponse } from '../utils/logging/serializeResponse' import { matchRequestUrl, Match, Path, PathParams, } from '../utils/matching/matchRequestUrl' import { toPublicUrl } from '../utils/request/toPublicUrl' import { getAllRequestCookies } from '../utils/request/getRequestCookies' import { cleanUrl } from '../utils/url/cleanUrl' import { RequestHandler, RequestHandlerDefaultInfo, RequestHandlerOptions, ResponseResolver, } from './RequestHandler' export type HttpHandlerMethod = string | RegExp export interface HttpHandlerInfo extends RequestHandlerDefaultInfo { method: HttpHandlerMethod path: HttpRequestPredicate } export enum HttpMethods { HEAD = 'HEAD', GET = 'GET', POST = 'POST', PUT = 'PUT', PATCH = 'PATCH', OPTIONS = 'OPTIONS', DELETE = 'DELETE', } export type RequestQuery = { [queryName: string]: string } export type HttpRequestParsedResult = { match: Match cookies: Record } export type HttpRequestResolverExtras = { params: Params cookies: Record } export type HttpCustomPredicate = (args: { request: Request cookies: Record }) => | HttpCustomPredicateResult | Promise> export type HttpCustomPredicateResult = | boolean | { matches: boolean params: Params } export type HttpRequestPredicate = | Path | HttpCustomPredicate /** * Request handler for HTTP requests. * Provides request matching based on method and URL. */ export class HttpHandler extends RequestHandler< HttpHandlerInfo, HttpRequestParsedResult, HttpRequestResolverExtras > { constructor( method: HttpHandlerMethod, predicate: HttpRequestPredicate, resolver: ResponseResolver, any, any>, options?: RequestHandlerOptions, ) { const displayPath = typeof predicate === 'function' ? '[custom predicate]' : predicate super({ info: { header: `${method}${displayPath ? ` ${displayPath}` : ''}`, path: predicate, method, }, resolver, options, }) this.checkRedundantQueryParameters() } private checkRedundantQueryParameters() { const { method, path } = this.info if (!path || path instanceof RegExp || typeof path === 'function') { return } const url = cleanUrl(path) // Bypass request handler URLs that have no redundant characters. if (url === path) { return } devUtils.warn( `Found a redundant usage of query parameters in the request handler URL for "${method} ${path}". Please match against a path instead and access query parameters using "new URL(request.url).searchParams" instead. Learn more: https://mswjs.io/docs/http/intercepting-requests#querysearch-parameters`, ) } async parse(args: { request: Request resolutionContext?: ResponseResolutionContext }) { const url = new URL(args.request.url) const cookies = getAllRequestCookies(args.request) /** * Handle custom predicate functions. * @note Invoke this during parsing so the user can parse the path parameters * manually. Otherwise, `params` is always an empty object, which isn't nice. */ if (typeof this.info.path === 'function') { const customPredicateResult = await this.info.path({ request: args.request, cookies, }) const match = typeof customPredicateResult === 'boolean' ? { matches: customPredicateResult, params: {}, } : customPredicateResult return { match, cookies, } } const match = this.info.path ? matchRequestUrl(url, this.info.path, args.resolutionContext?.baseUrl) : { matches: false, params: {} } return { match, cookies, } } async predicate(args: { request: Request parsedResult: HttpRequestParsedResult resolutionContext?: ResponseResolutionContext }) { const hasMatchingMethod = this.matchMethod(args.request.method) const hasMatchingUrl = args.parsedResult.match.matches return hasMatchingMethod && hasMatchingUrl } private matchMethod(actualMethod: string): boolean { return this.info.method instanceof RegExp ? this.info.method.test(actualMethod) : isStringEqual(this.info.method, actualMethod) } protected extendResolverArgs(args: { request: Request parsedResult: HttpRequestParsedResult }) { return { params: args.parsedResult.match?.params || {}, cookies: args.parsedResult.cookies, } } async log(args: { request: Request; response: Response }) { const publicUrl = toPublicUrl(args.request.url) const loggedRequest = await serializeRequest(args.request) const loggedResponse = await serializeResponse(args.response) const statusColor = getStatusCodeColor(loggedResponse.status) console.groupCollapsed( devUtils.formatMessage( `${getTimestamp()} ${args.request.method} ${publicUrl} (%c${ loggedResponse.status } ${loggedResponse.statusText}%c)`, ), `color:${statusColor}`, 'color:inherit', ) // eslint-disable-next-line no-console console.log('Request', loggedRequest) // eslint-disable-next-line no-console console.log('Handler:', this) // eslint-disable-next-line no-console console.log('Response', loggedResponse) console.groupEnd() } } ================================================ FILE: src/core/handlers/RequestHandler.ts ================================================ import { getCallFrame } from '../utils/internal/getCallFrame' import { AsyncIterable, Iterable, isIterable, } from '../utils/internal/isIterable' import type { ResponseResolutionContext } from '../utils/executeHandlers' import type { MaybePromise } from '../typeUtils' import { StrictRequest, HttpResponse, DefaultUnsafeFetchResponse, } from '../HttpResponse' import type { HandlerKind } from './common' import type { GraphQLRequestBody } from './GraphQLHandler' export type DefaultRequestMultipartBody = Record< string, string | File | Array > export type DefaultBodyType = | Record | DefaultRequestMultipartBody | string | number | boolean | null | undefined export type JsonBodyType = | Record | string | number | boolean | null | undefined export interface RequestHandlerDefaultInfo { header: string } export interface RequestHandlerInternalInfo { callFrame?: string } export type ResponseResolverReturnType< ResponseBodyType extends DefaultBodyType = undefined, > = // If ResponseBodyType is a union and one of the types is `undefined`, // allow plain Response as the type. | ([ResponseBodyType] extends [undefined] ? Response : /** * Treat GraphQL response body type as a special case. * For esome reason, making the default HttpResponse | DefaultUnsafeFetchResponse * union breaks the body type inference for HTTP requests. * @see https://github.com/mswjs/msw/issues/2130 */ ResponseBodyType extends GraphQLRequestBody ? HttpResponse | DefaultUnsafeFetchResponse : HttpResponse) | undefined | void export type MaybeAsyncResponseResolverReturnType< ResponseBodyType extends DefaultBodyType, > = MaybePromise> export type AsyncResponseResolverReturnType< ResponseBodyType extends DefaultBodyType, > = MaybePromise< | ResponseResolverReturnType | Iterable< MaybeAsyncResponseResolverReturnType, MaybeAsyncResponseResolverReturnType, MaybeAsyncResponseResolverReturnType > | AsyncIterable< MaybeAsyncResponseResolverReturnType, MaybeAsyncResponseResolverReturnType, MaybeAsyncResponseResolverReturnType > > export type ResponseResolverInfo< ResolverExtraInfo extends Record, RequestBodyType extends DefaultBodyType = DefaultBodyType, > = { request: StrictRequest requestId: string } & ResolverExtraInfo export type ResponseResolver< ResolverExtraInfo extends Record = Record, RequestBodyType extends DefaultBodyType = DefaultBodyType, ResponseBodyType extends DefaultBodyType = undefined, > = ( info: ResponseResolverInfo, ) => AsyncResponseResolverReturnType export interface RequestHandlerArgs< HandlerInfo, HandlerOptions extends RequestHandlerOptions, > { info: HandlerInfo resolver: ResponseResolver options?: HandlerOptions } export interface RequestHandlerOptions { once?: boolean } export interface RequestHandlerExecutionResult< ParsedResult extends object | undefined, > { handler: RequestHandler parsedResult?: ParsedResult request: Request requestId: string response?: Response } export abstract class RequestHandler< HandlerInfo extends RequestHandlerDefaultInfo = RequestHandlerDefaultInfo, ParsedResult extends Record | undefined = any, ResolverExtras extends Record = any, HandlerOptions extends RequestHandlerOptions = RequestHandlerOptions, > { static cache = new WeakMap< StrictRequest, StrictRequest >() private readonly __kind: HandlerKind public info: HandlerInfo & RequestHandlerInternalInfo /** * Indicates whether this request handler has been used * (its resolver has successfully executed). */ public isUsed: boolean protected resolver: ResponseResolver private resolverIterator?: | Iterator< MaybeAsyncResponseResolverReturnType, MaybeAsyncResponseResolverReturnType, MaybeAsyncResponseResolverReturnType > | AsyncIterator< MaybeAsyncResponseResolverReturnType, MaybeAsyncResponseResolverReturnType, MaybeAsyncResponseResolverReturnType > private resolverIteratorResult?: Response | HttpResponse private options?: HandlerOptions constructor(args: RequestHandlerArgs) { this.resolver = args.resolver this.options = args.options const callFrame = getCallFrame(new Error()) this.info = { ...args.info, callFrame, } this.isUsed = false this.__kind = 'RequestHandler' } /** * Determine if the intercepted request should be mocked. */ abstract predicate(args: { request: Request parsedResult: ParsedResult resolutionContext?: ResponseResolutionContext }): boolean | Promise /** * Print out the successfully handled request. */ abstract log(args: { request: Request response: Response parsedResult: ParsedResult }): void /** * Parse the intercepted request to extract additional information from it. * Parsed result is then exposed to other methods of this request handler. */ async parse(_args: { request: Request resolutionContext?: ResponseResolutionContext }): Promise { return {} as ParsedResult } /** * Test if this handler matches the given request. * * This method is not used internally but is exposed * as a convenience method for consumers writing custom * handlers. */ public async test(args: { request: Request resolutionContext?: ResponseResolutionContext }): Promise { const parsedResult = await this.parse({ request: args.request, resolutionContext: args.resolutionContext, }) return this.predicate({ request: args.request, parsedResult, resolutionContext: args.resolutionContext, }) } protected extendResolverArgs(_args: { request: Request parsedResult: ParsedResult }): ResolverExtras { return {} as ResolverExtras } // Clone the request instance before it's passed to the handler phases // and the response resolver so we can always read it for logging. // We only clone it once per request to avoid unnecessary overhead. private cloneRequestOrGetFromCache( request: StrictRequest, ): StrictRequest { const existingClone = RequestHandler.cache.get(request) if (typeof existingClone !== 'undefined') { return existingClone } const clonedRequest = request.clone() RequestHandler.cache.set(request, clonedRequest) return clonedRequest } /** * Execute this request handler and produce a mocked response * using the given resolver function. */ public async run(args: { request: StrictRequest requestId: string resolutionContext?: ResponseResolutionContext }): Promise | null> { if (this.isUsed && this.options?.once) { return null } // Clone the request. // If this is the first time MSW handles this request, a fresh clone // will be created and cached. Upon further handling of the same request, // the request clone from the cache will be reused to prevent abundant // "abort" listeners and save up resources on cloning. const requestClone = this.cloneRequestOrGetFromCache(args.request) const parsedResult = await this.parse({ request: args.request, resolutionContext: args.resolutionContext, }) const shouldInterceptRequest = await this.predicate({ request: args.request, parsedResult, resolutionContext: args.resolutionContext, }) if (!shouldInterceptRequest) { return null } // Re-check isUsed, in case another request hit this handler while we were // asynchronously parsing the request. if (this.isUsed && this.options?.once) { return null } // Preemptively mark the handler as used. // Generators will undo this because only when the resolver reaches the // "done" state of the generator that it considers the handler used. this.isUsed = true // Create a response extraction wrapper around the resolver // since it can be both an async function and a generator. const executeResolver = this.wrapResolver(this.resolver) const resolverExtras = this.extendResolverArgs({ request: args.request, parsedResult, }) const mockedResponsePromise = ( executeResolver({ ...resolverExtras, requestId: args.requestId, request: args.request, }) as Promise ).catch((errorOrResponse) => { // Allow throwing a Response instance in a response resolver. if (errorOrResponse instanceof Response) { return errorOrResponse } // Otherwise, throw the error as-is. throw errorOrResponse }) const mockedResponse = await mockedResponsePromise const executionResult = this.createExecutionResult({ // Pass the cloned request to the result so that logging // and other consumers could read its body once more. request: requestClone, requestId: args.requestId, response: mockedResponse, parsedResult, }) return executionResult } private wrapResolver( resolver: ResponseResolver, ): ResponseResolver { return async (info): Promise> => { if (!this.resolverIterator) { const result = await resolver(info) if (!isIterable(result)) { return result } this.resolverIterator = Symbol.iterator in result ? result[Symbol.iterator]() : result[Symbol.asyncIterator]() } // Opt-out from marking this handler as used. this.isUsed = false const { done, value } = await this.resolverIterator.next() const nextResponse = await value if (nextResponse) { this.resolverIteratorResult = nextResponse.clone() } if (done) { // A one-time generator resolver stops affecting the network // only after it's been completely exhausted. this.isUsed = true // Clone the previously stored response so it can be read // when receiving it repeatedly from the "done" generator. return this.resolverIteratorResult?.clone() } return nextResponse } } private createExecutionResult(args: { request: Request requestId: string parsedResult: ParsedResult response?: Response }): RequestHandlerExecutionResult { return { handler: this, request: args.request, requestId: args.requestId, response: args.response, parsedResult: args.parsedResult, } } } ================================================ FILE: src/core/handlers/WebSocketHandler.test.ts ================================================ import { WebSocketHandler } from './WebSocketHandler' describe('parse', () => { it('matches an exact url', () => { expect( new WebSocketHandler('ws://localhost:3000').parse({ url: new URL('ws://localhost:3000'), }), ).toEqual({ match: { matches: true, params: {}, }, }) }) it('ignores trailing slash', () => { expect( new WebSocketHandler('ws://localhost:3000').parse({ url: new URL('ws://localhost:3000/'), }), ).toEqual({ match: { matches: true, params: {}, }, }) expect( new WebSocketHandler('ws://localhost:3000/').parse({ url: new URL('ws://localhost:3000/'), }), ).toEqual({ match: { matches: true, params: {}, }, }) }) it('supports path parameters', () => { expect( new WebSocketHandler('ws://localhost:3000/:serviceName').parse({ url: new URL('ws://localhost:3000/auth'), }), ).toEqual({ match: { matches: true, params: { serviceName: 'auth', }, }, }) }) it('ignores "/socket.io/" prefix in the client url', () => { expect( new WebSocketHandler('ws://localhost:3000').parse({ url: new URL( 'ws://localhost:3000/socket.io/?EIO=4&transport=websocket', ), }), ).toEqual({ match: { matches: true, params: {}, }, }) expect( new WebSocketHandler('ws://localhost:3000/non-matching').parse({ url: new URL( 'ws://localhost:3000/socket.io/?EIO=4&transport=websocket', ), }), ).toEqual({ match: { matches: false, params: {}, }, }) }) it('preserves non-prefix "/socket.io/" path segment', () => { /** * @note It is highly unlikely but we still shouldn't modify the * WebSocket client URL if it contains a user-defined "socket.io" segment. */ expect( new WebSocketHandler('ws://localhost:3000/clients/socket.io/123').parse({ url: new URL('ws://localhost:3000/clients/socket.io/123'), }), ).toEqual({ match: { matches: true, params: {}, }, }) expect( new WebSocketHandler('ws://localhost:3000').parse({ url: new URL('ws://localhost:3000/clients/socket.io/123'), }), ).toEqual({ match: { matches: false, params: {}, }, }) }) it('supports a custom resolution context (base url)', () => { expect( new WebSocketHandler('/api/ws').parse({ url: new URL('ws://localhost:3000/api/ws'), resolutionContext: { baseUrl: 'ws://localhost:3000/', }, }), ).toEqual({ match: { matches: true, params: {}, }, }) }) }) ================================================ FILE: src/core/handlers/WebSocketHandler.ts ================================================ import { Emitter } from 'strict-event-emitter' import { createRequestId, resolveWebSocketUrl } from '@mswjs/interceptors' import type { WebSocketClientConnectionProtocol, WebSocketConnectionData, WebSocketServerConnectionProtocol, } from '@mswjs/interceptors/WebSocket' import { type Match, type Path, type PathParams, matchRequestUrl, } from '../utils/matching/matchRequestUrl' import { getCallFrame } from '../utils/internal/getCallFrame' import type { HandlerKind } from './common' type WebSocketHandlerParsedResult = { match: Match } export type WebSocketHandlerEventMap = { connection: [args: WebSocketHandlerConnection] } export interface WebSocketHandlerConnection { client: WebSocketClientConnectionProtocol server: WebSocketServerConnectionProtocol info: WebSocketConnectionData['info'] params: PathParams } export interface WebSocketResolutionContext { baseUrl?: string } export const kEmitter = Symbol('kEmitter') export const kSender = Symbol('kSender') const kStopPropagationPatched = Symbol('kStopPropagationPatched') const KOnStopPropagation = Symbol('KOnStopPropagation') export class WebSocketHandler { private readonly __kind: HandlerKind public id: string public callFrame?: string protected [kEmitter]: Emitter constructor(protected readonly url: Path) { this.id = createRequestId() this[kEmitter] = new Emitter() this.callFrame = getCallFrame(new Error()) this.__kind = 'EventHandler' } public parse(args: { url: URL resolutionContext?: WebSocketResolutionContext }): WebSocketHandlerParsedResult { const clientUrl = new URL(args.url) // Resolve the WebSocket handler path: // - Plain string URLs resolved as per the specification (via Interceptors). // - String URLs starting with a wildcard are preserved (prepending a scheme there will break them). // - RegExp paths are preserved. const resolvedHandlerUrl = this.url instanceof RegExp || this.url.startsWith('*') ? this.url : this.#resolveWebSocketUrl(this.url, args.resolutionContext?.baseUrl) /** * @note Remove the Socket.IO path prefix from the WebSocket * client URL. This is an exception to keep the users from * including the implementation details in their handlers. */ clientUrl.pathname = clientUrl.pathname.replace(/^\/socket.io\//, '/') const match = matchRequestUrl( clientUrl, resolvedHandlerUrl, args.resolutionContext?.baseUrl, ) return { match, } } public predicate(args: { url: URL parsedResult: WebSocketHandlerParsedResult }): boolean { return args.parsedResult.match.matches } public async run( connection: Omit, resolutionContext?: WebSocketResolutionContext, ): Promise { const parsedResult = this.parse({ url: connection.client.url, resolutionContext, }) if (!this.predicate({ url: connection.client.url, parsedResult })) { return false } const resolvedConnection: WebSocketHandlerConnection = { ...connection, params: parsedResult.match.params || {}, } return this.connect(resolvedConnection) } protected connect(connection: WebSocketHandlerConnection): boolean { // Support `event.stopPropagation()` for various client/server events. connection.client.addEventListener( 'message', createStopPropagationListener(this), ) connection.client.addEventListener( 'close', createStopPropagationListener(this), ) connection.server.addEventListener( 'open', createStopPropagationListener(this), ) connection.server.addEventListener( 'message', createStopPropagationListener(this), ) connection.server.addEventListener( 'error', createStopPropagationListener(this), ) connection.server.addEventListener( 'close', createStopPropagationListener(this), ) // Emit the connection event on the handler. // This is what the developer adds listeners for. return this[kEmitter].emit('connection', connection) } #resolveWebSocketUrl(url: string, baseUrl?: string): string { const resolvedUrl = resolveWebSocketUrl( baseUrl ? /** * @note Resolve against the base URL preemtively because `resolveWebSocketUrl` only * resolves against `location.href`, which is missing in Node.js. Base URL allows * the handler to accept a relative URL in Node.js. */ new URL(url, baseUrl) : url, ) /** * @note Omit the trailing slash. * While the browser always produces a trailing slash at the end of a WebSocket URL, * having it in as the handler's predicate would mean it is *required* in the actual URL. */ return resolvedUrl.replace(/\/$/, '') } } function createStopPropagationListener(handler: WebSocketHandler) { return function stopPropagationListener(event: Event) { const propagationStoppedAt = Reflect.get(event, 'kPropagationStoppedAt') as | string | undefined if (propagationStoppedAt && handler.id !== propagationStoppedAt) { event.stopImmediatePropagation() return } Object.defineProperty(event, KOnStopPropagation, { value(this: WebSocketHandler) { Object.defineProperty(event, 'kPropagationStoppedAt', { value: handler.id, }) }, configurable: true, }) // Since the same event instance is shared between all client/server objects, // make sure to patch its `stopPropagation` method only once. if (!Reflect.get(event, kStopPropagationPatched)) { event.stopPropagation = new Proxy(event.stopPropagation, { apply: (target, thisArg, args) => { Reflect.get(event, KOnStopPropagation)?.call(handler) return Reflect.apply(target, thisArg, args) }, }) Object.defineProperty(event, kStopPropagationPatched, { value: true, // If something else attempts to redefine this, throw. configurable: false, }) } } } ================================================ FILE: src/core/handlers/common.ts ================================================ export type HandlerKind = 'RequestHandler' | 'EventHandler' ================================================ FILE: src/core/http.test.ts ================================================ import { http } from './http' test('exports all REST API methods', () => { expect(http).toBeDefined() expect(Object.keys(http)).toEqual([ 'all', 'head', 'get', 'post', 'put', 'delete', 'patch', 'options', ]) }) ================================================ FILE: src/core/http.ts ================================================ import { DefaultBodyType, RequestHandlerOptions, ResponseResolver, } from './handlers/RequestHandler' import { HttpMethods, HttpHandler, HttpRequestResolverExtras, HttpRequestPredicate, } from './handlers/HttpHandler' import type { PathParams } from './utils/matching/matchRequestUrl' export type HttpRequestHandler = < Params extends PathParams = PathParams, RequestBodyType extends DefaultBodyType = DefaultBodyType, // Response body type MUST be undefined by default. // This is how we can distinguish between a handler that // returns plain "Response" and the one returning "HttpResponse" // to enforce a stricter response body type. ResponseBodyType extends DefaultBodyType = undefined, >( predicate: HttpRequestPredicate, resolver: HttpResponseResolver, options?: RequestHandlerOptions, ) => HttpHandler export type HttpResponseResolver< Params extends PathParams = PathParams, RequestBodyType extends DefaultBodyType = DefaultBodyType, ResponseBodyType extends DefaultBodyType = DefaultBodyType, > = ResponseResolver< HttpRequestResolverExtras, RequestBodyType, ResponseBodyType > function createHttpHandler( method: Method, ): HttpRequestHandler { return (predicate, resolver, options = {}) => { return new HttpHandler(method, predicate, resolver, options) } } /** * A namespace to intercept and mock HTTP requests. * * @example * http.get('/user', resolver) * http.post('/post/:id', resolver) * * @see {@link https://mswjs.io/docs/api/http `http` API reference} */ export const http = { all: createHttpHandler(/.+/), head: createHttpHandler(HttpMethods.HEAD), get: createHttpHandler(HttpMethods.GET), post: createHttpHandler(HttpMethods.POST), put: createHttpHandler(HttpMethods.PUT), delete: createHttpHandler(HttpMethods.DELETE), patch: createHttpHandler(HttpMethods.PATCH), options: createHttpHandler(HttpMethods.OPTIONS), } ================================================ FILE: src/core/index.ts ================================================ import { checkGlobals } from './utils/internal/checkGlobals' export { SetupApi } from './SetupApi' /* HTTP handlers */ export { RequestHandler } from './handlers/RequestHandler' export { http } from './http' export { HttpHandler, HttpMethods } from './handlers/HttpHandler' export { graphql } from './graphql' export { GraphQLHandler } from './handlers/GraphQLHandler' /* WebSocket handler */ export { ws, type WebSocketLink } from './ws' export { WebSocketHandler, type WebSocketHandlerEventMap, type WebSocketHandlerConnection, } from './handlers/WebSocketHandler' /* Server-Sent Events */ export { sse, type ServerSentEventRequestHandler, type ServerSentEventResolver, type ServerSentEventResolverExtras, type ServerSentEventMessage, } from './sse' import type { HttpHandler } from './handlers/HttpHandler' import type { GraphQLHandler } from './handlers/GraphQLHandler' import type { WebSocketHandler } from './handlers/WebSocketHandler' export type AnyHandler = HttpHandler | GraphQLHandler | WebSocketHandler /* Utils */ export { matchRequestUrl } from './utils/matching/matchRequestUrl' export { handleRequest, type HandleRequestOptions } from './utils/handleRequest' export { onUnhandledRequest, type UnhandledRequestStrategy, type UnhandledRequestCallback, } from './utils/request/onUnhandledRequest' export { getResponse } from './getResponse' export { cleanUrl } from './utils/url/cleanUrl' /** * Type definitions. */ export type { SharedOptions, LifeCycleEventsMap } from './sharedOptions' export type { ResponseResolver, ResponseResolverReturnType, AsyncResponseResolverReturnType, RequestHandlerOptions, DefaultBodyType, DefaultRequestMultipartBody, JsonBodyType, ResponseResolverInfo, } from './handlers/RequestHandler' export type { RequestQuery, HttpRequestParsedResult, HttpHandlerInfo, HttpRequestResolverExtras, HttpHandlerMethod, HttpCustomPredicate, } from './handlers/HttpHandler' export type { HttpRequestHandler, HttpResponseResolver } from './http' export type { GraphQLQuery, GraphQLVariables, GraphQLRequestBody, GraphQLResponseBody, GraphQLJsonRequestBody, GraphQLOperationType, GraphQLCustomPredicate, } from './handlers/GraphQLHandler' export type { GraphQLRequestHandler, GraphQLOperationHandler, GraphQLResponseResolver, GraphQLLinkHandlers, } from './graphql' export type { WebSocketData, WebSocketEventListener } from './ws' export type { Path, PathParams, Match } from './utils/matching/matchRequestUrl' export type { ParsedGraphQLRequest } from './utils/internal/parseGraphQLRequest' export type { ResponseResolutionContext } from './utils/executeHandlers' export { HttpResponse, type HttpResponseInit, type StrictRequest, type StrictResponse, } from './HttpResponse' export { delay, type DelayMode } from './delay' export { bypass } from './bypass' export { passthrough } from './passthrough' export { isCommonAssetRequest } from './isCommonAssetRequest' // Validate environmental globals before executing any code. // This ensures that the library gives user-friendly errors // when ran in the environments that require additional polyfills // from the end user. checkGlobals() ================================================ FILE: src/core/isCommonAssetRequest.ts ================================================ /** * Determines if the given request is a static asset request. * Useful when deciding which unhandled requests to ignore. * @note Despite being ignored, you can still intercept and mock * static assets by creating request handlers for them. * * @example * import { isCommonAssetRequest } from 'msw' * * await worker.start({ * onUnhandledRequest(request, print) { * if (!isCommonAssetRequest(request)) { * print.warning() * } * } * }) */ export function isCommonAssetRequest(request: Request): boolean { const url = new URL(request.url) // Ignore certain protocols. if (url.protocol === 'file:') { return true } // Ignore static assets hosts. if (/(fonts\.googleapis\.com)/.test(url.hostname)) { return true } // Ignore node modules served over HTTP. if (/node_modules/.test(url.pathname)) { return true } // Ignore internal Vite requests, like "/@vite/client". if (url.pathname.includes('@vite')) { return true } // Ignore common static assets. return /\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff|woff2|eot|gif|jpe?g|png|avif|webp|svg|mp4|webm|ogg|mov|mp3|wav|ogg|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test( url.pathname, ) } ================================================ FILE: src/core/passthrough.test.ts ================================================ // @vitest-environment node import { passthrough } from './passthrough' it('creates a 302 response with the intention header', () => { const response = passthrough() expect(response).toBeInstanceOf(Response) expect(response.status).toBe(302) expect(response.statusText).toBe('Passthrough') expect(response.headers.get('x-msw-intention')).toBe('passthrough') }) ================================================ FILE: src/core/passthrough.ts ================================================ import type { HttpResponse } from './HttpResponse' /** * Performs the intercepted request as-is. * * This stops request handler lookup so no other handlers * can affect this request past this point. * Unlike `bypass()`, this will not trigger an additional request. * * @example * http.get('/resource', () => { * return passthrough() * }) * * @see {@link https://mswjs.io/docs/api/passthrough `passthrough()` API reference} */ export function passthrough(): HttpResponse { return new Response(null, { status: 302, statusText: 'Passthrough', headers: { 'x-msw-intention': 'passthrough', }, }) as HttpResponse } ================================================ FILE: src/core/sharedOptions.ts ================================================ import type { Emitter } from 'strict-event-emitter' import type { UnhandledRequestStrategy } from './utils/request/onUnhandledRequest' export interface SharedOptions { /** * Specifies how to react to a request that has no corresponding * request handler. Warns on unhandled requests by default. * * @example worker.start({ onUnhandledRequest: 'bypass' }) * @example worker.start({ onUnhandledRequest: 'warn' }) * @example server.listen({ onUnhandledRequest: 'error' }) */ onUnhandledRequest?: UnhandledRequestStrategy } export type LifeCycleEventsMap = { 'request:start': [ args: { request: Request requestId: string }, ] 'request:match': [ args: { request: Request requestId: string }, ] 'request:unhandled': [ args: { request: Request requestId: string }, ] 'request:end': [ args: { request: Request requestId: string }, ] 'response:mocked': [ args: { response: Response request: Request requestId: string }, ] 'response:bypass': [ args: { response: Response request: Request requestId: string }, ] unhandledException: [ args: { error: Error request: Request requestId: string }, ] } export type LifeCycleEventEmitter< EventsMap extends Record, > = Pick, 'on' | 'removeListener' | 'removeAllListeners'> ================================================ FILE: src/core/sse.ts ================================================ import { invariant } from 'outvariant' import { Emitter } from 'strict-event-emitter' import type { ResponseResolver } from './handlers/RequestHandler' import { HttpHandler, type HttpRequestResolverExtras, type HttpRequestParsedResult, } from './handlers/HttpHandler' import type { ResponseResolutionContext } from '~/core/utils/executeHandlers' import type { Path, PathParams } from './utils/matching/matchRequestUrl' import { delay } from './delay' import { getTimestamp } from './utils/logging/getTimestamp' import { devUtils } from './utils/internal/devUtils' import { colors } from './ws/utils/attachWebSocketLogger' import { toPublicUrl } from './utils/request/toPublicUrl' type EventMapConstraint = { message?: unknown [key: string]: unknown [key: symbol | number]: never } export type ServerSentEventResolverExtras< EventMap extends EventMapConstraint, Params extends PathParams, > = HttpRequestResolverExtras & { client: ServerSentEventClient server: ServerSentEventServer } export type ServerSentEventResolver< EventMap extends EventMapConstraint, Params extends PathParams, > = ResponseResolver, any, any> export type ServerSentEventRequestHandler = < EventMap extends EventMapConstraint = { message: unknown }, Params extends PathParams = PathParams, RequestPath extends Path = Path, >( path: RequestPath, resolver: ServerSentEventResolver, ) => HttpHandler export type ServerSentEventMessage< EventMap extends EventMapConstraint = { message: unknown }, > = | ToEventDiscriminatedUnion | { id?: never event?: never data?: never retry: number } /** * Intercept Server-Sent Events (SSE). * * @example * sse('http://localhost:4321', ({ client }) => { * client.send({ data: 'hello world' }) * }) * * @see {@link https://mswjs.io/docs/sse/ Mocking Server-Sent Events} * @see {@link https://mswjs.io/docs/api/sse `sse()` API reference} */ export const sse: ServerSentEventRequestHandler = (path, resolver) => { return new ServerSentEventHandler(path, resolver) } const SSE_RESPONSE_INIT: ResponseInit = { headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }, } class ServerSentEventHandler< EventMap extends EventMapConstraint, > extends HttpHandler { #emitter: Emitter constructor(path: Path, resolver: ServerSentEventResolver) { invariant( typeof EventSource !== 'undefined', 'Failed to construct a Server-Sent Event handler for path "%s": the EventSource API is not supported in this environment', path, ) super('GET', path, async (info) => { const stream = new ReadableStream({ start: async (controller) => { const client = new ServerSentEventClient({ controller, emitter: this.#emitter, }) const server = new ServerSentEventServer({ request: info.request, client, }) await resolver({ ...info, client, server, }) }, }) return new Response(stream, SSE_RESPONSE_INIT) }) this.#emitter = new Emitter() } async predicate(args: { request: Request parsedResult: HttpRequestParsedResult resolutionContext?: ResponseResolutionContext }) { if (args.request.headers.get('accept') !== 'text/event-stream') { return false } const matches = await super.predicate(args) if (matches && !args.resolutionContext?.quiet) { /** * @note Log the intercepted request early. * Normally, the `this.log()` method is called when the handler returns a response. * For SSE, call that method earlier so the logs are in correct order. */ await super.log({ request: args.request, /** * @note Construct a placeholder response since SSE response * is being streamed and cannot be cloned/consumed for logging. */ response: new Response('[streaming]', SSE_RESPONSE_INIT), }) this.#attachClientLogger(args.request, this.#emitter) } return matches } async log(_args: { request: Request; response: Response }): Promise { /** * @note Skip the default `this.log()` logic so that when this handler is logged * upon handling the request, nothing is printed (we log SSE requests early). */ return } #attachClientLogger( request: Request, emitter: Emitter, ): void { const publicUrl = toPublicUrl(request.url) /* eslint-disable no-console */ emitter.on('message', (payload) => { console.groupCollapsed( devUtils.formatMessage( `${getTimestamp()} SSE %s %c⇣%c ${payload.event}`, ), publicUrl, `color:${colors.mocked}`, 'color:inherit', ) console.log(payload.frames) console.groupEnd() }) emitter.on('error', () => { console.groupCollapsed( devUtils.formatMessage(`${getTimestamp()} SSE %s %c\u00D7%c error`), publicUrl, `color: ${colors.system}`, 'color:inherit', ) console.log('Handler:', this) console.groupEnd() }) emitter.on('close', () => { console.groupCollapsed( devUtils.formatMessage(`${getTimestamp()} SSE %s %c■%c close`), publicUrl, `colors:${colors.system}`, 'color:inherit', ) console.log('Handler:', this) console.groupEnd() }) /* eslint-enable no-console */ } } type Values = T[keyof T] type Identity = { [K in keyof T]: T[K] } & unknown type ToEventDiscriminatedUnion = Values<{ [K in keyof T]: Identity< (K extends 'message' ? { id?: string event?: K data?: T[K] retry?: never } : { id?: string event: K data?: T[K] retry?: never }) & // Make the `data` field conditionally required through an intersection. (undefined extends T[K] ? unknown : { data: unknown }) > }> type ServerSentEventClientEventMap = { message: [ payload: { id?: string event: string data?: unknown frames: Array }, ] error: [] close: [] } class ServerSentEventClient< EventMap extends EventMapConstraint = { message: unknown }, > { #encoder: TextEncoder #controller: ReadableStreamDefaultController #emitter: Emitter constructor(args: { controller: ReadableStreamDefaultController emitter: Emitter }) { this.#encoder = new TextEncoder() this.#controller = args.controller this.#emitter = args.emitter } /** * Sends the given payload to the intercepted `EventSource`. */ public send(payload: ServerSentEventMessage): void { if ('retry' in payload && payload.retry != null) { this.#sendRetry(payload.retry) return } this.#sendMessage({ id: payload.id, event: payload.event, data: typeof payload.data === 'object' ? JSON.stringify(payload.data) : payload.data, }) } /** * Dispatches the given event on the intercepted `EventSource`. */ public dispatchEvent(event: Event) { if (event instanceof MessageEvent) { /** * @note Use the internal send mechanism to skip normalization * of the message data (already normalized by the server). */ this.#sendMessage({ id: event.lastEventId || undefined, event: event.type === 'message' ? undefined : event.type, data: event.data, }) return } if (event.type === 'error') { this.error() return } if (event.type === 'close') { this.close() return } } /** * Errors the underlying `EventSource`, closing the connection with an error. * This is equivalent to aborting the connection and will produce a `TypeError: Failed to fetch` * error. */ public error(): void { this.#controller.error() this.#emitter.emit('error') } /** * Closes the underlying `EventSource`, closing the connection. */ public close(): void { this.#controller.close() this.#emitter.emit('close') } #sendRetry(retry: number): void { if (typeof retry === 'number') { this.#controller.enqueue(this.#encoder.encode(`retry:${retry}\n\n`)) } } #sendMessage(message: { id?: string event?: unknown data: unknown | undefined }): void { const frames: Array = [] if (message.id) { frames.push(`id:${message.id}`) } if (message.event) { frames.push(`event:${message.event.toString()}`) } if (message.data != null) { /** * Split data on line terminators (LF, CR, or CRLF) and translate them to individual frames. * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream */ for (const line of message.data.toString().split(/\r\n|\r|\n/)) { frames.push(`data:${line}`) } } frames.push('', '') this.#controller.enqueue(this.#encoder.encode(frames.join('\n'))) this.#emitter.emit('message', { id: message.id, event: message.event?.toString() || 'message', data: message.data, frames, }) } } class ServerSentEventServer { #request: Request #client: ServerSentEventClient constructor(args: { request: Request; client: ServerSentEventClient }) { this.#request = args.request this.#client = args.client } /** * Establishes the actual connection for this SSE request * and returns the `EventSource` instance. */ public connect(): EventSource { const source = new ObservableEventSource(this.#request.url, { withCredentials: this.#request.credentials === 'include', headers: { /** * @note Mark this request as passthrough so it doesn't trigger * an infinite loop matching against the existing request handler. */ accept: 'msw/passthrough', }, }) source[kOnAnyMessage] = (event) => { Object.defineProperties(event, { target: { value: this, enumerable: true, writable: true, configurable: true, }, }) // Schedule the server-to-client forwarding for the next tick // so the user can prevent the message event. queueMicrotask(() => { if (!event.defaultPrevented) { this.#client.dispatchEvent(event) } }) } // Forward stream errors from the actual server to the client. source.addEventListener('error', (event) => { Object.defineProperties(event, { target: { value: this, enumerable: true, writable: true, configurable: true, }, }) queueMicrotask(() => { // Allow the user to opt-out from this forwarding. if (!event.defaultPrevented) { this.#client.dispatchEvent(event) } }) }) return source } } interface ObservableEventSourceInit extends EventSourceInit { headers?: HeadersInit } type EventHandler = ( this: EventSource, event: EventType, ) => any const kRequest = Symbol('kRequest') const kReconnectionTime = Symbol('kReconnectionTime') const kLastEventId = Symbol('kLastEventId') const kAbortController = Symbol('kAbortController') const kOnOpen = Symbol('kOnOpen') const kOnMessage = Symbol('kOnMessage') const kOnAnyMessage = Symbol('kOnAnyMessage') const kOnError = Symbol('kOnError') class ObservableEventSource extends EventTarget implements EventSource { static readonly CONNECTING = 0 static readonly OPEN = 1 static readonly CLOSED = 2 public readonly CONNECTING = ObservableEventSource.CONNECTING public readonly OPEN = ObservableEventSource.OPEN public readonly CLOSED = ObservableEventSource.CLOSED public readyState: number public url: string public withCredentials: boolean private [kRequest]: Request private [kReconnectionTime]: number private [kLastEventId]: string private [kAbortController]: AbortController private [kOnOpen]: EventHandler | null = null private [kOnMessage]: EventHandler | null = null private [kOnAnyMessage]: EventHandler | null = null private [kOnError]: EventHandler | null = null constructor(url: string | URL, init?: ObservableEventSourceInit) { super() this.url = new URL(url).href this.withCredentials = init?.withCredentials ?? false this.readyState = this.CONNECTING // Support custom request init. const headers = new Headers(init?.headers || {}) headers.append('accept', 'text/event-stream') this[kAbortController] = new AbortController() this[kReconnectionTime] = 2000 this[kLastEventId] = '' this[kRequest] = new Request(this.url, { method: 'GET', headers, credentials: this.withCredentials ? 'include' : 'omit', signal: this[kAbortController].signal, }) this.connect() } get onopen(): EventHandler | null { return this[kOnOpen] } set onopen(handler: EventHandler) { if (this[kOnOpen]) { this.removeEventListener('open', this[kOnOpen]) } this[kOnOpen] = handler.bind(this) this.addEventListener('open', this[kOnOpen]) } get onmessage(): EventHandler | null { return this[kOnMessage] } set onmessage(handler: EventHandler) { if (this[kOnMessage]) { this.removeEventListener('message', { handleEvent: this[kOnMessage] }) } this[kOnMessage] = handler.bind(this) this.addEventListener('message', { handleEvent: this[kOnMessage] }) } get onerror(): EventHandler | null { return this[kOnError] } set oneerror(handler: EventHandler) { if (this[kOnError]) { this.removeEventListener('error', { handleEvent: this[kOnError] }) } this[kOnError] = handler.bind(this) this.addEventListener('error', { handleEvent: this[kOnError] }) } public addEventListener( type: K, listener: EventHandler, options?: boolean | AddEventListenerOptions, ): void public addEventListener( type: string, listener: EventHandler, options?: boolean | AddEventListenerOptions, ): void public addEventListener( type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions, ): void public addEventListener( type: string, listener: EventHandler | EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions, ): void { super.addEventListener( type, listener as EventListenerOrEventListenerObject, options, ) } public removeEventListener( type: K, listener: (this: EventSource, ev: EventSourceEventMap[K]) => any, options?: boolean | EventListenerOptions, ): void public removeEventListener( type: string, listener: (this: EventSource, event: MessageEvent) => any, options?: boolean | EventListenerOptions, ): void public removeEventListener( type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions, ): void public removeEventListener( type: string, listener: EventHandler | EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions, ): void { super.removeEventListener( type, listener as EventListenerOrEventListenerObject, options, ) } public dispatchEvent(event: Event): boolean { return super.dispatchEvent(event) } public close(): void { this[kAbortController].abort() this.readyState = this.CLOSED } private async connect() { await fetch(this[kRequest]) .then((response) => { this.processResponse(response) }) .catch(() => { // Fail the connection on request errors instead of // throwing a generic "Failed to fetch" error. this.failConnection() }) } private processResponse(response: Response): void { if (!response.body) { this.failConnection() return } if (isNetworkError(response)) { this.reestablishConnection() return } if ( response.status !== 200 || response.headers.get('content-type') !== 'text/event-stream' ) { this.failConnection() return } this.announceConnection() this.interpretResponseBody(response) } private announceConnection(): void { queueMicrotask(() => { if (this.readyState !== this.CLOSED) { this.readyState = this.OPEN this.dispatchEvent(new Event('open')) } }) } private interpretResponseBody(response: Response): void { const parsingStream = new EventSourceParsingStream({ message: (message) => { if (message.id) { this[kLastEventId] = message.id } if (message.retry) { this[kReconnectionTime] = message.retry } const messageEvent = new MessageEvent( message.event ? message.event : 'message', { data: message.data, origin: this[kRequest].url, lastEventId: this[kLastEventId], cancelable: true, }, ) this[kOnAnyMessage]?.(messageEvent) this.dispatchEvent(messageEvent) }, abort: () => { throw new Error('Stream abort is not implemented') }, close: () => { this.failConnection() }, }) response .body!.pipeTo(parsingStream) .then(() => { this.processResponseEndOfBody(response) }) .catch(() => { this.failConnection() }) } private processResponseEndOfBody(response: Response): void { if (!isNetworkError(response)) { this.reestablishConnection() } } private async reestablishConnection(): Promise { queueMicrotask(() => { if (this.readyState === this.CLOSED) { return } this.readyState = this.CONNECTING this.dispatchEvent(new Event('error')) }) await delay(this[kReconnectionTime]) queueMicrotask(async () => { if (this.readyState !== this.CONNECTING) { return } if (this[kLastEventId] !== '') { this[kRequest].headers.set('last-event-id', this[kLastEventId]) } await this.connect() }) } private failConnection(): void { queueMicrotask(() => { if (this.readyState !== this.CLOSED) { this.readyState = this.CLOSED this.dispatchEvent(new Event('error')) } }) } } /** * Checks if the given `Response` instance is a network error. * @see https://fetch.spec.whatwg.org/#concept-network-error */ function isNetworkError(response: Response): boolean { return ( response.type === 'error' && response.status === 0 && response.statusText === '' && Array.from(response.headers.entries()).length === 0 && response.body === null ) } const enum ControlCharacters { NewLine = 10, CarriageReturn = 13, Space = 32, Colon = 58, } interface EventSourceMessage { id?: string event?: string data?: string retry?: number } class EventSourceParsingStream extends WritableStream { private decoder: TextDecoder private buffer?: Uint8Array private position: number private fieldLength?: number private discardTrailingNewline = false private message: EventSourceMessage = { id: undefined, event: undefined, data: undefined, retry: undefined, } constructor( private underlyingSink: { message: (message: EventSourceMessage) => void abort?: (reason: any) => void close?: () => void }, ) { super({ write: (chunk) => { this.processResponseBodyChunk(chunk) }, abort: (reason) => { this.underlyingSink.abort?.(reason) }, close: () => { this.underlyingSink.close?.() }, }) this.decoder = new TextDecoder() this.position = 0 } private resetMessage(): void { this.message = { id: undefined, event: undefined, data: undefined, retry: undefined, } } private processResponseBodyChunk(chunk: Uint8Array): void { if (this.buffer == null) { this.buffer = chunk this.position = 0 this.fieldLength = -1 } else { const nextBuffer = new Uint8Array(this.buffer.length + chunk.length) nextBuffer.set(this.buffer) nextBuffer.set(chunk, this.buffer.length) this.buffer = nextBuffer } const bufferLength = this.buffer.length let lineStart = 0 while (this.position < bufferLength) { if (this.discardTrailingNewline) { if (this.buffer[this.position] === ControlCharacters.NewLine) { lineStart = ++this.position } this.discardTrailingNewline = false } let lineEnd = -1 for (; this.position < bufferLength && lineEnd === -1; ++this.position) { switch (this.buffer[this.position]) { case ControlCharacters.Colon: { if (this.fieldLength === -1) { this.fieldLength = this.position - lineStart } break } case ControlCharacters.CarriageReturn: { this.discardTrailingNewline = true break } case ControlCharacters.NewLine: { lineEnd = this.position break } } } if (lineEnd === -1) { break } this.processLine( this.buffer.subarray(lineStart, lineEnd), this.fieldLength!, ) lineStart = this.position this.fieldLength = -1 } if (lineStart === bufferLength) { this.buffer = undefined } else if (lineStart !== 0) { this.buffer = this.buffer.subarray(lineStart) this.position -= lineStart } } private processLine(line: Uint8Array, fieldLength: number): void { // New line indicates the end of the message. Dispatch it. if (line.length === 0) { // Prevent dispatching the message if the data is an empty string. // That is a no-op per spec. if (this.message.data === undefined) { this.message.event = undefined return } this.underlyingSink.message(this.message) this.resetMessage() return } // Otherwise, keep accumulating message fields until the new line. if (fieldLength > 0) { const field = this.decoder.decode(line.subarray(0, fieldLength)) const valueOffset = fieldLength + (line[fieldLength + 1] === ControlCharacters.Space ? 2 : 1) const value = this.decoder.decode(line.subarray(valueOffset)) switch (field) { case 'data': { this.message.data = this.message.data ? this.message.data + '\n' + value : value break } case 'event': { this.message.event = value break } case 'id': { this.message.id = value break } case 'retry': { const retry = parseInt(value, 10) if (!isNaN(retry)) { this.message.retry = retry } break } } } } } ================================================ FILE: src/core/typeUtils.ts ================================================ type Fn = (...arg: any[]) => any export type MaybePromise = T | Promise export type RequiredDeep< Type, U extends Record | Fn | undefined = undefined, > = Type extends Fn ? Type : /** * @note The "Fn" type satisfies the predicate below. * It must always come first, before the Record check. */ Type extends Record ? { [Key in keyof Type]-?: NonNullable extends NonNullable ? NonNullable : RequiredDeep, U> } : Type /** * @fixme Remove this once TS 5.4 is the lowest supported version. * Because "NoInfer" is a built-in type utility there. */ export type NoInfer = [T][T extends any ? 0 : never] ================================================ FILE: src/core/utils/HttpResponse/decorators.ts ================================================ import statuses from '../../../shims/statuses' import { Headers as HeadersPolyfill } from 'headers-polyfill' import type { HttpResponseInit } from '../../HttpResponse' const { message } = statuses export const kSetCookie = Symbol('kSetCookie') export interface HttpResponseDecoratedInit extends HttpResponseInit { status: number statusText: string headers: Headers } export function normalizeResponseInit( init: HttpResponseInit = {}, ): HttpResponseDecoratedInit { const status = init?.status || 200 const statusText = init?.statusText || message[status] || '' const headers = new Headers(init?.headers) return { ...init, headers, status, statusText, } } export function decorateResponse( response: Response, init: HttpResponseDecoratedInit, ): Response { // Allow to mock the response type. if (init.type) { Object.defineProperty(response, 'type', { value: init.type, enumerable: true, writable: false, }) } const responseCookies = init.headers.get('set-cookie') if (responseCookies) { // Record the raw "Set-Cookie" response header provided // in the HeadersInit. This is later used to store these cookies // in cookie jar and return the right cookies in the "cookies" // response resolver argument. Object.defineProperty(response, kSetCookie, { value: responseCookies, enumerable: false, writable: false, }) // Cookie forwarding is only relevant in the browser. if (typeof document !== 'undefined') { // Write the mocked response cookies to the document. // Use `headers-polyfill` to get the Set-Cookie header value correctly. // This is an alternative until TypeScript 5.2 // and Node.js v20 become the minimum supported version // and getSetCookie in Headers can be used directly. const responseCookiePairs = HeadersPolyfill.prototype.getSetCookie.call( init.headers, ) for (const cookieString of responseCookiePairs) { // No need to parse the cookie headers because it's defined // as the valid cookie string to begin with. document.cookie = cookieString } } } return response } ================================================ FILE: src/core/utils/cookieStore.ts ================================================ import { isNodeProcess } from 'is-node-process' import { invariant } from 'outvariant' import { Cookie, CookieJar, MemoryCookieStore, type MemoryCookieStoreIndex, } from 'tough-cookie' import { jsonParse } from './internal/jsonParse' class CookieStore { #storageKey = '__msw-cookie-store__' #jar: CookieJar #memoryStore: MemoryCookieStore constructor() { if (!isNodeProcess()) { invariant( typeof localStorage !== 'undefined', 'Failed to create a CookieStore: `localStorage` is not available in this environment. This is likely an issue with your environment, which has been detected as browser (or browser-like) environment and must implement global browser APIs correctly.', ) } this.#memoryStore = new MemoryCookieStore() this.#memoryStore.idx = this.getCookieStoreIndex() this.#jar = new CookieJar(this.#memoryStore) } public getCookies(url: string): Array { return this.#jar.getCookiesSync(url) } public async setCookie(cookieName: string, url: string): Promise { await this.#jar.setCookie(cookieName, url) this.persist() } private getCookieStoreIndex(): MemoryCookieStoreIndex { if ( typeof localStorage === 'undefined' || typeof localStorage.getItem !== 'function' ) { return {} } const cookiesString = localStorage.getItem(this.#storageKey) if (cookiesString == null) { return {} } const rawCookies = jsonParse>>(cookiesString) if (rawCookies == null) { return {} } const cookies: MemoryCookieStoreIndex = {} for (const rawCookie of rawCookies) { const cookie = Cookie.fromJSON(rawCookie) if (cookie != null && cookie.domain != null && cookie.path != null) { cookies[cookie.domain] ||= {} cookies[cookie.domain][cookie.path] ||= {} cookies[cookie.domain][cookie.path][cookie.key] = cookie } } return cookies } private persist(): void { if ( typeof localStorage === 'undefined' || typeof localStorage.setItem !== 'function' ) { return } const data = [] const { idx } = this.#memoryStore for (const domain in idx) { for (const path in idx[domain]) { for (const key in idx[domain][path]) { data.push(idx[domain][path][key].toJSON()) } } } localStorage.setItem(this.#storageKey, JSON.stringify(data)) } } export const cookieStore = new CookieStore() ================================================ FILE: src/core/utils/executeHandlers.ts ================================================ import { RequestHandler, type RequestHandlerExecutionResult, } from '../handlers/RequestHandler' export interface HandlersExecutionResult { handler: RequestHandler parsedResult?: any response?: Response } export interface ResponseResolutionContext { /** * A base url to use when resolving relative urls. * @note This is primarily used by the `@mswjs/http-middleware` * to resolve relative urls in the context of the running server */ baseUrl?: string quiet?: boolean } /** * Executes the list of request handlers against the given request. * Returns the execution result object containing any matching request * handler and any mocked response it returned. */ export const executeHandlers = async >({ request, requestId, handlers, resolutionContext, }: { request: Request requestId: string handlers: Handlers resolutionContext?: ResponseResolutionContext }): Promise => { let matchingHandler: RequestHandler | null = null let result: RequestHandlerExecutionResult | null = null for (const handler of handlers) { result = await handler.run({ request, requestId, resolutionContext }) // If the handler produces some result for this request, // it automatically becomes matching. if (result !== null) { matchingHandler = handler } // Stop the lookup if this handler returns a mocked response. // If it doesn't, it will still be considered the last matching // handler until any of them returns a response. This way we can // distinguish between fallthrough handlers without responses // and the lack of a matching handler. if (result?.response) { break } } if (matchingHandler) { return { handler: matchingHandler, parsedResult: result?.parsedResult, response: result?.response, } } return null } ================================================ FILE: src/core/utils/handleRequest.test.ts ================================================ // @vitest-environment jsdom import { Emitter } from 'strict-event-emitter' import { createRequestId } from '@mswjs/interceptors' import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' import { RequestHandler } from '../handlers/RequestHandler' import { http } from '../http' import { handleRequest, HandleRequestOptions } from './handleRequest' import { RequiredDeep } from '../typeUtils' import { HttpResponse } from '../HttpResponse' import { passthrough } from '../passthrough' const options: RequiredDeep = { onUnhandledRequest: vi.fn(), } const handleRequestOptions: Partial> = { onPassthroughResponse: vi.fn(), onMockedResponse: vi.fn(), } function setup() { const emitter = new Emitter() const listener = vi.fn() const createMockListener = (name: string) => { return (...args: any) => { listener(name, ...args) } } emitter.on('request:start', createMockListener('request:start')) emitter.on('request:match', createMockListener('request:match')) emitter.on('request:unhandled', createMockListener('request:unhandled')) emitter.on('request:end', createMockListener('request:end')) emitter.on('response:mocked', createMockListener('response:mocked')) emitter.on('response:bypass', createMockListener('response:bypass')) const events = listener.mock.calls return { emitter, events } } beforeEach(() => { vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) }) afterEach(() => { vi.resetAllMocks() }) test('returns undefined for a request with the "accept: msw/passthrough" header equal to "bypass"', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user'), { headers: new Headers({ accept: 'msw/passthrough', }), }) const handlers: Array = [] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeUndefined() expect(events).toEqual([ ['request:start', { request, requestId }], ['request:end', { request, requestId }], ]) expect(options.onUnhandledRequest).not.toHaveBeenCalled() expect(handleRequestOptions.onPassthroughResponse).toHaveBeenNthCalledWith( 1, request, ) expect(handleRequestOptions.onMockedResponse).not.toHaveBeenCalled() }) test('does not bypass a request with "accept: msw/*" header set to arbitrary value', async () => { const { emitter } = setup() const request = new Request(new URL('http://localhost/user'), { headers: new Headers({ accept: 'msw/invalid', }), }) const handlers: Array = [ http.get('/user', () => { return HttpResponse.text('hello world') }), ] const result = await handleRequest( request, createRequestId(), handlers, options, emitter, handleRequestOptions, ) expect(result).not.toBeUndefined() expect(options.onUnhandledRequest).not.toHaveBeenCalled() expect(handleRequestOptions.onMockedResponse).toHaveBeenCalledTimes(1) }) test('reports request as unhandled when it has no matching request handlers', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const handlers: Array = [] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeUndefined() expect(events).toEqual([ ['request:start', { request, requestId }], ['request:unhandled', { request, requestId }], ['request:end', { request, requestId }], ]) expect(options.onUnhandledRequest).toHaveBeenNthCalledWith(1, request, { warning: expect.any(Function), error: expect.any(Function), }) expect(handleRequestOptions.onPassthroughResponse).toHaveBeenNthCalledWith( 1, request, ) expect(handleRequestOptions.onMockedResponse).not.toHaveBeenCalled() }) test('returns undefined on a request handler that returns no response', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const handlers: Array = [ http.get('/user', () => { // Intentionally blank response resolver. return }), ] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeUndefined() expect(events).toEqual([ ['request:start', { request, requestId }], ['request:end', { request, requestId }], ]) expect(options.onUnhandledRequest).not.toHaveBeenCalled() expect(handleRequestOptions.onPassthroughResponse).toHaveBeenNthCalledWith( 1, request, ) expect(handleRequestOptions.onMockedResponse).not.toHaveBeenCalled() /** * @note Returning undefined from a resolver no longer prints a warning. */ expect(console.warn).toHaveBeenCalledTimes(0) }) test('returns the mocked response for a request with a matching request handler', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const mockedResponse = HttpResponse.json({ firstName: 'John' }) const handlers: Array = [ http.get('/user', () => { return mockedResponse }), ] const lookupResult = { handler: handlers[0], response: mockedResponse, request, parsedResult: { match: { matches: true, params: {} }, cookies: {}, }, } const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toEqual(mockedResponse) expect(events).toEqual([ ['request:start', { request, requestId }], ['request:match', { request, requestId }], ['request:end', { request, requestId }], ]) expect(handleRequestOptions.onPassthroughResponse).not.toHaveBeenCalled() expect(handleRequestOptions.onMockedResponse).toHaveBeenCalledTimes(1) const [mockedResponseParam, lookupResultParam] = handleRequestOptions.onMockedResponse.mock.calls[0] expect(mockedResponseParam.status).toBe(mockedResponse.status) expect(mockedResponseParam.statusText).toBe(mockedResponse.statusText) expect(Object.fromEntries(mockedResponseParam.headers.entries())).toEqual( Object.fromEntries(mockedResponse.headers.entries()), ) expect(lookupResultParam).toEqual({ handler: lookupResult.handler, parsedResult: lookupResult.parsedResult, response: expect.objectContaining({ status: lookupResult.response.status, statusText: lookupResult.response.statusText, }), }) }) it('returns undefined without warning on a passthrough request', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const handlers: Array = [ http.get('/user', () => { return passthrough() }), ] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeUndefined() expect(events).toEqual([ ['request:start', { request, requestId }], ['request:end', { request, requestId }], ]) expect(options.onUnhandledRequest).not.toHaveBeenCalled() expect(handleRequestOptions.onPassthroughResponse).toHaveBeenNthCalledWith( 1, request, ) expect(handleRequestOptions.onMockedResponse).not.toHaveBeenCalled() }) it('calls the handler with the requestId', async () => { const { emitter } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const handlerFn = vi.fn() const handlers: Array = [http.get('/user', handlerFn)] await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(handlerFn).toHaveBeenCalledWith(expect.objectContaining({ requestId })) }) it('marks the first matching one-time handler as used', async () => { const { emitter } = setup() const oneTimeHandler = http.get( '/resource', () => { return HttpResponse.text('One-time') }, { once: true }, ) const anotherHandler = http.get('/resource', () => { return HttpResponse.text('Another') }) const handlers: Array = [oneTimeHandler, anotherHandler] const requestId = createRequestId() const request = new Request('http://localhost/resource') const firstResult = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(await firstResult?.text()).toBe('One-time') expect(oneTimeHandler.isUsed).toBe(true) expect(anotherHandler.isUsed).toBe(false) const secondResult = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(await secondResult?.text()).toBe('Another') expect(anotherHandler.isUsed).toBe(true) expect(oneTimeHandler.isUsed).toBe(true) }) it('does not mark non-matching one-time handlers as used', async () => { const { emitter } = setup() const oneTimeHandler = http.get( '/resource', () => { return HttpResponse.text('One-time') }, { once: true }, ) const anotherHandler = http.get( '/another', () => { return HttpResponse.text('Another') }, { once: true }, ) const handlers: Array = [oneTimeHandler, anotherHandler] const requestId = createRequestId() const firstResult = await handleRequest( new Request('http://localhost/another'), requestId, handlers, options, emitter, handleRequestOptions, ) expect(await firstResult?.text()).toBe('Another') expect(oneTimeHandler.isUsed).toBe(false) expect(anotherHandler.isUsed).toBe(true) const secondResult = await handleRequest( new Request('http://localhost/resource'), requestId, handlers, options, emitter, handleRequestOptions, ) expect(await secondResult?.text()).toBe('One-time') expect(anotherHandler.isUsed).toBe(true) expect(oneTimeHandler.isUsed).toBe(true) }) it('handles parallel requests with one-time handlers', async () => { const { emitter } = setup() const oneTimeHandler = http.get( '/resource', () => { return HttpResponse.text('One-time') }, { once: true }, ) const anotherHandler = http.get('/resource', () => { return HttpResponse.text('Another') }) const handlers: Array = [oneTimeHandler, anotherHandler] const requestId = createRequestId() const request = new Request('http://localhost/resource') const firstResultPromise = handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) const secondResultPromise = handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) const firstResult = await firstResultPromise const secondResult = await secondResultPromise expect(await firstResult?.text()).toBe('One-time') expect(await secondResult?.text()).toBe('Another') expect(oneTimeHandler.isUsed).toBe(true) expect(anotherHandler.isUsed).toBe(true) }) describe('[Private] - resolutionContext - used for extensions', () => { describe('#baseUrl', () => { test('when defined, handle requests to that base url only defining pathnames in the handler', async () => { const { emitter } = setup() const baseUrl = 'http://this-base-url-works.com' const handleRequestOptionsWithBaseUrl: HandleRequestOptions = { ...handleRequestOptions, resolutionContext: { baseUrl }, } const handler = http.get('/resource', () => { return HttpResponse.text('Mocked response') }) const handlers: Array = [handler] const requestId = createRequestId() const request = new Request(new URL('/resource', baseUrl)) const response = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptionsWithBaseUrl, ) expect(await response?.text()).toBe('Mocked response') }) test('when defined, do not handle requests to different base urls when defining pathnames in the handler', async () => { const { emitter } = setup() const baseUrl = 'http://this-base-url-works.com' const handleRequestOptionsWithBaseUrl: HandleRequestOptions = { ...handleRequestOptions, resolutionContext: { baseUrl }, } const handler = http.get('/resource', () => { return HttpResponse.text('Mocked response') }) const handlers: Array = [handler] const requestId = createRequestId() const request = new Request( new URL('/resource', `http://not-the-base-url.com`), ) const response = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptionsWithBaseUrl, ) expect(response).toBeUndefined() }) }) }) describe('handler with custom predicate', () => { test('matches if custom predicate returns true', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/login'), { method: 'POST', body: JSON.stringify({ username: 'test', password: 'password' }), headers: { 'Content-Type': 'application/json' }, }) const handlers: Array = [ http.post( async ({ request }) => { const body = await request.clone().json() return body.username === 'test' && body.password === 'password' }, () => HttpResponse.json({ success: true, }), ), ] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeDefined() expect(await result?.json()).toStrictEqual({ success: true }) expect(events).toEqual([ ['request:start', { request, requestId }], ['request:match', { request, requestId }], ['request:end', { request, requestId }], ]) expect(handleRequestOptions.onMockedResponse).toHaveBeenCalledTimes(1) }) test('does not match if custom predicate returns false', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/login'), { method: 'POST', body: JSON.stringify({ username: 'test', password: 'passwordd' }), headers: { 'Content-Type': 'application/json' }, }) const handlers: Array = [ http.post( async ({ request }) => { const body = await request.clone().json() return body.username === 'test' && body.password === 'password' }, () => HttpResponse.json({ success: true, }), ), ] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeUndefined() expect(events).toEqual([ ['request:start', { request, requestId }], ['request:unhandled', { request, requestId }], ['request:end', { request, requestId }], ]) expect(options.onUnhandledRequest).toHaveBeenCalledTimes(1) expect(handleRequestOptions.onPassthroughResponse).toHaveBeenCalledTimes(1) }) }) ================================================ FILE: src/core/utils/handleRequest.ts ================================================ import { until } from 'until-async' import { Emitter } from 'strict-event-emitter' import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' import { RequiredDeep } from '../typeUtils' import type { RequestHandler } from '../handlers/RequestHandler' import { type HandlersExecutionResult, type ResponseResolutionContext, executeHandlers, } from './executeHandlers' import { onUnhandledRequest } from './request/onUnhandledRequest' import { storeResponseCookies } from './request/storeResponseCookies' export interface HandleRequestOptions { /** * `resolutionContext` is not part of the general public api * but is exposed to aid in creating extensions like * `@mswjs/http-middleware`. */ resolutionContext?: ResponseResolutionContext /** * Invoked whenever a request is performed as-is. */ onPassthroughResponse?(request: Request): void /** * Invoked when the mocked response is ready to be sent. */ onMockedResponse?( response: Response, handler: RequiredDeep, ): void } export async function handleRequest( request: Request, requestId: string, handlers: Array, options: RequiredDeep, emitter: Emitter, handleRequestOptions?: HandleRequestOptions, ): Promise { emitter.emit('request:start', { request, requestId }) // Perform requests wrapped in "bypass()" as-is. if (request.headers.get('accept')?.includes('msw/passthrough')) { emitter.emit('request:end', { request, requestId }) handleRequestOptions?.onPassthroughResponse?.(request) return } // Resolve a mocked response from the list of request handlers. const [lookupError, lookupResult] = await until(() => { return executeHandlers({ request, requestId, handlers, resolutionContext: handleRequestOptions?.resolutionContext, }) }) if (lookupError) { // Allow developers to react to unhandled exceptions in request handlers. emitter.emit('unhandledException', { error: lookupError, request, requestId, }) throw lookupError } // If the handler lookup returned nothing, no request handler was found // matching this request. Report the request as unhandled. if (!lookupResult) { await onUnhandledRequest(request, options.onUnhandledRequest) emitter.emit('request:unhandled', { request, requestId }) emitter.emit('request:end', { request, requestId }) handleRequestOptions?.onPassthroughResponse?.(request) return } const { response } = lookupResult // When the handled request returned no mocked response, warn the developer, // as it may be an oversight on their part. Perform the request as-is. if (!response) { emitter.emit('request:end', { request, requestId }) handleRequestOptions?.onPassthroughResponse?.(request) return } // Perform the request as-is when the developer explicitly returned "req.passthrough()". // This produces no warning as the request was handled. if ( response.status === 302 && response.headers.get('x-msw-intention') === 'passthrough' ) { emitter.emit('request:end', { request, requestId }) handleRequestOptions?.onPassthroughResponse?.(request) return } // Store all the received response cookies in the cookie jar. await storeResponseCookies(request, response) emitter.emit('request:match', { request, requestId }) const requiredLookupResult = lookupResult as RequiredDeep handleRequestOptions?.onMockedResponse?.(response, requiredLookupResult) emitter.emit('request:end', { request, requestId }) return response } ================================================ FILE: src/core/utils/internal/Disposable.ts ================================================ export type DisposableSubscription = () => void export class Disposable { protected subscriptions: Array = [] public dispose() { let subscription: DisposableSubscription | undefined while ((subscription = this.subscriptions.shift())) { subscription() } } } ================================================ FILE: src/core/utils/internal/checkGlobals.ts ================================================ import { invariant } from 'outvariant' import { devUtils } from './devUtils' export function checkGlobals() { /** * MSW expects the "URL" constructor to be defined. * It's not present in React Native so suggest a polyfill * instead of failing silently. * @see https://github.com/mswjs/msw/issues/1408 */ invariant( typeof URL !== 'undefined', devUtils.formatMessage( `Global "URL" class is not defined. This likely means that you're running MSW in an environment that doesn't support all Node.js standard API (e.g. React Native). If that's the case, please use an appropriate polyfill for the "URL" class, like "react-native-url-polyfill".`, ), ) } ================================================ FILE: src/core/utils/internal/devUtils.test.ts ================================================ import { InternalError } from './devUtils' describe(InternalError, () => { it('creates an InternalError instance', () => { const error = new InternalError('Message') expect(error.name).toBe('InternalError') expect(error.message).toBe('Message') expect(error.toString()).toBe('InternalError: Message') expect(error.stack).toMatch(/\w+/) }) it('passes the identity check', () => { const error = new InternalError('Message') expect(error instanceof InternalError).toBe(true) expect(error instanceof Error).toBe(true) const extraneousError = new Error('Message') expect(extraneousError).not.toBeInstanceOf(InternalError) }) }) ================================================ FILE: src/core/utils/internal/devUtils.ts ================================================ import { format } from 'outvariant' const LIBRARY_PREFIX = '[MSW]' /** * Formats a given message by appending the library's prefix string. */ function formatMessage(message: string, ...positionals: any[]): string { const interpolatedMessage = format(message, ...positionals) return `${LIBRARY_PREFIX} ${interpolatedMessage}` } /** * Prints a library-specific warning. */ function warn(message: string, ...positionals: any[]): void { console.warn(formatMessage(message, ...positionals)) } /** * Prints a library-specific error. */ function error(message: string, ...positionals: any[]): void { console.error(formatMessage(message, ...positionals)) } export const devUtils = { formatMessage, warn, error, } /** * Internal error instance. * Used to differentiate the library errors that must be forwarded * to the user from the unhandled exceptions. Use this if you don't * wish for the error to be coerced to a 500 fallback response. */ export class InternalError extends Error { constructor(message: string) { super(message) this.name = 'InternalError' } } ================================================ FILE: src/core/utils/internal/getCallFrame.test.ts ================================================ /** * @vitest-environment jsdom */ import { getCallFrame } from './getCallFrame' class ErrorWithStack extends Error { constructor(stack: string[] | undefined | null) { super('') this.stack = stack?.join('\n') } } test('supports Node.js (Linux, MacOS) error stack', () => { const linuxError = new ErrorWithStack([ 'Error: ', ' at getCallFrame (/Users/mock/github/msw/lib/node/index.js:3735:22)', ' at Object.get (/Users/mock/github/msw/lib/node/index.js:3776:29)', ' at Object. (/Users/mock/github/msw/test/msw-api/setup-server/listHandlers.test.ts:13:8)', // <-- this one ' at Runtime._execModule (/Users/mock/github/msw/node_modules/jest-runtime/build/index.js:1299:24)', ' at Runtime._loadModule (/Users/mock/github/msw/node_modules/jest-runtime/build/index.js:898:12)', ' at Runtime.requireModule (/Users/mock/github/msw/node_modules/jest-runtime/build/index.js:746:10)', ' at jasmine2 (/Users/mock/github/msw/node_modules/jest-jasmine2/build/index.js:230:13)', ' at runTestInternal (/Users/mock/github/msw/node_modules/jest-runner/build/runTest.js:380:22)', ' at runTest (/Users/mock/github/msw/node_modules/jest-runner/build/runTest.js:472:34)', ]) expect(getCallFrame(linuxError)).toEqual( '/Users/mock/github/msw/test/msw-api/setup-server/listHandlers.test.ts:13:8', ) const macOsError = new ErrorWithStack([ 'Error: ', ' at getCallFrame (/Users/mock/git/msw/lib/node/index.js:3735:22)', ' at graphQLRequestHandler (/Users/mock/git/msw/lib/node/index.js:7071:25)', ' at Object.query (/Users/mock/git/msw/lib/node/index.js:7182:18)', ' at Object. (/Users/mock/git/msw/test/msw-api/setup-server/listHandlers.test.ts:14:11)', // <-- this one ' at Runtime._execModule (/Users/mock/git/msw/node_modules/jest-runtime/build/index.js:1299:24)', ' at Runtime._loadModule (/Users/mock/git/msw/node_modules/jest-runtime/build/index.js:898:12)', ' at Runtime.requireModule (/Users/mock/git/msw/node_modules/jest-runtime/build/index.js:746:10)', ' at jasmine2 (/Users/mock/git/msw/node_modules/jest-jasmine2/build/index.js:230:13)', ' at runTestInternal (/Users/mock/git/msw/node_modules/jest-runner/build/runTest.js:380:22)', ' at runTest (/Users/mock/git/msw/node_modules/jest-runner/build/runTest.js:472:34)', ]) expect(getCallFrame(macOsError)).toEqual( '/Users/mock/git/msw/test/msw-api/setup-server/listHandlers.test.ts:14:11', ) }) test('supports Node.js (Windows) error stack', () => { const error = new ErrorWithStack([ 'Error: ', ' at getCallFrame (C:\\Users\\mock\\git\\msw\\lib\\node\\index.js:3735:22)', ' at graphQLRequestHandler (C:\\Users\\mock\\git\\msw\\lib\\node\\index.js:7071:25)', ' at Object.query (C:\\Users\\mock\\git\\msw\\lib\\node\\index.js:7182:18)', ' at Object. (C:\\Users\\mock\\git\\msw\\test\\msw-api\\setup-server\\listHandlers.test.ts:75:13)', // <-- this one ' at Object.asyncJestTest (C:\\Users\\mock\\git\\msw\\node_modules\\jest-jasmine2\\build\\jasmineAsyncInstall.js:106:37)', ' at C:\\Users\\mock\\git\\msw\\node_modules\\jest-jasmine2\\build\\queueRunner.js:45:12', ' at new Promise ()', ' at mapper (C:\\Users\\mock\\git\\msw\\node_modules\\jest-jasmine2\\build\\queueRunner.js:28:19)', ' at C:\\Users\\mock\\git\\msw\\node_modules\\jest-jasmine2\\build\\queueRunner.js:75:41', ]) expect(getCallFrame(error)).toBe( 'C:\\Users\\mock\\git\\msw\\test\\msw-api\\setup-server\\listHandlers.test.ts:75:13', ) }) test('supports Chrome and Edge error stack', () => { const error = new ErrorWithStack([ 'Error', ' at getCallFrame (webpack:///./lib/browser/getCallFrame-deps.js?:272:20)', ' at Object.eval [as get] (webpack:///./lib/browser/rest-deps.js?:1402:90)', ' at eval (webpack:///./test/msw-api/setup-worker/listHandlers.mocks.ts?:6:113)', // <-- this one ' at Module../test/msw-api/setup-worker/listHandlers.mocks.ts (http://localhost:59464/main.js:1376:1)', ' at __webpack_require__ (http://localhost:59464/main.js:790:30)', ' at fn (http://localhost:59464/main.js:101:20)', ' at eval (webpack:///multi_(webpack)-dev-server/client?:4:18)', ' at Object.0 (http://localhost:59464/main.js:1399:1)', ' at __webpack_require__ (http://localhost:59464/main.js:790:30)', ' at http://localhost:59464/main.js:857:37', ]) expect(getCallFrame(error)).toBe( 'webpack:///./test/msw-api/setup-worker/listHandlers.mocks.ts?:6:113', ) }) test('supports Firefox (MacOS, Windows) error stack', () => { const error = new ErrorWithStack([ 'getCallFrame@webpack:///./lib/browser/getCallFrame-deps.js?:272:20', 'createRestHandler/<@webpack:///./lib/browser/rest-deps.js?:1402:90', '@webpack:///./test/msw-api/setup-worker/listHandlers.mocks.ts?:6:113', // <-- this one './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:59464/main.js:1376:1', '__webpack_require__@http://localhost:59464/main.js:790:30', 'fn@http://localhost:59464/main.js:101:20', '@webpack:///multi_(webpack)-dev-server/client?:4:18', '0@http://localhost:59464/main.js:1399:1', '__webpack_require__@http://localhost:59464/main.js:790:30', '@http://localhost:59464/main.js:857:37', ]) expect(getCallFrame(error)).toBe( 'webpack:///./test/msw-api/setup-worker/listHandlers.mocks.ts?:6:113', ) }) test('supports Safari (MacOS) error stack', () => { const errorOne = new ErrorWithStack([ 'getCallFrame', '', 'eval code', 'eval@[native code]', './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:59464/main.js:1376:5', // <-- this one '__webpack_require__@http://localhost:59464/main.js:790:34', 'fn@http://localhost:59464/main.js:101:39', 'eval code', 'eval@[native code]', 'http://localhost:59464/main.js:1399:5', '__webpack_require__@http://localhost:59464/main.js:790:34', 'http://localhost:59464/main.js:857:37', 'global code@http://localhost:59464/main.js:858:12', ]) expect(getCallFrame(errorOne)).toBe( './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:59464/main.js:1376:5', ) const errorTwo = new ErrorWithStack([ 'getCallFrame', 'graphQLRequestHandler', 'eval code', 'eval@[native code]', './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:56460/main.js:1376:5', // <-- this one '__webpack_require__@http://localhost:56460/main.js:790:34', 'fn@http://localhost:56460/main.js:101:39', 'eval code', 'eval@[native code]', 'http://localhost:56460/main.js:1399:5', '__webpack_require__@http://localhost:56460/main.js:790:34', 'http://localhost:56460/main.js:857:37', 'global code@http://localhost:56460/main.js:858:12', ]) expect(getCallFrame(errorTwo)).toBe( './test/msw-api/setup-worker/listHandlers.mocks.ts@http://localhost:56460/main.js:1376:5', ) }) test('handles the undefined stack trace', () => { expect(() => getCallFrame(new ErrorWithStack(undefined))).not.toThrow( TypeError, ) expect(() => getCallFrame(new ErrorWithStack(null))).not.toThrow(TypeError) }) ================================================ FILE: src/core/utils/internal/getCallFrame.ts ================================================ // Ignore the source files traces for local testing. const SOURCE_FRAME = /[/\\]msw[/\\]src[/\\](.+)/ const BUILD_FRAME = /(node_modules)?[/\\]lib[/\\](core|browser|node|native|iife)[/\\]|^[^/\\]*$/ /** * Return the stack trace frame of a function's invocation. */ export function getCallFrame(error: Error) { // In { return !(SOURCE_FRAME.test(frame) || BUILD_FRAME.test(frame)) }) if (!declarationFrame) { return } // Extract file reference from the stack frame. const declarationPath = declarationFrame .replace(/\s*at [^()]*\(([^)]+)\)/, '$1') .replace(/^@/, '') return declarationPath } ================================================ FILE: src/core/utils/internal/hasRefCounted.test.ts ================================================ import { hasRefCounted } from './hasRefCounted' it('returns true for objects with ref and unref methods', () => { expect( hasRefCounted({ ref() {}, unref() {}, }), ).toBe(true) }) it('returns false for a non-refcounted object', () => { expect(hasRefCounted({})).toBe(false) expect(hasRefCounted({ ref() {} })).toBe(false) expect(hasRefCounted({ unref() {} })).toBe(false) }) ================================================ FILE: src/core/utils/internal/hasRefCounted.ts ================================================ export function hasRefCounted( value: T, ): value is T & NodeJS.RefCounted { return ( typeof Reflect.get(value, 'ref') === 'function' && typeof Reflect.get(value, 'unref') === 'function' ) } ================================================ FILE: src/core/utils/internal/isHandlerKind.test.ts ================================================ import { GraphQLHandler } from '../../handlers/GraphQLHandler' import { HttpHandler } from '../../handlers/HttpHandler' import { RequestHandler } from '../../handlers/RequestHandler' import { WebSocketHandler } from '../../handlers/WebSocketHandler' import { isHandlerKind } from './isHandlerKind' it('returns true if expected a request handler and given a request handler', () => { expect( isHandlerKind('RequestHandler')(new HttpHandler('*', '*', () => {})), ).toBe(true) expect( isHandlerKind('RequestHandler')( new GraphQLHandler('all', '*', '*', () => {}), ), ).toBe(true) }) it('returns true if expected a request handler and given a custom request handler', () => { class MyHandler extends RequestHandler { constructor() { super({ info: { header: '*' }, resolver: () => {} }) } predicate = () => false log() {} } expect(isHandlerKind('RequestHandler')(new MyHandler())).toBe(true) }) it('returns false if expected a request handler but given event handler', () => { expect(isHandlerKind('RequestHandler')(new WebSocketHandler('*'))).toBe(false) }) it('returns false if expected a request handler but given arbitrary object', () => { expect(isHandlerKind('RequestHandler')(undefined)).toBe(false) expect(isHandlerKind('RequestHandler')(null)).toBe(false) expect(isHandlerKind('RequestHandler')({})).toBe(false) expect(isHandlerKind('RequestHandler')([])).toBe(false) expect(isHandlerKind('RequestHandler')(123)).toBe(false) expect(isHandlerKind('RequestHandler')('hello')).toBe(false) }) it('returns true if expected an event handler and given an event handler', () => { expect(isHandlerKind('EventHandler')(new WebSocketHandler('*'))).toBe(true) }) it('returns true if expected an event handler and given a custom event handler', () => { class MyEventHandler extends WebSocketHandler { constructor() { super('*') } } expect(isHandlerKind('EventHandler')(new MyEventHandler())).toBe(true) }) it('returns false if expected an event handler but given arbitrary object', () => { expect(isHandlerKind('EventHandler')(undefined)).toBe(false) expect(isHandlerKind('EventHandler')(null)).toBe(false) expect(isHandlerKind('EventHandler')({})).toBe(false) expect(isHandlerKind('EventHandler')([])).toBe(false) expect(isHandlerKind('EventHandler')(123)).toBe(false) expect(isHandlerKind('EventHandler')('hello')).toBe(false) }) ================================================ FILE: src/core/utils/internal/isHandlerKind.ts ================================================ import type { HandlerKind } from '../../handlers/common' import type { RequestHandler } from '../../handlers/RequestHandler' import type { WebSocketHandler } from '../../handlers/WebSocketHandler' /** * A filter function that ensures that the provided argument * is a handler of the given kind. This helps differentiate * between different kinds of handlers, e.g. request and event handlers. */ export function isHandlerKind(kind: K) { return ( input: unknown, ): input is K extends 'EventHandler' ? WebSocketHandler : RequestHandler => { return ( input != null && typeof input === 'object' && '__kind' in input && input.__kind === kind ) } } ================================================ FILE: src/core/utils/internal/isIterable.test.ts ================================================ import { isIterable } from './isIterable' test('returns true given an iterator', () => { expect( isIterable( (function* () { yield 2 })(), ), ).toEqual(true) }) test('returns false given a regular function', () => { expect( isIterable( (function () { return null })(), ), ).toEqual(false) expect(isIterable((() => null)())).toEqual(false) }) ================================================ FILE: src/core/utils/internal/isIterable.ts ================================================ /** * This is the same as TypeScript's `Iterable`, but with all three type parameters. * @todo Remove once TypeScript 5.6 is the minimum. */ export interface Iterable { [Symbol.iterator](): Iterator } /** * This is the same as TypeScript's `AsyncIterable`, but with all three type parameters. * @todo Remove once TypeScript 5.6 is the minimum. */ export interface AsyncIterable { [Symbol.asyncIterator](): AsyncIterator } /** * Determines if the given function is an iterator. */ export function isIterable( fn: any, ): fn is | Iterable | AsyncIterable { if (!fn) { return false } return ( Reflect.has(fn, Symbol.iterator) || Reflect.has(fn, Symbol.asyncIterator) ) } ================================================ FILE: src/core/utils/internal/isObject.test.ts ================================================ import { isObject } from './isObject' test('returns true given an object', () => { expect(isObject({})).toBe(true) expect(isObject({ a: 1 })).toBe(true) }) test('returns false given a non-object value', () => { expect(isObject(1)).toBe(false) expect(isObject('string')).toBe(false) expect(isObject([])).toBe(false) expect( isObject(function () { return 2 }), ).toBe(false) expect(isObject(false)).toBe(false) expect(isObject(undefined)).toBe(false) expect(isObject(null)).toBe(false) }) ================================================ FILE: src/core/utils/internal/isObject.ts ================================================ /** * Determines if the given value is an object. */ export function isObject(value: any): value is Record { return value != null && typeof value === 'object' && !Array.isArray(value) } ================================================ FILE: src/core/utils/internal/isStringEqual.test.ts ================================================ import { isStringEqual } from './isStringEqual' describe('isStringEqual', () => { describe('given two uppercase strings', () => { describe('and the strings are equal', () => { it('should return true', () => { expect(isStringEqual('GET', 'GET')).toBe(true) }) }) describe('and the strings are not equal', () => { it('should return false', () => { expect(isStringEqual('GET', 'POST')).toBe(false) }) }) }) describe('given two lowercase strings', () => { describe('and the strings are equal', () => { it('should return true', () => { expect(isStringEqual('get', 'get')).toBe(true) }) }) describe('and the strings are not equal', () => { it('should return false', () => { expect(isStringEqual('get', 'post')).toBe(false) }) }) }) describe('given two strings cased differently', () => { describe('and the strings are equal', () => { it('should return true', () => { expect(isStringEqual('get', 'GET')).toBe(true) }) }) describe('and the strings are not equal', () => { it('should return false', () => { expect(isStringEqual('get', 'POST')).toBe(false) }) }) }) }) ================================================ FILE: src/core/utils/internal/isStringEqual.ts ================================================ /** * Performs a case-insensitive comparison of two given strings. */ export function isStringEqual(actual: string, expected: string): boolean { return actual.toLowerCase() === expected.toLowerCase() } ================================================ FILE: src/core/utils/internal/jsonParse.test.ts ================================================ import { jsonParse } from './jsonParse' test('parses a given valid JSON string', () => { expect(jsonParse(`{"property":"value"}`)).toEqual({ property: 'value', }) }) test('returns undefined without an error given an invalid JSON string', () => { const parse = () => jsonParse(`{"property:val"ue$}`) expect(parse).not.toThrow() expect(parse()).toBeUndefined() }) ================================================ FILE: src/core/utils/internal/jsonParse.ts ================================================ /** * Parses a given value into a JSON. * Does not throw an exception on an invalid JSON string. */ export function jsonParse>( value: any, ): ValueType | undefined { try { return JSON.parse(value) } catch { return undefined } } ================================================ FILE: src/core/utils/internal/mergeRight.test.ts ================================================ import { mergeRight } from './mergeRight' test('shallowly merges two given objects', () => { expect(mergeRight({ a: 1, b: 2 }, { b: 3, c: 4 })).toEqual({ a: 1, b: 3, c: 4, }) }) test('deeply merges two given objects', () => { expect( mergeRight( { a: 'string', b: [1, 2], c: { d: 2, }, }, { a: 'another-string', b: [3], c: { e: 'five', f: { g: true, }, }, }, ), ).toEqual({ a: 'another-string', b: [1, 2, 3], c: { d: 2, e: 'five', f: { g: true, }, }, }) }) ================================================ FILE: src/core/utils/internal/mergeRight.ts ================================================ import { isObject } from './isObject' /** * Deeply merges two given objects with the right one * having a priority during property assignment. */ export function mergeRight( left: Record, right: Record, ) { return Object.entries(right).reduce( (result, [key, rightValue]) => { const leftValue = result[key] if (Array.isArray(leftValue) && Array.isArray(rightValue)) { result[key] = leftValue.concat(rightValue) return result } if (isObject(leftValue) && isObject(rightValue)) { result[key] = mergeRight(leftValue, rightValue) return result } result[key] = rightValue return result }, Object.assign({}, left), ) } ================================================ FILE: src/core/utils/internal/parseGraphQLRequest.test.ts ================================================ /** * @vitest-environment jsdom */ import { encodeBuffer } from '@mswjs/interceptors' import { OperationTypeNode } from 'graphql' import { ParsedGraphQLRequest, parseGraphQLRequest, } from './parseGraphQLRequest' test('returns true given a GraphQL-compatible request', async () => { const getRequest = new Request( new URL( 'http://localhost:8080/graphql?query=mutation Login { user { id } }', ), ) expect(await parseGraphQLRequest(getRequest)).toEqual< ParsedGraphQLRequest >({ operationType: OperationTypeNode.MUTATION, operationName: 'Login', query: `mutation Login { user { id } }`, variables: undefined, }) const postRequest = new Request(new URL('http://localhost:8080/graphql'), { method: 'POST', headers: new Headers({ 'Content-Type': 'application/json' }), body: encodeBuffer( JSON.stringify({ query: `query GetUser { user { firstName } }`, }), ), }) expect(await parseGraphQLRequest(postRequest)).toEqual< ParsedGraphQLRequest >({ operationType: OperationTypeNode.QUERY, operationName: 'GetUser', query: `query GetUser { user { firstName } }`, variables: undefined, }) }) test('throws an exception given an invalid GraphQL request', async () => { const getRequest = new Request( new URL('http://localhost:8080/graphql?query=mutation Login() { user { {}'), ) await expect(parseGraphQLRequest(getRequest)).rejects.toThrowError( '[MSW] Failed to intercept a GraphQL request to "GET http://localhost:8080/graphql": cannot parse query. See the error message from the parser below.', ) const postRequest = new Request(new URL('http://localhost:8080/graphql'), { method: 'POST', headers: new Headers({ 'Content-Type': 'application/json' }), body: encodeBuffer( JSON.stringify({ query: `query GetUser() { user {{}`, }), ), }) await expect(parseGraphQLRequest(postRequest)).rejects.toThrowError( '[MSW] Failed to intercept a GraphQL request to "POST http://localhost:8080/graphql": cannot parse query. See the error message from the parser below.\n\nSyntax Error: Expected "$", found ")".', ) }) test('returns false given a GraphQL-incompatible request', async () => { const getRequest = new Request(new URL('http://localhost:8080/graphql'), { headers: new Headers({ 'Content-Type': 'application/json' }), }) expect(await parseGraphQLRequest(getRequest)).toBeUndefined() const postRequest = new Request(new URL('http://localhost:8080/graphql'), { method: 'POST', headers: new Headers({ 'Content-Type': 'application/json' }), body: encodeBuffer( JSON.stringify({ queryUser: true, }), ), }) expect(await parseGraphQLRequest(postRequest)).toBeUndefined() }) test('does not read the original request body', async () => { const request = new Request(new URL('http://localhost/api'), { method: 'POST', body: JSON.stringify({ payload: 'value' }), }) await parseGraphQLRequest(request) // Must not read the original request body because GraphQL parsing // is an internal operation that must not lock the body stream. expect(request.bodyUsed).toBe(false) }) ================================================ FILE: src/core/utils/internal/parseGraphQLRequest.ts ================================================ import type { DocumentNode, OperationDefinitionNode, OperationTypeNode, } from 'graphql' import type { GraphQLVariables } from '../../handlers/GraphQLHandler' import { toPublicUrl } from '../request/toPublicUrl' import { devUtils } from './devUtils' import { jsonParse } from './jsonParse' import { parseMultipartData } from './parseMultipartData' interface GraphQLInput { query: string | null variables?: GraphQLVariables } export interface ParsedGraphQLQuery { operationType: OperationTypeNode operationName?: string } export type ParsedGraphQLRequest< VariablesType extends GraphQLVariables = GraphQLVariables, > = | (ParsedGraphQLQuery & { query: string variables?: VariablesType }) | undefined export function parseDocumentNode(node: DocumentNode): ParsedGraphQLQuery { const operationDef = node.definitions.find((definition) => { return definition.kind === 'OperationDefinition' }) as OperationDefinitionNode return { operationType: operationDef?.operation, operationName: operationDef?.name?.value, } } async function parseQuery(query: string): Promise { /** * @note Use `require` to get the "graphql" module here. * It has to be scoped to this function because this module leaks to the * root export. It has to be `require` because tools like Jest have trouble * handling dynamic imports. It gets replaced with a dynamic import on build time. */ // eslint-disable-next-line @typescript-eslint/no-require-imports const { parse } = require('graphql') try { const ast = parse(query) return parseDocumentNode(ast) } catch (error) { return error as Error } } export type GraphQLParsedOperationsMap = Record export type GraphQLMultipartRequestBody = { operations: string map?: string } & { [fileName: string]: File } function extractMultipartVariables( variables: VariablesType, map: GraphQLParsedOperationsMap, files: Record, ) { const operations = { variables } for (const [key, pathArray] of Object.entries(map)) { if (!(key in files)) { throw new Error(`Given files do not have a key '${key}' .`) } for (const dotPath of pathArray) { const [lastPath, ...reversedPaths] = dotPath.split('.').reverse() const paths = reversedPaths.reverse() let target: Record = operations for (const path of paths) { if (!(path in target)) { throw new Error(`Property '${path}' is not in operations.`) } target = target[path] } target[lastPath] = files[key] } } return operations.variables } async function getGraphQLInput(request: Request): Promise { switch (request.method) { case 'GET': { const url = new URL(request.url) const query = url.searchParams.get('query') const variables = url.searchParams.get('variables') || '' return { query, variables: jsonParse(variables), } } case 'POST': { // Clone the request so we could read its body without locking // the body stream to the downward consumers. const requestClone = request.clone() // Handle multipart body GraphQL operations. if ( request.headers.get('content-type')?.includes('multipart/form-data') ) { const responseJson = parseMultipartData( await requestClone.text(), request.headers, ) if (!responseJson) { return null } const { operations, map, ...files } = responseJson const parsedOperations = jsonParse<{ query?: string; variables?: GraphQLVariables }>( operations, ) || {} if (!parsedOperations.query) { return null } const parsedMap = jsonParse(map || '') || {} const variables = parsedOperations.variables ? extractMultipartVariables( parsedOperations.variables, parsedMap, files, ) : {} return { query: parsedOperations.query, variables, } } // Handle plain POST GraphQL operations. const requestJson: { query: string variables?: GraphQLVariables operations?: any /** @todo Annotate this */ } = await requestClone.json().catch(() => null) if (requestJson?.query) { const { query, variables } = requestJson return { query, variables, } } return null } default: return null } } /** * Determines if a given request can be considered a GraphQL request. * Does not parse the query and does not guarantee its validity. */ export async function parseGraphQLRequest( request: Request, ): Promise { const input = await getGraphQLInput(request) if (!input || !input.query) { return } const { query, variables } = input const parsedResult = await parseQuery(query) if (parsedResult instanceof Error) { const requestPublicUrl = toPublicUrl(request.url) throw new Error( devUtils.formatMessage( 'Failed to intercept a GraphQL request to "%s %s": cannot parse query. See the error message from the parser below.\n\n%s', request.method, requestPublicUrl, parsedResult.message, ), ) } return { query: input.query, operationType: parsedResult.operationType, operationName: parsedResult.operationName, variables, } } ================================================ FILE: src/core/utils/internal/parseMultipartData.test.ts ================================================ /** * @vitest-environment jsdom */ import { parseMultipartData } from './parseMultipartData' test('parses a given valid multipart string', async () => { expect.assertions(3) await testMultipartDataWithContentType( 'multipart/form-data; boundary=WebKitFormBoundaryvZ1cVXWyK0ilQdab', ) }) test('parses a given valid multipart string given non-pretty content-type', async () => { expect.assertions(3) // node-fetch will serialize content-type in this format, which is valid according to HTTP // https://github.com/node-fetch/node-fetch/blob/d8fc32d6b29bd43d1ad377e80b3e439fe37f2904/test/main.js#L1438 await testMultipartDataWithContentType( 'multipart/form-data;boundary=WebKitFormBoundaryvZ1cVXWyK0ilQdab', ) }) test('parses a given valid multipart string given content-type with extra spaces', async () => { expect.assertions(3) await testMultipartDataWithContentType( 'multipart/form-data; boundary=WebKitFormBoundaryvZ1cVXWyK0ilQdab', ) }) async function testMultipartDataWithContentType( contentType: string, ): Promise { const body = `\ ------WebKitFormBoundaryvZ1cVXWyK0ilQdab\r Content-Disposition: form-data; name="file"; filename="file1.txt"\r Content-Type: application/octet-stream\r \r file content\r ------WebKitFormBoundaryvZ1cVXWyK0ilQdab\r Content-Disposition: form-data; name="text"\r \r text content\r ------WebKitFormBoundaryvZ1cVXWyK0ilQdab\r Content-Disposition: form-data; name="text2"\r \r another text content\r ------WebKitFormBoundaryvZ1cVXWyK0ilQdab\r Content-Disposition: form-data; name="text2"\r \r \r another text content 2\r \r ------WebKitFormBoundaryvZ1cVXWyK0ilQdab--` const headers = new Headers({ 'content-type': contentType, }) const parsed = parseMultipartData(body, headers) // Workaround: JSDOM does not have `Blob.text` implementation. // see https://github.com/jsdom/jsdom/issues/2555 expect(parsed).toHaveProperty('file.name', 'file1.txt') expect(parsed).toHaveProperty('text', 'text content') expect(parsed).toHaveProperty('text2', [ 'another text content', '\r\nanother text content 2\r\n', ]) } test('returns undefined without an error given an invalid multipart string', () => { const headers = new Headers({ 'content-type': 'multipart/form-data; boundary=dummyBoundary', }) const parse = () => parseMultipartData(`{"invalid": ["multipart"]}`, headers) expect(parse).not.toThrow() expect(parse()).toBeUndefined() }) ================================================ FILE: src/core/utils/internal/parseMultipartData.ts ================================================ import { stringToHeaders } from 'headers-polyfill' import { DefaultRequestMultipartBody } from '../../handlers/RequestHandler' interface ParsedContentHeaders { name: string filename?: string contentType: string } interface ContentDispositionDirective { [key: string]: string | undefined name: string filename?: string 'form-data': string } function parseContentHeaders(headersString: string): ParsedContentHeaders { const headers = stringToHeaders(headersString) const contentType = headers.get('content-type') || 'text/plain' const disposition = headers.get('content-disposition') if (!disposition) { throw new Error('"Content-Disposition" header is required.') } const directives = disposition.split(';').reduce((acc, chunk) => { const [name, ...rest] = chunk.trim().split('=') acc[name] = rest.join('=') return acc }, {} as ContentDispositionDirective) const name = directives.name?.slice(1, -1) const filename = directives.filename?.slice(1, -1) return { name, filename, contentType, } } /** * Parses a given string as a multipart/form-data. * Does not throw an exception on an invalid multipart string. */ export function parseMultipartData( data: string, headers?: Headers, ): T | undefined { const contentType = headers?.get('content-type') if (!contentType) { return undefined } const [, ...directives] = contentType.split(/; */) const boundary = directives .filter((d) => d.startsWith('boundary=')) .map((s) => s.replace(/^boundary=/, ''))[0] if (!boundary) { return undefined } const boundaryRegExp = new RegExp( `--+${boundary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, ) const fields = data .split(boundaryRegExp) .filter((chunk) => chunk.startsWith('\r\n') && chunk.endsWith('\r\n')) .map((chunk) => chunk.trimStart().replace(/\r\n$/, '')) if (!fields.length) { return undefined } const parsedBody: DefaultRequestMultipartBody = {} try { for (const field of fields) { const [contentHeaders, ...rest] = field.split('\r\n\r\n') const contentBody = rest.join('\r\n\r\n') const { contentType, filename, name } = parseContentHeaders(contentHeaders) const value = filename === undefined ? contentBody : new File([contentBody], filename, { type: contentType }) const parsedValue = parsedBody[name] if (parsedValue === undefined) { parsedBody[name] = value } else if (Array.isArray(parsedValue)) { parsedBody[name] = [...parsedValue, value] } else { parsedBody[name] = [parsedValue, value] } } return parsedBody as T } catch { return undefined } } ================================================ FILE: src/core/utils/internal/pipeEvents.test.ts ================================================ import { Emitter } from 'strict-event-emitter' import { pipeEvents } from './pipeEvents' it('pipes events from the source emitter to the destination emitter', () => { const source = new Emitter() const destination = new Emitter() pipeEvents(source, destination) const callback = vi.fn() destination.on('hello', callback) source.emit('hello', 'world', { data: true }) expect(callback).toHaveBeenNthCalledWith(1, 'world', { data: true }) }) ================================================ FILE: src/core/utils/internal/pipeEvents.ts ================================================ import { Emitter, EventMap } from 'strict-event-emitter' /** * Pipes all emitted events from one emitter to another. */ export function pipeEvents( source: Emitter, destination: Emitter, ): void { const rawEmit: typeof source.emit & { _isPiped?: boolean } = source.emit if (rawEmit._isPiped) { return } const sourceEmit: typeof source.emit & { _isPiped?: boolean } = function sourceEmit(this: typeof source, event, ...data) { destination.emit(event, ...data) return rawEmit.call(this, event, ...data) } sourceEmit._isPiped = true source.emit = sourceEmit } ================================================ FILE: src/core/utils/internal/requestHandlerUtils.ts ================================================ import { RequestHandler } from '../../handlers/RequestHandler' export function use( currentHandlers: Array, ...handlers: Array ): void { currentHandlers.unshift(...handlers) } export function restoreHandlers(handlers: Array): void { handlers.forEach((handler) => { handler.isUsed = false }) } export function resetHandlers( initialHandlers: Array, ...nextHandlers: Array ) { return nextHandlers.length > 0 ? [...nextHandlers] : [...initialHandlers] } ================================================ FILE: src/core/utils/internal/toReadonlyArray.test.ts ================================================ 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]) }) ================================================ FILE: src/core/utils/internal/toReadonlyArray.ts ================================================ /** * Creates an immutable copy of the given array. */ export function toReadonlyArray(source: Array): ReadonlyArray { const clone = [...source] as Array Object.freeze(clone) return clone } ================================================ FILE: src/core/utils/internal/tryCatch.test.ts ================================================ import { tryCatch } from './tryCatch' test('returns the function payload', () => { const result = tryCatch(() => 'hello') expect(result).toEqual('hello') }) test('silences exceptions by default', () => { const result = tryCatch(() => { throw new Error('Exception') }) expect(result).toBeUndefined() }) test('executes a custom callback function when an exception occurs', async () => { await new Promise((resolve) => { tryCatch( () => { throw new Error('Exception') }, (error) => { expect(error).toBeInstanceOf(Error) expect(error.message).toEqual('Exception') resolve() }, ) }) }) ================================================ FILE: src/core/utils/internal/tryCatch.ts ================================================ export function tryCatch any>( fn: Fn, onException?: (error: Error) => void, ): ReturnType | undefined { try { const result = fn() return result } catch (error) { onException?.(error as Error) } } ================================================ FILE: src/core/utils/logging/getStatusCodeColor.test.ts ================================================ import { getStatusCodeColor } from './getStatusCodeColor' test('returns a green color for status codes lower than 300', () => { expect(getStatusCodeColor(100)).toBe('#69AB32') expect(getStatusCodeColor(200)).toBe('#69AB32') expect(getStatusCodeColor(204)).toBe('#69AB32') }) test('returns a yellow color for status codes between 201 and 400', () => { expect(getStatusCodeColor(300)).toBe('#F0BB4B') expect(getStatusCodeColor(304)).toBe('#F0BB4B') }) test('returns a red color for status codes higher than 400', () => { expect(getStatusCodeColor(400)).toBe('#E95F5D') expect(getStatusCodeColor(404)).toBe('#E95F5D') expect(getStatusCodeColor(500)).toBe('#E95F5D') }) test('returns a red color for unknown status code', () => { expect(getStatusCodeColor(700)).toBe('#E95F5D') }) ================================================ FILE: src/core/utils/logging/getStatusCodeColor.ts ================================================ export enum StatusCodeColor { Success = '#69AB32', Warning = '#F0BB4B', Danger = '#E95F5D', } /** * Returns a HEX color for a given response status code number. */ export function getStatusCodeColor(status: number): StatusCodeColor { if (status < 300) { return StatusCodeColor.Success } if (status < 400) { return StatusCodeColor.Warning } return StatusCodeColor.Danger } ================================================ FILE: src/core/utils/logging/getTimestamp.test.ts ================================================ import { getTimestamp } from './getTimestamp' beforeAll(() => { vi.useFakeTimers() }) afterAll(() => { vi.useRealTimers() }) test('returns a timestamp string of the invocation time', () => { vi.setSystemTime(new Date('2024-01-01 12:4:8')) const timestamp = getTimestamp() expect(timestamp).toBe('12:04:08') }) test('returns a timestamp with milliseconds', () => { vi.setSystemTime(new Date('2024-01-01 12:4:8')) expect(getTimestamp({ milliseconds: true })).toBe('12:04:08.000') vi.setSystemTime(new Date('2024-01-01 12:4:8.000')) expect(getTimestamp({ milliseconds: true })).toBe('12:04:08.000') vi.setSystemTime(new Date('2024-01-01 12:4:8.4')) expect(getTimestamp({ milliseconds: true })).toBe('12:04:08.400') vi.setSystemTime(new Date('2024-01-01 12:4:8.123')) expect(getTimestamp({ milliseconds: true })).toBe('12:04:08.123') vi.setSystemTime(new Date('2024-01-01 12:00:00')) expect(getTimestamp({ milliseconds: true })).toBe('12:00:00.000') }) ================================================ FILE: src/core/utils/logging/getTimestamp.ts ================================================ interface GetTimestampOptions { milliseconds?: boolean } /** * Returns a timestamp string in a "HH:MM:SS" format. */ export function getTimestamp(options?: GetTimestampOptions): string { const now = new Date() const timestamp = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}` if (options?.milliseconds) { return `${timestamp}.${now.getMilliseconds().toString().padStart(3, '0')}` } return timestamp } ================================================ FILE: src/core/utils/logging/serializeRequest.test.ts ================================================ import { encodeBuffer } from '@mswjs/interceptors' import { serializeRequest } from './serializeRequest' test('serializes given Request instance into a plain object', async () => { const request = await serializeRequest( new Request(new URL('http://test.mswjs.io/user'), { method: 'POST', headers: new Headers({ 'Content-Type': 'text/plain', 'X-Header': 'secret', }), body: encodeBuffer('text-body'), }), ) expect(request.method).toBe('POST') expect(request.url.href).toBe('http://test.mswjs.io/user') expect(request.headers).toEqual({ 'content-type': 'text/plain', 'x-header': 'secret', }) expect(request.body).toBe('text-body') }) ================================================ FILE: src/core/utils/logging/serializeRequest.ts ================================================ export interface LoggedRequest { url: URL method: string headers: Record body: string } /** * Formats a mocked request for introspection in browser's console. */ export async function serializeRequest( request: Request, ): Promise { const requestClone = request.clone() const requestText = await requestClone.text() return { url: new URL(request.url), method: request.method, headers: Object.fromEntries(request.headers.entries()), body: requestText, } } ================================================ FILE: src/core/utils/logging/serializeResponse.test.ts ================================================ /** * @vitest-environment node */ import { encodeBuffer } from '@mswjs/interceptors' import { serializeResponse } from './serializeResponse' it('serializes response without body', async () => { const result = await serializeResponse(new Response(null)) expect(result.status).toBe(200) expect(result.statusText).toBe('OK') expect(result.headers).toEqual({}) expect(result.body).toBe('') }) it('serializes a plain text response', async () => { const result = await serializeResponse( new Response('hello world', { status: 201, statusText: 'Created', headers: { 'Content-Type': 'text/plain', }, }), ) expect(result.status).toBe(201) expect(result.statusText).toBe('Created') expect(result.headers).toEqual({ 'content-type': 'text/plain', }) expect(result.body).toBe('hello world') }) it('serializes a JSON response', async () => { const response = new Response(JSON.stringify({ users: ['John'] }), { headers: { 'Content-Type': 'application/json', }, }) const result = await serializeResponse(response) expect(result.headers).toEqual({ 'content-type': 'application/json', }) expect(result.body).toBe(JSON.stringify({ users: ['John'] })) }) it('serializes a ArrayBuffer response', async () => { const data = encodeBuffer('hello world') const response = new Response(data) const result = await serializeResponse(response) expect(result.body).toBe('hello world') }) it('serializes a Blob response', async () => { const response = new Response(new Blob(['hello world'])) const result = await serializeResponse(response) expect(result.body).toBe('hello world') }) it('serializes a FormData response', async () => { const data = new FormData() data.set('firstName', 'Alice') data.set('age', '32') const response = new Response(data) const result = await serializeResponse(response) expect(result.body).toContain( `Content-Disposition: form-data; name="firstName"\r\n\r\nAlice`, ) expect(result.body).toContain( `Content-Disposition: form-data; name="age"\r\n\r\n32`, ) }) ================================================ FILE: src/core/utils/logging/serializeResponse.ts ================================================ import statuses from '../../../shims/statuses' const { message } = statuses export interface SerializedResponse { status: number statusText: string headers: Record body: string } export async function serializeResponse( response: Response, ): Promise { const responseClone = response.clone() const responseText = await responseClone.text() // Normalize the response status and status text when logging // since the default Response instance doesn't infer status texts // from status codes. This has no effect on the actual response instance. const responseStatus = responseClone.status || 200 const responseStatusText = responseClone.statusText || message[responseStatus] || 'OK' return { status: responseStatus, statusText: responseStatusText, headers: Object.fromEntries(responseClone.headers.entries()), body: responseText, } } ================================================ FILE: src/core/utils/matching/matchRequestUrl.test.ts ================================================ /** * @vitest-environment jsdom */ import { coercePath, matchRequestUrl } from './matchRequestUrl' describe('matchRequestUrl', () => { test('returns true when matches against an exact URL', () => { expect( matchRequestUrl( new URL('https://test.mswjs.io'), 'https://test.mswjs.io', ), ).toEqual({ matches: true, params: {}, }) }) test('returns true when matched against a wildcard', () => { expect(matchRequestUrl(new URL('https://test.mswjs.io'), '*')).toEqual({ matches: true, params: { '0': 'https://test.mswjs.io/', }, }) }) test('returns true when matched against a RegExp', () => { expect( matchRequestUrl(new URL('https://test.mswjs.io'), /test\.mswjs\.io/), ).toEqual({ matches: true, params: {}, }) }) test('returns path parameters when matched', () => { expect( matchRequestUrl( new URL('https://test.mswjs.io/user/abc-123'), 'https://test.mswjs.io/user/:userId', ), ).toEqual({ matches: true, params: { userId: 'abc-123', }, }) }) test('decodes path parameters', () => { const url = 'http://example.com:5001/example' expect( matchRequestUrl( new URL(`https://test.mswjs.io/reflect-url/${encodeURIComponent(url)}`), 'https://test.mswjs.io/reflect-url/:url', ), ).toEqual({ matches: true, params: { url, }, }) }) test('returns false when does not match against the request URL', () => { expect( matchRequestUrl(new URL('https://test.mswjs.io'), 'https://google.com'), ).toEqual({ matches: false, params: {}, }) }) test('returns true when matching optional path parameters', () => { expect( matchRequestUrl( new URL('https://test.mswjs.io/user/123'), 'https://test.mswjs.io/user/:userId?', ), ).toEqual({ matches: true, params: { userId: '123', }, }) }) test('returns true for matching WebSocket URL', () => { expect( matchRequestUrl(new URL('ws://test.mswjs.io'), 'ws://test.mswjs.io'), ).toEqual({ matches: true, params: {}, }) expect( matchRequestUrl(new URL('wss://test.mswjs.io'), 'wss://test.mswjs.io'), ).toEqual({ matches: true, params: {}, }) }) test('returns false for non-matching WebSocket URL', () => { expect( matchRequestUrl(new URL('ws://test.mswjs.io'), 'ws://foo.mswjs.io'), ).toEqual({ matches: false, params: {}, }) expect( matchRequestUrl(new URL('wss://test.mswjs.io'), 'wss://completely.diff'), ).toEqual({ matches: false, params: {}, }) }) test('returns path parameters when matched a WebSocket URL', () => { expect( matchRequestUrl( new URL('wss://test.mswjs.io'), 'wss://:service.mswjs.io', ), ).toEqual({ matches: true, params: { service: 'test', }, }) }) }) describe('coercePath', () => { test('escapes the colon in protocol', () => { expect(coercePath('https://example.com')).toEqual('https\\://example.com') expect(coercePath('https://example.com/:userId')).toEqual( 'https\\://example.com/:userId', ) expect(coercePath('http://localhost:3000')).toEqual( 'http\\://localhost\\:3000', ) }) test('escapes the colon before the port number', () => { expect(coercePath('localhost:8080')).toEqual('localhost\\:8080') expect(coercePath('http://127.0.0.1:8080')).toEqual( 'http\\://127.0.0.1\\:8080', ) expect(coercePath('https://example.com:1234')).toEqual( 'https\\://example.com\\:1234', ) expect(coercePath('localhost:8080/:5678')).toEqual('localhost\\:8080/:5678') expect(coercePath('https://example.com:8080/:5678')).toEqual( 'https\\://example.com\\:8080/:5678', ) }) test('replaces wildcard with an unnnamed capturing group', () => { expect(coercePath('*')).toEqual('(.*)') expect(coercePath('**')).toEqual('(.*)') expect(coercePath('/us*')).toEqual('/us(.*)') expect(coercePath('/user/*')).toEqual('/user/(.*)') expect(coercePath('https://example.com/user/*')).toEqual( 'https\\://example.com/user/(.*)', ) expect(coercePath('https://example.com/us*')).toEqual( 'https\\://example.com/us(.*)', ) }) test('preserves path parameter modifiers', () => { expect(coercePath(':name*')).toEqual(':name*') expect(coercePath('/foo/:name*')).toEqual('/foo/:name*') expect(coercePath('/foo/**:name*')).toEqual('/foo/(.*):name*') expect(coercePath('**/foo/*/:name*')).toEqual('(.*)/foo/(.*)/:name*') expect(coercePath('/foo/:first/bar/:second*/*')).toEqual( '/foo/:first/bar/:second*/(.*)', ) }) }) ================================================ FILE: src/core/utils/matching/matchRequestUrl.ts ================================================ import { match } from 'path-to-regexp' import { getCleanUrl } from '@mswjs/interceptors' import { normalizePath } from './normalizePath' export type Path = string | RegExp export type PathParams = { [ParamName in KeyType]?: string | ReadonlyArray } export interface Match { matches: boolean params?: PathParams } /** * Coerce a path supported by MSW into a path * supported by "path-to-regexp". */ export function coercePath(path: string): string { return ( path /** * Replace wildcards ("*") with unnamed capturing groups * because "path-to-regexp" doesn't support wildcards. * Ignore path parameter' modifiers (i.e. ":name*"). */ .replace( /([:a-zA-Z_-]*)(\*{1,2})+/g, (_, parameterName: string | undefined, wildcard: string) => { const expression = '(.*)' if (!parameterName) { return expression } return parameterName.startsWith(':') ? `${parameterName}${wildcard}` : `${parameterName}${expression}` }, ) /** * Escape the port so that "path-to-regexp" can match * absolute URLs including port numbers. */ .replace(/([^/])(:)(?=\d+)/, '$1\\$2') /** * Escape the protocol so that "path-to-regexp" could match * absolute URL. * @see https://github.com/pillarjs/path-to-regexp/issues/259 */ .replace(/^([^/]+)(:)(?=\/\/)/, '$1\\$2') ) } /** * Returns the result of matching given request URL against a mask. */ export function matchRequestUrl(url: URL, path: Path, baseUrl?: string): Match { const normalizedPath = normalizePath(path, baseUrl) const cleanPath = typeof normalizedPath === 'string' ? coercePath(normalizedPath) : normalizedPath const cleanUrl = getCleanUrl(url) const result = match(cleanPath, { decode: decodeURIComponent })(cleanUrl) const params = (result && (result.params as PathParams)) || {} return { matches: result !== false, params, } } export function isPath(value: unknown): value is Path { return typeof value === 'string' || value instanceof RegExp } ================================================ FILE: src/core/utils/matching/normalizePath.node.test.ts ================================================ /** * @vitest-environment node */ import { normalizePath } from './normalizePath' test('returns RegExp as-is', () => { const path = /s/ expect(normalizePath(path)).toEqual(path) }) test('returns a clean absolute URL as-is', () => { const path = 'https://test.mswjs.io/path' expect(normalizePath(path)).toEqual(path) }) test('returns a relative URL as-is given a string path', () => { const path = '/relative/url' expect(normalizePath(path)).toEqual(path) }) test('rebases a relative URL against a custom base URL', () => { const path = '/relative/url' expect(normalizePath(path, 'https://test.mswjs.io')).toEqual( 'https://test.mswjs.io/relative/url', ) }) test('removes query parameters and hashes from an absolute URL', () => { expect(normalizePath('https://test.mswjs.io/user?query=123')).toEqual( 'https://test.mswjs.io/user', ) expect(normalizePath('https://test.mswjs.io/user#some')).toEqual( 'https://test.mswjs.io/user', ) expect(normalizePath('https://test.mswjs.io/user?query=123#some')).toEqual( 'https://test.mswjs.io/user', ) }) test('removes query parameters and hashes from a relative URL', () => { expect(normalizePath('/user?query=123')).toEqual('/user') expect(normalizePath('/user#some')).toEqual('/user') expect(normalizePath('/user?query=123#some')).toEqual('/user') }) ================================================ FILE: src/core/utils/matching/normalizePath.test.ts ================================================ /** * @vitest-environment jsdom */ import { normalizePath } from './normalizePath' test('returns RegExp as-is', () => { const path = /\/user\/(.+?)\// expect(normalizePath(path)).toEqual(path) }) test('returns a clean absolute URL as-is', () => { const path = 'https://test.mswjs.io/user' expect(normalizePath(path)).toEqual(path) }) test('rebases a relative URL against the current location', () => { const path = '/relative/url' expect(normalizePath(path)).toBe(`${location.origin}/relative/url`) }) test('rebases a relative URL against a custom base URL', () => { const path = '/relative/url' expect(normalizePath(path, 'https://test.mswjs.io')).toEqual( 'https://test.mswjs.io/relative/url', ) }) test('removes query parameters and hashes from an absolute URL', () => { const path = 'https://test.mswjs.io/user?query=123#some' expect(normalizePath(path)).toEqual('https://test.mswjs.io/user') }) test('removes query parameters and hashes from a relative URL', () => { expect(normalizePath('/user?query=123')).toEqual(`${location.origin}/user`) expect(normalizePath('/user#some')).toEqual(`${location.origin}/user`) expect(normalizePath('/user?query=123#some')).toEqual( `${location.origin}/user`, ) }) test('returns a path pattern string as-is', () => { expect(normalizePath(':api/user')).toEqual('http://localhost/:api/user') expect(normalizePath('*/resource/*')).toEqual('*/resource/*') }) test('removes query parameters and hashes from a path pattern string', () => { expect(normalizePath(':api/user?query=123#some')).toEqual( 'http://localhost/:api/user', ) }) test('preserves optional path parameters', () => { expect(normalizePath('/user/:userId?')).toEqual( 'http://localhost/user/:userId?', ) }) ================================================ FILE: src/core/utils/matching/normalizePath.ts ================================================ import type { Path } from './matchRequestUrl' import { cleanUrl } from '../url/cleanUrl' import { getAbsoluteUrl } from '../url/getAbsoluteUrl' /** * Normalizes a given request handler path: * - Preserves RegExp. * - Removes query parameters and hashes. * - Rebases relative URLs against the "baseUrl" or the current location. * - Preserves relative URLs in Node.js, unless specified otherwise. * - Preserves optional path parameters. */ export function normalizePath(path: Path, baseUrl?: string): Path { // RegExp paths do not need normalization. if (path instanceof RegExp) { return path } const maybeAbsoluteUrl = getAbsoluteUrl(path, baseUrl) return cleanUrl(maybeAbsoluteUrl) } ================================================ FILE: src/core/utils/request/getAllAcceptedMimeTypes.test.ts ================================================ import { getAllAcceptedMimeTypes } from './getAllAcceptedMimeTypes' it('returns an empty array for null accept header', () => { expect(getAllAcceptedMimeTypes(null)).toEqual([]) }) it('returns a single mime type as-is', () => { expect(getAllAcceptedMimeTypes('application/json')).toEqual([ 'application/json', ]) }) it('returns multiple mime types in order', () => { expect(getAllAcceptedMimeTypes('text/html, application/json')).toEqual([ 'text/html', 'application/json', ]) }) it('sorts by quality value (q parameter)', () => { expect( getAllAcceptedMimeTypes('text/plain;q=0.5, application/json;q=0.9'), ).toEqual(['application/json', 'text/plain']) }) it('excludes types with q=0', () => { expect(getAllAcceptedMimeTypes('text/html, text/plain;q=0')).toEqual([ 'text/html', ]) }) it('returns an empty array when all types have q=0', () => { expect(getAllAcceptedMimeTypes('text/html;q=0, text/plain;q=0')).toEqual([]) }) it('treats missing q as q=1 (default)', () => { expect(getAllAcceptedMimeTypes('text/plain;q=0.5, application/json')).toEqual( ['application/json', 'text/plain'], ) }) it('sorts by specificity when quality is equal (type/subtype > type/* > */*)', () => { expect(getAllAcceptedMimeTypes('*/*, text/*, text/html')).toEqual([ 'text/html', 'text/*', '*/*', ]) }) it('sorts by parameter count when quality and specificity are equal', () => { expect( getAllAcceptedMimeTypes( 'text/plain;format=fixed;charset=utf-8, text/plain;charset=utf-8', ), ).toEqual(['text/plain', 'text/plain']) }) it('applies full precedence: quality > specificity > parameter count', () => { expect( getAllAcceptedMimeTypes( 'text/*;q=0.8, application/json, text/html;q=0.8, */*;q=0.1', ), ).toEqual(['application/json', 'text/html', 'text/*', '*/*']) }) it('handles whitespace around values', () => { expect( getAllAcceptedMimeTypes(' text/html , application/json ; q=0.9 '), ).toEqual(['text/html', 'application/json']) }) it('handles a realistic browser accept header', () => { expect( getAllAcceptedMimeTypes( 'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8', ), ).toEqual(['text/html', 'application/xhtml+xml', 'application/xml', '*/*']) }) it('handles the graphql-over-http accept header', () => { expect( getAllAcceptedMimeTypes( 'application/graphql-response+json, application/json', ), ).toEqual(['application/graphql-response+json', 'application/json']) }) ================================================ FILE: src/core/utils/request/getAllAcceptedMimeTypes.ts ================================================ /** * Returns all accepted mime types, ordered by precedence as defined * in [RFC 7231 Section 5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2). * * Precedence rules (highest to lowest): * 1. Quality value (`q` parameter, default 1). * 2. Specificity: `type/subtype` > `type/*` > `*\/*`. * 3. Number of media type parameters (more = more specific). * * Types with `q=0` are excluded (explicitly not acceptable). */ export function getAllAcceptedMimeTypes( acceptHeader: string | null, ): Array { if (acceptHeader == null) { return [] } const accepted: Array<{ type: string quality: number specificity: number parameterCount: number }> = [] for (const part of acceptHeader.split(',')) { const [type, ...params] = part.split(';').map((v) => v.trim()) let quality = 1 let parameterCount = 0 for (const param of params) { const [key, value] = param.split('=').map((v) => v.trim()) if (key === 'q') { quality = Number(value) } else { parameterCount++ } } // RFC 7231: a quality value of 0 indicates "not acceptable". if (quality === 0) { continue } const [mediaType, mediaSubtype] = type.split('/') const specificity = mediaType === '*' ? 0 : mediaSubtype === '*' ? 1 : 2 accepted.push({ type, quality, specificity, parameterCount }) } if (!accepted.length) { return [] } return accepted .sort((left, right) => { if (right.quality !== left.quality) { return right.quality - left.quality } if (right.specificity !== left.specificity) { return right.specificity - left.specificity } return right.parameterCount - left.parameterCount }) .map((entry) => entry.type) } ================================================ FILE: src/core/utils/request/getRequestCookies.ts ================================================ import { parse as parseCookie, serialize as serializeCookie, } from '../../../shims/cookie' import { cookieStore } from '../cookieStore' function parseCookies(input: string): Record { const parsedCookies = parseCookie(input) const cookies: Record = {} for (const cookieName in parsedCookies) { if (typeof parsedCookies[cookieName] !== 'undefined') { cookies[cookieName] = parsedCookies[cookieName] } } return cookies } function getAllDocumentCookies() { return parseCookies(document.cookie) } function getDocumentCookies(request: Request): Record { if (typeof document === 'undefined' || typeof location === 'undefined') { return {} } switch (request.credentials) { case 'same-origin': { const requestUrl = new URL(request.url) // Return document cookies only when requested a resource // from the same origin as the current document. return location.origin === requestUrl.origin ? getAllDocumentCookies() : {} } case 'include': { // Return all document cookies. return getAllDocumentCookies() } default: { return {} } } } export function getAllRequestCookies(request: Request): Record { /** * @note While the "cookie" header is a forbidden header field * in the browser, you can read it in Node.js. We need to respect * it for mocking in Node.js. */ const requestCookieHeader = request.headers.get('cookie') const cookiesFromHeaders = requestCookieHeader ? parseCookies(requestCookieHeader) : {} const cookiesFromDocument = getDocumentCookies(request) // Forward the document cookies to the request headers. for (const name in cookiesFromDocument) { request.headers.append( 'cookie', serializeCookie(name, cookiesFromDocument[name]), ) } const cookiesFromStore = cookieStore.getCookies(request.url) const storedCookiesObject = Object.fromEntries( cookiesFromStore.map((cookie) => [cookie.key, cookie.value]), ) // Forward the raw stored cookies to request headers // so they contain metadata like "expires", "secure", etc. for (const cookie of cookiesFromStore) { request.headers.append('cookie', cookie.toString()) } return { ...cookiesFromDocument, ...storedCookiesObject, ...cookiesFromHeaders, } } ================================================ FILE: src/core/utils/request/onUnhandledRequest.node.test.ts ================================================ // @vitest-environment node import { onUnhandledRequest } from './onUnhandledRequest' const fixtures = { warningWithoutSuggestions: (url = `/api`) => `\ [MSW] Warning: intercepted a request without a matching request handler: • GET ${url} If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`, warningWithResponseBody: (url = `/api`) => `\ [MSW] Warning: intercepted a request without a matching request handler: • POST ${url} • Request body: {"variables":{"id":"abc-123"},"query":"query UserName($id: String!) { user(id: $id) { name } }"} If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`, errorWithoutSuggestions: `\ [MSW] Error: intercepted a request without a matching request handler: • GET /api If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`, } beforeAll(() => { vi.spyOn(console, 'warn').mockImplementation(() => void 0) vi.spyOn(console, 'error').mockImplementation(() => void 0) }) afterEach(() => { vi.clearAllMocks() }) afterAll(() => { vi.restoreAllMocks() }) test('prints with an absolute URL and search params', async () => { await onUnhandledRequest( new Request(new URL('https://mswjs.io/api?foo=boo')), 'warn', ) expect(console.warn).toHaveBeenCalledWith( fixtures.warningWithoutSuggestions(`https://mswjs.io/api?foo=boo`), ) await onUnhandledRequest( new Request(new URL('http://localhost/api?foo=boo')), 'warn', ) expect(console.warn).toHaveBeenCalledWith( fixtures.warningWithoutSuggestions(`http://localhost/api?foo=boo`), ) }) ================================================ FILE: src/core/utils/request/onUnhandledRequest.test.ts ================================================ // @vitest-environment jsdom import { onUnhandledRequest, UnhandledRequestCallback, } from './onUnhandledRequest' const fixtures = { warningWithoutSuggestions: (url = `/api`) => `\ [MSW] Warning: intercepted a request without a matching request handler: • GET ${url} If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`, warningWithResponseBody: (url = `/api`) => `\ [MSW] Warning: intercepted a request without a matching request handler: • POST ${url} • Request body: {"variables":{"id":"abc-123"},"query":"query UserName($id: String!) { user(id: $id) { name } }"} If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`, errorWithoutSuggestions: `\ [MSW] Error: intercepted a request without a matching request handler: • GET /api If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`, } beforeEach(() => { vi.spyOn(console, 'warn').mockImplementation(() => void 0) vi.spyOn(console, 'error').mockImplementation(() => void 0) }) afterEach(() => { vi.restoreAllMocks() }) test('supports the "bypass" request strategy', async () => { await onUnhandledRequest( new Request(new URL('http://localhost/api')), 'bypass', ) expect(console.warn).not.toHaveBeenCalled() expect(console.error).not.toHaveBeenCalled() }) test('supports the "warn" request strategy', async () => { await onUnhandledRequest(new Request(new URL('http://localhost/api')), 'warn') expect(console.warn).toHaveBeenCalledWith( fixtures.warningWithoutSuggestions(), ) }) test('supports the "warn" request strategy with request body', async () => { await onUnhandledRequest( new Request(new URL('http://localhost/api'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ variables: { id: 'abc-123', }, query: 'query UserName($id: String!) { user(id: $id) { name } }', }), }), ) expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithResponseBody()) }) test('supports the "error" request strategy', async () => { await expect( onUnhandledRequest(new Request(new URL('http://localhost/api')), 'error'), ).rejects.toThrow( '[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.', ) expect(console.error).toHaveBeenCalledWith(fixtures.errorWithoutSuggestions) }) test('supports a custom callback function', async () => { const callback = vi.fn((request) => { console.warn(`callback: ${request.method} ${request.url}`) }) const request = new Request(new URL('/user', 'http://localhost:3000')) await onUnhandledRequest(request, callback) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith(request, { warning: expect.any(Function), error: expect.any(Function), }) // Check that the custom logic in the callback was called. expect(console.warn).toHaveBeenCalledWith( `callback: GET http://localhost:3000/user`, ) }) test('supports calling default strategies from the custom callback function', async () => { const callback = vi.fn((request, print) => { // Call the default "error" strategy. print.error() }) const request = new Request(new URL('http://localhost/api')) await expect(onUnhandledRequest(request, callback)).rejects.toThrow( `[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.`, ) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith(request, { warning: expect.any(Function), error: expect.any(Function), }) // Check that the default strategy was called. expect(console.error).toHaveBeenCalledWith(fixtures.errorWithoutSuggestions) }) test('does not print any suggestions given no handlers to suggest', async () => { await onUnhandledRequest(new Request(new URL('http://localhost/api')), 'warn') expect(console.warn).toHaveBeenCalledWith( fixtures.warningWithoutSuggestions(), ) }) test('throws an exception given unknown request strategy', async () => { await expect( onUnhandledRequest( new Request(new URL('http://localhost/api')), // @ts-expect-error Intentional unknown strategy. 'invalid-strategy', ), ).rejects.toThrow( '[MSW] Failed to react to an unhandled request: unknown strategy "invalid-strategy". Please provide one of the supported strategies ("bypass", "warn", "error") or a custom callback function as the value of the "onUnhandledRequest" option.', ) }) test('prints with a relative URL and search params', async () => { await onUnhandledRequest( new Request(new URL('http://localhost/api?foo=boo')), 'warn', ) expect(console.warn).toHaveBeenCalledWith( fixtures.warningWithoutSuggestions(`/api?foo=boo`), ) }) test('prints with an absolute URL and search params', async () => { await onUnhandledRequest( new Request(new URL('https://mswjs.io/api?foo=boo')), 'warn', ) expect(console.warn).toHaveBeenCalledWith( fixtures.warningWithoutSuggestions(`https://mswjs.io/api?foo=boo`), ) }) test('ignores common static assets when using the "warn" strategy', async () => { await Promise.allSettled([ onUnhandledRequest( new Request(new URL('https://example.com/main.css')), 'warn', ), onUnhandledRequest( new Request(new URL('https://example.com/index.mjs')), 'warn', ), onUnhandledRequest( new Request(new URL('https://example.com/node_modules/abc-123')), 'warn', ), onUnhandledRequest( new Request(new URL('https://fonts.googleapis.com/some-font')), 'warn', ), ]) expect(console.warn).not.toHaveBeenCalled() }) test('ignores common static assets when using the "error" strategy', async () => { await Promise.allSettled([ onUnhandledRequest( new Request(new URL('https://example.com/main.css')), 'error', ), onUnhandledRequest( new Request(new URL('https://example.com/index.mjs')), 'error', ), onUnhandledRequest( new Request(new URL('https://example.com/node_modules/abc-123')), 'error', ), onUnhandledRequest( new Request(new URL('https://fonts.googleapis.com/some-font')), 'error', ), ]) expect(console.error).not.toHaveBeenCalled() }) test('exposes common static assets to the explicit callback', async () => { let callbackRequest!: Request await onUnhandledRequest( new Request(new URL('https://example.com/main.css')), (request) => { callbackRequest = request }, ) expect(callbackRequest).toBeInstanceOf(Request) expect(callbackRequest.url).toBe('https://example.com/main.css') }) ================================================ FILE: src/core/utils/request/onUnhandledRequest.ts ================================================ import { toPublicUrl } from './toPublicUrl' import { InternalError, devUtils } from '../internal/devUtils' import { isCommonAssetRequest } from '../../isCommonAssetRequest' export interface UnhandledRequestPrint { warning(): void error(): void } export type UnhandledRequestCallback = ( request: Request, print: UnhandledRequestPrint, ) => void export type UnhandledRequestStrategy = | 'bypass' | 'warn' | 'error' | UnhandledRequestCallback export async function onUnhandledRequest( request: Request, strategy: UnhandledRequestStrategy = 'warn', ): Promise { const url = new URL(request.url) const publicUrl = toPublicUrl(url) + url.search const requestBody = request.method === 'HEAD' || request.method === 'GET' ? null : await request.clone().text() const messageDetails = `\n\n \u2022 ${request.method} ${publicUrl}\n\n${requestBody ? ` \u2022 Request body: ${requestBody}\n\n` : ''}` const unhandledRequestMessage = `intercepted a request without a matching request handler:${messageDetails}If you still wish to intercept this unhandled request, please create a request handler for it.\nRead more: https://mswjs.io/docs/http/intercepting-requests` function applyStrategy(strategy: UnhandledRequestStrategy) { switch (strategy) { case 'error': { // Print a developer-friendly error. devUtils.error('Error: %s', unhandledRequestMessage) // Throw an exception to halt request processing and not perform the original request. throw new InternalError( devUtils.formatMessage( 'Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.', ), ) } case 'warn': { devUtils.warn('Warning: %s', unhandledRequestMessage) break } case 'bypass': break default: throw new InternalError( devUtils.formatMessage( 'Failed to react to an unhandled request: unknown strategy "%s". Please provide one of the supported strategies ("bypass", "warn", "error") or a custom callback function as the value of the "onUnhandledRequest" option.', strategy, ), ) } } if (typeof strategy === 'function') { strategy(request, { warning: applyStrategy.bind(null, 'warn'), error: applyStrategy.bind(null, 'error'), }) return } // Ignore common static asset requests when using a built-in strategy. // There's a slight overhead here because this utility will create a request URL // instance again despite us having done so previously in this function. if (!isCommonAssetRequest(request)) { applyStrategy(strategy) } } ================================================ FILE: src/core/utils/request/storeResponseCookies.ts ================================================ import { cookieStore } from '../cookieStore' import { kSetCookie } from '../HttpResponse/decorators' export async function storeResponseCookies( request: Request, response: Response, ): Promise { // Grab the raw "Set-Cookie" response header provided // in the HeadersInit for this mocked response. const responseCookies = Reflect.get(response, kSetCookie) as | string | undefined if (responseCookies) { await cookieStore.setCookie(responseCookies, request.url) } } ================================================ FILE: src/core/utils/request/toPublicUrl.node.test.ts ================================================ // @vitest-environment node import { toPublicUrl } from './toPublicUrl' test('returns an absolute request URL without search params', () => { expect(toPublicUrl(new URL('https://test.mswjs.io/path'))).toBe( 'https://test.mswjs.io/path', ) expect(toPublicUrl(new URL('http://192.168.0.10/path'))).toBe( 'http://192.168.0.10/path', ) expect( toPublicUrl(new URL('http://localhost/path?foo=bar')), 'Must not return relative URL in Node.js', ).toBe('http://localhost/path') }) ================================================ FILE: src/core/utils/request/toPublicUrl.test.ts ================================================ // @vitest-environment jsdom import { toPublicUrl } from './toPublicUrl' test('returns an absolute request URL without search params', () => { expect(toPublicUrl(new URL('https://test.mswjs.io/path'))).toBe( 'https://test.mswjs.io/path', ) expect(toPublicUrl(new URL('http://localhost/path'))).toBe('/path') expect(toPublicUrl(new URL('http://localhost/path?foo=bar'))).toBe('/path') }) it('returns a relative URL given the request to the same origin', () => { expect(toPublicUrl('http://localhost/user')).toBe('/user') }) ================================================ FILE: src/core/utils/request/toPublicUrl.ts ================================================ /** * Returns a relative URL if the given request URL is relative * to the current origin. Otherwise returns an absolute URL. */ export function toPublicUrl(url: string | URL): string { const urlInstance = url instanceof URL ? url : new URL(url) if ( typeof location !== 'undefined' && urlInstance.origin === location.origin ) { return urlInstance.pathname } return urlInstance.origin + urlInstance.pathname } ================================================ FILE: src/core/utils/toResponseInit.ts ================================================ export function toResponseInit(response: Response): ResponseInit { return { status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()), } } ================================================ FILE: src/core/utils/url/cleanUrl.test.ts ================================================ import { cleanUrl } from './cleanUrl' it('removes query parameters from a URL string', () => { expect(cleanUrl('/user?id=123')).toEqual('/user') expect(cleanUrl('/user?id=123&id=456')).toEqual('/user') expect(cleanUrl('/user?id=123&role=admin')).toEqual('/user') }) it('removes hashes from a URL string', () => { expect(cleanUrl('/user#hash')).toEqual('/user') expect(cleanUrl('/user#hash-with-dashes')).toEqual('/user') }) it('removes both query parameters and hashes from a URL string', () => { expect(cleanUrl('/user?id=123#some')).toEqual('/user') expect(cleanUrl('/user?id=123&role=admin#some')).toEqual('/user') }) it('preserves optional path parameters', () => { expect(cleanUrl('/user/:id?')).toEqual('/user/:id?') expect(cleanUrl('/user/:id?/:messageId?')).toEqual('/user/:id?/:messageId?') }) ================================================ FILE: src/core/utils/url/cleanUrl.ts ================================================ const REDUNDANT_CHARACTERS_EXP = /[?|#].*$/g /** * Removes search parameters and the fragment * from a given URL string. */ export function cleanUrl(path: string): string { // If the path ends with an optional path parameter, // return it as-is. if (path.endsWith('?')) { return path } // Otherwise, remove the search and fragment from it. return path.replace(REDUNDANT_CHARACTERS_EXP, '') } ================================================ FILE: src/core/utils/url/getAbsoluteUrl.node.test.ts ================================================ /** * @vitest-environment node */ import { getAbsoluteUrl } from './getAbsoluteUrl' it('returns a given relative URL as-is', () => { expect(getAbsoluteUrl('/reviews')).toBe('/reviews') }) it('rebases a relative URL against a custom base URL', () => { expect(getAbsoluteUrl('/user', 'https://api.github.com')).toEqual( 'https://api.github.com/user', ) }) it('returns a given absolute URL as-is', () => { expect(getAbsoluteUrl('https://api.mswjs.io/users')).toBe( 'https://api.mswjs.io/users', ) }) ================================================ FILE: src/core/utils/url/getAbsoluteUrl.test.ts ================================================ // @vitest-environment jsdom import { getAbsoluteUrl } from './getAbsoluteUrl' const rawLocation = window.location afterAll(() => { Object.defineProperty(window, 'location', { value: rawLocation, }) }) it('resolves a relative URL against the current location (default)', () => { expect(getAbsoluteUrl('/reviews')).toBe('http://localhost/reviews') }) it('supports relative URLs starting with search parameters', () => { Object.defineProperty(window, 'location', { value: { href: 'http://localhost/nested', }, }) expect(getAbsoluteUrl('?resourceId=abc-123')).toBe( 'http://localhost/nested?resourceId=abc-123', ) }) it('resolves a relative URL against a custom base URL', () => { expect(getAbsoluteUrl('/user', 'https://api.github.com')).toBe( 'https://api.github.com/user', ) }) it('returns a given absolute URL as-is', () => { expect(getAbsoluteUrl('https://api.mswjs.io/users')).toBe( 'https://api.mswjs.io/users', ) }) it('returns an absolute URL given a relative path without a leading slash', () => { expect(getAbsoluteUrl('users')).toBe('http://localhost/users') }) it('returns a path with a pattern as-is', () => { expect(getAbsoluteUrl(':api/user')).toBe('http://localhost/:api/user') expect(getAbsoluteUrl('*/resource/*')).toBe('*/resource/*') }) ================================================ FILE: src/core/utils/url/getAbsoluteUrl.ts ================================================ import { isAbsoluteUrl } from './isAbsoluteUrl' /** * Returns an absolute URL based on the given path. */ export function getAbsoluteUrl(path: string, baseUrl?: string): string { // already absolute URL if (isAbsoluteUrl(path)) { return path } // Ignore path with pattern start with * if (path.startsWith('*')) { return path } // Resolve a relative request URL against a given custom "baseUrl" // or the document baseURI (in the case of browser/browser-like environments). const origin = baseUrl || (typeof location !== 'undefined' && location.href) return origin ? // Encode and decode the path to preserve escaped characters. decodeURI(new URL(encodeURI(path), origin).href) : path } ================================================ FILE: src/core/utils/url/isAbsoluteUrl.test.ts ================================================ /** * @vitest-environment node */ import { isAbsoluteUrl } from './isAbsoluteUrl' it('returns true for the "http" scheme', () => { expect(isAbsoluteUrl('http://www.domain.com')).toEqual(true) }) it('returns true for the "https" scheme', () => { expect(isAbsoluteUrl('https://www.domain.com')).toEqual(true) }) it('returns true for the "ws" scheme', () => { expect(isAbsoluteUrl('ws://www.domain.com')).toEqual(true) }) it('returns true for the "ftp" scheme', () => { expect(isAbsoluteUrl('ftp://www.domain.com')).toEqual(true) }) it('returns true for the custom scheme', () => { expect(isAbsoluteUrl('web+my://www.example.com')).toEqual(true) }) it('returns false for the relative URL', () => { expect(isAbsoluteUrl('/test')).toEqual(false) }) it('returns false for the relative URL without a leading slash', () => { expect(isAbsoluteUrl('test')).toEqual(false) }) ================================================ FILE: src/core/utils/url/isAbsoluteUrl.ts ================================================ /** * Determines if the given URL string is an absolute URL. */ export function isAbsoluteUrl(url: string): boolean { return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url) } ================================================ FILE: src/core/ws/WebSocketClientManager.test.ts ================================================ // @vitest-environment node-websocket import { setMaxListeners } from 'node:events' import { WebSocketClientConnection, WebSocketData, WebSocketTransport, } from '@mswjs/interceptors/WebSocket' import { WebSocketClientManager, WebSocketBroadcastChannelMessage, } from './WebSocketClientManager' const channel = new BroadcastChannel('test:channel') /** * @note Increase the number of maximum event listeners * because the same channel is shared between different * manager instances in different tests. */ setMaxListeners(Number.MAX_SAFE_INTEGER, channel) vi.spyOn(channel, 'postMessage') const socket = new WebSocket('ws://localhost') class TestWebSocketTransport extends EventTarget implements WebSocketTransport { send(_data: WebSocketData): void {} close(_code?: number | undefined, _reason?: string | undefined): void {} } afterEach(() => { vi.resetAllMocks() }) it('adds a client from this runtime to the list of clients', async () => { const manager = new WebSocketClientManager(channel) const connection = new WebSocketClientConnection( socket, new TestWebSocketTransport(), ) await manager.addConnection(connection) // Must add the client to the list of clients. expect(Array.from(manager.clients.values())).toEqual([connection]) }) it('adds multiple clients from this runtime to the list of clients', async () => { const manager = new WebSocketClientManager(channel) const connectionOne = new WebSocketClientConnection( socket, new TestWebSocketTransport(), ) await manager.addConnection(connectionOne) // Must add the client to the list of clients. expect(Array.from(manager.clients.values())).toEqual([connectionOne]) const connectionTwo = new WebSocketClientConnection( socket, new TestWebSocketTransport(), ) await manager.addConnection(connectionTwo) // Must add the new client to the list as well. expect(Array.from(manager.clients.values())).toEqual([ connectionOne, connectionTwo, ]) }) it('replays a "send" event coming from another runtime', async () => { const manager = new WebSocketClientManager(channel) const connection = new WebSocketClientConnection( socket, new TestWebSocketTransport(), ) await manager.addConnection(connection) vi.spyOn(connection, 'send') // Emulate another runtime signaling this connection to receive data. channel.dispatchEvent( new MessageEvent('message', { data: { type: 'extraneous:send', payload: { clientId: connection.id, data: 'hello', }, }, }), ) await vi.waitFor(() => { // Must execute the requested operation on the connection. expect(connection.send).toHaveBeenCalledWith('hello') expect(connection.send).toHaveBeenCalledTimes(1) }) }) it('replays a "close" event coming from another runtime', async () => { const manager = new WebSocketClientManager(channel) const connection = new WebSocketClientConnection( socket, new TestWebSocketTransport(), ) await manager.addConnection(connection) vi.spyOn(connection, 'close') // Emulate another runtime signaling this connection to close. channel.dispatchEvent( new MessageEvent('message', { data: { type: 'extraneous:close', payload: { clientId: connection.id, code: 1000, reason: 'Normal closure', }, }, }), ) await vi.waitFor(() => { // Must execute the requested operation on the connection. expect(connection.close).toHaveBeenCalledWith(1000, 'Normal closure') expect(connection.close).toHaveBeenCalledTimes(1) }) }) it('removes the extraneous message listener when the connection closes', async () => { const manager = new WebSocketClientManager(channel) const transport = new TestWebSocketTransport() const connection = new WebSocketClientConnection(socket, transport) vi.spyOn(connection, 'close').mockImplementationOnce(() => { /** * @note This is a nasty hack so we don't have to uncouple * the connection from transport. Creating a mock transport * is difficult because it relies on the `WebSocketOverride` class. * All we care here is that closing the connection triggers * the transport closure, which it always does. */ transport.dispatchEvent(new Event('close')) }) vi.spyOn(connection, 'send') await manager.addConnection(connection) connection.close() // Signals from other runtimes have no effect on the closed connection. channel.dispatchEvent( new MessageEvent('message', { data: { type: 'extraneous:send', payload: { clientId: connection.id, data: 'hello', }, }, }), ) expect(connection.send).not.toHaveBeenCalled() }) ================================================ FILE: src/core/ws/WebSocketClientManager.ts ================================================ import type { WebSocketData, WebSocketClientConnectionProtocol, WebSocketClientEventMap, } from '@mswjs/interceptors/WebSocket' import { WebSocketClientStore } from './WebSocketClientStore' import { WebSocketMemoryClientStore } from './WebSocketMemoryClientStore' import { WebSocketIndexedDBClientStore } from './WebSocketIndexedDBClientStore' export type WebSocketBroadcastChannelMessage = | { type: 'extraneous:send' payload: { clientId: string data: WebSocketData } } | { type: 'extraneous:close' payload: { clientId: string code?: number reason?: string } } /** * A manager responsible for accumulating WebSocket client * connections across different browser runtimes. */ export class WebSocketClientManager { private store: WebSocketClientStore private runtimeClients: Map private allClients: Set constructor(private channel: BroadcastChannel) { // Store the clients in the IndexedDB in the browser, // otherwise, store the clients in memory. this.store = typeof indexedDB !== 'undefined' ? new WebSocketIndexedDBClientStore() : new WebSocketMemoryClientStore() this.runtimeClients = new Map() this.allClients = new Set() this.channel.addEventListener('message', (message) => { if (message.data?.type === 'db:update') { this.flushDatabaseToMemory() } }) if (typeof window !== 'undefined') { window.addEventListener('message', async (message) => { if (message.data?.type === 'msw/worker:stop') { await this.removeRuntimeClients() } }) } } private async flushDatabaseToMemory() { const storedClients = await this.store.getAll() this.allClients = new Set( storedClients.map((client) => { const runtimeClient = this.runtimeClients.get(client.id) /** * @note For clients originating in this runtime, use their * direct references. No need to wrap them in a remote connection. */ if (runtimeClient) { return runtimeClient } return new WebSocketRemoteClientConnection( client.id, new URL(client.url), this.channel, ) }), ) } private async removeRuntimeClients(): Promise { await this.store.deleteMany(Array.from(this.runtimeClients.keys())) this.runtimeClients.clear() await this.flushDatabaseToMemory() this.notifyOthersAboutDatabaseUpdate() } /** * All active WebSocket client connections. */ get clients(): Set { return this.allClients } /** * Notify other runtimes about the database update * using the shared `BroadcastChannel` instance. */ private notifyOthersAboutDatabaseUpdate(): void { this.channel.postMessage({ type: 'db:update' }) } private async addClient( client: WebSocketClientConnectionProtocol, ): Promise { await this.store.add(client) // Sync the in-memory clients in this runtime with the // updated database. This pulls in all the stored clients. await this.flushDatabaseToMemory() this.notifyOthersAboutDatabaseUpdate() } /** * Adds the given `WebSocket` client connection to the set * of all connections. The given connection is always the complete * connection object because `addConnection()` is called only * for the opened connections in the same runtime. */ public async addConnection( client: WebSocketClientConnectionProtocol, ): Promise { // Store this client in the map of clients created in this runtime. // This way, the manager can distinguish between this runtime clients // and extraneous runtime clients when synchronizing clients storage. this.runtimeClients.set(client.id, client) // Add the new client to the storage. await this.addClient(client) // Handle the incoming BroadcastChannel messages from other runtimes // that attempt to control this runtime (via a remote connection wrapper). // E.g. another runtime calling `client.send()` for the client in this runtime. const handleExtraneousMessage = ( message: MessageEvent, ) => { const { type, payload } = message.data // Ignore broadcasted messages for other clients. if ( typeof payload === 'object' && 'clientId' in payload && payload.clientId !== client.id ) { return } switch (type) { case 'extraneous:send': { client.send(payload.data) break } case 'extraneous:close': { client.close(payload.code, payload.reason) break } } } const abortController = new AbortController() this.channel.addEventListener('message', handleExtraneousMessage, { signal: abortController.signal, }) // Once closed, this connection cannot be operated on. // This must include the extraneous runtimes as well. client.addEventListener('close', () => abortController.abort(), { once: true, }) } } /** * A wrapper class to operate with WebSocket client connections * from other runtimes. This class maintains 1-1 public API * compatibility to the `WebSocketClientConnection` but relies * on the given `BroadcastChannel` to communicate instructions * with the client connections from other runtimes. */ export class WebSocketRemoteClientConnection implements WebSocketClientConnectionProtocol { constructor( public readonly id: string, public readonly url: URL, private channel: BroadcastChannel, ) {} send(data: WebSocketData): void { this.channel.postMessage({ type: 'extraneous:send', payload: { clientId: this.id, data, }, } as WebSocketBroadcastChannelMessage) } close(code?: number | undefined, reason?: string | undefined): void { this.channel.postMessage({ type: 'extraneous:close', payload: { clientId: this.id, code, reason, }, } as WebSocketBroadcastChannelMessage) } addEventListener( _type: EventType, _listener: ( this: WebSocket, event: WebSocketClientEventMap[EventType], ) => void, _options?: AddEventListenerOptions | boolean, ): void { throw new Error( 'WebSocketRemoteClientConnection.addEventListener is not supported', ) } removeEventListener( _event: EventType, _listener: ( this: WebSocket, event: WebSocketClientEventMap[EventType], ) => void, _options?: EventListenerOptions | boolean, ): void { throw new Error( 'WebSocketRemoteClientConnection.removeEventListener is not supported', ) } } ================================================ FILE: src/core/ws/WebSocketClientStore.ts ================================================ import type { WebSocketClientConnectionProtocol } from '@mswjs/interceptors/WebSocket' export interface SerializedWebSocketClient { id: string url: string } export abstract class WebSocketClientStore { public abstract add(client: WebSocketClientConnectionProtocol): Promise public abstract getAll(): Promise> public abstract deleteMany(clientIds: Array): Promise } ================================================ FILE: src/core/ws/WebSocketIndexedDBClientStore.ts ================================================ import { DeferredPromise } from '@open-draft/deferred-promise' import { WebSocketClientConnectionProtocol } from '@mswjs/interceptors/WebSocket' import { type SerializedWebSocketClient, WebSocketClientStore, } from './WebSocketClientStore' const DB_NAME = 'msw-websocket-clients' const DB_STORE_NAME = 'clients' export class WebSocketIndexedDBClientStore implements WebSocketClientStore { private db: Promise constructor() { this.db = this.createDatabase() } public async add(client: WebSocketClientConnectionProtocol): Promise { const promise = new DeferredPromise() const store = await this.getStore() /** * @note Use `.put()` instead of `.add()` to allow setting clients * that already exist in the database. This can happen if a single page * has multiple event handlers. Each handler will receive the "connection" * event in parallel, and try to set that WebSocket client in the database. */ const request = store.put({ id: client.id, url: client.url.href, } satisfies SerializedWebSocketClient) request.onsuccess = () => { promise.resolve() } request.onerror = () => { console.error(request.error) promise.reject( new Error( `Failed to add WebSocket client "${client.id}". There is likely an additional output above.`, ), ) } return promise } public async getAll(): Promise> { const promise = new DeferredPromise>() const store = await this.getStore() const request = store.getAll() as IDBRequest< Array > request.onsuccess = () => { promise.resolve(request.result) } request.onerror = () => { console.error(request.error) promise.reject( new Error( `Failed to get all WebSocket clients. There is likely an additional output above.`, ), ) } return promise } public async deleteMany(clientIds: Array): Promise { const promise = new DeferredPromise() const store = await this.getStore() for (const clientId of clientIds) { store.delete(clientId) } store.transaction.oncomplete = () => { promise.resolve() } store.transaction.onerror = () => { console.error(store.transaction.error) promise.reject( new Error( `Failed to delete WebSocket clients [${clientIds.join(', ')}]. There is likely an additional output above.`, ), ) } return promise } private async createDatabase(): Promise { const promise = new DeferredPromise() const request = indexedDB.open(DB_NAME, 1) request.onsuccess = ({ currentTarget }) => { const db = Reflect.get(currentTarget!, 'result') as IDBDatabase if (db.objectStoreNames.contains(DB_STORE_NAME)) { return promise.resolve(db) } } request.onupgradeneeded = async ({ currentTarget }) => { const db = Reflect.get(currentTarget!, 'result') as IDBDatabase if (db.objectStoreNames.contains(DB_STORE_NAME)) { return } const store = db.createObjectStore(DB_STORE_NAME, { keyPath: 'id' }) store.transaction.oncomplete = () => { promise.resolve(db) } store.transaction.onerror = () => { console.error(store.transaction.error) promise.reject( new Error( 'Failed to create WebSocket client store. There is likely an additional output above.', ), ) } } request.onerror = () => { console.error(request.error) promise.reject( new Error( 'Failed to open an IndexedDB database. There is likely an additional output above.', ), ) } return promise } private async getStore(): Promise { const db = await this.db return db.transaction(DB_STORE_NAME, 'readwrite').objectStore(DB_STORE_NAME) } } ================================================ FILE: src/core/ws/WebSocketMemoryClientStore.ts ================================================ import { WebSocketClientConnectionProtocol } from '@mswjs/interceptors/WebSocket' import { SerializedWebSocketClient, WebSocketClientStore, } from './WebSocketClientStore' export class WebSocketMemoryClientStore implements WebSocketClientStore { private store: Map constructor() { this.store = new Map() } public async add(client: WebSocketClientConnectionProtocol): Promise { this.store.set(client.id, { id: client.id, url: client.url.href }) } public getAll(): Promise> { return Promise.resolve(Array.from(this.store.values())) } public async deleteMany(clientIds: Array): Promise { for (const clientId of clientIds) { this.store.delete(clientId) } } } ================================================ FILE: src/core/ws/handleWebSocketEvent.ts ================================================ import type { WebSocketConnectionData } from '@mswjs/interceptors/WebSocket' import { RequestHandler } from '../handlers/RequestHandler' import { WebSocketHandler } from '../handlers/WebSocketHandler' import { webSocketInterceptor } from './webSocketInterceptor' import { onUnhandledRequest, UnhandledRequestStrategy, } from '../utils/request/onUnhandledRequest' import { isHandlerKind } from '../utils/internal/isHandlerKind' interface HandleWebSocketEventOptions { getUnhandledRequestStrategy: () => UnhandledRequestStrategy getHandlers: () => Array onMockedConnection: (connection: WebSocketConnectionData) => void onPassthroughConnection: (onnection: WebSocketConnectionData) => void } export function handleWebSocketEvent(options: HandleWebSocketEventOptions) { webSocketInterceptor.on('connection', async (connection) => { const handlers = options.getHandlers().filter(isHandlerKind('EventHandler')) // Ignore this connection if the user hasn't defined any handlers. if (handlers.length > 0) { options?.onMockedConnection(connection) await Promise.all( handlers.map((handler) => { // Iterate over the handlers and forward the connection // event to WebSocket event handlers. This is equivalent // to dispatching that event onto multiple listeners. return handler.run(connection) }), ) return } // Construct a request representing this WebSocket connection. const request = new Request(connection.client.url, { headers: { upgrade: 'websocket', connection: 'upgrade', }, }) await onUnhandledRequest( request, options.getUnhandledRequestStrategy(), ).catch((error) => { const errorEvent = new Event('error') Object.defineProperty(errorEvent, 'cause', { enumerable: true, configurable: false, value: error, }) connection.client.socket.dispatchEvent(errorEvent) }) options?.onPassthroughConnection(connection) // If none of the "ws" handlers matched, // establish the WebSocket connection as-is. connection.server.connect() }) } ================================================ FILE: src/core/ws/utils/attachWebSocketLogger.ts ================================================ import type { WebSocketClientConnection, WebSocketConnectionData, WebSocketData, } from '@mswjs/interceptors/WebSocket' import { devUtils } from '../../utils/internal/devUtils' import { getTimestamp } from '../../utils/logging/getTimestamp' import { toPublicUrl } from '../../utils/request/toPublicUrl' import { getMessageLength } from './getMessageLength' import { getPublicData } from './getPublicData' export const colors = { system: '#3b82f6', outgoing: '#22c55e', incoming: '#ef4444', mocked: '#ff6a33', } export function attachWebSocketLogger( connection: WebSocketConnectionData, ): void { const { client, server } = connection logConnectionOpen(client) // Log the events sent from the WebSocket client. // WebSocket client connection object is written from the // server's perspective so these message events are outgoing. /** * @todo Provide the reference to the exact event handler * that called this `client.send()`. */ client.addEventListener('message', (event) => { logOutgoingClientMessage(event) }) client.addEventListener('close', (event) => { logConnectionClose(event) }) // Log client errors (connection closures due to errors). client.socket.addEventListener('error', (event) => { logClientError(event) }) client.send = new Proxy(client.send, { apply(target, thisArg, args) { const [data] = args const messageEvent = new MessageEvent('message', { data }) Object.defineProperties(messageEvent, { currentTarget: { enumerable: true, writable: false, value: client.socket, }, target: { enumerable: true, writable: false, value: client.socket, }, }) queueMicrotask(() => { logIncomingMockedClientMessage(messageEvent) }) return Reflect.apply(target, thisArg, args) }, }) server.addEventListener( 'open', () => { server.addEventListener('message', (event) => { logIncomingServerMessage(event) }) }, { once: true }, ) // Log outgoing client events initiated by the event handler. // The actual client never sent these but the handler did. server.send = new Proxy(server.send, { apply(target, thisArg, args) { const [data] = args const messageEvent = new MessageEvent('message', { data }) Object.defineProperties(messageEvent, { currentTarget: { enumerable: true, writable: false, value: server.socket, }, target: { enumerable: true, writable: false, value: server.socket, }, }) logOutgoingMockedClientMessage(messageEvent) return Reflect.apply(target, thisArg, args) }, }) } /** * Prints the WebSocket connection. * This is meant to be logged by every WebSocket handler * that intercepted this connection. This helps you see * what handlers observe this connection. */ export function logConnectionOpen(client: WebSocketClientConnection) { const publicUrl = toPublicUrl(client.url) console.groupCollapsed( devUtils.formatMessage(`${getTimestamp()} %c▶%c ${publicUrl}`), `color:${colors.system}`, 'color:inherit', ) // eslint-disable-next-line no-console console.log('Client:', client.socket) console.groupEnd() } function logConnectionClose(event: CloseEvent) { const target = event.target as WebSocket const publicUrl = toPublicUrl(target.url) console.groupCollapsed( devUtils.formatMessage( `${getTimestamp({ milliseconds: true })} %c■%c ${publicUrl}`, ), `color:${colors.system}`, 'color:inherit', ) // eslint-disable-next-line no-console console.log(event) console.groupEnd() } function logClientError(event: Event) { const socket = event.target as WebSocket const publicUrl = toPublicUrl(socket.url) console.groupCollapsed( devUtils.formatMessage( `${getTimestamp({ milliseconds: true })} %c\u00D7%c ${publicUrl}`, ), `color:${colors.system}`, 'color:inherit', ) // eslint-disable-next-line no-console console.log(event) console.groupEnd() } /** * Prints the outgoing client message. */ async function logOutgoingClientMessage(event: MessageEvent) { const byteLength = getMessageLength(event.data) const publicData = await getPublicData(event.data) const arrow = event.defaultPrevented ? '⇡' : '⬆' console.groupCollapsed( devUtils.formatMessage( `${getTimestamp({ milliseconds: true })} %c${arrow}%c ${publicData} %c${byteLength}%c`, ), `color:${colors.outgoing}`, 'color:inherit', 'color:gray;font-weight:normal', 'color:inherit;font-weight:inherit', ) // eslint-disable-next-line no-console console.log(event) console.groupEnd() } /** * Prints the outgoing client message initiated * by `server.send()` in the event handler. */ async function logOutgoingMockedClientMessage( event: MessageEvent, ) { const byteLength = getMessageLength(event.data) const publicData = await getPublicData(event.data) console.groupCollapsed( devUtils.formatMessage( `${getTimestamp({ milliseconds: true })} %c⬆%c ${publicData} %c${byteLength}%c`, ), `color:${colors.mocked}`, 'color:inherit', 'color:gray;font-weight:normal', 'color:inherit;font-weight:inherit', ) // eslint-disable-next-line no-console console.log(event) console.groupEnd() } /** * Prints the outgoing client message initiated * by `client.send()` in the event handler. */ async function logIncomingMockedClientMessage( event: MessageEvent, ) { const byteLength = getMessageLength(event.data) const publicData = await getPublicData(event.data) console.groupCollapsed( devUtils.formatMessage( `${getTimestamp({ milliseconds: true })} %c⬇%c ${publicData} %c${byteLength}%c`, ), `color:${colors.mocked}`, 'color:inherit', 'color:gray;font-weight:normal', 'color:inherit;font-weight:inherit', ) // eslint-disable-next-line no-console console.log(event) console.groupEnd() } async function logIncomingServerMessage(event: MessageEvent) { const byteLength = getMessageLength(event.data) const publicData = await getPublicData(event.data) const arrow = event.defaultPrevented ? '⇣' : '⬇' console.groupCollapsed( devUtils.formatMessage( `${getTimestamp({ milliseconds: true })} %c${arrow}%c ${publicData} %c${byteLength}%c`, ), `color:${colors.incoming}`, 'color:inherit', 'color:gray;font-weight:normal', 'color:inherit;font-weight:inherit', ) // eslint-disable-next-line no-console console.log(event) console.groupEnd() } ================================================ FILE: src/core/ws/utils/getMessageLength.test.ts ================================================ import { getMessageLength } from './getMessageLength' it('returns the length of the string', () => { expect(getMessageLength('')).toBe(0) expect(getMessageLength('hello')).toBe(5) }) it('returns the size of the Blob', () => { expect(getMessageLength(new Blob())).toBe(0) expect(getMessageLength(new Blob(['hello']))).toBe(5) }) it('returns the byte length of ArrayBuffer', () => { expect(getMessageLength(new ArrayBuffer(0))).toBe(0) expect(getMessageLength(new ArrayBuffer(5))).toBe(5) }) ================================================ FILE: src/core/ws/utils/getMessageLength.ts ================================================ import type { WebSocketData } from '@mswjs/interceptors/WebSocket' import { isObject } from '../../utils/internal/isObject' /** * Returns the byte length of the given WebSocket message. * @example * getMessageLength('hello') // 5 * getMessageLength(new Blob(['hello'])) // 5 */ export function getMessageLength(data: WebSocketData): number { if (data instanceof Blob) { return data.size } if (isObject(data) && 'byteLength' in data) { return data.byteLength } return new Blob([data as any]).size } ================================================ FILE: src/core/ws/utils/getPublicData.test.ts ================================================ import { getPublicData } from './getPublicData' it('returns a short string as-is', async () => { expect(await getPublicData('')).toBe('') expect(await getPublicData('hello')).toBe('hello') }) it('returns a truncated long string', async () => { expect(await getPublicData('this is a very long string')).toBe( 'this is a very long stri…', ) }) it('returns a short Blob text as-is', async () => { expect(await getPublicData(new Blob(['']))).toBe('Blob()') expect(await getPublicData(new Blob(['hello']))).toBe('Blob(hello)') }) it('returns a truncated long Blob text', async () => { expect(await getPublicData(new Blob(['this is a very long string']))).toBe( 'Blob(this is a very long stri…)', ) }) it('returns a short ArrayBuffer text as-is', async () => { expect(await getPublicData(new TextEncoder().encode(''))).toBe( 'ArrayBuffer()', ) expect(await getPublicData(new TextEncoder().encode('hello'))).toBe( 'ArrayBuffer(hello)', ) }) it('returns a truncated ArrayBuffer text', async () => { expect( await getPublicData(new TextEncoder().encode('this is a very long string')), ).toBe('ArrayBuffer(this is a very long stri…)') }) ================================================ FILE: src/core/ws/utils/getPublicData.ts ================================================ import type { WebSocketData } from '@mswjs/interceptors/WebSocket' import { isObject } from '../../utils/internal/isObject' import { truncateMessage } from './truncateMessage' export async function getPublicData(data: WebSocketData): Promise { if (data instanceof Blob) { const text = await data.text() return `Blob(${truncateMessage(text)})` } // Handle all ArrayBuffer-like objects. if (isObject(data)) { const text = new TextDecoder().decode(data as ArrayBuffer) return `ArrayBuffer(${truncateMessage(text)})` } return truncateMessage(data) } ================================================ FILE: src/core/ws/utils/truncateMessage.test.ts ================================================ import { truncateMessage } from './truncateMessage' it('returns a short string as-is', () => { expect(truncateMessage('')).toBe('') expect(truncateMessage('hello')).toBe('hello') }) it('truncates a long string', () => { expect(truncateMessage('this is a very long string')).toBe( 'this is a very long stri…', ) }) ================================================ FILE: src/core/ws/utils/truncateMessage.ts ================================================ const MAX_LENGTH = 24 export function truncateMessage(message: string): string { if (message.length <= MAX_LENGTH) { return message } return `${message.slice(0, MAX_LENGTH)}…` } ================================================ FILE: src/core/ws/webSocketInterceptor.ts ================================================ import { WebSocketInterceptor } from '@mswjs/interceptors/WebSocket' export const webSocketInterceptor = new WebSocketInterceptor() ================================================ FILE: src/core/ws.test.ts ================================================ /** * @vitest-environment node-websocket */ import { ws } from './ws' it('exports the "link()" method', () => { expect(ws).toHaveProperty('link') expect(ws.link).toBeInstanceOf(Function) }) it('throws an error when calling "ws.link()" without a URL argument', () => { expect(() => // @ts-expect-error Intentionally invalid call. ws.link(), ).toThrow('Expected a WebSocket server URL but got undefined') }) it('throws an error when given a non-path argument to "ws.link()"', () => { expect(() => // @ts-expect-error Intentionally invalid argument. ws.link(2), ).toThrow('Expected a WebSocket server URL to be a valid path but got number') }) ================================================ FILE: src/core/ws.ts ================================================ import { invariant } from 'outvariant' import type { WebSocketData, WebSocketClientConnectionProtocol, } from '@mswjs/interceptors/WebSocket' import { WebSocketHandler, kEmitter, type WebSocketHandlerEventMap, } from './handlers/WebSocketHandler' import { hasRefCounted } from './utils/internal/hasRefCounted' import { Path, isPath } from './utils/matching/matchRequestUrl' import { WebSocketClientManager } from './ws/WebSocketClientManager' const webSocketChannel = new BroadcastChannel('msw:websocket-client-manager') if (hasRefCounted(webSocketChannel)) { // Allows the Node.js thread to exit if it is the only active handle in the event system. // https://nodejs.org/api/worker_threads.html#broadcastchannelunref webSocketChannel.unref() } export type WebSocketEventListener< EventType extends keyof WebSocketHandlerEventMap, > = (...args: WebSocketHandlerEventMap[EventType]) => void export type WebSocketLink = { /** * A set of all WebSocket clients connected * to this link. * * @see {@link https://mswjs.io/docs/api/ws#clients `clients` API reference} */ clients: Set /** * Adds an event listener to this WebSocket link. * * @example * const chat = ws.link('wss://chat.example.com') * chat.addEventListener('connection', listener) * * @see {@link https://mswjs.io/docs/api/ws#onevent-listener `on()` API reference} */ addEventListener( event: EventType, listener: WebSocketEventListener, ): WebSocketHandler /** * Broadcasts the given data to all WebSocket clients. * * @example * const service = ws.link('wss://example.com') * service.addEventListener('connection', () => { * service.broadcast('hello, everyone!') * }) * * @see {@link https://mswjs.io/docs/api/ws#broadcastdata `broadcast()` API reference} */ broadcast(data: WebSocketData): void /** * Broadcasts the given data to all WebSocket clients * except the ones provided in the `clients` argument. * * @example * const service = ws.link('wss://example.com') * service.addEventListener('connection', ({ client }) => { * service.broadcastExcept(client, 'hi, the rest of you!') * }) * * @see {@link https://mswjs.io/docs/api/ws#broadcastexceptclients-data `broadcast()` API reference} */ broadcastExcept( clients: | WebSocketClientConnectionProtocol | Array, data: WebSocketData, ): void } /** * Intercepts outgoing WebSocket connections to the given URL. * * @example * const chat = ws.link('wss://chat.example.com') * chat.addEventListener('connection', ({ client }) => { * client.send('hello from server!') * }) */ function createWebSocketLinkHandler(url: Path): WebSocketLink { invariant(url, 'Expected a WebSocket server URL but got undefined') invariant( isPath(url), 'Expected a WebSocket server URL to be a valid path but got %s', typeof url, ) const clientManager = new WebSocketClientManager(webSocketChannel) return { get clients() { return clientManager.clients }, addEventListener(event, listener) { const handler = new WebSocketHandler(url) // Add the connection event listener for when the // handler matches and emits a connection event. // When that happens, store that connection in the // set of all connections for reference. handler[kEmitter].on('connection', async ({ client }) => { await clientManager.addConnection(client) }) // The "handleWebSocketEvent" function will invoke // the "run()" method on the WebSocketHandler. // If the handler matches, it will emit the "connection" // event. Attach the user-defined listener to that event. handler[kEmitter].on(event, listener) return handler }, broadcast(data) { // This will invoke "send()" on the immediate clients // in this runtime and post a message to the broadcast channel // to trigger send for the clients in other runtimes. this.broadcastExcept([], data) }, broadcastExcept(clients, data) { const ignoreClients = Array.prototype .concat(clients) .map((client) => client.id) clientManager.clients.forEach((otherClient) => { if (!ignoreClients.includes(otherClient.id)) { otherClient.send(data) } }) }, } } /** * A namespace to intercept and mock WebSocket connections. * * @example * const chat = ws.link('wss://chat.example.com') * * @see {@link https://mswjs.io/docs/api/ws `ws` API reference} * @see {@link https://mswjs.io/docs/basics/handling-websocket-events Handling WebSocket events} */ export const ws = { link: createWebSocketLinkHandler, } export { WebSocketData } ================================================ FILE: src/iife/index.ts ================================================ export * from '~/core' export * from '../browser' ================================================ FILE: src/mockServiceWorker.js ================================================ /* eslint-disable */ /* tslint:disable */ /** * Mock Service Worker. * @see https://github.com/mswjs/msw * - Please do NOT modify this file. */ const PACKAGE_VERSION = '' const INTEGRITY_CHECKSUM = '' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() addEventListener('install', function () { self.skipWaiting() }) addEventListener('activate', function (event) { event.waitUntil(self.clients.claim()) }) addEventListener('message', async function (event) { const clientId = Reflect.get(event.source || {}, 'id') if (!clientId || !self.clients) { return } const client = await self.clients.get(clientId) if (!client) { return } const allClients = await self.clients.matchAll({ type: 'window', }) switch (event.data) { case 'KEEPALIVE_REQUEST': { sendToClient(client, { type: 'KEEPALIVE_RESPONSE', }) break } case 'INTEGRITY_CHECK_REQUEST': { sendToClient(client, { type: 'INTEGRITY_CHECK_RESPONSE', payload: { packageVersion: PACKAGE_VERSION, checksum: INTEGRITY_CHECKSUM, }, }) break } case 'MOCK_ACTIVATE': { activeClientIds.add(clientId) sendToClient(client, { type: 'MOCKING_ENABLED', payload: { client: { id: client.id, frameType: client.frameType, }, }, }) break } case 'CLIENT_CLOSED': { activeClientIds.delete(clientId) const remainingClients = allClients.filter((client) => { return client.id !== clientId }) // Unregister itself when there are no more clients if (remainingClients.length === 0) { self.registration.unregister() } break } } }) addEventListener('fetch', function (event) { const requestInterceptedAt = Date.now() // Bypass navigation requests. if (event.request.mode === 'navigate') { return } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. if ( event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin' ) { return } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests // after it's been terminated (still remains active until the next reload). if (activeClientIds.size === 0) { return } const requestId = crypto.randomUUID() event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) }) /** * @param {FetchEvent} event * @param {string} requestId * @param {number} requestInterceptedAt */ async function handleRequest(event, requestId, requestInterceptedAt) { const client = await resolveMainClient(event) const requestCloneForEvents = event.request.clone() const response = await getResponse( event, client, requestId, requestInterceptedAt, ) // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { const serializedRequest = await serializeRequest(requestCloneForEvents) // Clone the response so both the client and the library could consume it. const responseClone = response.clone() sendToClient( client, { type: 'RESPONSE', payload: { isMockedResponse: IS_MOCKED_RESPONSE in response, request: { id: requestId, ...serializedRequest, }, response: { type: responseClone.type, status: responseClone.status, statusText: responseClone.statusText, headers: Object.fromEntries(responseClone.headers.entries()), body: responseClone.body, }, }, }, responseClone.body ? [serializedRequest.body, responseClone.body] : [], ) } return response } /** * Resolve the main client for the given event. * Client that issues a request doesn't necessarily equal the client * that registered the worker. It's with the latter the worker should * communicate with during the response resolving phase. * @param {FetchEvent} event * @returns {Promise} */ async function resolveMainClient(event) { const client = await self.clients.get(event.clientId) if (activeClientIds.has(event.clientId)) { return client } if (client?.frameType === 'top-level') { return client } const allClients = await self.clients.matchAll({ type: 'window', }) return allClients .filter((client) => { // Get only those clients that are currently visible. return client.visibilityState === 'visible' }) .find((client) => { // Find the client ID that's recorded in the // set of clients that have registered the worker. return activeClientIds.has(client.id) }) } /** * @param {FetchEvent} event * @param {Client | undefined} client * @param {string} requestId * @param {number} requestInterceptedAt * @returns {Promise} */ async function getResponse(event, client, requestId, requestInterceptedAt) { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). const requestClone = event.request.clone() function passthrough() { // Cast the request headers to a new Headers instance // so the headers can be manipulated with. const headers = new Headers(requestClone.headers) // Remove the "accept" header value that marked this request as passthrough. // This prevents request alteration and also keeps it compliant with the // user-defined CORS policies. const acceptHeader = headers.get('accept') if (acceptHeader) { const values = acceptHeader.split(',').map((value) => value.trim()) const filteredValues = values.filter( (value) => value !== 'msw/passthrough', ) if (filteredValues.length > 0) { headers.set('accept', filteredValues.join(', ')) } else { headers.delete('accept') } } return fetch(requestClone, { headers }) } // Bypass mocking when the client is not active. if (!client) { return passthrough() } // Bypass initial page load requests (i.e. static assets). // The absence of the immediate/parent client in the map of the active clients // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { return passthrough() } // Notify the client that a request has been intercepted. const serializedRequest = await serializeRequest(event.request) const clientMessage = await sendToClient( client, { type: 'REQUEST', payload: { id: requestId, interceptedAt: requestInterceptedAt, ...serializedRequest, }, }, [serializedRequest.body], ) switch (clientMessage.type) { case 'MOCK_RESPONSE': { return respondWithMock(clientMessage.data) } case 'PASSTHROUGH': { return passthrough() } } return passthrough() } /** * @param {Client} client * @param {any} message * @param {Array} transferrables * @returns {Promise} */ function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { const channel = new MessageChannel() channel.port1.onmessage = (event) => { if (event.data && event.data.error) { return reject(event.data.error) } resolve(event.data) } client.postMessage(message, [ channel.port2, ...transferrables.filter(Boolean), ]) }) } /** * @param {Response} response * @returns {Response} */ function respondWithMock(response) { // Setting response status code to 0 is a no-op. // However, when responding with a "Response.error()", the produced Response // instance will have status code set to 0. Since it's not possible to create // a Response instance with status code 0, handle that use-case separately. if (response.status === 0) { return Response.error() } const mockedResponse = new Response(response.body, response) Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { value: true, enumerable: true, }) return mockedResponse } /** * @param {Request} request */ async function serializeRequest(request) { return { url: request.url, mode: request.mode, method: request.method, headers: Object.fromEntries(request.headers.entries()), cache: request.cache, credentials: request.credentials, destination: request.destination, integrity: request.integrity, redirect: request.redirect, referrer: request.referrer, referrerPolicy: request.referrerPolicy, body: await request.arrayBuffer(), keepalive: request.keepalive, } } ================================================ FILE: src/native/index.ts ================================================ import { FetchInterceptor } from '@mswjs/interceptors/fetch' import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest' import type { RequestHandler } from '~/core/handlers/RequestHandler' import { SetupServerCommonApi } from '../node/SetupServerCommonApi' /** * 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()` API reference} */ export function setupServer( ...handlers: Array ): SetupServerCommonApi { // Provision request interception via patching the `XMLHttpRequest` class only // in React Native. There is no `http`/`https` modules in that environment. return new SetupServerCommonApi( [new FetchInterceptor(), new XMLHttpRequestInterceptor()], handlers, ) } ================================================ FILE: src/node/SetupServerApi.ts ================================================ import { AsyncLocalStorage } from 'node:async_hooks' import type { HttpRequestEventMap, Interceptor } from '@mswjs/interceptors' import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest' import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest' import { FetchInterceptor } from '@mswjs/interceptors/fetch' import { HandlersController } from '~/core/SetupApi' import type { RequestHandler } from '~/core/handlers/RequestHandler' import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler' import type { SetupServer } from './glossary' import { SetupServerCommonApi } from './SetupServerCommonApi' const store = new AsyncLocalStorage() type RequestHandlersContext = { initialHandlers: Array handlers: Array } /** * A handlers controller that utilizes `AsyncLocalStorage` in Node.js * to prevent the request handlers list from being a shared state * across multiple tests. */ class AsyncHandlersController implements HandlersController { private rootContext: RequestHandlersContext constructor(initialHandlers: Array) { this.rootContext = { initialHandlers, handlers: [] } } get context(): RequestHandlersContext { return store.getStore() || this.rootContext } public prepend(runtimeHandlers: Array) { this.context.handlers.unshift(...runtimeHandlers) } public reset(nextHandlers: Array) { const context = this.context context.handlers = [] context.initialHandlers = nextHandlers.length > 0 ? nextHandlers : context.initialHandlers } public currentHandlers(): Array { const { initialHandlers, handlers } = this.context return handlers.concat(initialHandlers) } } export class SetupServerApi extends SetupServerCommonApi implements SetupServer { constructor( handlers: Array, interceptors: Array> = [ new ClientRequestInterceptor(), new XMLHttpRequestInterceptor(), new FetchInterceptor(), ], ) { super(interceptors, handlers) this.handlersController = new AsyncHandlersController(handlers) } public boundary, R>( callback: (...args: Args) => R, ): (...args: Args) => R { return (...args: Args): R => { return store.run( { initialHandlers: this.handlersController.currentHandlers(), handlers: [], }, callback, ...args, ) } } public close(): void { super.close() store.disable() } } ================================================ FILE: src/node/SetupServerCommonApi.ts ================================================ /** * @note This API is extended by both "msw/node" and "msw/native" * so be minding about the things you import! */ import type { RequiredDeep } from 'type-fest' import { invariant } from 'outvariant' import { BatchInterceptor, InterceptorReadyState, type HttpRequestEventMap, type Interceptor, } from '@mswjs/interceptors' import type { LifeCycleEventsMap, SharedOptions } from '~/core/sharedOptions' import { SetupApi } from '~/core/SetupApi' import { handleRequest } from '~/core/utils/handleRequest' import type { RequestHandler } from '~/core/handlers/RequestHandler' import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler' import { mergeRight } from '~/core/utils/internal/mergeRight' import { InternalError, devUtils } from '~/core/utils/internal/devUtils' import type { SetupServerCommon } from './glossary' import { handleWebSocketEvent } from '~/core/ws/handleWebSocketEvent' import { webSocketInterceptor } from '~/core/ws/webSocketInterceptor' import { isHandlerKind } from '~/core/utils/internal/isHandlerKind' const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { onUnhandledRequest: 'warn', } export class SetupServerCommonApi extends SetupApi implements SetupServerCommon { protected readonly interceptor: BatchInterceptor< Array>, HttpRequestEventMap > private resolvedOptions: RequiredDeep constructor( interceptors: Array>, handlers: Array, ) { super(...handlers) this.interceptor = new BatchInterceptor({ name: 'setup-server', interceptors, }) this.resolvedOptions = {} as RequiredDeep } /** * Subscribe to all requests that are using the interceptor object */ private init(): void { this.interceptor.on( 'request', async ({ request, requestId, controller }) => { const response = await handleRequest( request, requestId, this.handlersController .currentHandlers() .filter(isHandlerKind('RequestHandler')), this.resolvedOptions, this.emitter, { onPassthroughResponse(request) { const acceptHeader = request.headers.get('accept') /** * @note Remove the internal bypass request header. * In the browser, this is done by the worker script. * In Node.js, it has to be done here. */ if (acceptHeader) { const nextAcceptHeader = acceptHeader.replace( /(,\s+)?msw\/passthrough/, '', ) if (nextAcceptHeader) { request.headers.set('accept', nextAcceptHeader) } else { request.headers.delete('accept') } } }, }, ) if (response) { controller.respondWith(response) } return }, ) this.interceptor.on('unhandledException', ({ error }) => { if (error instanceof InternalError) { throw error } }) this.interceptor.on( 'response', ({ response, isMockedResponse, request, requestId }) => { this.emitter.emit( isMockedResponse ? 'response:mocked' : 'response:bypass', { response, request, requestId, }, ) }, ) // Preconfigure the WebSocket interception but don't enable it just yet. // It will be enabled when the server starts. handleWebSocketEvent({ getUnhandledRequestStrategy: () => { return this.resolvedOptions.onUnhandledRequest }, getHandlers: () => { return this.handlersController.currentHandlers() }, onMockedConnection: () => {}, onPassthroughConnection: () => {}, }) } public listen(options: Partial = {}): void { this.resolvedOptions = mergeRight( DEFAULT_LISTEN_OPTIONS, options, ) as RequiredDeep // Apply the interceptor when starting the server. // Attach the event listeners to the interceptor here // so they get re-attached whenever `.listen()` is called. this.interceptor.apply() this.init() this.subscriptions.push(() => this.interceptor.dispose()) // Apply the WebSocket interception. webSocketInterceptor.apply() this.subscriptions.push(() => webSocketInterceptor.dispose()) // 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 close(): void { this.dispose() } } ================================================ FILE: src/node/glossary.ts ================================================ import type { PartialDeep } from 'type-fest' import type { RequestHandler } from '~/core/handlers/RequestHandler' import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler' import type { LifeCycleEventEmitter, LifeCycleEventsMap, SharedOptions, } from '~/core/sharedOptions' export interface SetupServerCommon { /** * Starts requests interception based on the previously provided request handlers. * * @see {@link https://mswjs.io/docs/api/setup-server/listen `server.listen()` API reference} */ listen(options?: PartialDeep): void /** * Stops requests interception by restoring all augmented modules. * * @see {@link https://mswjs.io/docs/api/setup-server/close `server.close()` API reference} */ close(): void /** * Prepends given request handlers to the list of existing handlers. * * @see {@link https://mswjs.io/docs/api/setup-server/use `server.use()` API reference} */ use(...handlers: Array): void /** * Marks all request handlers that respond using `res.once()` as unused. * * @see {@link https://mswjs.io/docs/api/setup-server/restore-handlers `server.restore-handlers()` API reference} */ restoreHandlers(): void /** * Resets request handlers to the initial list given to the `setupServer` call, or to the explicit next request handlers list, if given. * * @see {@link https://mswjs.io/docs/api/setup-server/reset-handlers `server.reset-handlers()` API reference} */ resetHandlers(...nextHandlers: Array): void /** * Returns a readonly list of currently active request handlers. * * @see {@link https://mswjs.io/docs/api/setup-server/list-handlers `server.listHandlers()` API reference} */ listHandlers(): ReadonlyArray /** * Life-cycle events. * Life-cycle events allow you to subscribe to the internal library events occurring during the request/response handling. * * @see {@link https://mswjs.io/docs/api/life-cycle-events Life-cycle Events API reference} */ events: LifeCycleEventEmitter } export interface SetupServer extends SetupServerCommon { /** * Wraps the given function in a boundary. Any changes to the * network behavior (e.g. adding runtime request handlers via * `server.use()`) will be scoped to this boundary only. * @param callback A function to run (e.g. a test) * * @see {@link https://mswjs.io/docs/api/setup-server/boundary `server.boundary()` API reference} */ boundary, R>( callback: (...args: Args) => R, ): (...args: Args) => R } ================================================ FILE: src/node/index.ts ================================================ export type { SetupServer } from './glossary' export { SetupServerApi } from './SetupServerApi' export { setupServer } from './setupServer' ================================================ FILE: src/node/setupServer.ts ================================================ import type { RequestHandler } from '~/core/handlers/RequestHandler' import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler' import { SetupServerApi } from './SetupServerApi' /** * Sets up a requests interception in Node.js with the given request handlers. * @param {RequestHandler[]} handlers List of request handlers. * * @see {@link https://mswjs.io/docs/api/setup-server `setupServer()` API reference} */ export const setupServer = ( ...handlers: Array ): SetupServerApi => { return new SetupServerApi(handlers) } ================================================ FILE: src/package.json ================================================ { "type": "module" } ================================================ FILE: src/shims/cookie.ts ================================================ import * as allCookie from 'cookie' const cookie = (allCookie as any).default || allCookie export const parse = cookie.parse as typeof import('cookie').parse export const serialize = cookie.serialize as typeof import('cookie').serialize export default cookie ================================================ FILE: src/shims/statuses.ts ================================================ import * as allStatuses from 'statuses' const statuses = (allStatuses as any).default || allStatuses export const message = statuses.message as typeof import('statuses').message export default statuses ================================================ FILE: src/tsconfig.core.build.json ================================================ { "extends": "./tsconfig.src.json", "compilerOptions": { "composite": false } } ================================================ FILE: src/tsconfig.node.build.json ================================================ { "extends": "./tsconfig.node.json", "compilerOptions": { "composite": false } } ================================================ FILE: src/tsconfig.node.json ================================================ { "extends": "./tsconfig.src.json", "compilerOptions": { "types": ["node"] }, "include": ["./node", "./native"], "exclude": ["**/*.test.ts"] } ================================================ FILE: src/tsconfig.src.json ================================================ { // Common configuration for everything // living in the "src" directory. "extends": "../tsconfig.base.json", "compilerOptions": { "composite": true }, "include": ["../global.d.ts", "./**/*.ts"], "exclude": ["./**/*.test.ts"] } ================================================ FILE: src/tsconfig.worker.json ================================================ { "include": ["./mockServiceWorker.js"], "compilerOptions": { "strict": true, "allowJs": true, "checkJs": true, "noEmit": true, "target": "esnext", "module": "esnext", "lib": ["esnext"], "types": ["@types/serviceworker"] } } ================================================ FILE: test/README.md ================================================ # Integration tests This directory contains a list of integration tests for the Mock Service Worker library. Our integration tests are example-driven, meaning that each test features a specific usage example and asserts its behavior. We use the [`page-with`](https://github.com/kettanaito/page-with) package to compile and run a usage example within an automated browser environment. This directory categorizes all test suites based on the library's execution or API domain: - `/rest-api`, tests for RESTful API mocking. - `/graphql-api`, rests for GraphQL API mocking. - `/msw-api`, tests for the library's API. ## Test structure Example-driven test consists of two parts: - `*.mocks.ts`, a usage example and also a code snippet to test. - `*.test.ts`, an actual test suite for Vitest. ## Contributing Please see the [Contribution guidelines](/CONTRIBUTING.md) for the instructions on how to run and add new tests. Thank you. ================================================ FILE: test/browser/graphql-api/anonymous-operation.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start() Object.assign(window, { msw: { worker, graphql, HttpResponse, }, }) ================================================ FILE: test/browser/graphql-api/anonymous-operation.test.ts ================================================ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' import { waitFor } from '../../support/waitFor' declare namespace window { export const msw: { worker: import('msw/browser').SetupWorkerApi graphql: typeof import('msw').graphql HttpResponse: typeof import('msw').HttpResponse } } const httpServer = new HttpServer((app) => { app.post('/graphql', (req, res) => { res.json({ data: { user: { id: 'abc-123', }, }, }) }) }) test.beforeAll(async () => { await httpServer.listen() }) test.afterAll(async () => { await httpServer.close() }) test('does not warn on anonymous GraphQL operation when no GraphQL handlers are present', async ({ loadExample, query, spyOnConsole, }) => { await loadExample(new URL('./anonymous-operation.mocks.ts', import.meta.url)) const consoleSpy = spyOnConsole() const endpointUrl = httpServer.http.url('/graphql') const response = await query(endpointUrl, { // Intentionally anonymous query. query: gql` query { user { id } } `, }) const json = await response.json() // Must get the original server response. expect(json).toEqual({ data: { user: { id: 'abc-123', }, }, }) await waitFor(() => { // Must print a generic unhandled GraphQL request warning. // This has nothing to do with the operation being anonymous. expect(consoleSpy.get('warning')).toEqual([ `\ [MSW] Warning: intercepted a request without a matching request handler: • POST ${endpointUrl} • Request body: {"query":"\\n query {\\n user {\\n id\\n }\\n }\\n "} If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`, ]) }) }) test('warns on handled anonymous GraphQL operation', async ({ loadExample, query, spyOnConsole, page, }) => { await loadExample(new URL('./anonymous-operation.mocks.ts', import.meta.url)) const consoleSpy = spyOnConsole() await page.evaluate(() => { const { worker, graphql, HttpResponse } = window.msw worker.use( // This handler will have no effect on the anonymous operation performed. graphql.query('IrrelevantQuery', () => { return HttpResponse.json({ data: { user: { id: 'mocked-123', }, }, }) }), ) }) const endpointUrl = httpServer.http.url('/graphql') const response = await query(endpointUrl, { // Intentionally anonymous query. // It will be handled in the "graphql.operation()" handler above. query: gql` query { user { id } } `, }) const json = await response.json() // Must get the original response because the "graphql.query()" // handler won't match an anonymous GraphQL operation. expect(json).toEqual({ data: { user: { id: 'abc-123', }, }, }) // Must print the warning because an anonymous operation has been performed. await waitFor(() => { expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ `[MSW] Failed to intercept a GraphQL request at "POST ${endpointUrl}": anonymous GraphQL operations are not supported. Consider naming this operation or using "graphql.operation()" request handler to intercept GraphQL requests regardless of their operation name/type. Read more: https://mswjs.io/docs/api/graphql/#graphqloperationresolver`, ]), ) }) }) test('does not print a warning on anonymous GraphQL operation handled by "graphql.operation()"', async ({ loadExample, spyOnConsole, page, query, }) => { await loadExample(new URL('./anonymous-operation.mocks.ts', import.meta.url)) const consoleSpy = spyOnConsole() await page.evaluate(() => { const { worker, graphql, HttpResponse } = window.msw worker.use( // This handler will match ANY anonymous GraphQL operation. // It's a good idea to include some matching logic to differentiate // between those operations. We're omitting it for testing purposes. graphql.operation(() => { return HttpResponse.json({ data: { user: { id: 'mocked-123', }, }, }) }), ) }) const endpointUrl = httpServer.http.url('/graphql') const response = await query(endpointUrl, { // Intentionally anonymous query. // It will be handled in the "graphql.operation()" handler above. query: gql` query { user { id } } `, }) const json = await response.json() // Must get the mocked response. expect(json).toEqual({ data: { user: { id: 'mocked-123', }, }, }) // Must not print any warnings because a permissive "graphql.operation()" // handler was used to intercept and mock the anonymous GraphQL operation. expect(consoleSpy.get('warning')).toBeUndefined() }) ================================================ FILE: test/browser/graphql-api/cookies.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( graphql.query('GetUser', () => { return HttpResponse.json( { data: { firstName: 'John', }, }, { headers: { 'Set-Cookie': 'test-cookie=value', }, }, ) }), ) worker.start() ================================================ FILE: test/browser/graphql-api/cookies.test.ts ================================================ import { parse as parseCookie } from '../../../src/shims/cookie' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' test('sets cookie on the mocked GraphQL response', async ({ loadExample, query, page, }) => { await loadExample(new URL('./cookies.mocks.ts', import.meta.url)) const res = await query('/graphql', { query: gql` query GetUser { firstName } `, }) const headers = await res.allHeaders() const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(headers).not.toHaveProperty('set-cookie') expect(body).toEqual({ data: { firstName: 'John', }, }) // Should be able to access the response cookies. const cookieString = await page.evaluate(() => { return document.cookie }) const allCookies = parseCookie(cookieString) expect(allCookies).toHaveProperty('test-cookie', 'value') }) ================================================ FILE: test/browser/graphql-api/custom-predicate.mocks.ts ================================================ import { graphql } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start() window.msw = { // @ts-expect-error worker, graphql, } ================================================ FILE: test/browser/graphql-api/custom-predicate.test.ts ================================================ import { graphql } from 'msw' import { SetupWorkerApi } from 'msw/browser' import { gql } from '../../support/graphql' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi graphql: typeof graphql } } const PREDICATE_EXAMPLE = new URL( './custom-predicate.mocks.ts', import.meta.url, ) test('matches requests when the predicate function returns true', async ({ loadExample, query, page, }) => { await loadExample(PREDICATE_EXAMPLE) await page.evaluate(() => { const { worker, graphql } = window.msw worker.use( graphql.query( ({ variables }) => { return variables.id === 'abc-123' }, ({ variables }) => { return Response.json({ data: { user: { id: variables.id } }, }) }, ), ) }) const response = await query('/irrelevant', { query: gql` query GetUser($id: String!) { user(id: $id) { id } } `, variables: { id: 'abc-123', }, }) expect(response.status()).toBe(200) await expect(response.json()).resolves.toEqual({ data: { user: { id: 'abc-123', }, }, }) }) test('does not match requests when the predicate function returns false', async ({ loadExample, query, page, }) => { await loadExample(PREDICATE_EXAMPLE) await page.evaluate(() => { const { worker, graphql } = window.msw worker.use( graphql.query( ({ variables }) => { return variables.id === 'abc-123' }, ({ variables }) => { return Response.json({ data: { user: { id: variables.id } }, }) }, ), graphql.operation(() => { return Response.json({ data: { fallback: true } }) }), ) }) const response = await query('/irrelevant', { query: gql` query GetUser($id: String!) { user(id: $id) { id } } `, variables: { id: 'non-matching-query', }, }) await expect(response.json()).resolves.toEqual({ data: { fallback: true, }, }) }) ================================================ FILE: test/browser/graphql-api/document-node.mocks.ts ================================================ import { parse } from 'graphql' import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const GetUser = parse(` query GetUser { user { firstName } } `) const Login = parse(` mutation Login($username: String!) { session { id } user { username } } `) const GetSubscription = parse(` query GetSubscription { subscription { id } } `) const github = graphql.link('https://api.github.com/graphql') const worker = setupWorker( // "DocumentNode" can be used as the expected query/mutation. graphql.query(GetUser, () => { return HttpResponse.json({ data: { // Note that inferring the query body and variables // is impossible with the native "DocumentNode". // Consider using tools like GraphQL Code Generator. user: { firstName: 'John', }, }, }) }), graphql.mutation(Login, ({ variables }) => { return HttpResponse.json({ data: { session: { id: 'abc-123', }, user: { username: variables.username, }, }, }) }), github.query(GetSubscription, () => { return HttpResponse.json({ data: { subscription: { id: 123, }, }, }) }), ) worker.start() ================================================ FILE: test/browser/graphql-api/errors.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( graphql.query('Login', () => { return HttpResponse.json({ errors: [ { message: 'This is a mocked error', locations: [ { line: 1, column: 2, }, ], }, ], }) }), ) worker.start() ================================================ FILE: test/browser/graphql-api/errors.test.ts ================================================ import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' test('mocks a GraphQL error response', async ({ loadExample, query }) => { await loadExample(new URL('./errors.mocks.ts', import.meta.url)) const res = await query('/graphql', { query: gql` query Login { user { id } } `, }) const body = await res.json() expect(res.status()).toBe(200) expect(body).toEqual({ errors: [ { message: 'This is a mocked error', locations: [ { line: 1, column: 2, }, ], }, ], }) }) ================================================ FILE: test/browser/graphql-api/extensions.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' interface LoginQuery { user: { id: number name: string password: string } } const worker = setupWorker( graphql.query('Login', () => { return HttpResponse.json({ data: { user: { id: 1, name: 'Joe Bloggs', password: 'HelloWorld!', }, }, extensions: { message: 'This is a mocked extension', tracking: { version: '0.1.2', page: '/test/', }, }, }) }), ) worker.start() ================================================ FILE: test/browser/graphql-api/extensions.test.ts ================================================ import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' test('mocks a GraphQL response with both data and extensions', async ({ loadExample, query, }) => { await loadExample(new URL('./extensions.mocks.ts', import.meta.url)) const res = await query('/graphql', { query: gql` query Login { user { id name password } } `, }) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(body).toEqual({ data: { user: { id: 1, name: 'Joe Bloggs', password: 'HelloWorld!', }, }, extensions: { message: 'This is a mocked extension', tracking: { version: '0.1.2', page: '/test/', }, }, }) }) ================================================ FILE: test/browser/graphql-api/link.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const github = graphql.link('https://api.github.com/graphql') const stripe = graphql.link('https://api.stripe.com/graphql') interface GetUserQuery { user: { id: string username: string } } interface PaymentQuery { bankAccount: { totalFunds: number } } interface GetUserQuery { user: { id: string username: string } } const worker = setupWorker( github.query( 'GetUser', ({ variables }) => { return HttpResponse.json({ data: { user: { id: '46cfe8ff-a79b-42af-9699-b56e2239d1bb', username: variables.username, }, }, }) }, ), stripe.mutation( 'Payment', ({ variables }) => { return HttpResponse.json({ data: { bankAccount: { totalFunds: 100 + variables.amount, }, }, }) }, ), graphql.query( 'GetUser', ({ variables }) => { return HttpResponse.json( { data: { user: { id: '46cfe8ff-a79b-42af-9699-b56e2239d1bb', username: variables.username, }, }, }, { headers: { 'X-Request-Handler': 'fallback', }, }, ) }, ), ) worker.start() ================================================ FILE: test/browser/graphql-api/link.test.ts ================================================ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' const LINK_EXAMPLE = new URL('./link.mocks.ts', import.meta.url) const server = new HttpServer((app) => { app.post('/graphql', (req, res) => { res.status(500).end() }) }) test.beforeEach(async () => { await server.listen() }) test.afterEach(async () => { await server.close() }) test('mocks a GraphQL query to the GitHub GraphQL API', async ({ loadExample, query, }) => { await loadExample(LINK_EXAMPLE) const res = await query('https://api.github.com/graphql', { query: gql` query GetUser($username: String!) { user(username: $username) { id username } } `, variables: { username: 'john', }, }) const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) expect(headers).toHaveProperty('content-type', 'application/json') expect(body).toEqual({ data: { user: { id: '46cfe8ff-a79b-42af-9699-b56e2239d1bb', username: 'john', }, }, }) }) test('mocks a GraphQL mutation to the Stripe GraphQL API', async ({ loadExample, query, }) => { await loadExample(LINK_EXAMPLE) const res = await query('https://api.stripe.com/graphql', { query: gql` mutation Payment($amount: Int!) { bankAccount { totalFunds } } `, variables: { amount: 350, }, }) const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) expect(headers).toHaveProperty('content-type', 'application/json') expect(body).toEqual({ data: { bankAccount: { totalFunds: 450, }, }, }) }) test('falls through to the matching GraphQL operation to an unknown endpoint', async ({ loadExample, query, }) => { await loadExample(LINK_EXAMPLE) const res = await query('/graphql', { query: gql` query GetUser($username: String!) { user(username: $username) { id username } } `, variables: { username: 'john', }, }) const headers = await res.allHeaders() const body = await res.json() expect(headers).toHaveProperty('x-request-handler', 'fallback') expect(body).toEqual({ data: { user: { id: '46cfe8ff-a79b-42af-9699-b56e2239d1bb', username: 'john', }, }, }) }) test('bypasses a GraphQL operation to an unknown endpoint', async ({ loadExample, query, }) => { await loadExample(LINK_EXAMPLE) const res = await query(server.http.url('/graphql'), { query: gql` mutation Payment($amount: Int!) { bankAccount { totalFunds } } `, variables: { amount: 350, }, }) expect(res.status()).toBe(500) }) ================================================ FILE: test/browser/graphql-api/logging.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' interface GetUserDetailQuery { user: { firstName: string lastName: string } } interface LoginQuery { user: { id: string } } const worker = setupWorker( graphql.query('GetUserDetail', () => { return HttpResponse.json({ data: { user: { firstName: 'John', lastName: 'Maverick', }, }, }) }), graphql.mutation('Login', () => { return HttpResponse.json({ data: { user: { id: 'abc-123', }, }, }) }), graphql.operation(() => { return HttpResponse.json( { data: { ok: true, }, }, { status: 301, }, ) }), ) worker.start() ================================================ FILE: test/browser/graphql-api/logging.test.ts ================================================ import { StatusCodeColor } from '../../../src/core/utils/logging/getStatusCodeColor' import { waitFor } from '../../support/waitFor' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' const LOGGING_EXAMPLE = new URL('./logging.mocks.ts', import.meta.url) test('prints a log for a GraphQL query', async ({ loadExample, spyOnConsole, query, }) => { const consoleSpy = spyOnConsole() await loadExample(LOGGING_EXAMPLE) await query('/graphql', { query: gql` query GetUserDetail { user { firstName lastName } } `, }) await waitFor(() => { expect(consoleSpy.get('raw')?.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( new RegExp( `^\\[MSW\\] \\d{2}:\\d{2}:\\d{2} query GetUserDetail \\(%c200 OK%c\\) color:${StatusCodeColor.Success} color:inherit$`, ), ), ]), ) }) }) test('prints a log for a GraphQL mutation', async ({ loadExample, spyOnConsole, query, }) => { const consoleSpy = spyOnConsole() await loadExample(LOGGING_EXAMPLE) await query('/graphql', { query: gql` mutation Login { user { id } } `, }) await waitFor(() => { expect(consoleSpy.get('raw')?.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( new RegExp( `\\[MSW\\] \\d{2}:\\d{2}:\\d{2} mutation Login \\(%c200 OK%c\\) color:${StatusCodeColor.Success} color:inherit$`, ), ), ]), ) }) }) test('prints a log for a GraphQL query intercepted via "graphql.operation"', async ({ loadExample, spyOnConsole, query, }) => { const consoleSpy = spyOnConsole() await loadExample(LOGGING_EXAMPLE) await query('/graphql', { query: gql` query GetLatestPosts { posts { title } } `, }) await waitFor(() => { expect(consoleSpy.get('raw')?.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( new RegExp( `\\[MSW\\] \\d{2}:\\d{2}:\\d{2} query GetLatestPosts \\(%c301 Moved Permanently%c\\) color:${StatusCodeColor.Warning} color:inherit$`, ), ), ]), ) }) }) test('prints a log for a GraphQL mutation intercepted via "graphql.operation"', async ({ loadExample, spyOnConsole, query, }) => { const consoleSpy = spyOnConsole() await loadExample(LOGGING_EXAMPLE) await query('/graphql', { query: gql` mutation CreatePost { post { id } } `, }) await waitFor(() => { expect(consoleSpy.get('raw')?.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( new RegExp( `^\\[MSW\\] \\d{2}:\\d{2}:\\d{2} mutation CreatePost \\(%c301 Moved Permanently%c\\) color:${StatusCodeColor.Warning} color:inherit$`, ), ), ]), ) }) }) ================================================ FILE: test/browser/graphql-api/multipart-data.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( graphql.mutation< { multipart: { file1?: string file2?: string files?: Array plainText?: string } }, { file1?: File file2?: File files?: Array plainText?: string } >('UploadFile', async ({ variables }) => { const { file1, file2, files = [], plainText } = variables const filesResponse = await Promise.all(files.map((file) => file.text())) return HttpResponse.json({ data: { multipart: { file1: await file1?.text(), file2: await file2?.text(), files: filesResponse, plainText, }, }, }) }), ) worker.start() ================================================ FILE: test/browser/graphql-api/multipart-data.test.ts ================================================ import { test, expect } from '../playwright.extend' test('accepts a file from a GraphQL mutation', async ({ loadExample, query, }) => { await loadExample(new URL('./multipart-data.mocks.ts', import.meta.url)) const UPLOAD_MUTATION = ` mutation UploadFile( $file1: Upload $file2: Upload $plainText: String ) { multipart( file1: $file1 file2: $file2 plainText: $plainText ){ file1 file2 plainText } } ` const res = await query('/graphql', { query: UPLOAD_MUTATION, variables: { file1: null, file2: null, files: [null, null], plainText: 'text', }, multipartOptions: { map: { '0': ['variables.file1', 'variables.files.0'], '1': ['variables.file2', 'variables.files.1'], }, fileContents: ['file1 content', 'file2 content'], }, }) const body = await res.json() expect(res.status()).toEqual(200) expect(body).toEqual({ data: { multipart: { file1: 'file1 content', file2: 'file2 content', files: ['file1 content', 'file2 content'], plainText: 'text', }, }, }) }) ================================================ FILE: test/browser/graphql-api/mutation.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' interface LogoutQuery { logout: { userSession: boolean } } const worker = setupWorker( graphql.mutation('Logout', () => { return HttpResponse.json({ data: { logout: { userSession: false, }, }, }) }), ) worker.start() ================================================ FILE: test/browser/graphql-api/mutation.test.ts ================================================ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' const MUTATION_EXAMPLE = new URL('./mutation.mocks.ts', import.meta.url) const server = new HttpServer((app) => { app.use('*', (req, res) => res.status(405).end()) }) function endpoint(): string { return server.http.url('/graphql') } test.beforeEach(async () => { await server.listen() }) test.afterEach(async () => { await server.close() }) test('sends a mocked response to a GraphQL mutation', async ({ loadExample, query, }) => { await loadExample(MUTATION_EXAMPLE) const res = await query(endpoint(), { query: gql` mutation Logout { logout { userSession } } `, }) const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) expect(headers).toHaveProperty('content-type', 'application/json') expect(body).toEqual({ data: { logout: { userSession: false, }, }, }) }) test('prints a warning when intercepted an anonymous GraphQL mutation', async ({ loadExample, spyOnConsole, query, }) => { const consoleSpy = spyOnConsole() await loadExample(MUTATION_EXAMPLE) const res = await query(endpoint(), { query: gql` mutation { logout { userSession } } `, }) expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ expect.stringContaining( `\ [MSW] Failed to intercept a GraphQL request at "POST ${endpoint()}": anonymous GraphQL operations are not supported. Consider naming this operation or using "graphql.operation()" request handler to intercept GraphQL requests regardless of their operation name/type. Read more: https://mswjs.io/docs/api/graphql/#graphqloperationresolver\ `, ), ]), ) // The actual GraphQL server is hit. expect(res.status()).toBe(405) }) ================================================ FILE: test/browser/graphql-api/operation-reference.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( graphql.query('GetUser', async ({ query, variables }) => { return HttpResponse.json({ data: { query, variables, }, }) }), graphql.mutation('Login', ({ query, variables }) => { return HttpResponse.json({ data: { query, variables, }, }) }), ) worker.start() ================================================ FILE: test/browser/graphql-api/operation-reference.test.ts ================================================ import { test, expect } from '../playwright.extend' const OPERATION_REFERENCE_EXAMPLE = new URL( './operation-reference.mocks.ts', import.meta.url, ) test('allows referencing the request body in the GraphQL query handler', async ({ loadExample, query, }) => { await loadExample(OPERATION_REFERENCE_EXAMPLE) const GET_USER_QUERY = ` query GetUser($id: String!) { query variables } ` const res = await query('/graphql', { query: GET_USER_QUERY, variables: { id: 'abc-123', }, }) const body = await res.json() expect(res.status()).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ data: { query: GET_USER_QUERY, variables: { id: 'abc-123', }, }, }) }) test('allows referencing the request body in the GraphQL mutation handler', async ({ loadExample, query, }) => { await loadExample(OPERATION_REFERENCE_EXAMPLE) const LOGIN_MUTATION = ` mutation Login($username: String!, $password: String!) { mutation variables } ` const res = await query('/graphql', { query: LOGIN_MUTATION, variables: { username: 'john', password: 'super-secret', }, }) const body = await res.json() expect(res.status()).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ data: { query: LOGIN_MUTATION, variables: { username: 'john', password: 'super-secret', }, }, }) }) ================================================ FILE: test/browser/graphql-api/operation.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( graphql.operation(async ({ query, variables }) => { return HttpResponse.json({ data: { query, variables, }, }) }), ) worker.start({ onUnhandledRequest: 'warn', }) ================================================ FILE: test/browser/graphql-api/operation.test.ts ================================================ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' const OPERATION_EXAMPLE = new URL('./operation.mocks.ts', import.meta.url) const server = new HttpServer((app) => { app.post('/search', (req, res) => { return res.json({ results: [1, 2, 3] }) }) }) test.beforeEach(async () => { await server.listen() }) test.afterEach(async () => { await server.close() }) test('intercepts and mocks a GraphQL query', async ({ loadExample, spyOnConsole, query, }) => { const consoleSpy = spyOnConsole() await loadExample(OPERATION_EXAMPLE) const GET_USER_QUERY = gql` query GetUser($id: String!) { query variables } ` const res = await query('/graphql', { query: GET_USER_QUERY, variables: { id: 'abc-123', }, }) const body = await res.json() expect(res.status()).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ data: { query: GET_USER_QUERY, variables: { id: 'abc-123', }, }, }) expect(consoleSpy.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching(/\[MSW\] \d{2}:\d{2}:\d{2} query GetUser 200 OK/), ]), ) }) test('intercepts and mocks an anonymous GraphQL query', async ({ loadExample, spyOnConsole, query, }) => { const consoleSpy = spyOnConsole() await loadExample(OPERATION_EXAMPLE) const ANONYMOUS_QUERY = gql` query { anonymousQuery { query variables } } ` const res = await query('/graphql', { query: ANONYMOUS_QUERY, variables: { id: 'abc-123', }, }) expect(consoleSpy.get('warning')).toBeUndefined() expect(res.status()).toBe(200) expect(res.fromServiceWorker()).toBe(true) const body = await res.json() expect(body).toEqual({ data: { query: ANONYMOUS_QUERY, variables: { id: 'abc-123', }, }, }) expect(consoleSpy.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching(/\[MSW\] \d{2}:\d{2}:\d{2} anonymous query 200 OK/), ]), ) }) test('intercepts and mocks a GraphQL mutation', async ({ loadExample, query, }) => { await loadExample(OPERATION_EXAMPLE) const LOGIN_MUTATION = gql` mutation Login($username: String!, $password: String!) { mutation variables } ` const res = await query('/graphql', { query: LOGIN_MUTATION, variables: { username: 'john', password: 'super-secret', }, }) const body = await res.json() expect(res.status()).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ data: { query: LOGIN_MUTATION, variables: { username: 'john', password: 'super-secret', }, }, }) }) test('propagates parsing errors from the invalid GraphQL requests', async ({ loadExample, spyOnConsole, query, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(OPERATION_EXAMPLE) const INVALID_QUERY = ` # Intentionally invalid GraphQL query. query GetUser() { user { id } ` query('/graphql', { query: INVALID_QUERY, }) await waitFor(() => { expect(consoleSpy.get('error')).toEqual( expect.arrayContaining([ expect.stringContaining( 'Failed to intercept a GraphQL request to "POST http://localhost:8080/graphql": cannot parse query. See the error message from the parser below.\n\nSyntax Error: Expected "$", found ")".', ), ]), ) }) }) test('bypasses seemingly compatible REST requests', async ({ loadExample, query, }) => { await loadExample(OPERATION_EXAMPLE) const res = await query(server.http.url('/search'), { query: 'favorite books', }) const body = await res.json() expect(res.status()).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ results: [1, 2, 3], }) }) ================================================ FILE: test/browser/graphql-api/query.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' interface GetUserDetailQuery { user: { firstName: string lastName: string } } const worker = setupWorker( graphql.query('GetUserDetail', () => { return HttpResponse.json({ data: { user: { firstName: 'John', lastName: 'Maverick', }, }, }) }), ) worker.start() ================================================ FILE: test/browser/graphql-api/query.test.ts ================================================ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' const EXAMPLE_PATH = new URL('./query.mocks.ts', import.meta.url) const server = new HttpServer((app) => { app.use('*', (_, res) => res.status(405).end()) }) const endpoint = () => { return server.http.url('/graphql') } test.beforeEach(async () => { await server.listen() }) test.afterEach(async () => { await server.close() }) test('mocks a GraphQL query issued with a GET request', async ({ loadExample, query, }) => { await loadExample(EXAMPLE_PATH) const res = await query(endpoint(), { method: 'GET', query: gql` query GetUserDetail { user { firstName lastName } } `, }) const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) expect(headers).toHaveProperty('content-type', 'application/json') expect(body).toEqual({ data: { user: { firstName: 'John', lastName: 'Maverick', }, }, }) }) test('mocks a GraphQL query issued with a POST request', async ({ loadExample, query, }) => { await loadExample(EXAMPLE_PATH) const res = await query(endpoint(), { method: 'POST', query: gql` query GetUserDetail { user { firstName lastName } } `, }) const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toBe(200) expect(headers).toHaveProperty('content-type', 'application/json') expect(body).toEqual({ data: { user: { firstName: 'John', lastName: 'Maverick', }, }, }) }) test('prints a warning when intercepted an anonymous GraphQL query', async ({ loadExample, spyOnConsole, query, }) => { const consoleSpy = spyOnConsole() await loadExample(EXAMPLE_PATH) const res = await query(endpoint(), { query: gql` query { user { firstName } } `, }) expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ expect.stringContaining( `\ [MSW] Failed to intercept a GraphQL request at "POST ${endpoint()}": anonymous GraphQL operations are not supported. Consider naming this operation or using "graphql.operation()" request handler to intercept GraphQL requests regardless of their operation name/type. Read more: https://mswjs.io/docs/api/graphql/#graphqloperationresolver`, ), ]), ) // The actual GraphQL server is hit. expect(res.status()).toBe(405) }) ================================================ FILE: test/browser/graphql-api/response-patching.mocks.ts ================================================ import { graphql, bypass, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' import { createGraphQLClient, gql } from '../../support/graphql' interface GetUserQuery { user: { firstName: string lastName: string } } const worker = setupWorker( graphql.query('GetUser', async ({ request }) => { const originalResponse = await fetch(bypass(request)) const originalJson = await originalResponse.json() return HttpResponse.json({ data: { user: { firstName: 'Christian', lastName: originalJson.data?.user?.lastName, }, }, errors: originalJson.errors, }) }), ) Object.assign(window, { msw: { registration: worker.start(), }, dispatchGraphQLQuery: (uri: string) => { const client = createGraphQLClient({ uri }) return client({ query: gql` query GetUser { user { firstName lastName } } `, }) }, }) ================================================ FILE: test/browser/graphql-api/response-patching.test.ts ================================================ import type { ExecutionResult } from 'graphql' import { buildSchema, graphql } from 'graphql' import { SetupWorkerApi } from 'msw/browser' import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../playwright.extend' import { gql } from '../../support/graphql' declare namespace window { export const dispatchGraphQLQuery: (uri: string) => Promise export const msw: { registration: SetupWorkerApi['start'] } } // This test server simulates a production GraphQL server // and uses a hard-coded `rootValue` to resolve queries // against the schema. const httpServer = new HttpServer((app) => { app.post('/graphql', async (req, res) => { const result = await graphql({ schema: buildSchema(gql` type User { firstName: String! lastName: String! } type Query { user: User! } `), source: req.body.query, rootValue: { user: { firstName: 'John', lastName: 'Maverick', }, }, }) return res.status(200).json(result) }) }) test.beforeEach(async () => { await httpServer.listen() }) test.afterEach(async () => { await httpServer.close() }) test('patches a GraphQL response', async ({ loadExample, page }) => { await loadExample(new URL('./response-patching.mocks.ts', import.meta.url)) const endpointUrl = httpServer.http.url('/graphql') await page.evaluate(() => { return window.msw.registration }) const res = await page.evaluate( ([url]) => { return window.dispatchGraphQLQuery(url) }, [endpointUrl], ) expect(res.errors).toBeUndefined() expect(res.data).toHaveProperty('user', { firstName: 'Christian', lastName: 'Maverick', }) }) ================================================ FILE: test/browser/graphql-api/variables.mocks.ts ================================================ import { graphql, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' interface GetGitHubUserQuery { user: { username: string firstName: string } } interface GetGitHubUserQueryVariables { username: string } interface DeletePostQuery { deletePost: { postId: string } } interface DeletePostQueryVariables { postId: string } interface GetActiveUserQuery { user: { id: number } } interface GetActiveUserQueryVariables { userId: string } const worker = setupWorker( graphql.query( 'GetGithubUser', ({ variables }) => { const { username } = variables return HttpResponse.json({ data: { user: { username, firstName: 'John', }, }, }) }, ), graphql.mutation( 'DeletePost', ({ variables }) => { const { postId } = variables return HttpResponse.json({ data: { deletePost: { postId, }, }, }) }, ), graphql.query( 'GetActiveUser', ({ variables }) => { // Intentionally unused variable const { userId } = variables return HttpResponse.json({ data: { user: { id: 1, }, }, }) }, ), ) worker.start() ================================================ FILE: test/browser/graphql-api/variables.test.ts ================================================ import { gql } from '../../support/graphql' import { test, expect } from '../playwright.extend' const EXAMPLE_PATH = new URL('./variables.mocks.ts', import.meta.url) test('can access variables from a GraphQL query', async ({ loadExample, query, }) => { await loadExample(EXAMPLE_PATH) const res = await query('/graphql', { query: gql` query GetGithubUser($username: String!) { user(login: $username) { firstName username } } `, variables: { username: 'octocat', }, }) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(body).toEqual({ data: { user: { firstName: 'John', username: 'octocat', }, }, }) }) test('can access variables from a GraphQL mutation', async ({ loadExample, query, }) => { await loadExample(EXAMPLE_PATH) const res = await query('/graphql', { query: gql` mutation DeletePost($postId: String!) { deletePost(id: $postId) { postId } } `, variables: { postId: 'abc-123', }, }) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(body).toEqual({ data: { deletePost: { postId: 'abc-123', }, }, }) }) test('returns an empty object when accessing variables from a GraphQL operation without them', async ({ loadExample, query, }) => { await loadExample(EXAMPLE_PATH) const res = await query('/graphql', { query: gql` query GetActiveUser { user { id } } `, }) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(body).toEqual({ data: { user: { id: 1, }, }, }) }) ================================================ FILE: test/browser/msw-api/context/delay.mocks.ts ================================================ import { http, delay, DelayMode, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/delay', async ({ request }) => { const url = new URL(request.url) const mode = url.searchParams.get('mode') as DelayMode const duration = url.searchParams.get('duration') await delay(duration ? Number(duration) : mode || undefined) return HttpResponse.json({ mocked: true }) }), ) worker.start() ================================================ FILE: test/browser/msw-api/context/delay.test.ts ================================================ import { test, expect } from '../../playwright.extend' const DELAY_EXAMPLE = new URL('./delay.mocks.ts', import.meta.url) declare global { namespace PlaywrightTest { interface Matchers { toRoughlyEqual(expected: number, deviation: number): R } } } expect.extend({ /** * Asserts a given actual number to roughly equal to the expected number, * taking the maximum allowed delta `deviation` into account. */ toRoughlyEqual(actual: number, expected: number, deviation: number) { const diff = Math.abs(actual - expected) const passes = diff <= deviation if (passes) { return { pass: true, message: () => `expected ${actual} not to be roughly equal to ${expected} (deviation: ${deviation})`, } } return { pass: false, message: () => `expected ${actual} to be roughly equal to ${expected} (deviation: ${deviation})`, } }, }) test('uses explicit server response delay', async ({ loadExample, fetch }) => { await loadExample(DELAY_EXAMPLE) const res = await fetch('/delay?duration=1200') const timing = res.request().timing() expect(timing.responseStart).toRoughlyEqual(1200, 250) const status = res.status() const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(status).toBe(200) expect(body).toEqual({ mocked: true }) }) test('uses realistic server response delay when no delay value is provided', async ({ loadExample, fetch, }) => { await loadExample(DELAY_EXAMPLE) const res = await fetch('/delay') const timing = res.request().timing() expect(timing.responseStart).toRoughlyEqual(250, 300) const status = res.status() const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(status).toBe(200) expect(body).toEqual({ mocked: true, }) }) test('uses realistic server response delay when "real" delay mode is provided', async ({ loadExample, fetch, }) => { await loadExample(DELAY_EXAMPLE) const res = await fetch('/delay?mode=real') const timing = res.request().timing() expect(timing.responseStart).toRoughlyEqual(250, 300) const status = res.status() const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(status).toBe(200) expect(body).toEqual({ mocked: true, }) }) ================================================ FILE: test/browser/msw-api/distribution/iife.mocks.js ================================================ const { setupWorker, http, HttpResponse } = MockServiceWorker const worker = setupWorker( http.get('/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) worker.start() ================================================ FILE: test/browser/msw-api/distribution/iife.test.ts ================================================ import fs from 'node:fs' import { test, expect } from '../../playwright.extend' test('supports the usage of the iife bundle in a `, beforeNavigation(compilation) { compilation.use((router) => { router.get('/iife/index.js', (_, res) => { fs.createReadStream( new URL('../../../../lib/iife/index.js', import.meta.url), ).pipe(res) return res }) }) }, }) expect(consoleSpy.get('error')).toBeUndefined() const response = await fetch('/user') expect(response.status()).toBe(200) expect(await response.json()).toEqual({ firstName: 'John', }) }) ================================================ FILE: test/browser/msw-api/exception-handling.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://api.github.com/users/:username', () => { // @ts-expect-error nonExisting should not be defined nonExisting() return }), ) worker.start() ================================================ FILE: test/browser/msw-api/exception-handling.test.ts ================================================ import { test, expect } from '../playwright.extend' test('activates the worker without errors', async ({ loadExample, spyOnConsole, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./exception-handling.mocks.ts', import.meta.url)) expect(consoleSpy.get('error')).toBeUndefined() }) test('transforms uncaught exceptions into a 500 response', async ({ loadExample, fetch, spyOnConsole, }) => { await loadExample(new URL('./exception-handling.mocks.ts', import.meta.url)) const consoleSpy = spyOnConsole() const res = await fetch('https://api.github.com/users/octocat') expect(res.status()).toBe(500) expect(res.statusText()).toBe('Request Handler Error') expect(res.fromServiceWorker()).toBe(true) expect(await res.json()).toEqual({ name: 'ReferenceError', message: 'nonExisting is not defined', stack: expect.stringContaining( 'ReferenceError: nonExisting is not defined', ), }) const errors = consoleSpy.get('error') expect(errors).toEqual( expect.arrayContaining([ expect.stringContaining('ReferenceError: nonExisting is not defined'), expect.stringContaining(' at '), ]), ) }) ================================================ FILE: test/browser/msw-api/hard-reload.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://example.com/resource', () => { return HttpResponse.json({ mocked: true }) }), ) worker.start() ================================================ FILE: test/browser/msw-api/hard-reload.test.ts ================================================ import { test, expect } from '../playwright.extend' test('keeps the mocking enabled after hard-reload of the page', async ({ loadExample, page, fetch, waitForMswActivation, }) => { await loadExample(new URL('./hard-reload.mocks.ts', import.meta.url)) page.evaluate(() => { /** * Emulate a forced reload. * Since `location.reload(true)` is deprecated, use a workaround. * @see https://stackoverflow.com/a/65544086/2754939 */ location.replace(location.href) }) await waitForMswActivation() const res = await fetch('https://example.com/resource') const body = await res.json() // Still intercepts and mocks responses after a hard-reload. expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ mocked: true }) }) ================================================ FILE: test/browser/msw-api/integrity-check-invalid.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://example.com/users/:username', () => { return HttpResponse.json({ mocked: true, }) }), ) worker.start({ serviceWorker: { // Use custom Service Worker URL for the purpose of intentionally // registering an outdated worker. Please do not use this as an example. url: './mockServiceWorker-outdated.js', options: { scope: '/', }, }, }) ================================================ FILE: test/browser/msw-api/integrity-check-valid.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://example.com/users/octocat', () => { return HttpResponse.json({ mocked: true }) }), ) worker.start() ================================================ FILE: test/browser/msw-api/integrity-check.test.ts ================================================ import fs from 'node:fs' import { test, expect } from '../playwright.extend' import copyServiceWorker from '../../../config/copyServiceWorker' import packageJson from '../../../package.json' assert { type: 'json' } // @ts-expect-error Importing a Javascript module. import { SERVICE_WORKER_SOURCE_PATH } from '../../../config/constants.js' test('activates the worker without errors given the latest integrity', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample( new URL('./integrity-check-valid.mocks.ts', import.meta.url), ) expect(consoleSpy.get('error')).toBeUndefined() const res = await fetch('https://example.com/users/octocat') const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ mocked: true, }) }) test('errors when activating the worker with an outdated integrity', async ({ loadExample, spyOnConsole, fetch, waitFor, }) => { const TEMP_SERVICE_WORKER_PATH = new URL( '../../tmp/mockServiceWorker-outdated.js', import.meta.url, ).pathname // Manually create a Service Worker file with invalid integrity await copyServiceWorker( SERVICE_WORKER_SOURCE_PATH, TEMP_SERVICE_WORKER_PATH, 'intentionally-invalid-checksum', ) const consoleSpy = spyOnConsole() await loadExample( new URL('./integrity-check-invalid.mocks.ts', import.meta.url), { skipActivation: true, beforeNavigation(compilation) { compilation.use((router) => { router.use('/', (_, res, next) => { // Appended router are relative to the compilation path. // Allow the nested worker script to control the root scope. res.setHeader('Service-Worker-Allowed', '/') next() }) router.get('/mockServiceWorker-outdated.js', (_, res) => { return res .set('content-type', 'application/javascript') .send(fs.readFileSync(TEMP_SERVICE_WORKER_PATH, 'utf8')) }) }) }, }, ) await waitFor(() => { // Produces a meaningful error in the browser's console. expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ `[MSW] The currently registered Service Worker has been generated by a different version of MSW (${packageJson.version}) and may not be fully compatible with the installed version. It's recommended you update your worker script by running this command: \u2022 npx msw init You can also automate this process and make the worker script update automatically upon the library installations. Read more: https://mswjs.io/docs/cli/init.`, ]), ) }) // Still keeps the mocking enabled. const res = await fetch('https://example.com/users/octocat') const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ mocked: true, }) fs.unlinkSync(TEMP_SERVICE_WORKER_PATH) }) ================================================ FILE: test/browser/msw-api/regression/2129-worker-use.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start() worker.use( http.get('/v1/issues', () => { return HttpResponse.text('get-body') }), ) worker.use( http.post('/v1/issues', () => { return HttpResponse.text('post-body') }), ) ================================================ FILE: test/browser/msw-api/regression/2129-worker-use.test.ts ================================================ /** * @see https://github.com/mswjs/msw/issues/2129 */ import { test, expect } from '../../playwright.extend' test('handles a stream response without throwing a timeout error', async ({ loadExample, fetch, }) => { await loadExample(new URL('./2129-worker-use.mocks.ts', import.meta.url)) const getResponse = await fetch('/v1/issues') expect(await getResponse.text()).toBe('get-body') const postResponse = await fetch('/v1/issues', { method: 'POST' }) expect(await postResponse.text()).toBe('post-body') }) ================================================ FILE: test/browser/msw-api/regression/handle-stream.mocks.ts ================================================ import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.events.on('response:bypass', async ({ response }) => { const responseText = await response.clone().text() console.warn(`[response:bypass] ${responseText}`) }) worker.start({ onUnhandledRequest: 'bypass' }) ================================================ FILE: test/browser/msw-api/regression/handle-stream.test.ts ================================================ import { test, expect } from '../../playwright.extend' test('handles a stream response without throwing a timeout error', async ({ createServer, loadExample, spyOnConsole, fetch, page, waitFor, }) => { const server = await createServer((app) => { app.get('/stream', (_, res) => { res.write('first-chunk') setTimeout(() => { res.write(' last-chunk') res.end() }, 250 * 6) }) }) const consoleSpy = spyOnConsole() await loadExample(new URL('./handle-stream.mocks.ts', import.meta.url)) const getStreamResponse = () => { return page.evaluate( ([endpointUrl]) => { const abortController = new AbortController() const abortTimeout = setTimeout(() => abortController.abort(), 250 * 5) return new Promise((resolve, reject) => { let textResponse = '' const decoder = new TextDecoder() return fetch(endpointUrl, { signal: abortController.signal, }) .then((response) => { // @ts-expect-error Response is a runtime object. const reader = response.body.getReader() clearTimeout(abortTimeout) reader.read().then(function processStream({ done, value }) { if (done) { resolve(textResponse) return } textResponse += decoder.decode(value, { stream: true }) return reader.read().then(processStream) }) }) .catch(reject) }) }, [server.http.url('/stream')], ) } const response = await getStreamResponse() await waitFor(() => { expect(consoleSpy.get('warning')).toEqual([ `[response:bypass] first-chunk last-chunk`, ]) }) expect(response).toEqual('first-chunk last-chunk') }) ================================================ FILE: test/browser/msw-api/regression/null-body.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get<{ code: string }>('/api/:code', ({ params }) => { return new HttpResponse(null, { status: parseInt(params.code) }) }), ) worker.start() ================================================ FILE: test/browser/msw-api/regression/null-body.test.ts ================================================ import { expect, test } from '../../playwright.extend' for (const code of [204, 205, 304]) { test(`gracefully handles a ${code} response null body during life-cycle events`, async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./null-body.mocks.ts', import.meta.url)) const errors: Array = [] page.on('pageerror', (pageError) => { errors.push(pageError) }) await fetch(`/api/${code}`) expect(errors).toEqual([]) }) } ================================================ FILE: test/browser/msw-api/req/passthrough.mocks.ts ================================================ import { http, passthrough, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.post('/', () => { return passthrough() }), ) worker.start() Object.assign(window, { msw: { worker, http, passthrough, HttpResponse, }, }) ================================================ FILE: test/browser/msw-api/req/passthrough.test.ts ================================================ import { HttpResponse, http, passthrough } from 'msw' import { SetupWorkerApi } from 'msw/browser' import { expect, test } from '../../playwright.extend' const PASSTHROUGH_EXAMPLE = new URL('./passthrough.mocks.ts', import.meta.url) declare namespace window { export const msw: { worker: SetupWorkerApi http: typeof http passthrough: typeof passthrough HttpResponse: typeof HttpResponse } } interface ResponseBody { name: string } test('performs request as-is when returning "req.passthrough" call in the resolver', async ({ createServer, loadExample, spyOnConsole, fetch, page, }) => { const server = await createServer((app) => { app.post('/user', (_, res) => { res.json({ name: 'John' }) }) }) const consoleSpy = spyOnConsole() await loadExample(PASSTHROUGH_EXAMPLE) const endpointUrl = server.http.url('/user') await page.evaluate((endpointUrl) => { const { worker, http, passthrough } = window.msw worker.use( http.post(endpointUrl, () => { return passthrough() }), ) }, endpointUrl) const res = await fetch(endpointUrl, { method: 'POST' }) const headers = await res.allHeaders() const json = await res.json() expect(headers).toHaveProperty('x-powered-by', 'Express') expect(json).toEqual({ name: 'John', }) expect(consoleSpy.get('warning')).toBeUndefined() }) test('does not allow fall-through when returning "req.passthrough" call in the resolver', async ({ createServer, loadExample, spyOnConsole, fetch, page, }) => { const server = await createServer((app) => { app.post('/user', (_, res) => { res.json({ name: 'John' }) }) }) const consoleSpy = spyOnConsole() await loadExample(PASSTHROUGH_EXAMPLE) const endpointUrl = server.http.url('/user') await page.evaluate((endpointUrl) => { const { worker, http, passthrough, HttpResponse } = window.msw worker.use( http.post(endpointUrl, () => { return passthrough() }), http.post(endpointUrl, () => { return HttpResponse.json({ name: 'Kate' }) }), ) }, endpointUrl) const res = await fetch(endpointUrl, { method: 'POST' }) const headers = await res.allHeaders() const json = await res.json() expect(headers).toHaveProperty('x-powered-by', 'Express') expect(json).toEqual({ name: 'John', }) expect(consoleSpy.get('warning')).toBeUndefined() }) test('performs a request as-is if nothing was returned from the resolver', async ({ createServer, loadExample, fetch, page, }) => { const server = await createServer((app) => { app.post('/user', (_, res) => { res.json({ name: 'John' }) }) }) await loadExample(PASSTHROUGH_EXAMPLE) const endpointUrl = server.http.url('/user') await page.evaluate((endpointUrl) => { const { worker, http } = window.msw worker.use( http.post(endpointUrl, () => { return }), ) }, endpointUrl) const res = await fetch(endpointUrl, { method: 'POST' }) const headers = await res.allHeaders() const json = await res.json() expect(headers).toHaveProperty('x-powered-by', 'Express') expect(json).toEqual({ name: 'John', }) }) for (const code of [204, 205, 304]) { test(`performs a ${code} request as-is if passthrough was returned from the resolver`, async ({ createServer, loadExample, fetch, page, }) => { const server = await createServer((app) => { app.post('/user', (_, res) => { res.status(code).send() }) }) await loadExample(PASSTHROUGH_EXAMPLE) const endpointUrl = server.http.url('/user') const errors: Array = [] page.on('pageerror', (pageError) => { errors.push(pageError) }) await page.evaluate((endpointUrl) => { const { worker, http, passthrough } = window.msw worker.use( http.post(endpointUrl, () => { return passthrough() }), ) }, endpointUrl) const res = await fetch(endpointUrl, { method: 'POST' }) expect(res.status()).toBe(code) expect(errors).toEqual([]) }) test(`performs a ${code} request as-is if nothing was returned from the resolver`, async ({ createServer, loadExample, fetch, page, }) => { const server = await createServer((app) => { app.post('/user', (_, res) => { res.status(code).send() }) }) await loadExample(PASSTHROUGH_EXAMPLE) const endpointUrl = server.http.url('/user') const errors: Array = [] page.on('pageerror', (pageError) => { errors.push(pageError) }) await page.evaluate((endpointUrl) => { const { worker, http } = window.msw worker.use( http.post(endpointUrl, () => { return }), ) }, endpointUrl) const res = await fetch(endpointUrl, { method: 'POST' }) expect(res.status()).toBe(code) expect(errors).toEqual([]) }) } ================================================ FILE: test/browser/msw-api/res/network-error.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.error() }), ) worker.start() ================================================ FILE: test/browser/msw-api/res/network-error.test.ts ================================================ import { test, expect } from '../../playwright.extend' test('throws a network error', async ({ spyOnConsole, loadExample, page }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./network-error.mocks.ts', import.meta.url)) // Do not use `runtime.request()`, because it always awaits a response. // In this case we await a network error, performing a request manually. const requestPromise = page.evaluate(() => { return fetch('/user') }) await expect(requestPromise).rejects.toThrow( // The `fetch` call itself rejects with the "Failed to fetch" error, // the same error that happens on a regular network error. 'TypeError: Failed to fetch', ) // Assert a network error message printed into the console // before `fetch` rejects. expect(consoleSpy.get('error')).toEqual([ 'Failed to load resource: net::ERR_FAILED', ]) }) ================================================ FILE: test/browser/msw-api/setup-worker/fallback-mode/fallback-mode.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('*/user', () => { return HttpResponse.json({ name: 'John Maverick' }) }), ) worker.start() Object.assign(window, { worker }) ================================================ FILE: test/browser/msw-api/setup-worker/fallback-mode/fallback-mode.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { createTeardown } from 'fs-teardown' import { Page } from '@playwright/test' import { HttpServer } from '@open-draft/test-server/lib/http.js' import { fromTemp } from '../../../../support/utils' import { test, expect } from '../../../playwright.extend' declare namespace window { export const worker: SetupWorkerApi } const fsMock = createTeardown({ rootDir: fromTemp('fallback-mode'), }) let server: HttpServer async function gotoStaticPage(page: Page, workerIndex: number): Promise { await page.goto( `file://${fsMock.resolve(`worker-${workerIndex}/index.html`)}`, { waitUntil: 'networkidle' }, ) } interface DirectFetchResponse { status: number statusText: string headers: Record body: Record } function createFetchWithoutNetwork(page: Page) { return ( input: RequestInfo, init?: RequestInit, ): Promise => { return page.evaluate( ([input, init]) => { return fetch(input, init) .then((res) => { const headers: Record = {} res.headers.forEach((value, key) => { headers[key] = value }) return res.json().then((body) => ({ status: res.status, statusText: res.statusText, headers, body, })) }) .catch(() => null as any) }, [input, init] as [RequestInfo, RequestInit], ) } } test.beforeAll(async () => { await fsMock.prepare() }) test.beforeEach(async ({ webpackServer }, testInfo) => { const compilation = await webpackServer.compile([ new URL('./fallback-mode.mocks.ts', import.meta.url).pathname, ]) const bundleUrl = new URL('./main.js', compilation.previewUrl) await fsMock.create({ // Scope static files per worker to prevent shared state. // The tests below are run in parallel. [`worker-${testInfo.workerIndex}`]: { 'index.html': ``, }, }) }) test.beforeEach(async ({ createServer }) => { server = await createServer((app) => { app.get('/user', (_, res) => { res.json({ name: 'Actual User' }) }) }) }) test.afterAll(async () => { await fsMock.cleanup() }) test('prints a fallback start message in the console', async ({ spyOnConsole, page, waitFor, }, testInfo) => { const consoleSpy = spyOnConsole() await gotoStaticPage(page, testInfo.workerIndex) const consoleGroups = consoleSpy.get('startGroupCollapsed') await waitFor(() => { expect(consoleGroups).toContain('[MSW] Mocking enabled (fallback mode).') }) }) test('responds with a mocked response to a handled request', async ({ spyOnConsole, waitFor, page, }, testInfo) => { const fetch = createFetchWithoutNetwork(page) const consoleSpy = spyOnConsole() await gotoStaticPage(page, testInfo.workerIndex) const response = await fetch(server.https.url('/user')) // Prints the request message group in the console. await waitFor(() => { expect(consoleSpy.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /\[MSW\] \d{2}:\d{2}:\d{2} GET https:\/\/127\.0\.0\.1:\d+\/user 200 OK/, ), ]), ) }) // Responds with a mocked response. expect(response.status).toEqual(200) expect(response.statusText).toEqual('OK') expect(response.body).toEqual({ name: 'John Maverick', }) }) test('warns on the unhandled request by default', async ({ spyOnConsole, page, }, testInfo) => { const fetch = createFetchWithoutNetwork(page) const consoleSpy = spyOnConsole() await gotoStaticPage(page, testInfo.workerIndex) await fetch(server.http.url('/unknown-resource')) expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ expect.stringContaining(`\ [MSW] Warning: intercepted a request without a matching request handler: • GET ${server.http.url('/unknown-resource')} If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`), ]), ) }) test('stops the fallback interceptor when called "worker.stop()"', async ({ spyOnConsole, page, }, testInfo) => { const fetch = createFetchWithoutNetwork(page) const consoleSpy = spyOnConsole() await gotoStaticPage(page, testInfo.workerIndex) await page.evaluate(() => { window.worker.stop() }) expect(consoleSpy.get('log')).toContain('[MSW] Mocking disabled.') const response = await fetch(server.http.url('/user')) expect(response.status).toBe(200) expect(response.body).toEqual({ name: 'Actual User' }) }) ================================================ FILE: test/browser/msw-api/setup-worker/input-validation.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' //@ts-expect-error invalid parameter provided to setupWorker so we can validate it throws const worker = setupWorker([ http.get('/book/:bookId', function originalResolver() { return HttpResponse.json({ title: 'Original title' }) }), ]) worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/input-validation.test.ts ================================================ import { test, expect } from '../../playwright.extend' test('throws an error given an Array of request handlers to "setupWorker"', async ({ loadExample, page, waitFor, }) => { await loadExample(new URL('./input-validation.mocks.ts', import.meta.url), { skipActivation: true, }) const exceptions: Array = [] page.on('pageerror', (error) => { exceptions.push(error.message) }) await page.reload({ waitUntil: 'networkidle' }) await waitFor(() => { expect(exceptions).toEqual( expect.arrayContaining([ expect.stringContaining( '[MSW] Failed to apply given request handlers: invalid input. Did you forget to spread the request handlers Array?', ), ]), ) }) }) ================================================ FILE: test/browser/msw-api/setup-worker/life-cycle-events/on.mocks.ts ================================================ import { HttpResponse, http, LifeCycleEventsMap, passthrough, bypass, } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('*/user', () => { return HttpResponse.text('response-body', { status: 400 }) }), http.post('*/no-response', () => { return }), http.get('*/passthrough', () => { return passthrough() }), http.get('*/bypass', async ({ request }) => { return fetch(bypass(request, { method: 'POST' })) }), http.get('*/unhandled-exception', () => { throw new Error('Unhandled resolver error') }), ) worker.events.on('request:start', ({ request, requestId }) => { console.warn(`[request:start] ${request.method} ${request.url} ${requestId}`) }) worker.events.on('request:match', ({ request, requestId }) => { console.warn(`[request:match] ${request.method} ${request.url} ${requestId}`) }) worker.events.on('request:unhandled', ({ request, requestId }) => { console.warn( `[request:unhandled] ${request.method} ${request.url} ${requestId}`, ) }) const requestEndListener: ( ...args: LifeCycleEventsMap['request:end'] ) => void = ({ request, requestId }) => { console.warn(`[request:end] ${request.method} ${request.url} ${requestId}`) } worker.events.on('request:end', requestEndListener) worker.events.on( 'response:mocked', async ({ response, request, requestId }) => { const body = await response.clone().text() console.warn( `[response:mocked] ${response.status} ${response.url} ${body} ${request.method} ${request.url} ${requestId}`, ) }, ) worker.events.on( 'response:bypass', async ({ response, request, requestId }) => { const body = await response.clone().text() console.warn( `[response:bypass] ${response.status} ${response.url} ${body} ${request.method} ${request.url} ${requestId}`, ) }, ) worker.events.on('unhandledException', ({ error, request, requestId }) => { console.warn( `[unhandledException] ${request.method} ${request.url} ${requestId} ${error.message}`, ) }) worker.start({ onUnhandledRequest: 'bypass', }) Object.assign(window, { msw: { worker, requestEndListener, }, }) ================================================ FILE: test/browser/msw-api/setup-worker/life-cycle-events/on.test.ts ================================================ import type { SetupWorker } from 'msw/browser' import { HttpServer } from '@open-draft/test-server/lib/http.js' import type { ConsoleMessages } from 'page-with' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorker } } const ON_EXAMPLE = new URL('./on.mocks.ts', import.meta.url) const server = new HttpServer((app) => { app.post('/no-response', (_req, res) => { res.send('original-response') }) app.get('/unknown-route', (_req, res) => { res.status(404) res.send('majestic-unknown') }) app.get('/passthrough', (_req, res) => { res.send('passthrough-response') }) app.post('/bypass', (_req, res) => { res.send('bypassed-response') }) }) export function getRequestId(messages: ConsoleMessages) { const requestStartMessage = messages.get('warning')?.find((message) => { return message.startsWith('[request:start]') }) return requestStartMessage?.split(' ')?.[3] } test.beforeAll(async () => { await server.listen() }) test.afterAll(async () => { await server.close() }) test('emits events for a handled request and mocked response', async ({ loadExample, spyOnConsole, fetch, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(ON_EXAMPLE) const url = server.http.url('/user') await fetch(url) const requestId = getRequestId(consoleSpy) await waitFor(() => { expect(consoleSpy.get('warning')).toContainEqual( expect.stringContaining('[response:mocked]'), ) }) expect(consoleSpy.get('warning')).toEqual([ `[request:start] GET ${url} ${requestId}`, `[request:match] GET ${url} ${requestId}`, `[request:end] GET ${url} ${requestId}`, `[response:mocked] 400 ${url} response-body GET ${url} ${requestId}`, ]) }) test('emits events for a handled request with no response', async ({ loadExample, spyOnConsole, fetch, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(ON_EXAMPLE) const url = server.http.url('/no-response') await fetch(url, { method: 'POST' }) const requestId = getRequestId(consoleSpy) await waitFor(() => { expect(consoleSpy.get('warning')).toContainEqual( expect.stringContaining('[response:bypass]'), ) }) expect(consoleSpy.get('warning')).toEqual([ `[request:start] POST ${url} ${requestId}`, `[request:end] POST ${url} ${requestId}`, `[response:bypass] 200 ${url} original-response POST ${url} ${requestId}`, ]) }) test('emits events for an unhandled request', async ({ loadExample, spyOnConsole, fetch, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(ON_EXAMPLE) const url = server.http.url('/unknown-route') await fetch(url) const requestId = getRequestId(consoleSpy) await waitFor(() => { expect(consoleSpy.get('warning')).toContainEqual( expect.stringContaining('[response:bypass]'), ) }) expect(consoleSpy.get('warning')).toEqual([ `[request:start] GET ${url} ${requestId}`, `[request:unhandled] GET ${url} ${requestId}`, `[request:end] GET ${url} ${requestId}`, `[response:bypass] 404 ${url} majestic-unknown GET ${url} ${requestId}`, ]) }) test('emits events for a passthrough request', async ({ loadExample, spyOnConsole, fetch, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(ON_EXAMPLE) // Explicit "passthrough()" request must go through the // same request processing pipeline to contain both // "request" and "response" in the life-cycle event listener. const url = server.http.url('/passthrough') await fetch(url) const requestId = getRequestId(consoleSpy) await waitFor(() => { expect(consoleSpy.get('warning')).toEqual([ `[request:start] GET ${url} ${requestId}`, `[request:end] GET ${url} ${requestId}`, `[response:bypass] 200 ${url} passthrough-response GET ${url} ${requestId}`, ]) }) }) test('emits events for a bypassed request', async ({ loadExample, spyOnConsole, fetch, waitFor, page, }) => { const consoleSpy = spyOnConsole() await loadExample(ON_EXAMPLE) const pageErrors: Array = [] page.on('pageerror', (error) => pageErrors.push(error)) const url = server.http.url('/bypass') await fetch(url) await waitFor(() => { // First, must print the events for the original (mocked) request. expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ expect.stringContaining(`[request:start] GET ${url}`), expect.stringContaining(`[request:end] GET ${url}`), expect.stringContaining( `[response:mocked] 200 ${url} bypassed-response GET ${url}`, ), ]), ) // Then, must also print events for the bypassed request. expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ expect.stringContaining(`[request:start] POST ${url}`), expect.stringContaining(`[request:end] POST ${url}`), expect.stringContaining( `[response:bypass] 200 ${url} bypassed-response POST ${url}`, ), ]), ) expect(pageErrors).toEqual([]) }) }) test('emits unhandled exceptions in the request handler', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(ON_EXAMPLE) const url = server.http.url('/unhandled-exception') await fetch(url) const requestId = getRequestId(consoleSpy) expect(consoleSpy.get('warning')).toContain( `[unhandledException] GET ${url} ${requestId} Unhandled resolver error`, ) }) test('stops emitting events once the worker is stopped', async ({ loadExample, spyOnConsole, fetch, page, }) => { const consoleSpy = spyOnConsole() await loadExample(ON_EXAMPLE) await page.evaluate(() => { return window.msw.worker.stop() }) await fetch('/unknown-route') expect(consoleSpy.get('warning')).toBeUndefined() }) ================================================ FILE: test/browser/msw-api/setup-worker/life-cycle-events/removeAllListeners.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi } } const ON_EXAMPLE = new URL('./on.mocks.ts', import.meta.url) test('removes all listeners attached to the worker instance', async ({ loadExample, spyOnConsole, fetch, page, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(ON_EXAMPLE) const url = 'http://localhost/user' await fetch(url) await waitFor(() => { expect(consoleSpy.get('warning')).toContainEqual( expect.stringContaining('[response:mocked]'), ) }) // Remove all life-cycle events listeners. await page.evaluate(() => { const { msw } = window msw.worker.events.removeAllListeners() }) // Request the same endpoint again. consoleSpy.clear() await fetch(url) // Negative assertion. We don't want this to pass. const promise = waitFor(() => { const warnings = consoleSpy.get('warning') expect(warnings).toBeDefined() expect(warnings).toContainEqual( expect.stringContaining('[response:mocked]'), ) }) await expect(promise).rejects.toThrow() }) test('removes all the listeners by the event name', async ({ loadExample, spyOnConsole, fetch, page, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(ON_EXAMPLE) const url = 'http://localhost/user' await fetch(url) await waitFor(() => { expect(consoleSpy.get('warning')).toContainEqual( expect.stringContaining('[response:mocked]'), ) }) // Request the same endpoint again. await page.evaluate(() => { const { msw } = window msw.worker.events.removeAllListeners('request:end') }) // Request the same endpoint again. consoleSpy.clear() await fetch(url) await waitFor(() => { expect(consoleSpy.get('warning')).toContainEqual( expect.stringContaining('[response:mocked]'), ) }) expect(consoleSpy.get('warning')).not.toContainEqual( expect.stringContaining('[request:end]'), ) }) ================================================ FILE: test/browser/msw-api/setup-worker/life-cycle-events/removeListener.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi requestEndListener: any } } test('removes a listener by the event name', async ({ loadExample, spyOnConsole, fetch, page, waitFor, makeUrl, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./on.mocks.ts', import.meta.url)) await page.evaluate(() => { const { msw } = window msw.worker.events.removeListener('request:end', msw.requestEndListener) }) const url = makeUrl('/user') await fetch(url) await waitFor(() => { expect(consoleSpy.get('warning')).toContainEqual( expect.stringContaining('[response:mocked]'), ) }) expect(consoleSpy.get('warning')).not.toContainEqual( expect.stringContaining('[request:end]'), ) }) ================================================ FILE: test/browser/msw-api/setup-worker/listHandlers.mocks.ts ================================================ import { http, graphql } from 'msw' import { setupWorker } from 'msw/browser' const resolver = () => void 0 const github = graphql.link('https://api.github.com') const worker = setupWorker( http.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), ) worker.start() Object.assign(window, { msw: { worker, http, graphql, }, }) ================================================ FILE: test/browser/msw-api/setup-worker/listHandlers.test.ts ================================================ import { http, graphql } from 'msw' import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi http: typeof http graphql: typeof graphql } } const LIST_HANDLER_EXAMPLE = new URL('./listHandlers.mocks.ts', import.meta.url) test('lists all current request handlers', async ({ loadExample, page }) => { await loadExample(LIST_HANDLER_EXAMPLE) const handlerHeaders = await 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 ({ loadExample, page, }) => { await loadExample(LIST_HANDLER_EXAMPLE) /** * @note For some reason, property assignment on frozen object * does not throw an error: handlers[0] = 1 */ await expect( 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 ({ loadExample, page, }) => { await loadExample(LIST_HANDLER_EXAMPLE) const handlerHeaders = await page.evaluate(() => { const { worker, http, graphql } = window.msw worker.use( http.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)', ]) }) ================================================ FILE: test/browser/msw-api/setup-worker/resetHandlers.test.ts ================================================ import { http, HttpResponse } from 'msw' import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../playwright.extend' declare namespace window { // Annotate global references to the worker and rest request handlers. export const msw: { worker: SetupWorkerApi http: typeof http HttpResponse: typeof HttpResponse } } const USE_EXAMPLE = new URL('./use.mocks.ts', import.meta.url) test('removes all runtime request handlers when resetting without explicit next handlers', async ({ loadExample, page, fetch, }) => { await loadExample(USE_EXAMPLE) await page.evaluate(() => { const { msw } = window // Add a request handler on runtime msw.worker.use( msw.http.post('/login', () => { return msw.HttpResponse.json({ accepted: true }) }), ) }) // Request handlers added on runtime affect the network communication. const loginResponse = await fetch('/login', { method: 'POST', }) const loginStatus = loginResponse.status() const loginBody = await loginResponse.json() expect(loginStatus).toBe(200) expect(loginBody).toEqual({ accepted: true }) // Reset request handlers to initial handlers. await page.evaluate(() => { const { msw } = window msw.worker.resetHandlers() }) // Any runtime request handlers are removed upon reset. const secondLoginResponse = await fetch('/login', { method: 'POST', }) const secondLoginStatus = secondLoginResponse.status() expect(secondLoginStatus).toBe(404) // Initial request handlers (given to `setupWorker`) are not affected. const bookResponse = await fetch('/book/abc-123') const bookStatus = bookResponse.status() const bookBody = await bookResponse.json() expect(bookStatus).toBe(200) expect(bookBody).toEqual({ title: 'Original title' }) }) test('replaces all handlers with the explicit next runtime handlers upon reset', async ({ loadExample, page, fetch, }) => { await loadExample(USE_EXAMPLE) // Add a runtime request handler. await page.evaluate(() => { const { msw } = window msw.worker.use( msw.http.post('/login', () => { return msw.HttpResponse.json({ accepted: true }) }), ) }) // Reset request handlers with explicit next handlers. await page.evaluate(() => { const { msw } = window msw.worker.resetHandlers( msw.http.get('/products', () => { return msw.HttpResponse.json([1, 2, 3]) }), ) }) // Any runtime request handlers must be removed. const loginResponse = await fetch('/login', { method: 'POST', }) const secondLoginStatus = loginResponse.status() expect(secondLoginStatus).toBe(404) // Any initial request handler must be removed. const bookResponse = await fetch('/book/abc-123') const bookStatus = bookResponse.status() expect(bookStatus).toEqual(404) // Should leave only explicit reset request handlers. const productsResponse = await fetch('/products') const productsStatus = productsResponse.status() const productsBody = await productsResponse.json() expect(productsStatus).toBe(200) expect(productsBody).toEqual([1, 2, 3]) }) ================================================ FILE: test/browser/msw-api/setup-worker/response-logging.test.ts ================================================ import { test, expect } from '../../playwright.extend' function createResponseLogRegexp(username: string): RegExp { return new RegExp( `^\\[MSW\\] \\d{2}:\\d{2}:\\d{2} GET https://example\\.com/users/${username} 200 OK$`, ) } test('prints the response info to the console', async ({ loadExample, spyOnConsole, fetch, waitFor, }) => { await loadExample(new URL('../../rest-api/basic.mocks.ts', import.meta.url)) const consoleSpy = spyOnConsole() const waitForResponseLog = async (exp: RegExp) => { await waitFor(() => { expect(consoleSpy.get('startGroupCollapsed')).toEqual( expect.arrayContaining([expect.stringMatching(exp)]), ) }) } const getResponseLogs = (exp: RegExp) => { return consoleSpy.get('startGroupCollapsed')?.filter((log) => { return exp.test(log) }) } const firstResponseLogRegexp = createResponseLogRegexp('octocat') await fetch('https://example.com/users/octocat') await waitForResponseLog(firstResponseLogRegexp) // Must print the response summary to the console. expect(getResponseLogs(firstResponseLogRegexp)).toHaveLength(1) const secondResponseLogRegExp = createResponseLogRegexp('john.doe') await fetch('https://example.com/users/john.doe') await waitForResponseLog(secondResponseLogRegExp) /** * Must not duplicate response logs for the current and previous requests. * @see https://github.com/mswjs/msw/issues/1411 */ expect(getResponseLogs(secondResponseLogRegExp)).toHaveLength(1) expect(getResponseLogs(firstResponseLogRegexp)).toHaveLength(1) }) ================================================ FILE: test/browser/msw-api/setup-worker/restoreHandlers.test.ts ================================================ import { http, HttpResponse } from 'msw' import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi http: typeof http HttpResponse: typeof HttpResponse } } test('returns a mocked response from the used one-time request handler when restored', async ({ loadExample, page, fetch, }) => { await loadExample(new URL('./use.mocks.ts', import.meta.url)) await page.evaluate(() => { const { worker, http, HttpResponse } = window.msw worker.use( http.get<{ bookId: string }>( '/book/:bookId', () => { return HttpResponse.json({ title: 'One-time override' }) }, { once: true }, ), ) }) // One-time request handler hasn't been used yet, // so expect its response upon first request hit. const bookResponse = await fetch('/book/abc-123') const bookStatus = bookResponse.status() const bookBody = await bookResponse.json() expect(bookStatus).toBe(200) expect(bookBody).toEqual({ title: 'One-time override' }) // One-time request handler has been used, so expect // the original response. const secondBookResponse = await fetch('/book/abc-123') const secondBookStatus = secondBookResponse.status() const secondBookBody = await secondBookResponse.json() expect(secondBookStatus).toBe(200) expect(secondBookBody).toEqual({ title: 'Original title' }) // Restore the one-time request handlers, marking them as unused. await page.evaluate(() => { const { worker } = window.msw worker.restoreHandlers() }) // Once restored, one-time request handler affect network again. const thirdBookResponse = await fetch('/book/abc-123') const thirdBookStatus = thirdBookResponse.status() const thirdBookBody = await thirdBookResponse.json() expect(thirdBookStatus).toBe(200) expect(thirdBookBody).toEqual({ title: 'One-time override' }) }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/custom-transformers.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' import * as JSONbig from 'json-bigint' const worker = setupWorker( http.get('/user', () => { return new HttpResponse( JSONbig.stringify({ username: 'john.maverick', balance: BigInt(1597928668063727616), }), { headers: { 'Content-Type': 'application/json', }, }, ) }), ) worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/custom-transformers.test.ts ================================================ import * as JSONBig from 'json-bigint' import { test, expect } from '../../../playwright.extend' test('uses a custom transformer to parse BigInt in response body', async ({ loadExample, fetch, }) => { await loadExample(new URL('./custom-transformers.mocks.ts', import.meta.url)) const res = await fetch('/user') const body = await res.text() expect(body).toEqual( JSONBig.stringify({ username: 'john.maverick', balance: BigInt(1597928668063727616), }), ) }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/errors/internal-error.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { throw new Error('Custom error message') }), ) worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/errors/internal-error.test.ts ================================================ import { test, expect } from '../../../../playwright.extend' test('propagates the exception originating from a handled request', async ({ loadExample, spyOnConsole, fetch, waitFor, makeUrl, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./internal-error.mocks.ts', import.meta.url)) const endpointUrl = makeUrl('/user') const res = await fetch(endpointUrl) // Expect the exception to be handled as a 500 error response. expect(res.status()).toBe(500) expect(res.statusText()).toBe('Request Handler Error') expect(await res.json()).toEqual({ name: 'Error', message: 'Custom error message', stack: expect.stringContaining('Error: Custom error message'), }) // Expect standard request failure message from the browser. await waitFor(() => { expect(consoleSpy.get('error')).toEqual( expect.arrayContaining([ expect.stringContaining( 'Failed to load resource: the server responded with a status of 500', ), ]), ) }) expect(consoleSpy.get('error')).toEqual( expect.arrayContaining([ expect.stringContaining(`\ [MSW] Uncaught exception in the request handler for "GET ${endpointUrl}": Error: Custom error message `), ]), ) }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/errors/network-error.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.error() }), ) worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/errors/network-error.test.ts ================================================ import { until } from 'until-async' import { test, expect } from '../../../../playwright.extend' test('propagates a mocked network error', async ({ loadExample, spyOnConsole, fetch, page, makeUrl, }) => { const consoleSpy = spyOnConsole() const { workerConsole } = await loadExample( new URL('./network-error.mocks.ts', import.meta.url), ) const endpointUrl = makeUrl('/user') await until(() => page.evaluate((url) => fetch(url), endpointUrl)) // Expect the fetch error message. expect(consoleSpy.get('error')).toEqual( expect.arrayContaining([ expect.stringContaining('Failed to load resource: net::ERR_FAILED'), ]), ) // The worker must not produce any errors. expect(workerConsole.messages.get('error')).toBeUndefined() }) test('propagates a CORS violation error from a non-matching request', async ({ loadExample, spyOnConsole, page, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./network-error.mocks.ts', import.meta.url)) await until(() => page.evaluate(() => fetch('/user'))) // Must print the failed fetch error to the console. await waitFor(() => { expect(consoleSpy.get('error')).toEqual( expect.arrayContaining([ expect.stringContaining('Failed to load resource: net::ERR_FAILED'), ]), ) }) /** * @todo Ideally, assert the Chromium warning about * the Service Worker responding to the fetch event * with a network error response. For some reason, * it never appears in the console/worker console messages. */ }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/fall-through.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('*', () => console.log('[get] first')), http.get('/us*', () => console.log('[get] second')), http.get('/user', () => HttpResponse.json({ firstName: 'John' })), http.get('/user', () => console.log('[get] third')), http.post('/blog/*', () => console.log('[post] first')), http.post('/blog/article', () => console.log('[post] second')), ) worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/fall-through.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('falls through all relevant request handlers until response is returned', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./fall-through.mocks.ts', import.meta.url)) const res = await fetch('/user') const body = await res.json() // One of the handlers returns a mocked response. expect(body).toEqual({ firstName: 'John' }) // These two handlers execute before the one that returned the response. expect(consoleSpy.get('log')).toContain('[get] first') expect(consoleSpy.get('log')).toContain('[get] second') // The third handler is listed after the one that returns the response, // so it must never execute (response is sent). expect(consoleSpy.get('log')).not.toContain('[get] third') }) test('falls through all relevant handler even if none returns response', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./fall-through.mocks.ts', import.meta.url)) const res = await fetch('/blog/article', { method: 'POST', }) const status = res.status() // Neither of request handlers returned a mocked response. expect(status).toBe(404) expect(consoleSpy.get('log')).toContain('[post] first') expect(consoleSpy.get('log')).toContain('[post] second') }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe/app.html ================================================ ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe/iframe.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('*/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe/iframe.test.ts ================================================ import { Frame } from '@playwright/test' import * as express from 'express' import { test, expect } from '../../../../playwright.extend' declare global { interface Window { request(): Promise } } function findFrame(frame: Frame) { return frame.name() === '' } const staticMiddleware = (router: express.Router) => { router.use(express.static(new URL('./', import.meta.url).pathname)) } test('intercepts a request from an iframe (nested client)', async ({ loadExample, page, }) => { await loadExample(new URL('./iframe.mocks.ts', import.meta.url), { markup: new URL('page-in-iframe.html', import.meta.url).pathname, beforeNavigation(compilation) { compilation.use(staticMiddleware) }, }) const frame = page.mainFrame().childFrames().find(findFrame)! await frame.evaluate(() => window.request()) const firstNameElement = await frame.waitForSelector('#first-name') const firstName = await firstNameElement.evaluate((node) => node.textContent) expect(firstName).toBe('John') }) test('intercepts a request from a deeply nested iframe', async ({ loadExample, page, }) => { await loadExample(new URL('./iframe.mocks.ts', import.meta.url), { markup: new URL('page-in-nested-iframe.html', import.meta.url).pathname, beforeNavigation(compilation) { compilation.use(staticMiddleware) }, }) const deepFrame = page .mainFrame() .childFrames() .find(findFrame)! .childFrames() .find(findFrame)! await deepFrame.evaluate(() => window.request()) const firstNameElement = await deepFrame.waitForSelector('#first-name') const firstName = await firstNameElement.evaluate((node) => node.textContent) expect(firstName).toBe('John') }) test('intercepts a request from a deeply nested iframe given MSW is registered in a parent nested iframe', async ({ webpackServer, loadExample, page, }) => { await loadExample(new URL('./iframe.mocks.ts', import.meta.url), { markup: new URL('page-in-iframe.html', import.meta.url).pathname, beforeNavigation(compilation) { compilation.use(staticMiddleware) }, }) // Intentionally empty compilation just to serve // a custom page with an embedded iframe. await webpackServer.compile([], { markup: ``, }) const deepFrame = page.mainFrame().childFrames().find(findFrame)! await deepFrame.evaluate(() => window.request()) const firstNameElement = await deepFrame.waitForSelector('#first-name') const firstName = await firstNameElement.evaluate((node) => node.textContent) expect(firstName).toBe('John') }) test('intercepts a request from an iframe given MSW is registered in a sibling iframe', async ({ webpackServer, loadExample, page, context, }) => { // A frame that registers MSW, but does no requests. await loadExample(new URL('./iframe.mocks.ts', import.meta.url)) // A request-issuing frame. Here lives the `window.fetch` call. const requestPage = await context.newPage() const requestCompilation = await webpackServer.compile([], { markup: new URL('page-in-iframe.html', import.meta.url).pathname, }) requestCompilation.use(staticMiddleware) await requestPage.goto(requestCompilation.previewUrl) // A parent frame that hosts two frames above. const parentPage = await context.newPage() const parentCompilation = await webpackServer.compile([], { markup: ` `, }) await parentPage.goto(parentCompilation.previewUrl) await parentPage.bringToFront() const frame = parentPage .mainFrame() .childFrames() .find(findFrame)! .childFrames() .find(findFrame)! await frame.evaluate(() => window.request()) const firstNameElement = await frame.waitForSelector('#first-name') const firstName = await firstNameElement.evaluate((node) => node.textContent) expect(firstName).toBe('John') }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe/multiple-workers/child.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/resource', () => { return new Response('hello world') }), ) worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe/multiple-workers/iframe-multiple-workers.test.ts ================================================ import { test, expect } from '../../../../../playwright.extend' test('intercepts a request issued by child frame when both child and parent have MSW', async ({ webpackServer, page, }) => { const parentCompilation = await webpackServer.compile([ new URL('./parent.mocks.ts', import.meta.url).pathname, ]) const childCompilation = await webpackServer.compile([ new URL('./child.mocks.ts', import.meta.url).pathname, ]) await page.goto(parentCompilation.previewUrl, { waitUntil: 'networkidle' }) await page.evaluate((childFrameUrl) => { const iframe = document.createElement('iframe') iframe.setAttribute('id', 'child-frame') iframe.setAttribute('src', childFrameUrl) document.body.appendChild(iframe) }, childCompilation.previewUrl) const childFrameElement = await page.locator('#child-frame').elementHandle() const childFrame = await childFrameElement!.contentFrame() await childFrame!.waitForLoadState('networkidle') const responseText = await childFrame!.evaluate(async () => { const response = await fetch('/resource') return response.text() }) expect(responseText).toBe('hello world') }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe/multiple-workers/parent.mocks.ts ================================================ import { setupWorker } from 'msw/browser' // The parent frame has a worker without any handlers. const worker = setupWorker() // This registration is awaited by the `loadExample` command in the test. worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe/page-in-iframe.html ================================================ ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe/page-in-nested-iframe.html ================================================ ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe-isolated-response/app.html ================================================ ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe-isolated-response/iframe-isolated-response.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start() window.msw = { // @ts-expect-error worker, http, } ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe-isolated-response/iframe-isolated-response.test.ts ================================================ import * as express from 'express' import type { Frame, Page } from '@playwright/test' import { test, expect } from '../../../../playwright.extend' const staticMiddleware = (router: express.Router) => { router.use(express.static(new URL('./', import.meta.url).pathname)) } export function getFrameById(id: string, page: Page): Frame { const frame = page .mainFrame() .childFrames() .find((frame) => frame.name() === id) if (!frame) { throw new Error(`Unable to find frame with id "${id}" on the page`) } return frame } test('responds with different responses for the same request based on request referrer (frame url)', async ({ loadExample, page, }) => { await loadExample( new URL('./iframe-isolated-response.mocks.ts', import.meta.url), { markup: new URL('app.html', import.meta.url).pathname, beforeNavigation(compilation) { compilation.use(staticMiddleware) }, }, ) // Add iframes dynamically after `window.msw` is set on the parent // to prevent the iframe scripts from racing with the mocks setup. await page.evaluate(() => { for (const [id, src] of [ ['frame-one', './one.html'], ['frame-two', './two.html'], ]) { const iframe = document.createElement('iframe') iframe.id = id iframe.name = id iframe.src = src document.body.appendChild(iframe) } }) // Wait for child frames to be attached and navigated. await page.waitForFunction(() => { return document.querySelectorAll('iframe').length === 2 }) const frameOne = getFrameById('frame-one', page) const frameTwo = getFrameById('frame-two', page) // Wait for the iframe scripts to load and define `window.request`. await Promise.all([ frameOne.waitForFunction(() => typeof window.request === 'function'), frameTwo.waitForFunction(() => typeof window.request === 'function'), ]) await Promise.all([ frameOne.evaluate(() => window.request()), frameTwo.evaluate(() => window.request()), ]) /** * @note Each frame is able to receive a unique response * because it uses the `isolatedResolver` utility. * It's IMPORTANT it runs in the frame's context. We cannot * ship that logic in MSW because MSW always runs in the * main thread (the top-level client, which is the parent). */ await expect(frameOne.getByText('one')).toBeVisible() await expect(frameTwo.getByText('two')).toBeVisible() }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe-isolated-response/one.html ================================================ ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/iframe-isolated-response/two.html ================================================ ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/scope/scope-nested-quiet.mocks.ts ================================================ import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start({ quiet: true, serviceWorker: { url: './public/mockServiceWorker.js', }, }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/scope/scope-nested.mocks.ts ================================================ import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start({ serviceWorker: { url: './public/mockServiceWorker.js', }, }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/scope/scope-root.mocks.ts ================================================ import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start({ serviceWorker: { url: '/mockServiceWorker.js', }, }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/scope/scope-validation.test.ts ================================================ import { test, expect } from '../../../../playwright.extend' // @ts-expect-error Importing a JavaScript module. import { SERVICE_WORKER_BUILD_PATH } from '../../../../../../config/constants.js' test('warns when visiting the page outside of the worker scope', async ({ loadExample, spyOnConsole, }) => { const consoleSpy = spyOnConsole() const { compilation } = await loadExample( new URL('./scope-nested.mocks.ts', import.meta.url), { beforeNavigation(compilation) { // Create a proxy to the worker script located under a nested ("/public") path. compilation.use((router) => { router.get('/public/mockServiceWorker.js', (_, res) => { res.sendFile(SERVICE_WORKER_BUILD_PATH) }) }) }, }, ) // Must display the out-of-scope warning. const workerScope = new URL('./public', compilation.previewUrl) expect(consoleSpy.get('warning')).toContain( `[MSW] Cannot intercept requests on this page because it's outside of the worker's scope ("${workerScope}/"). If you wish to mock API requests on this page, you must resolve this scope issue. - (Recommended) Register the worker at the root level ("/") of your application. - Set the "Service-Worker-Allowed" response header to allow out-of-scope workers.`, ) expect(consoleSpy.get('error')).toEqual(undefined) }) test('does not print the scope warning when the page is within the worker scope', async ({ loadExample, spyOnConsole, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./scope-root.mocks.ts', import.meta.url)) expect(consoleSpy.get('warning')).toEqual(undefined) expect(consoleSpy.get('error')).toEqual(undefined) }) test('does not print the scope warning when the "quiet" option is enabled', async ({ loadExample, spyOnConsole, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./scope-nested-quiet.mocks.ts', import.meta.url), { skipActivation: true, beforeNavigation(compilation) { // Create a proxy to the worker script located under a nested ("/public") path. compilation.use((router) => { router.get('/public/mockServiceWorker.js', (_, res) => { res.sendFile(SERVICE_WORKER_BUILD_PATH) }) }) }, }) // Although the page is out of the worker's scope, // the "quiet" option is set to true. expect(consoleSpy.get('warning')).toBeUndefined() }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/shared-worker/shared-worker.mocks.ts ================================================ import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/shared-worker/shared-worker.test.ts ================================================ /** * @vitest-environment node */ import express from 'express' import { test, expect } from '../../../../playwright.extend' test('does not interfere with a shared worker', async ({ loadExample, spyOnConsole, waitFor, page, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./shared-worker.mocks.ts', import.meta.url), { beforeNavigation(compilation) { compilation.use((router) => { router.use(express.static(new URL('./', import.meta.url).pathname)) }) }, }) await page.evaluate(() => { const worker = new SharedWorker('./worker.js') worker.addEventListener('error', () => console.error('There is an error with worker'), ) worker.port.onmessage = (event) => { console.log(event.data) } worker.port.postMessage('john') }) await waitFor(() => { expect(consoleSpy.get('error')).toBeUndefined() expect(consoleSpy.get('log')).toContain('hello, john') }) }) ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/shared-worker/worker.js ================================================ onconnect = (event) => { const port = event.ports[0] port.onmessage = (event) => { port.postMessage(`hello, ${event.data}`) } } ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/text-event-stream.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return new HttpResponse() }), ) worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/scenarios/text-event-stream.test.ts ================================================ import { test, expect } from '../../../playwright.extend' import { sleep } from '../../../../support/utils' test('bypasses the unhandled request with the "Accept" header containing "text/event-stream"', async ({ loadExample, spyOnConsole, createServer, page, waitFor, }) => { const server = await createServer((app) => { app.get('/user', async (req, res) => { res.set({ 'Content-Type': 'text/event-stream', Connection: 'keep-alive', }) res.flushHeaders() const chunks = ['hello', 'beautiful', 'world'] for (const chunk of chunks) { res.write(`data: ${chunk}\n\n`) await sleep(150) } }) }) const consoleSpy = spyOnConsole() await loadExample(new URL('./text-event-stream.mocks.ts', import.meta.url)) await page.evaluate((endpointUrl) => { const source = new EventSource(endpointUrl) source.addEventListener('message', (message) => { console.log(message.data) }) }, server.http.url('/user')) await waitFor(() => { expect(consoleSpy.get('error')).toBeUndefined() expect(consoleSpy.get('log')).toEqual( expect.arrayContaining(['hello', 'beautiful', 'world']), ) }) }) ================================================ FILE: test/browser/msw-api/setup-worker/start/error.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return new Response() }), ) Object.assign(window, { msw: { worker, }, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/error.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi } } test('rejects with a custom error message when given a non-existing worker script', async ({ loadExample, page, }) => { await loadExample(new URL('./error.mocks.ts', import.meta.url), { skipActivation: true, }) // Playwright is so fast we need to await the runtime module // exposing MSW on the global scope. await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) await expect( page.evaluate(() => { return window.msw.worker.start({ serviceWorker: { url: 'invalidServiceWorker', }, }) }), ).rejects.toThrowError(/\[MSW\] Failed/) }) ================================================ FILE: test/browser/msw-api/setup-worker/start/find-worker.error.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return new Response() }), ) Object.assign(window, { msw: { registration: worker .start({ findWorker: (scriptURL, _mockServiceWorkerUrl) => { return scriptURL.includes('some-bad-filename-that-does-not-exist.js') }, }) .then((registration) => { console.log('Registration Promise resolved') // This will throw as as there is no instance returned with a non-matching worker name. return registration?.constructor.name }) .catch((error) => { console.error('Error - no worker instance after starting', error) throw error }), }, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/find-worker.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return new Response() }), ) Object.assign(window, { msw: { registration: worker .start({ // This is the default matching behavior if left unspecified. findWorker(scriptURL, mockServiceWorkerUrl) { return scriptURL === mockServiceWorkerUrl }, }) .then((registration) => { console.log('Registration Promise resolved', registration) return registration?.constructor.name }), }, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/find-worker.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { registration: ReturnType } } test('resolves the "start" Promise and returns a ServiceWorkerRegistration when using a findWorker that returns true', async ({ loadExample, spyOnConsole, page, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./find-worker.mocks.ts', import.meta.url)) await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) const resolvedPayload = await page.evaluate(() => { return window.msw.registration }) expect(resolvedPayload).toBe('ServiceWorkerRegistration') const activationMessageIndex = consoleSpy.get('startGroupCollapsed')?.findIndex((text) => { return text.includes('[MSW] Mocking enabled') }) ?? -1 const customMessageIndex = consoleSpy.get('log')?.findIndex((text) => { return text.includes('Registration Promise resolved') }) ?? -1 expect(activationMessageIndex).toBeGreaterThan(-1) expect(customMessageIndex).toBeGreaterThan(-1) expect(customMessageIndex).toBeGreaterThan(activationMessageIndex) }) test('fails to return a ServiceWorkerRegistration when using a findWorker that returns false', async ({ loadExample, spyOnConsole, page, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./find-worker.error.mocks.ts', import.meta.url), { skipActivation: true, }) await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) const workerStartError = await page.evaluate(() => { return window.msw.registration.catch((error) => error.message) }) const activationMessage = consoleSpy .get('startGroupCollapsed') ?.findIndex((text) => { return text.includes('[MSW] Mocking enabled') }) const errorMessageIndex = consoleSpy.get('error')?.findIndex((text) => { return text.includes('Error - no worker instance after starting') }) expect(workerStartError).toMatch(`\ [MSW] Failed to locate the Service Worker registration using a custom "findWorker" predicate. Please ensure that the custom predicate properly locates the Service Worker registration at "/mockServiceWorker.js". More details: https://mswjs.io/docs/api/setup-worker/start#findworker\ `) expect(activationMessage).toBeUndefined() expect(errorMessageIndex).toBeGreaterThan(-1) }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/bypass.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) worker.start({ onUnhandledRequest: 'bypass', }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/bypass.test.ts ================================================ import { test, expect } from '../../../../playwright.extend' test('bypasses an unhandled request', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./bypass.mocks.ts', import.meta.url)) const res = await fetch('https://mswjs.io/non-existing-page') const status = res.status() expect(consoleSpy.get('error')).toBeUndefined() expect(consoleSpy.get('warning')).toBeUndefined() // Performs the request as-is. expect(status).toBe(404) }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/callback-print.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) worker.start({ onUnhandledRequest(request, print) { console.log(`Oops, unhandled ${request.method} ${request.url}`) const url = new URL(request.url) if (url.pathname.includes('/use-warn')) { // Using "print" allows you to execute the default strategy. print.warning() } else { print.error() } }, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/callback-print.test.ts ================================================ import { test, expect } from '../../../../playwright.extend' test('executes a default "warn" strategy in a custom callback', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./callback-print.mocks.ts', import.meta.url)) const res = await fetch('https://mswjs.io/use-warn') const status = res.status() // Request is performed as-is. expect(status).toBe(404) // Custom callback executed. expect(consoleSpy.get('log')).toContain( 'Oops, unhandled GET https://mswjs.io/use-warn', ) expect(consoleSpy.get('error')).toBeUndefined() // Prints the unhandled request warning upon `print.warning()`. expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ expect.stringContaining(`\ [MSW] Warning: intercepted a request without a matching request handler: • GET https://mswjs.io/use-warn If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`), ]), ) }) test('executes a default "error" strategy in a custom callback', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./callback-print.mocks.ts', import.meta.url)) const res = await fetch('https://mswjs.io/use-error') const status = res.status() // Request is performed as-is. expect(status).toBe(500) // Custom callback executed. expect(consoleSpy.get('log')).toContain( 'Oops, unhandled GET https://mswjs.io/use-error', ) expect(consoleSpy.get('warning')).toBeUndefined() // Prints the unhandled request error upon `print.error()`. expect(consoleSpy.get('error')).toEqual( expect.arrayContaining([ expect.stringContaining(`\ [MSW] Error: intercepted a request without a matching request handler: • GET https://mswjs.io/use-error If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`), ]), ) }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/callback-throws.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) worker.start({ onUnhandledRequest(request) { throw new Error(`Forbid unhandled ${request.method} ${request.url}`) }, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/callback.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) worker.start({ onUnhandledRequest(request) { console.log(`Oops, unhandled ${request.method} ${request.url}`) }, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/callback.test.ts ================================================ import { test, expect } from '../../../../playwright.extend' test('executes a given callback on an unhandled request', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./callback.mocks.ts', import.meta.url)) const res = await fetch('https://mswjs.io/non-existing-page') const status = res.status() // Request is performed as-is. expect(status).toBe(404) // Custom callback executed. expect(consoleSpy.get('log')).toContain( 'Oops, unhandled GET https://mswjs.io/non-existing-page', ) // No warnings/errors produced by MSW. expect(consoleSpy.get('error')).toBeUndefined() expect(consoleSpy.get('warning')).toBeUndefined() }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/default.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) // By default any unhandled requests are performed as-is // but produce warnings. worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/default.test.ts ================================================ import { test, expect } from '../../../../playwright.extend' test('warns on unhandled requests by default', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./default.mocks.ts', import.meta.url)) const res = await fetch('https://mswjs.io/non-existing-page') const status = res.status() expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ expect.stringMatching( /\[MSW\] Warning: intercepted a request without a matching request handler/, ), ]), ) expect(consoleSpy.get('error')).toBeUndefined() // Performs the request as-is. expect(status).toBe(404) }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/warn.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.json({ firstName: 'John' }) }), http.post('/explicit-return', () => { // Short-circuiting in a handler makes it perform the request as-is, // but still treats this request as handled. return }), http.post('/implicit-return', () => { // The handler that has no return also performs the request as-is, // still treating this request as handled. }), ) worker.start({ // Warn on the requests that are not handled in the request handlers above. // Does not cancel the request. onUnhandledRequest: 'warn', }) ================================================ FILE: test/browser/msw-api/setup-worker/start/on-unhandled-request/warn.test.ts ================================================ import { test, expect } from '../../../../playwright.extend' test('warns on an unhandled REST API request with an absolute URL', async ({ loadExample, spyOnConsole, fetch, createServer, }) => { const server = await createServer((app) => { app.get('/resource', (req, res) => res.status(404).end()) }) const consoleSpy = spyOnConsole() await loadExample(new URL('./warn.mocks.ts', import.meta.url)) const res = await fetch(server.http.url('/resource')) const status = res.status() expect(status).toBe(404) expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ expect.stringContaining(`\ [MSW] Warning: intercepted a request without a matching request handler: • GET ${server.http.url('/resource')} If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`), ]), ) }) test('warns on an unhandled REST API request with a relative URL', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./warn.mocks.ts', import.meta.url)) const res = await fetch('/user-details') const status = res.status() expect(status).toBe(404) expect(consoleSpy.get('warning')).toEqual( expect.arrayContaining([ expect.stringContaining(`\ [MSW] Warning: intercepted a request without a matching request handler: • GET /user-details If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`), ]), ) }) test('does not warn on request which handler explicitly returns no mocked response', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./warn.mocks.ts', import.meta.url)) const res = await fetch('/explicit-return', { method: 'POST' }) const status = res.status() expect(status).toBe(404) expect(consoleSpy.get('warning')).toEqual( expect.not.arrayContaining([ expect.stringContaining( '[MSW] Warning: intercepted a request without a matching request handler', ), ]), ) }) test('does not warn on request which handler implicitly returns no mocked response', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./warn.mocks.ts', import.meta.url)) const res = await fetch('/implicit-return', { method: 'POST' }) const status = res.status() expect(status).toBe(404) expect(consoleSpy.get('warning')).toEqual( expect.not.arrayContaining([ expect.stringContaining( '[MSW] Warning: intercepted a request without a matching request handler', ), ]), ) }) test('ignores common static assets when using the "warn" strategy', async ({ loadExample, spyOnConsole, page, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./warn.mocks.ts', import.meta.url)) // This request will error so perform it accordingly. await page.evaluate(() => { return fetch('http://localhost/styles/main.css').catch(() => null) }) expect(consoleSpy.get('warning')).toBeUndefined() }) ================================================ FILE: test/browser/msw-api/setup-worker/start/options-sw-scope.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) worker.start({ serviceWorker: { options: { // Service Worker would control the network traffic // outgoing only from the "/profile/*" pages. scope: '/profile', }, }, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/options-sw-scope.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('respects a custom "scope" Service Worker option', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('options-sw-scope.mocks.ts', import.meta.url)) expect(consoleSpy.get('startGroupCollapsed')).toEqual( expect.arrayContaining([expect.stringContaining('[MSW] Mocking enabled.')]), ) const res = await fetch('/user') const status = res.status() // Since the root "/" page lies outside of the custom worker scope, // it won't be able to intercept an otherwise matching request. expect(status).toBe(404) }) ================================================ FILE: test/browser/msw-api/setup-worker/start/quiet.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.json({ firstName: 'John', age: 32, }) }), ) Object.assign(window, { msw: { registration: worker.start({ // Disable logging of matched requests into browser's console quiet: true, }), }, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/quiet.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { registration: ReturnType } } test('does not log the intercepted request when the "quiet" option is set to "true"', async ({ loadExample, spyOnConsole, page, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./quiet.mocks.ts', import.meta.url), { // Using the "quiet" option to suppress the activation message. skipActivation: true, }) await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) await page.evaluate(() => { return window.msw.registration }) expect(consoleSpy.get('startGroupCollapsed')).toBeUndefined() const res = await fetch('/user') const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ firstName: 'John', age: 32, }) expect(consoleSpy.get('startGroupCollapsed')).toBeUndefined() }) ================================================ FILE: test/browser/msw-api/setup-worker/start/start.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return new Response() }), ) Object.assign(window, { msw: { async startWorker() { await worker.start({ serviceWorker: { // Use a custom Service Worker for this test that intentionally // delays the worker installation time. This allows us to test // that the "worker.start()" Promise indeed resolves only after // the worker has been activated and not just registered. url: './worker.js', }, }) }, }, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/start.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { TestFixtures, test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { startWorker(): ReturnType } } const exampleOptions: Parameters = [ new URL('./start.mocks.ts', import.meta.url), { skipActivation: true, beforeNavigation(compilation) { compilation.use((router) => { router.get('/worker.js', (_, res) => { res.sendFile(new URL('worker.delayed.js', import.meta.url).pathname) }) }) }, }, ] test('resolves the "start" Promise when the worker has been activated', async ({ loadExample, spyOnConsole, waitFor, page, }) => { await loadExample(...exampleOptions) const consoleSpy = spyOnConsole() const events: Array = [] const untilWorkerActivated = page .evaluate(() => { return new Promise((resolve) => { navigator.serviceWorker.addEventListener('controllerchange', resolve) }) }) .then(() => events.push('worker activated')) await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) const untilStartResolved = page .evaluate(() => window.msw.startWorker()) .then(() => events.push('start resolved')) const untilActivationMessage = waitFor(() => { expect(consoleSpy.get('startGroupCollapsed')).toContain( '[MSW] Mocking enabled.', ) events.push('enabled message') }) await Promise.all([ untilActivationMessage, untilWorkerActivated, untilStartResolved, ]) expect(events[0]).toEqual('worker activated') expect(events[1]).toEqual('start resolved') expect(events[2]).toEqual('enabled message') expect(events).toHaveLength(3) }) test('prints the start message when the worker has been registered', async ({ loadExample, spyOnConsole, page, }) => { const { compilation } = await loadExample(...exampleOptions) const consoleSpy = spyOnConsole() const expectedWorkerScope = new URL('.', compilation.previewUrl).href const expectedWorkerUrl = new URL('./worker.js', compilation.previewUrl).href await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) await page.evaluate(() => { return window.msw.startWorker() }) expect(consoleSpy.get('log')).toContain( `Worker scope: ${expectedWorkerScope}`, ) expect(consoleSpy.get('log')).toContain( `Worker script URL: ${expectedWorkerUrl}`, ) }) test('prints a warning if "worker.start()" is called multiple times', async ({ loadExample, spyOnConsole, page, }) => { await loadExample(...exampleOptions) const consoleSpy = spyOnConsole() await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) await page.evaluate(async () => { await window.msw.startWorker() await window.msw.startWorker() }) // The activation message ise printed only once. expect(consoleSpy.get('startGroupCollapsed')).toEqual([ '[MSW] Mocking enabled.', ]) // The warning is printed about multiple calls of "worker.start()". expect(consoleSpy.get('warning')).toEqual([ `[MSW] Found a redundant "worker.start()" call. Note that starting the worker while mocking is already enabled will have no effect. Consider removing this "worker.start()" call.`, ]) }) ================================================ FILE: test/browser/msw-api/setup-worker/start/warn-on-wait-until-ready.mocks.ts ================================================ import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start({ /** * @note This option is being deprecated. */ waitUntilReady: true, }) ================================================ FILE: test/browser/msw-api/setup-worker/start/warn-on-wait-until-ready.test.ts ================================================ import { test, expect } from '../../../playwright.extend' import { waitFor } from '../../../../support/waitFor' test('warns on the "waitUntilReady" option in "worker.start()"', async ({ loadExample, spyOnConsole, }) => { const consoleSpy = spyOnConsole() await loadExample( new URL('./warn-on-wait-until-ready.mocks.ts', import.meta.url), ) await waitFor(() => { expect(consoleSpy.get('warning')).toEqual([ `[MSW] The "waitUntilReady" option has been deprecated. Please remove it from this "worker.start()" call. Follow the recommended Browser integration (https://mswjs.io/docs/integrations/browser) to eliminate any race conditions between the Service Worker registration and any requests made by your application on initial render.`, ]) }) }) ================================================ FILE: test/browser/msw-api/setup-worker/start/worker.delayed.js ================================================ importScripts('/mockServiceWorker.js') self.addEventListener('install', (event) => { event.waitUntil( new Promise((resolve) => { // Emulate long worker installation. setTimeout(resolve, 500) }), ) }) ================================================ FILE: test/browser/msw-api/setup-worker/stop/in-flight-request.mocks.ts ================================================ import { http, HttpResponse, delay } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/resource', async () => { await delay(500) return HttpResponse.text('hello world') }), ) worker.start() window.msw = { // @ts-expect-error worker, } ================================================ FILE: test/browser/msw-api/setup-worker/stop/in-flight-request.test.ts ================================================ import type { SetupWorkerApi } from '../../../../../src/browser' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi } } test.beforeEach(() => { test.setTimeout(5000) }) test('resolves in-flight requests before the worker was stopped', async ({ loadExample, page, }) => { await loadExample(new URL('./in-flight-request.mocks.ts', import.meta.url)) const dataPromise: Promise = page.evaluate(() => { return fetch('/resource').then((response) => response.text()) }) await page.evaluate(() => { window.msw.worker.stop() }) await expect(dataPromise).resolves.toBe('hello world') }) test('bypasses requests made after the worker was stopped', async ({ loadExample, page, fetch, }) => { const { compilation } = await loadExample( new URL('./in-flight-request.mocks.ts', import.meta.url), { beforeNavigation(compilation) { compilation.use((router) => { router.get('/resource', (_req, res) => { res.send('original response') }) }) }, }, ) const resourceUrl = new URL('./resource', compilation.previewUrl) await page.evaluate(() => { window.msw.worker.stop() }) const data = await page.evaluate((url) => { return fetch(url).then((response) => response.text()) }, resourceUrl.href) expect(data).toBe('original response') }) ================================================ FILE: test/browser/msw-api/setup-worker/stop/quiet.mocks.ts ================================================ import { setupWorker } from 'msw/browser' const worker = setupWorker() Object.assign(window, { msw: { worker, }, }) ================================================ FILE: test/browser/msw-api/setup-worker/stop/quiet.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi } } const QUIET_EXAMPLE = new URL('./quiet.mocks.ts', import.meta.url) test('prints out the console stop message', async ({ loadExample, spyOnConsole, page, }) => { const consoleSpy = spyOnConsole() await loadExample(QUIET_EXAMPLE, { // Because the worker is started within the test. skipActivation: true, }) await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) await page.evaluate(() => { return window.msw.worker.start() }) await page.evaluate(() => { return window.msw.worker.stop() }) expect(consoleSpy.get('log')).toContain('[MSW] Mocking disabled.') }) test('does not print out any console stop message when in "quite" mode', async ({ loadExample, spyOnConsole, page, }) => { const consoleSpy = spyOnConsole() await loadExample(QUIET_EXAMPLE, { skipActivation: true, }) await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) await page.evaluate(() => { return window.msw.worker.start({ quiet: true }) }) await page.evaluate(() => { return window.msw.worker.stop() }) expect(consoleSpy.get('log')).toBeUndefined() }) ================================================ FILE: test/browser/msw-api/setup-worker/stop/removes-all-listeners.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const createWorker = () => { return setupWorker( http.get('/user', () => { return new HttpResponse() }), ) } Object.assign(window, { msw: { createWorker, }, }) ================================================ FILE: test/browser/msw-api/setup-worker/stop/removes-all-listeners.test.ts ================================================ import type { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { createWorker(): SetupWorkerApi } } test('removes all listeners when the worker is stopped', async ({ loadExample, spyOnConsole, browser, page, fetch, }) => { const firstPageConsoleSpy = spyOnConsole() await loadExample( new URL('./removes-all-listeners.mocks.ts', import.meta.url), { skipActivation: true, }, ) await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) await page.evaluate(async () => { await window.msw.createWorker().start() }) const secondPage = await browser.newPage() const secondPageConsoleSpy = spyOnConsole(secondPage) await secondPage.goto(page.url()) await secondPage.evaluate(async () => { const worker = window.msw.createWorker() await worker.start() worker.stop() }) expect(firstPageConsoleSpy.get('startGroupCollapsed')).toEqual([ '[MSW] Mocking enabled.', ]) expect(secondPageConsoleSpy.get('startGroupCollapsed')).toEqual([ '[MSW] Mocking enabled.', ]) expect(secondPageConsoleSpy.get('log')).toContain('[MSW] Mocking disabled.') await page.evaluate(() => fetch('/user')) await page.waitForLoadState('networkidle') expect(firstPageConsoleSpy.get('startGroupCollapsed')).toEqual([ '[MSW] Mocking enabled.', expect.stringContaining('GET /user'), ]) await secondPage.evaluate(() => fetch('/user')) expect(secondPageConsoleSpy.get('startGroupCollapsed')).toEqual([ '[MSW] Mocking enabled.', ]) }) ================================================ FILE: test/browser/msw-api/setup-worker/stop.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://api.github.com', () => { return HttpResponse.json({ mocked: true }) }), ) worker.start() Object.assign(window, { msw: { worker, }, }) ================================================ FILE: test/browser/msw-api/setup-worker/stop.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { Page } from '@playwright/test' import { test, expect } from '../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi } } const stopWorkerOn = async (page: Page) => { page.evaluate(() => { return window.msw.worker.stop() }) await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Failed to await the worker stop console message!')) }, 5000) page.on('console', (message) => { const text = message.text() if ( // Successful stop console message. text.includes('[MSW] Mocking disabled.') || // Warning when calling "stop()" multiple times. text.includes('worker.stop()') ) { clearTimeout(timeout) resolve() } }) }) } test('disables the mocking when the worker is stopped', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./stop.mocks.ts', import.meta.url)) await stopWorkerOn(page) const res = await fetch('https://api.github.com') const body = await res.json() expect.soft(res.fromServiceWorker()).toBe(true) expect.soft(body).not.toEqual({ mocked: true, }) }) test('keeps the mocking enabled in one tab when stopping the worker in another tab', async ({ loadExample, context, fetch, }) => { const { compilation } = await loadExample( new URL('./stop.mocks.ts', import.meta.url), ) const firstPage = await context.newPage() await firstPage.goto(compilation.previewUrl, { waitUntil: 'networkidle', }) const secondPage = await context.newPage() await secondPage.goto(compilation.previewUrl, { waitUntil: 'networkidle', }) await stopWorkerOn(firstPage) // Switch to another page. await secondPage.bringToFront() const res = await fetch('https://api.github.com', undefined, { page: secondPage, }) const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ mocked: true, }) }) test('prints a warning on multiple "worker.stop()" calls', async ({ loadExample, spyOnConsole, page, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./stop.mocks.ts', import.meta.url)) function byStopMessage(text: string): boolean { return text === '[MSW] Mocking disabled.' } await stopWorkerOn(page) // Prints the stop message and no warnings. expect(consoleSpy.get('log')!.filter(byStopMessage)).toHaveLength(1) expect(consoleSpy.get('warning')).toBeUndefined() await stopWorkerOn(page) // Does not print a duplicate stop message. expect(consoleSpy.get('log')!.filter(byStopMessage)).toHaveLength(1) // Prints a warning so the user knows something is not right. expect(consoleSpy.get('warning')).toEqual([ `[MSW] Found a redundant "worker.stop()" call. Notice that stopping the worker after it has already been stopped has no effect. Consider removing this "worker.stop()" call.`, ]) }) ================================================ FILE: test/browser/msw-api/setup-worker/use.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/book/:bookId', function originalResolver() { return HttpResponse.json({ title: 'Original title', }) }), ) worker.start() Object.assign(window, { msw: { worker, http, HttpResponse, }, }) ================================================ FILE: test/browser/msw-api/setup-worker/use.test.ts ================================================ import { http, HttpResponse } from 'msw' import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi http: typeof http HttpResponse: typeof HttpResponse } } test('returns a mocked response from a runtime request handler upon match', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./use.mocks.ts', import.meta.url)) await page.evaluate(() => { const { worker, http, HttpResponse } = window.msw worker.use( http.post('/login', function postLoginResolver() { return HttpResponse.json({ accepted: true }) }), ) }) const loginResponse = await fetch('/login', { method: 'POST', }) const loginStatus = loginResponse.status() const loginBody = await loginResponse.json() expect(loginStatus).toBe(200) expect(loginBody).toEqual({ accepted: true }) // Other request handlers are preserved, if there are no overlaps. const bookResponse = await fetch('/book/abc-123') const bookStatus = bookResponse.status() const bookBody = await bookResponse.json() expect(bookStatus).toBe(200) expect(bookBody).toEqual({ title: 'Original title' }) }) test('returns a mocked response from a persistent request handler override', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./use.mocks.ts', import.meta.url)) await page.evaluate(() => { const { worker, http, HttpResponse } = window.msw worker.use( http.get('/book/:bookId', function permanentOverride() { return HttpResponse.json({ title: 'Permanent override' }) }), ) }) const bookResponse = await fetch('/book/abc-123') const bookStatus = bookResponse.status() const bookBody = await bookResponse.json() expect(bookStatus).toBe(200) expect(bookBody).toEqual({ title: 'Permanent override' }) const anotherBookResponse = await fetch('/book/abc-123') const anotherBookStatus = anotherBookResponse.status() const anotherBookBody = await anotherBookResponse.json() expect(anotherBookStatus).toBe(200) expect(anotherBookBody).toEqual({ title: 'Permanent override' }) }) test('returns a mocked response from a one-time request handler override only upon first request match', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./use.mocks.ts', import.meta.url)) await page.evaluate(() => { const { worker, http, HttpResponse } = window.msw worker.use( http.get( '/book/:bookId', function oneTimeOverride() { return HttpResponse.json({ title: 'One-time override' }) }, { once: true }, ), ) }) const bookResponse = await fetch('/book/abc-123') const bookStatus = bookResponse.status() const bookBody = await bookResponse.json() expect(bookStatus).toBe(200) expect(bookBody).toEqual({ title: 'One-time override' }) const anotherBookResponse = await fetch('/book/abc-123') const anotherBookStatus = anotherBookResponse.status() const anotherBookBody = await anotherBookResponse.json() expect(anotherBookStatus).toBe(200) expect(anotherBookBody).toEqual({ title: 'Original title' }) }) test('returns a mocked response from a one-time request handler override only upon first request match with parallel requests', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./use.mocks.ts', import.meta.url)) await page.evaluate(() => { const { worker, http, HttpResponse } = window.msw worker.use( http.get<{ bookId: string }>( '/book/:bookId', function oneTimeOverride({ params }) { const { bookId } = params return HttpResponse.json({ title: 'One-time override', bookId }) }, { once: true }, ), ) }) const bookPromise = fetch('/book/abc-123') const anotherBookPromise = fetch('/book/abc-123') const bookResponse = await bookPromise const bookStatus = bookResponse.status() const bookBody = await bookResponse.json() expect(bookStatus).toBe(200) expect(bookBody).toEqual({ title: 'One-time override', bookId: 'abc-123' }) const anotherBookResponse = await anotherBookPromise const anotherBookStatus = anotherBookResponse.status() const anotherBookBody = await anotherBookResponse.json() expect(anotherBookStatus).toBe(200) expect(anotherBookBody).toEqual({ title: 'Original title' }) }) ================================================ FILE: test/browser/msw-api/setup-worker/worker-passthrough-header.mocks.ts ================================================ import { http, passthrough } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('*/resource', function originalResolver() { return passthrough() }), ) worker.start() ================================================ FILE: test/browser/msw-api/setup-worker/worker-passthrough-header.test.ts ================================================ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../../playwright.extend' const httpServer = new HttpServer((app) => { app.get('/resource', (req, res) => { res.set(req.headers) res.send('hello world') }) }) test.beforeAll(async () => { await httpServer.listen() }) test.afterAll(async () => { await httpServer.close() }) test('removes the internal passthrough request header', async ({ loadExample, fetch, }) => { await loadExample( new URL('./worker-passthrough-header.mocks.ts', import.meta.url), ) const response = await fetch(httpServer.http.url('/resource'), { headers: { 'x-custom-header': 'yes' }, }) const headers = await response.allHeaders() expect(headers).toMatchObject({ // The default header value. accept: '*/*', 'x-custom-header': 'yes', }) await expect(response.text()).resolves.toBe('hello world') }) test('preserves existing "accept" header values when removing the internal passthrough request header', async ({ loadExample, fetch, }) => { await loadExample( new URL('./worker-passthrough-header.mocks.ts', import.meta.url), ) const response = await fetch(httpServer.http.url('/resource'), { headers: { accept: 'text/plain, application/json', 'x-custom-header': 'yes', }, }) const headers = await response.allHeaders() expect(headers).toMatchObject({ accept: 'text/plain, application/json', 'x-custom-header': 'yes', }) await expect(response.text()).resolves.toBe('hello world') }) ================================================ FILE: test/browser/msw-api/unregister.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('*/resource', () => { return HttpResponse.json({ mocked: true }) }), ) Object.assign(window, { msw: { worker, }, }) ================================================ FILE: test/browser/msw-api/unregister.test.ts ================================================ import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi } } test('unregisters itself when not prompted to be activated again', async ({ loadExample, page, fetch, waitForMswActivation, }) => { const { compilation } = await loadExample( new URL('./unregister.mocks.ts', import.meta.url), { skipActivation: true, beforeNavigation(compilation) { compilation.use((router) => { router.get('/resource', (_, res) => { res.json({ original: true }) }) }) }, }, ) const resourceUrl = new URL('./resource', compilation.previewUrl).href await page.waitForFunction(() => { return typeof window.msw !== 'undefined' }) await page.evaluate(() => { return window.msw.worker.start() }) await waitForMswActivation() // Should have the mocking enabled. const firstResponse = await fetch(resourceUrl) const body = await firstResponse.json() expect(firstResponse.fromServiceWorker()).toBe(true) expect(body).toEqual({ mocked: true }) // Reload the page, not starting the worker manually this time. await page.reload({ waitUntil: 'networkidle' }) const secondResponse = await fetch(resourceUrl) const secondBody = await secondResponse.json() expect(secondResponse.fromServiceWorker()).toBe(false) expect(secondBody).toEqual({ original: true }) // Refresh the page the second time. await page.reload() const thirdResponse = await fetch(resourceUrl) const thirdBody = await thirdResponse.json() expect(secondResponse.fromServiceWorker()).toBe(false) expect(thirdBody).toEqual({ original: true }) }) ================================================ FILE: test/browser/playwright.config.ts ================================================ import { type PlaywrightTestConfig, devices } from '@playwright/test' export default { projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], timeout: 10_000, retries: process.env.CI ? 1 : undefined, workers: process.env.CI ? 3 : undefined, use: { trace: 'on-first-retry', launchOptions: { devtools: !process.env.CI, }, screenshot: 'only-on-failure', video: 'retain-on-failure', }, outputDir: './test-results', snapshotDir: './test-snapshots', forbidOnly: !!process.env.CI, reporter: 'list', // Run every test in parallel to ensure that tests are isolated // and there's no shared state leaking from the shared compilation // server or the preview server. fullyParallel: true, } satisfies PlaywrightTestConfig ================================================ FILE: test/browser/playwright.extend.ts ================================================ import url from 'node:url' import crypto from 'node:crypto' import { test as base, expect, type Response, Page } from '@playwright/test' import { Headers, headersToObject, flattenHeadersObject, type FlatHeadersObject, } from 'headers-polyfill' import { spyOnConsole, ConsoleMessages } from 'page-with' import { HttpServer, HttpServerMiddleware, } from '@open-draft/test-server/lib/http.js' import { Compilation, CompilationOptions, WebpackHttpServer, } from 'webpack-http-server' import { waitFor } from '../support/waitFor' import { WorkerConsole } from './setup/workerConsole' import { getWebpackServer } from './setup/webpackHttpServer' import { WebSocketServer } from '../support/WebSocketServer' export interface TestFixtures { /** * Create a test server instance. */ createServer(...middleware: Array): Promise webpackServer: WebpackHttpServer loadExample( entry: string | Array | URL, options?: CompilationOptions & { /** * Do not await the "Mocking enabled" message in the console. */ skipActivation?: boolean beforeNavigation?(compilation: Compilation): void }, ): Promise<{ compilation: Compilation workerConsole: WorkerConsole }> fetch( url: string, init?: RequestInit, options?: FetchOptions, ): Promise query(uri: string, options: GraphQLQueryOptions): Promise makeUrl(path: string): string spyOnConsole(page?: Page): ConsoleMessages waitFor(predicate: () => unknown): Promise waitForMswActivation(): Promise defineWebSocketServer(): Promise } interface FetchOptions { page?: Page waitForResponse?(res: Response): Promise | boolean } interface GraphQLQueryOptions { method?: 'GET' | 'POST' query: string variables?: Record multipartOptions?: GraphQLMultipartDataOptions } interface GraphQLMultipartDataOptions { map: Record> fileContents: Array } export const test = base.extend({ async createServer({}, use) { let server: HttpServer | undefined await use(async (...middleware) => { server = new HttpServer(...middleware) await server.listen() return server }) await server?.close() }, async webpackServer({}, use) { use(await getWebpackServer()) }, async loadExample({ page, webpackServer, waitForMswActivation }, use) { const pageExceptions: Array = [] page.on('pageerror', (error) => pageExceptions.push(error)) const workerConsole = new WorkerConsole() let compilation: Compilation | undefined await use(async (entry, options = {}) => { const resolvedEntry = entry instanceof URL ? url.fileURLToPath(entry) : entry compilation = await webpackServer.compile( Array.prototype.concat([], resolvedEntry), options, ) // Allow arbitrary setup code before navigating to the compilation preview. // This is useful to set up console watchers and other side-effects. options.beforeNavigation?.(compilation) // Forward browser runtime errors/warnings to the test runner. page.on('pageerror', console.error) const oncePageReady = [ page.waitForLoadState('domcontentloaded', { timeout: 15_000 }), page.waitForEvent('load', { timeout: 30_000 }), page.waitForLoadState('networkidle', { timeout: 5_000 }), ] page.goto(compilation.previewUrl) await Promise.all(oncePageReady) // All examples await the MSW activation message by default. // Support opting-out from this behavior for tests where activation // is not expected (e.g. when testing activation errors). if (!options.skipActivation) { await waitForMswActivation() } await workerConsole.init(page) return { compilation, workerConsole, } }) workerConsole.removeAllListeners() await compilation?.dispose() }, async waitFor({}, use) { await use(waitFor) }, async waitForMswActivation({ spyOnConsole }, use) { const consoleSpy = spyOnConsole() await use(() => { return waitFor(() => { const groupMessages = consoleSpy.get('startGroupCollapsed') if (groupMessages?.includes('[MSW] Mocking enabled.')) { consoleSpy.clear() return Promise.resolve() } return Promise.reject() }) }) consoleSpy.clear() }, async fetch({ page }, use) { await use(async (url, init, options = {}) => { const target = options.page || page const requestId = crypto.randomBytes(16).toString('hex') const fetchOptions = init || {} const initialHeaders = fetchOptions.headers || {} const requestHeaders = new Headers(initialHeaders) const identityHeaderName = 'accept-language' requestHeaders.set(identityHeaderName, requestId) const resolvedInit = { ...fetchOptions, headers: flattenHeadersObject(headersToObject(requestHeaders)), } // Don't await the request here so that we can await the response // later on based on the request identity headers. This way we can // perform multiple requests in parallel. target.evaluate( ([url, init]) => fetch(url, init as RequestInit), [url, resolvedInit], ) return target.waitForResponse(async (res) => { if (typeof options.waitForResponse !== 'undefined') { return options.waitForResponse(res) } const { [identityHeaderName]: actualRequestId, ['x-msw-bypass']: isBypassRequest, } = res.request().headers() return isBypassRequest !== 'true' && actualRequestId === requestId }) }) }, async query({ page }, use) { await use(async (uri, options) => { const requestId = crypto.randomUUID() const method = options.method || 'POST' const requestUrl = new URL(uri, 'http://localhost:8080') const headers: FlatHeadersObject = { 'x-request-id': requestId, } if (method === 'GET') { requestUrl.searchParams.set('query', options.query) if (options.variables) { requestUrl.searchParams.set( 'variables', JSON.stringify(options.variables), ) } } const responsePromise = page.evaluate< globalThis.Response, { url: string method: string headers: FlatHeadersObject options: GraphQLQueryOptions } >( ({ url, method, headers, options }) => { const init: RequestInit = { method, headers, } const operations = JSON.stringify({ query: options.query, variables: options.variables, }) function getMultipartGraphQLBody() { if (!options.multipartOptions) { throw new Error( 'Failed to construct a multi-part data GraphQL request: no options provided', ) } if (method !== 'POST') { throw new Error( 'Cannot perform a multi-part data GraphQL request: must use "POST" method', ) } const body = new FormData() body.set('operations', operations) body.set('map', JSON.stringify(options.multipartOptions.map)) options.multipartOptions.fileContents.forEach( (fileContent, index) => { const file = new File([fileContent], `file${index}.txt`) body.append(index.toString(), file) }, ) return body } if (method === 'POST') { if (!options.multipartOptions) { headers['Content-Type'] = 'application/json' } init.body = options.multipartOptions ? getMultipartGraphQLBody() : operations } return fetch(url, init).catch(() => { /** * @note Silence request rejections so that request errors * could be asserted in tests. */ return new Response(null, { status: 508 }) }) }, { url: requestUrl.href, method, headers, options, }, ) return new Promise((resolve, reject) => { responsePromise.catch(reject) return page .waitForResponse(async (res) => { const { ['x-request-id']: actualRequestId, ['x-msw-bypass']: isBypassRequest, } = res.request().headers() return isBypassRequest !== 'true' && actualRequestId === requestId }) .then(resolve) .catch(resolve) }) }) }, async makeUrl({ webpackServer }, use) { await use((pathname) => { return new URL(pathname, webpackServer.serverUrl).href }) }, async spyOnConsole({ page }, use) { let messages: ConsoleMessages | undefined await use((customPage) => { messages = spyOnConsole(customPage || (page as any)) return messages }) messages?.clear() }, async defineWebSocketServer({}, use) { const server = new WebSocketServer() await use(async () => { await server.listen() return server }) await server.close() }, }) export { expect } ================================================ FILE: test/browser/rest-api/204-response.test.ts ================================================ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../playwright.extend' const httpServer = new HttpServer((app) => { /** * @todo Test setup already spawns a single API server. * Reuse it instead of creating a new one in tests. */ app.get('/posts', (req, res) => { return res.status(204).end() }) }) test.beforeAll(async () => { await httpServer.listen() }) test.afterEach(async () => { await httpServer.close() }) test('handles a 204 status response without Response instance exceptions', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./basic.mocks.ts', import.meta.url)) let pageError: Error page.on('pageerror', (error) => { pageError = error }) const res = await fetch(httpServer.http.url('/posts')) // There must be no such exception: // Failed to construct 'Response': Response with null body status cannot have body expect(pageError).toBeUndefined() expect(res.status()).toBe(204) }) ================================================ FILE: test/browser/rest-api/206-response.mocks.ts ================================================ import { HttpResponse, http } from 'msw' import { setupWorker } from 'msw/browser' const data = 'hello world' const buffer = new TextEncoder().encode(data) const totalSize = buffer.byteLength const worker = setupWorker( http.get('/mocked-range', ({ request }) => { const range = request.headers.get('Range') if (!range) { throw HttpResponse.text('Missing Range', { status: 400 }) } const ranges = range.replace(/bytes=/, '').split('-') const start = +ranges[0] const end = ranges[1] ? +ranges[1] : totalSize - 1 const content = buffer.slice(start, end) return HttpResponse.arrayBuffer(content, { status: 206, headers: { 'Accept-Range': 'bytes', 'Content-Range': `bytes=${start}-${end}/${totalSize}`, 'Content-Length': content.byteLength.toString(), 'Content-Type': 'text/plain', }, }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/206-response.test.ts ================================================ /** * @see https://github.com/mswjs/msw/issues/1972 */ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { expect, test } from '../playwright.extend' const encoder = new TextEncoder() const server = new HttpServer((app) => { const data = 'hello world' const buffer = encoder.encode(data) const totalSize = buffer.byteLength app.get('/range', (req, res) => { const { range } = req.headers if (range) { const ranges = range.replace(/bytes=/, '').split('-') const start = +ranges[0] const end = ranges[1] ? +ranges[1] : totalSize - 1 const content = buffer.slice(start, end) res.writeHead(206, { 'Accept-Range': 'bytes', 'Content-Range': `bytes=${start}-${end}/${totalSize}`, 'Content-Length': content.byteLength, 'Content-Type': 'text/plain', }) return res.end(content) } res.writeHead(400) res.end('range missing') }) }) test.beforeAll(async () => { await server.listen() }) test.afterAll(async () => { await server.close() }) test('forwards the 206 response to a bypassed "Range" request', async ({ loadExample, fetch, }) => { await loadExample(new URL('./206-response.mocks.ts', import.meta.url)) const response = await fetch(server.http.url('/range'), { headers: { Range: 'bytes=2-8', }, }) expect(response.status()).toBe(206) expect(response.headers()).toMatchObject({ 'accept-range': 'bytes', 'content-range': 'bytes=2-8/11', 'content-length': '6', 'content-type': 'text/plain', }) expect(await response.text()).toBe('llo wo') }) test('responds with a 206 response to a mocked "Range" request', async ({ loadExample, fetch, }) => { await loadExample(new URL('./206-response.mocks.ts', import.meta.url)) const response = await fetch('/mocked-range', { headers: { Range: 'bytes=2-8', }, }) expect(response.status()).toBe(206) expect(response.headers()).toMatchObject({ 'accept-range': 'bytes', 'content-range': 'bytes=2-8/11', 'content-length': '6', 'content-type': 'text/plain', }) expect(await response.text()).toBe('llo wo') }) ================================================ FILE: test/browser/rest-api/basic.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://example.com/users/:username', ({ params }) => { const { username } = params return HttpResponse.json({ name: 'John Maverick', originalUsername: username, }) }), ) worker.start() Object.assign(window, { worker }) ================================================ FILE: test/browser/rest-api/basic.test.ts ================================================ import { test, expect } from '../playwright.extend' test('mocks response to a GET request', async ({ loadExample, fetch }) => { await loadExample(new URL('./basic.mocks.ts', import.meta.url)) const response = await fetch('https://example.com/users/octocat') const status = response.status() const body = await response.json() expect(status).toBe(200) expect(response.fromServiceWorker()).toBe(true) expect(body).toEqual({ name: 'John Maverick', originalUsername: 'octocat', }) }) ================================================ FILE: test/browser/rest-api/body.mocks.ts ================================================ import { http, HttpResponse, ResponseResolver } from 'msw' import { setupWorker } from 'msw/browser' const forwardRequestBody: ResponseResolver = async ({ request }) => { const requestText = request.headers.get('Content-Type') === 'application/json' && request.body ? await request.json() : await request.text() return HttpResponse.json({ value: requestText }) } const forwardMultipartRequestBody: ResponseResolver = async ({ request, }) => { const formData = await request.formData() const responseBody: Record> = {} for (const [name, value] of formData.entries()) { const nextValue = value instanceof File ? await value.text() : value if (responseBody[name]) { responseBody[name] = Array.prototype.concat( [], responseBody[name], nextValue, ) } else { responseBody[name] = nextValue } } return HttpResponse.json(responseBody) } const worker = setupWorker( http.get('/resource', forwardRequestBody), http.post('/resource', forwardRequestBody), http.post('/upload', forwardMultipartRequestBody), ) worker.start() ================================================ FILE: test/browser/rest-api/body.test.ts ================================================ import { test, expect } from '../playwright.extend' const EXAMPLE_PATH = new URL('./body.mocks.ts', import.meta.url) test('handles a GET request without a body', async ({ loadExample, fetch }) => { await loadExample(EXAMPLE_PATH) const res = await fetch('/resource') const body = await res.json() expect(body).toEqual({ value: '' }) }) test('handles a POST request with an explicit empty body', async ({ loadExample, fetch, }) => { await loadExample(EXAMPLE_PATH) const res = await fetch('/resource', { method: 'POST', body: '', }) const json = await res.json() expect(json).toEqual({ value: '' }) }) test('handles a POST request with a textual body', async ({ loadExample, fetch, }) => { await loadExample(EXAMPLE_PATH) const res = await fetch('/resource', { method: 'POST', body: 'text-body', }) const json = await res.json() expect(json).toEqual({ value: 'text-body' }) }) test('handles a POST request with a JSON body and "Content-Type: application/json" header', async ({ loadExample, fetch, }) => { await loadExample(EXAMPLE_PATH) const res = await fetch('/resource', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ firstName: 'John', }), }) const json = await res.json() expect(json).toEqual({ value: { firstName: 'John', }, }) }) test('handles a POST request with a multipart body and "Content-Type: multipart/form-data" header', async ({ loadExample, fetch, page, }) => { await loadExample(EXAMPLE_PATH) await page.evaluate(() => { const data = new FormData() data.set('file', new File(['file content'], 'file1.txt')) data.set('text', 'text content') data.set('text2', 'another text content') data.append('text2', 'another text content 2') fetch('/upload', { method: 'POST', body: data, }) }) const res = await page.waitForResponse(/\/upload/) const body = await res.json() expect(body).toEqual({ file: 'file content', text: 'text content', text2: ['another text content', 'another text content 2'], }) }) ================================================ FILE: test/browser/rest-api/context.mocks.ts ================================================ import { http, HttpResponse, delay } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://test.mswjs.io/', async () => { await delay(2000) return HttpResponse.json( { mocked: true }, { status: 201, statusText: 'Yahoo!', headers: { Accept: 'foo/bar', 'Custom-Header': 'arbitrary-value', }, }, ) }), ) worker.start() ================================================ FILE: test/browser/rest-api/context.test.ts ================================================ import { test, expect } from '../playwright.extend' test('composes various context utilities into a valid mocked response', async ({ loadExample, fetch, }) => { await loadExample(new URL('./context.mocks.ts', import.meta.url)) const res = await fetch('https://test.mswjs.io/') const headers = await res.allHeaders() const body = await res.json() expect(res.status()).toEqual(201) expect(res.statusText()).toEqual('Yahoo!') expect(res.fromServiceWorker()).toBe(true) expect(headers).toHaveProperty('content-type', 'application/json') expect(headers).toHaveProperty('accept', 'foo/bar') expect(headers).toHaveProperty('custom-header', 'arbitrary-value') expect(body).toEqual({ mocked: true, }) }) ================================================ FILE: test/browser/rest-api/cors.mocks.ts ================================================ import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start() ================================================ FILE: test/browser/rest-api/cors.test.ts ================================================ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../playwright.extend' const server = new HttpServer((app) => { // Enable a strict CORS policy on this test server. // Requests from the test must use `mode: "no-cors"` to obtain the response. app.use('*', (req, res, next) => { res.set('Access-Control-Allow-Origin', server.http.url()) next() }) app.get('/', (req, res) => { res.status(200).send('hello') }) }) test.beforeEach(async () => { await server.listen() }) test.afterEach(async () => { await server.close() }) test('handles a CORS request with an "opaque" response', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./cors.mocks.ts', import.meta.url)) const errors = [] page.on('pageerror', (error) => errors.push(error)) const res = await fetch(server.http.url(), { mode: 'no-cors', }) expect(res.status()).toBe(200) expect(await res.text()).toBe('hello') expect(errors).toEqual([]) }) ================================================ FILE: test/browser/rest-api/generator.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get<{ maxCount: string }>('/polling/:maxCount', function* ({ params }) { const { maxCount } = params let count = 0 while (count < Number(maxCount)) { count += 1 yield HttpResponse.json({ status: 'pending', count, }) } return HttpResponse.json({ status: 'complete', count, }) }), http.get<{ maxCount: string }>( '/polling/once/:maxCount', function* ({ params }) { const { maxCount } = params let count = 0 while (count < Number(maxCount)) { count += 1 yield HttpResponse.json({ status: 'pending', count, }) } return HttpResponse.json({ status: 'complete', count, }) }, { once: true }, ), http.get('/polling/once/:maxCount', () => { return HttpResponse.json({ status: 'done' }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/generator.test.ts ================================================ import { test, expect } from '../playwright.extend' type ExpectedResponseBody = | { status: 'pending' | 'complete' count: number } | { status: 'done' } test('supports a generator function as the response resolver', async ({ loadExample, fetch, }) => { await loadExample(new URL('./generator.mocks.ts', import.meta.url)) const assertRequest = async (expectedBody: ExpectedResponseBody) => { const res = await fetch('/polling/3') const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(res.status()).toBe(200) expect(body).toEqual(expectedBody) } await assertRequest({ status: 'pending', count: 1 }) await assertRequest({ status: 'pending', count: 2 }) await assertRequest({ status: 'pending', count: 3 }) await assertRequest({ status: 'complete', count: 3 }) await assertRequest({ status: 'complete', count: 3 }) await assertRequest({ status: 'complete', count: 3 }) }) test('supports one-time handlers with the generator as the response resolver', async ({ loadExample, fetch, }) => { await loadExample(new URL('./generator.mocks.ts', import.meta.url)) const assertRequest = async (expectedBody: ExpectedResponseBody) => { const res = await fetch('/polling/once/3') const body = await res.json() expect(res.fromServiceWorker()).toBe(true) expect(res.status()).toBe(200) expect(body).toEqual(expectedBody) } await assertRequest({ status: 'pending', count: 1 }) await assertRequest({ status: 'pending', count: 2 }) await assertRequest({ status: 'pending', count: 3 }) await assertRequest({ status: 'complete', count: 3 }) await assertRequest({ status: 'done' }) await assertRequest({ status: 'done' }) }) ================================================ FILE: test/browser/rest-api/headers-multiple.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.post('https://test.mswjs.io', ({ request }) => { return HttpResponse.json({ 'x-header': request.headers.get('x-header'), }) }), http.get('https://test.mswjs.io', () => { return HttpResponse.json( { mocked: true, }, { headers: { // List header values separated by comma // to set multie-value header on the mocked response. Accept: 'application/json, image/png', }, }, ) }), ) worker.start() ================================================ FILE: test/browser/rest-api/headers-multiple.test.ts ================================================ import { test, expect } from '../playwright.extend' const EXAMPLE_PATH = new URL('./headers-multiple.mocks.ts', import.meta.url) test('receives all headers from the request header with multiple values', async ({ loadExample, fetch, }) => { await loadExample(EXAMPLE_PATH) const headers = new Headers({ 'x-header': 'application/json' }) headers.append('x-header', 'application/hal+json') const res = await fetch('https://test.mswjs.io', { method: 'POST', headers: Object.fromEntries(headers.entries()), }) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(body).toEqual({ /** * @fixme Multiple headers value becomes incompatible * with the latest testing setup changes. */ 'x-header': 'application/json, application/hal+json', }) }) test('supports setting a header with multiple values on the mocked response', async ({ loadExample, fetch, }) => { await loadExample(EXAMPLE_PATH) const res = await fetch('https://test.mswjs.io') const status = res.status() const headers = await res.allHeaders() const body = await res.json() expect(status).toBe(200) expect(headers).toHaveProperty('accept', 'application/json, image/png') expect(body).toEqual({ mocked: true, }) }) ================================================ FILE: test/browser/rest-api/logging.test.ts ================================================ import { test, expect } from '../playwright.extend' import { StatusCodeColor } from '../../../src/core/utils/logging/getStatusCodeColor' import { waitFor } from '../../support/waitFor' test('prints the intercepted request info into browser console', async ({ loadExample, spyOnConsole, fetch, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./basic.mocks.ts', import.meta.url)) await fetch('https://example.com/users/octocat') await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( new RegExp( `^\\[MSW\\] \\d{2}:\\d{2}:\\d{2} GET https://example.com/users/octocat \\(%c200 OK%c\\) color:${StatusCodeColor.Success} color:inherit$`, ), ), ]), ) }) }) ================================================ FILE: test/browser/rest-api/params.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' type RequestParams = { username: string messageId: string } const worker = setupWorker( http.get( 'https://api.github.com/users/:username/messages/:messageId', ({ params }) => { const { username, messageId } = params return HttpResponse.json({ username, messageId, }) }, ), ) worker.start() ================================================ FILE: test/browser/rest-api/params.test.ts ================================================ import { test, expect } from '../playwright.extend' test('parses request URL parameters', async ({ loadExample, fetch }) => { await loadExample(new URL('./params.mocks.ts', import.meta.url)) const res = await fetch( 'https://api.github.com/users/octocat/messages/abc-123', ) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ username: 'octocat', messageId: 'abc-123', }) }) ================================================ FILE: test/browser/rest-api/plain-response.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/greeting', () => { return new Response('Hello, world!') }), ) worker.start() ================================================ FILE: test/browser/rest-api/plain-response.test.ts ================================================ import { test, expect } from '../playwright.extend' test('returns a plain Response as a mocked response', async ({ loadExample, fetch, spyOnConsole, }) => { await loadExample(new URL('./plain-response.mocks.ts', import.meta.url)) const consoleSpy = spyOnConsole() const response = await fetch('/greeting') const status = response.status() const body = await response.text() // Must return the correct response. expect(status).toBe(200) expect(response.fromServiceWorker()).toBe(true) expect(body).toEqual('Hello, world!') // Must print the correct log message in the console. expect(consoleSpy.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching(/\[MSW\] \d{2}:\d{2}:\d{2} GET \/greeting 200 OK/), ]), ) }) ================================================ FILE: test/browser/rest-api/query-params-warning.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( // WARNING: Intentionally invalid example of including a query parameter // in the request handler URL. Don't do that, consider matching against a path // and accessing query parameters in the response resolver instead. http.get('/user?name=admin', () => { return HttpResponse.text('user-response') }), http.post('/login?id=123&type=auth', () => { return HttpResponse.text('login-response') }), ) worker.start() ================================================ FILE: test/browser/rest-api/query-params-warning.test.ts ================================================ import { test, expect } from '../playwright.extend' test('warns when a request handler URL contains query parameters', async ({ loadExample, fetch, spyOnConsole, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./query-params-warning.mocks.ts', import.meta.url)) expect(consoleSpy.get('warning')).toEqual([ `[MSW] Found a redundant usage of query parameters in the request handler URL for "GET /user?name=admin". Please match against a path instead and access query parameters using "new URL(request.url).searchParams" instead. Learn more: https://mswjs.io/docs/http/intercepting-requests#querysearch-parameters`, `[MSW] Found a redundant usage of query parameters in the request handler URL for "POST /login?id=123&type=auth". Please match against a path instead and access query parameters using "new URL(request.url).searchParams" instead. Learn more: https://mswjs.io/docs/http/intercepting-requests#querysearch-parameters`, ]) await fetch('/user?name=admin').then(async (res) => { expect(res.status()).toBe(200) expect(await res.text()).toBe('user-response') }) await fetch('/user').then(async (res) => { expect(res.status()).toBe(200) expect(await res.text()).toBe('user-response') }) await fetch('/login?id=123&type=auth', { method: 'POST', }).then(async (res) => { expect(res.status()).toBe(200) expect(await res.text()).toBe('login-response') }) await fetch('/login', { method: 'POST', }).then(async (res) => { expect(res.status()).toBe(200) expect(await res.text()).toBe('login-response') }) }) ================================================ FILE: test/browser/rest-api/query.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://test.mswjs.io/api/books', ({ request }) => { const url = new URL(request.url) const bookId = url.searchParams.get('id') return HttpResponse.json({ bookId }) }), http.post('https://test.mswjs.io/products', ({ request }) => { const url = new URL(request.url) const productIds = url.searchParams.getAll('id') return HttpResponse.json({ productIds }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/query.test.ts ================================================ import { test, expect } from '../playwright.extend' const EXAMPLE_PATH = new URL('./query.mocks.ts', import.meta.url) test('retrieves a single request URL query parameter', async ({ loadExample, fetch, }) => { await loadExample(EXAMPLE_PATH) const res = await fetch('https://test.mswjs.io/api/books?id=abc-123') const status = res.status() const body = await res.json() expect(status).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ bookId: 'abc-123', }) }) test('retrieves multiple request URL query parameters', async ({ loadExample, fetch, }) => { await loadExample(EXAMPLE_PATH) const res = await fetch('https://test.mswjs.io/products?id=1&id=2&id=3', { method: 'POST', }) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ productIds: ['1', '2', '3'], }) }) ================================================ FILE: test/browser/rest-api/redirect.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/login', () => { return HttpResponse.text(null, { status: 307, headers: { Location: '/user', }, }) }), http.get('/user', () => { return HttpResponse.json({ firstName: 'John', lastName: 'Maverick', }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/redirect.test.ts ================================================ import { test, expect } from '../playwright.extend' test('supports redirect in a mocked response', async ({ loadExample, fetch, makeUrl, page, }) => { await loadExample(new URL('./redirect.mocks.ts', import.meta.url)) const [res, redirectRes] = await Promise.all([ await fetch('/login'), await page.waitForResponse(makeUrl('/user')), ]) const headers = await res.allHeaders() // Assert the original response returns redirect. expect(headers).toHaveProperty('location', '/user') expect(res.fromServiceWorker()).toBe(true) expect(res.status()).toBe(307) const redirectStatus = redirectRes.status() const redirectBody = await redirectRes.json() // Assert redirect gets requested and mocked. expect(redirectStatus).toBe(200) expect(redirectRes.fromServiceWorker()).toBe(true) expect(redirectBody).toEqual({ firstName: 'John', lastName: 'Maverick', }) }) ================================================ FILE: test/browser/rest-api/request/body/body-arraybuffer-range.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const buffer = new TextEncoder().encode('hello world') const worker = setupWorker( http.get('/resource', async ({ request }) => { const range = request.headers.get('range') if (!range) { throw new Response('Missing range', { status: 400 }) } const ranges = range.replace(/bytes=/, '').split('-') const start = +ranges[0] const end = ranges[1] ? +ranges[1] : buffer.byteLength - 1 const content = buffer.slice(start, end) return HttpResponse.arrayBuffer(content, { status: 206, headers: { 'accept-range': 'bytes', 'content-range': `bytes=${start}-${end}/${buffer.byteLength}`, 'content-length': content.byteLength.toString(), 'content-type': 'text/plain', }, }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/request/body/body-arraybuffer-range.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('responds with a range of a mocked buffer response', async ({ loadExample, fetch, }) => { await loadExample( new URL('./body-arraybuffer-range.mocks.ts', import.meta.url), ) const response = await fetch('/resource', { headers: { range: 'bytes=4-8', }, }) expect.soft(response.status()).toBe(206) await expect.soft(response.text()).resolves.toBe('o wo') }) ================================================ FILE: test/browser/rest-api/request/body/body-arraybuffer.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('reads request body as array buffer', async ({ loadExample, fetch }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const res = await fetch('/json', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ firstName: 'John' }), }) const body = await res.body() expect(res.status()).toBe(200) expect(body).toEqual(Buffer.from(JSON.stringify({ firstName: 'John' }))) }) test('reads buffer request body as array buffer', async ({ loadExample, fetch, page, makeUrl, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) page.evaluate(() => { return fetch('/json', { method: 'POST', body: new TextEncoder().encode(JSON.stringify({ firstName: 'John' })), }) }) const res = await page.waitForResponse(makeUrl('/json')) const body = await res.body() expect(res.status()).toBe(200) expect(body).toEqual(Buffer.from(JSON.stringify({ firstName: 'John' }))) }) test('reads null request body as empty array buffer', async ({ loadExample, page, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const [body, status] = await page.evaluate(() => { return fetch('/arrayBuffer', { method: 'POST', body: null, }).then((res) => res .arrayBuffer() .then((body) => [new TextDecoder().decode(body), res.status]), ) }) expect(status).toBe(200) expect(body).toBe('') }) test('reads undefined request body as empty array buffer', async ({ loadExample, page, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const [body, status] = await page.evaluate(() => { return fetch('/arrayBuffer', { method: 'POST', body: undefined, }).then((res) => res .arrayBuffer() .then((body) => [new TextDecoder().decode(body), res.status]), ) }) expect(status).toBe(200) expect(body).toBe('') }) ================================================ FILE: test/browser/rest-api/request/body/body-form-data.page.html ================================================ ================================================ FILE: test/browser/rest-api/request/body/body-form-data.test.ts ================================================ import { test, expect } from '../../../playwright.extend' declare global { interface Window { makeRequest(): void } } test('handles FormData as a request body', async ({ loadExample, page, makeUrl, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url), { markup: new URL('./body-form-data.page.html', import.meta.url), }) await page.evaluate(() => window.makeRequest()) const res = await page.waitForResponse(makeUrl('/formData')) const status = res.status() const json = await res.json() expect(status).toBe(200) expect(json).toEqual({ name: 'Alice', file: 'hello world', ids: [1, 2, 3], }) }) ================================================ FILE: test/browser/rest-api/request/body/body-json.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('reads request body as json', async ({ loadExample, fetch, page }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const res = await fetch('/json', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ firstName: 'John' }), }) const json = await res.json() expect(res.status()).toBe(200) expect(json).toEqual({ firstName: 'John' }) }) test('reads a single number as json request body', async ({ loadExample, fetch, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const res = await fetch('/json', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(123), }) const json = await res.json() expect(res.status()).toBe(200) expect(json).toEqual(123) }) test('reads request body using json() method', async ({ loadExample, fetch, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const res = await fetch('/json', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ firstName: 'John' }), }) const json = await res.json() expect(res.status()).toBe(200) expect(json).toEqual({ firstName: 'John' }) }) test('reads array buffer request body using json() method', async ({ loadExample, fetch, page, makeUrl, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) page.evaluate(() => { return fetch('/json', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: new TextEncoder().encode( JSON.stringify({ firstName: 'John', }), ), }) }) const res = await page.waitForResponse(makeUrl('/json')) const json = await res.json() expect(res.status()).toBe(200) expect(json).toEqual({ firstName: 'John' }) }) ================================================ FILE: test/browser/rest-api/request/body/body-text.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('reads plain text request body as text', async ({ loadExample, fetch, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const res = await fetch('/text', { method: 'POST', headers: { 'Content-Type': 'text/plain', }, body: 'hello-world', }) const body = await res.text() expect(res.status()).toBe(200) expect(body).toBe('hello-world') }) test('reads json request body as text', async ({ loadExample, fetch }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const res = await fetch('/text', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ firstName: 'John' }), }) const body = await res.text() expect(res.status()).toBe(200) expect(body).toBe(`{"firstName":"John"}`) }) test('reads buffer request body as text', async ({ loadExample, page, makeUrl, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) page.evaluate(() => { return fetch('/text', { method: 'POST', body: new TextEncoder().encode('hello-world'), }) }) const res = await page.waitForResponse(makeUrl('/text')) const body = await res.text() expect(res.status()).toBe(200) expect(body).toBe('hello-world') }) test('reads null request body as empty text', async ({ loadExample, page }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const [body, status] = await page.evaluate(() => { return fetch('/text', { method: 'POST', body: null, }).then((res) => res.text().then((text) => [text, res.status])) }) expect(status).toBe(200) expect(body).toBe('') }) test('reads undefined request body as empty text', async ({ loadExample, page, }) => { await loadExample(new URL('./body.mocks.ts', import.meta.url)) const [body, status] = await page.evaluate(() => { return fetch('/text', { method: 'POST', body: undefined, }).then((res) => res.text().then((text) => [text, res.status])) }) expect(status).toBe(200) expect(body).toBe('') }) ================================================ FILE: test/browser/rest-api/request/body/body.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.post('/text', async ({ request }) => { return HttpResponse.text(await request.text()) }), http.post('/json', async ({ request }) => { return HttpResponse.json(await request.json()) }), http.post('/arrayBuffer', async ({ request }) => { return HttpResponse.arrayBuffer(await request.arrayBuffer()) }), http.post('/formData', async ({ request }) => { const data = await request.formData() const name = data.get('name') const file = data.get('file') as File const fileText = await file.text() const ids = data.get('ids') as File const idsJson = JSON.parse(await ids.text()) return HttpResponse.json({ name, file: fileText, ids: idsJson, }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/request/matching/all.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.all('*/api/*', () => { return HttpResponse.text('hello world') }), http.all('*', () => { return HttpResponse.text('welcome to the jungle') }), ) worker.start() ================================================ FILE: test/browser/rest-api/request/matching/all.test.ts ================================================ /** * @vitest-environment node */ import { Response } from '@playwright/test' import { test, expect } from '../../../playwright.extend' function forEachMethod( callback: (method: string) => Promise, ): Promise { return Promise.all( ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'].map((method) => callback(method), ), ) } test('respects custom path when matching requests', async ({ loadExample, fetch, }) => { await loadExample(new URL('./all.mocks.ts', import.meta.url)) // Root request. const rootResponses = await forEachMethod((method) => { return fetch('http://localhost/api/', { method }) }) for (const response of rootResponses) { expect(response.status()).toEqual(200) expect(await response.text()).toEqual('hello world') } // Nested request. const nestedResponses = await forEachMethod((method) => { return fetch('http://localhost/api/user', { method }) }) for (const response of nestedResponses) { expect(response.status()).toBe(200) expect(await response.text()).toBe('hello world') } // Mismatched request. // There's a fallback "http.all()" in this test that acts // as a fallback request handler for all otherwise mismatched requests. const mismatchedResponses = await forEachMethod((method) => { return fetch('http://localhost/foo', { method }) }) for (const response of mismatchedResponses) { expect(response.status()).toEqual(200) expect(await response.text()).toEqual('welcome to the jungle') } }) ================================================ FILE: test/browser/rest-api/request/matching/custom-predicate.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker() worker.start() window.msw = { // @ts-expect-error worker, http, } ================================================ FILE: test/browser/rest-api/request/matching/custom-predicate.test.ts ================================================ import { http } from 'msw' import { SetupWorkerApi } from 'msw/browser' import { test, expect } from '../../../playwright.extend' declare namespace window { export const msw: { worker: SetupWorkerApi http: typeof http } } const PREDICATE_EXAMPLE = new URL( './custom-predicate.mocks.ts', import.meta.url, ) test('matches the request when the predicate function returns true', async ({ loadExample, fetch, page, }) => { await loadExample(PREDICATE_EXAMPLE) await page.evaluate(() => { const { worker, http } = window.msw worker.use( http.post( async ({ request }) => { const requestBody = await request.clone().text() return requestBody === 'hello world' }, ({ request }) => { return new Response(request.clone().body, request) }, ), ) }) const response = await fetch('/irrelevant', { method: 'POST', body: 'hello world', }) expect.soft(response.status()).toBe(200) await expect.soft(response.text()).resolves.toBe('hello world') }) test('does not match the request when the predicate function returns false', async ({ loadExample, fetch, page, }) => { await loadExample(PREDICATE_EXAMPLE) await page.evaluate(() => { const { worker, http } = window.msw worker.use( http.post( async ({ request }) => { const requestBody = await request.clone().text() return requestBody === 'hello world' }, ({ request }) => { return new Response(request.clone().body, request) }, ), ) }) const response = await fetch('/irrelevant', { method: 'POST', body: 'non-matching-request', }) expect(response.status()).toBe(404) }) ================================================ FILE: test/browser/rest-api/request/matching/method.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.post('*/user', () => { return HttpResponse.json({ mocked: true }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/request/matching/method.test.ts ================================================ import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../../../playwright.extend' const server = new HttpServer((app) => { app.get('/user', (req, res) => { res.json({ uses: 'original' }) }) app.post('/user', (req, res) => { res.status(500).json({ mocked: false }) }) }) test.beforeEach(async () => { await server.listen() }) test.afterEach(async () => { await server.close() }) test('sends a mocked response to a matching method and url', async ({ loadExample, fetch, }) => { await loadExample(new URL('./method.mocks.ts', import.meta.url)) const res = await fetch(server.http.url('/user'), { method: 'POST', }) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ mocked: true, }) }) test('sends original response to a non-matching request', async ({ loadExample, fetch, }) => { await loadExample(new URL('./method.mocks.ts', import.meta.url)) const res = await fetch(server.http.url('/user')) const status = res.status() const headers = await res.allHeaders() const body = await res.json() expect(status).toBe(200) expect(headers).toHaveProperty('x-powered-by', 'Express') expect(body).toEqual({ uses: 'original' }) }) ================================================ FILE: test/browser/rest-api/request/matching/path-params-decode.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://test.mswjs.io/reflect-url/:url', ({ params }) => { const { url } = params return HttpResponse.json({ url }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/request/matching/path-params-decode.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('decodes url componets', async ({ loadExample, fetch }) => { await loadExample(new URL('./path-params-decode.mocks.ts', import.meta.url)) const url = 'http://example.com:5001/example' const res = await fetch( `https://test.mswjs.io/reflect-url/${encodeURIComponent(url)}`, ) expect(res.status()).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(await res.json()).toEqual({ url, }) }) ================================================ FILE: test/browser/rest-api/request/matching/uri.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://api.github.com/made-up', () => { return HttpResponse.json({ mocked: true }) }), http.get('https://test.mswjs.io/messages/:messageId', ({ params }) => { const { messageId } = params return HttpResponse.json({ messageId }) }), http.get('https://test.mswjs.io/messages/:messageId/items', ({ params }) => { const { messageId } = params return HttpResponse.json({ messageId }) }), http.get(/(.+?)\.google\.com\/path/, () => { return HttpResponse.json({ mocked: true }) }), http.get(`/resource\\('id'\\)`, () => { return HttpResponse.json({ mocked: true }) }), http.get('./', ({ request }) => { const url = new URL(request.url) if (url.searchParams.has('resourceId')) { return HttpResponse.json({ mocked: true }) } }), ) worker.start() ================================================ FILE: test/browser/rest-api/request/matching/uri.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('matches an exact string with the same request URL with a trailing slash', async ({ loadExample, fetch, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await fetch('https://api.github.com/made-up/') expect(response.status()).toEqual(200) expect(response.fromServiceWorker()).toBe(true) await expect(response.json()).resolves.toEqual({ mocked: true, }) }) test('does not match an exact string with a different request URL with a trailing slash', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await page.evaluate(() => fetch('https://api.github.com/other/'), ) expect(response.status).not.toBe(200) }) test('matches an exact string with the same request URL without a trailing slash', async ({ loadExample, fetch, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await fetch('https://api.github.com/made-up') expect(response.status()).toEqual(200) expect(response.fromServiceWorker()).toBe(true) await expect(response.json()).resolves.toEqual({ mocked: true, }) }) test('does not match an exact string with a different request URL without a trailing slash', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await page.evaluate(() => fetch('https://api.github.com/other'), ) expect(response.status).not.toBe(200) }) test('matches a mask against a matching request URL', async ({ loadExample, fetch, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await fetch('https://test.mswjs.io/messages/abc-123') expect(response.status()).toEqual(200) expect(response.fromServiceWorker()).toBe(true) await expect(response.json()).resolves.toEqual({ messageId: 'abc-123', }) }) test('ignores query parameters when matching a mask against a matching request URL', async ({ loadExample, fetch, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await fetch( 'https://test.mswjs.io/messages/abc-123/items?hello=true', ) expect(response.status()).toEqual(200) expect(response.fromServiceWorker()).toBe(true) await expect(response.json()).resolves.toEqual({ messageId: 'abc-123', }) }) test('does not match a mask against a non-matching request URL', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await page.evaluate(() => fetch('https://test.mswjs.io/users/def-456').catch(() => null), ) expect(response).toBeNull() }) test('matches a RegExp against a matching request URL', async ({ loadExample, fetch, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await fetch('https://mswjs.google.com/path') expect(response.status()).toEqual(200) expect(response.fromServiceWorker()).toBe(true) await expect(response.json()).resolves.toEqual({ mocked: true, }) }) test('does not match a RegExp against a non-matching request URL', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await page.evaluate(() => fetch('https://mswjs.google.com/other').catch(() => null), ) expect(response).toBeNull() }) test('supports escaped parentheses in the request URL', async ({ loadExample, fetch, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await fetch(`/resource('id')`) expect(response.status()).toEqual(200) expect(response.fromServiceWorker()).toBe(true) await expect(response.json()).resolves.toEqual({ mocked: true, }) }) test('matches a relative URL starting with search parameters', async ({ loadExample, fetch, }) => { await loadExample(new URL('./uri.mocks.ts', import.meta.url)) const response = await fetch('?resourceId=abc-123') expect(response.status()).toEqual(200) expect(response.fromServiceWorker()).toBe(true) await expect(response.json()).resolves.toEqual({ mocked: true, }) }) ================================================ FILE: test/browser/rest-api/request/request-cookies.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('*/cookies', ({ cookies }) => { return HttpResponse.json(cookies) }), http.post('/set-cookies', async ({ request }) => { return new HttpResponse(null, { headers: { 'Set-Cookie': await request.clone().text(), }, }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/request/request-cookies.test.ts ================================================ import type { Page } from '@playwright/test' import { test, expect } from '../../playwright.extend' async function bakeCookies(page: Page, cookies: Array) { await page.evaluate((cookies) => { cookies.forEach((cookie) => { document.cookie = cookie }) }, cookies) } test('returns empty object if document has no cookies', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) const response = await fetch('/cookies') const documentCookies = await page.evaluate(() => document.cookie) expect(response.status()).toBe(200) await expect(response.json()).resolves.toEqual({}) expect(documentCookies).toBe('') }) test('returns empty object for request with "credentials: omit"', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) await bakeCookies(page, ['documentCookie=value']) const response = await fetch('/cookies', { credentials: 'omit' }) expect.soft(response.status()).toBe(200) await expect.soft(response.json()).resolves.toEqual({}) }) test('returns empty object for cross-origin request with "credentials: same-origin"', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) await bakeCookies(page, ['documentCookie=value']) const response = await fetch('https://example.com/cookies', { credentials: 'same-origin', }) expect.soft(response.status()).toBe(200) await expect.soft(response.json()).resolves.toEqual({}) }) test('returns cookies for same-origin request with "credentials: same-origin"', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) await bakeCookies(page, ['documentCookie=value']) const response = await fetch('/cookies', { credentials: 'same-origin', }) expect.soft(response.status()).toBe(200) await expect.soft(response.json()).resolves.toEqual({ documentCookie: 'value', }) }) test('returns cookies for same-origin request with "credentials: include"', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) await bakeCookies(page, ['firstCookie=value', 'secondCookie=anotherValue']) const response = await fetch('/cookies', { credentials: 'include', }) expect.soft(response.status()).toBe(200) await expect.soft(response.json()).resolves.toEqual({ firstCookie: 'value', secondCookie: 'anotherValue', }) }) test('returns cookies for cross-origin request with "credentials: include"', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) await bakeCookies(page, ['documentCookie=value']) const response = await fetch('https://example.com/cookies', { credentials: 'include', }) expect.soft(response.status()).toBe(200) await expect.soft(response.json()).resolves.toEqual({ documentCookie: 'value', }) }) test('inherits mocked cookies', async ({ loadExample, fetch, page }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) await bakeCookies(page, ['documentCookie=value']) // Make a request that sends mocked cookies. await fetch('/set-cookies', { method: 'POST', body: 'mockedCookie=mockedValue', }) const response = await fetch('/cookies', { credentials: 'include', }) expect.soft(response.status()).toBe(200) await expect.soft(response.json()).resolves.toEqual({ documentCookie: 'value', mockedCookie: 'mockedValue', }) }) test('inherits mocked cookies after page reload', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) await bakeCookies(page, ['documentCookie=value']) await fetch('/set-cookies', { method: 'POST', body: 'mockedCookie=mockedValue', }) // Reload the page to ensure that the mocked cookies persist. await page.reload({ waitUntil: 'networkidle' }) const response = await fetch('/cookies', { credentials: 'include', }) expect.soft(response.status()).toBe(200) await expect.soft(response.json()).resolves.toEqual({ documentCookie: 'value', mockedCookie: 'mockedValue', }) }) test('inherits mocked "HttpOnly" cookies', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) await bakeCookies(page, ['documentCookie=value']) await fetch('/set-cookies', { method: 'POST', body: 'mockedCookie=mockedValue; HttpOnly', }) await page.reload({ waitUntil: 'networkidle' }) const response = await fetch('/cookies', { credentials: 'include', }) expect.soft(response.status()).toBe(200) await expect.soft(response.json()).resolves.toEqual({ documentCookie: 'value', mockedCookie: 'mockedValue', }) }) test('respects cookie "Path" when exposing cookies', async ({ loadExample, fetch, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) /** * @note I tried including the `document.cookie` with * a specific `Path` but it behaves differently. It doesn't * even expose the cookie unless the PAGE path matches the * cookie path (reproducible in the browser). */ await fetch('/set-cookies', { method: 'POST', body: `mockedCookie=mockedValue; Path=/dashboard`, }) const nonMatchingResponse = await fetch('/cookies') // Must not return cookies for the request under a different path. await expect(nonMatchingResponse.json()).resolves.toEqual({}) // Must return the mocked cookie for a request with a matching path. const matchingResponse = await fetch('/dashboard/cookies') await expect(matchingResponse.json()).resolves.toEqual({ mockedCookie: 'mockedValue', }) }) test('deletes a cookie when sending "max-age=0" in a mocked response', async ({ loadExample, fetch, }) => { await loadExample(new URL('./request-cookies.mocks.ts', import.meta.url)) // First, set the cookie. await fetch('/set-cookies', { method: 'POST', body: `mockedCookie=mockedValue`, }) // Must forward the mocked cookied to the matching request. await expect(fetch('/cookies').then((res) => res.json())).resolves.toEqual({ mockedCookie: 'mockedValue', }) // Next, delete the cookie by setting "max-age=0". await fetch('/set-cookies', { method: 'POST', body: `mockedCookie=mockedValue; max-age=0`, }) // Must NOT have any cookies on the matching request. await expect( fetch('/cookies').then((response) => response.json()), ).resolves.toEqual({}) }) ================================================ FILE: test/browser/rest-api/response/body/body-binary.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' import base64Image from 'url-loader!../../../../fixtures/image.jpg' const worker = setupWorker( http.get('/images/:imageId', async () => { const imageBuffer = await fetch(base64Image).then((res) => res.arrayBuffer(), ) return HttpResponse.arrayBuffer(imageBuffer, { headers: { 'Content-Type': 'image/jpeg', }, }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/body/body-binary.test.ts ================================================ import fs from 'node:fs' import { test, expect } from '../../../playwright.extend' test('responds with a given binary body', async ({ loadExample, fetch }) => { await loadExample(new URL('./body-binary.mocks.ts', import.meta.url)) const res = await fetch('/images/abc-123') const status = res.status() const body = await res.body() const expectedBuffer = fs.readFileSync( new URL('../../../../fixtures/image.jpg', import.meta.url), ) expect(status).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(new Uint8Array(body)).toEqual(new Uint8Array(expectedBuffer)) }) ================================================ FILE: test/browser/rest-api/response/body/body-blob.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/greeting', async () => { const blob = new Blob(['hello world'], { type: 'text/plain', }) return new HttpResponse(blob) }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/body/body-blob.test.ts ================================================ import { test, expect } from '../.././../playwright.extend' test('responds to a request with a Blob', async ({ loadExample, fetch }) => { await loadExample(new URL('./body-blob.mocks.ts', import.meta.url)) const res = await fetch('/greeting') const headers = await res.allHeaders() expect(headers).toHaveProperty('content-type', 'text/plain') expect(res.fromServiceWorker()).toBe(true) const text = await res.text() expect(text).toBe('hello world') }) ================================================ FILE: test/browser/rest-api/response/body/body-formdata.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', async () => { const data = new FormData() data.append('name', 'Alice') data.append('age', '32') return HttpResponse.formData(data) }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/body/body-formdata.test.ts ================================================ import { test, expect } from '../.././../playwright.extend' test('responds to a request with FormData', async ({ loadExample, fetch }) => { await loadExample(new URL('./body-formdata.mocks.ts', import.meta.url)) const res = await fetch('/user') const headers = await res.allHeaders() expect(headers).toHaveProperty( 'content-type', expect.stringContaining('multipart/form-data'), ) expect(res.fromServiceWorker()).toBe(true) const text = await res.text() expect(text).toMatch( /------WebKitFormBoundary.+?\r\nContent-Disposition: form-data; name="name"\r\n\r\nAlice\r\n------WebKitFormBoundary.+?\r\nContent-Disposition: form-data; name="age"\r\n\r\n32\r\n------WebKitFormBoundary.+?--/gm, ) }) ================================================ FILE: test/browser/rest-api/response/body/body-html.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.html(`

Jane Doe

`) }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/body/body-html.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('responds with an HTML response body', async ({ loadExample, fetch }) => { await loadExample(new URL('./body-html.mocks.ts', import.meta.url)) const res = await fetch('/user') const status = res.status() const headers = await res.allHeaders() const text = await res.text() expect(status).toBe(200) expect(headers['content-type']).toBe('text/html') expect(text).toEqual(`

Jane Doe

`) }) ================================================ FILE: test/browser/rest-api/response/body/body-json.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/json', () => { return HttpResponse.json({ firstName: 'John' }) }), http.get('/number', () => { return HttpResponse.json(123) }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/body/body-json.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('responds with a JSON response body', async ({ loadExample, fetch }) => { await loadExample(new URL('./body-json.mocks.ts', import.meta.url)) const res = await fetch('/json') const headers = await res.allHeaders() const json = await res.json() expect(headers).toHaveProperty('content-type', 'application/json') expect(json).toEqual({ firstName: 'John' }) }) test('responds with a single number JSON response body', async ({ loadExample, fetch, }) => { await loadExample(new URL('./body-json.mocks.ts', import.meta.url)) const res = await fetch('/number') const headers = await res.allHeaders() const json = await res.json() expect(headers).toHaveProperty('content-type', 'application/json') expect(json).toEqual(123) }) ================================================ FILE: test/browser/rest-api/response/body/body-stream.mocks.ts ================================================ import { http, HttpResponse, delay } from 'msw' import { setupWorker } from 'msw/browser' const encoder = new TextEncoder() const chunks = ['hello', 'streaming', 'world'] const worker = setupWorker( http.get('/stream', () => { const stream = new ReadableStream({ async start(controller) { for (const chunk of chunks) { controller.enqueue(encoder.encode(chunk)) await delay(250) } controller.close() }, }) return new HttpResponse(stream, { headers: { 'Content-Type': 'application/octet-stream', 'Content-Length': chunks.join('').length.toString(), }, }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/body/body-stream.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('responds with a mocked ReadableStream response', async ({ loadExample, page, }) => { await loadExample(new URL('./body-stream.mocks.ts', import.meta.url)) const chunks = await page.evaluate(() => { return fetch('/stream').then(async (res) => { if (res.body === null) { return [] } const decoder = new TextDecoder() const chunks: Array<{ text: string; timestamp: number }> = [] const reader = res.body.getReader() while (true) { const { value, done } = await reader.read() if (done) { return chunks } chunks.push({ text: decoder.decode(value), timestamp: performance.now(), }) } }) }) // Must stream the mocked response in three chunks. const chunksText = chunks.map((chunk) => chunk.text) expect(chunksText).toEqual(['hello', 'streaming', 'world']) const chunkDeltas = chunks.map((chunk, index) => { const prevChunk = chunks[index - 1] return prevChunk ? chunk.timestamp - prevChunk.timestamp : 0 }) expect(chunkDeltas[0]).toBe(0) expect(chunkDeltas[1]).toBeGreaterThanOrEqual(200) expect(chunkDeltas[2]).toBeGreaterThanOrEqual(200) }) ================================================ FILE: test/browser/rest-api/response/body/body-text.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/text', () => { return HttpResponse.text('hello world') }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/body/body-text.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('responds with a text response body', async ({ loadExample, fetch }) => { await loadExample(new URL('./body-text.mocks.ts', import.meta.url)) const res = await fetch('/text') const headers = await res.allHeaders() const text = await res.text() expect(headers).toHaveProperty('content-type', 'text/plain') expect(text).toBe('hello world') }) ================================================ FILE: test/browser/rest-api/response/body/body-xml.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/user', () => { return HttpResponse.xml(` abc-123 John Maverick `) }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/body/body-xml.test.ts ================================================ import { test, expect } from '../../../playwright.extend' test('responds with an XML response body', async ({ loadExample, fetch }) => { await loadExample(new URL('./body-xml.mocks.ts', import.meta.url)) const res = await fetch('/user') const status = res.status() const headers = await res.allHeaders() const text = await res.text() expect(status).toBe(200) expect(headers['content-type']).toBe('text/xml') expect(text).toEqual(` abc-123 John Maverick `) }) ================================================ FILE: test/browser/rest-api/response/response-cookies.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/single-cookie', () => { return new HttpResponse(null, { headers: { 'Set-Cookie': 'myCookie=value', }, }) }), http.get('/multiple-cookies', () => { return new HttpResponse(null, { headers: [ ['Set-Cookie', 'firstCookie=yes'], ['Set-Cookie', 'secondCookie=no; Max-Age=1000'], ['Set-Cookie', 'thirdCookie=1,2,3'], ], }) }), http.get('/cookies-via-headers', () => { const headers = new Headers({ 'Set-Cookie': 'myCookie=value', }) return new HttpResponse(null, { headers }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/response-cookies.test.ts ================================================ import { test, expect } from '../../playwright.extend' test('supports mocking a single response cookie', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./response-cookies.mocks.ts', import.meta.url)) const response = await fetch('/single-cookie') const documentCookies = await page.evaluate(() => document.cookie) expect(response.status()).toBe(200) // Must not expose the forbidden "Set-Cookie" header. expect(await response.allHeaders()).not.toHaveProperty('set-cookie') // Must set the mocked cookie onto the document. expect(documentCookies).toBe('myCookie=value') }) test('supports mocking multiple response cookies', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./response-cookies.mocks.ts', import.meta.url)) const response = await fetch('/multiple-cookies') const documentCookies = await page.evaluate(() => document.cookie) expect(response.status()).toBe(200) expect(await response.allHeaders()).not.toHaveProperty('set-cookie') /** * @note The `Max-Age` attribute is not propagated onto the document. * If that's unexpected, raise an issue. */ expect(documentCookies).toBe( 'firstCookie=yes; secondCookie=no; thirdCookie=1,2,3', ) }) test('supports mocking cookies via a standalone Headers instance', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./response-cookies.mocks.ts', import.meta.url)) const response = await fetch('/cookies-via-headers') const documentCookies = await page.evaluate(() => document.cookie) expect(response.status()).toBe(200) expect(await response.allHeaders()).not.toHaveProperty('set-cookie') expect(documentCookies).toBe('myCookie=value') }) ================================================ FILE: test/browser/rest-api/response/response-error.mocks.ts ================================================ import { http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/resource', () => { return Response.error() }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/response-error.test.ts ================================================ import { test, expect } from '../../playwright.extend' test('responds with a network error using "Response.error" shorthand', async ({ loadExample, page, }) => { await loadExample(new URL('./response-error.mocks.ts', import.meta.url)) const networkError = await page.evaluate(() => { return fetch('/resource') .then(() => null) .catch((error) => ({ name: error.name, message: error.message, stack: error.stack, cause: error.cause, })) }) // Responding with a "Response.error()" produced a "Failed to fetch" error, // breaking the request. This is analogous to a network error. expect(networkError?.name).toBe('TypeError') expect(networkError?.message).toBe('Failed to fetch') // Guard against false positives due to exceptions arising from the library. expect(networkError?.cause).toBeUndefined() }) ================================================ FILE: test/browser/rest-api/response/throw-response.mocks.ts ================================================ import { HttpResponse, http } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/throw/plain', () => { throw new Response('hello world') }), http.get('/throw/http-response', () => { throw HttpResponse.text('hello world') }), http.get('/throw/error', () => { throw HttpResponse.text('invalid input', { status: 400 }) }), http.get('/throw/network-error', () => { throw HttpResponse.error() }), http.get('/middleware', ({ request }) => { const url = new URL(request.url) if (!url.searchParams.has('id')) { throw HttpResponse.text('must have id', { status: 400 }) } return HttpResponse.text('ok') }), http.get('/throw/non-response-error', () => { throw new Error('Oops!') }), ) worker.start() ================================================ FILE: test/browser/rest-api/response/throw-response.test.ts ================================================ import { test, expect } from '../../playwright.extend' test('supports throwing a plain Response in a response resolver', async ({ loadExample, fetch, }) => { await loadExample(new URL('./throw-response.mocks.ts', import.meta.url)) const response = await fetch('/throw/plain') expect(response.status()).toBe(200) expect(await response.text()).toBe('hello world') }) test('supports throwing an HttpResponse in a response resolver', async ({ loadExample, fetch, }) => { await loadExample(new URL('./throw-response.mocks.ts', import.meta.url)) const response = await fetch('/throw/http-response') expect(response.status()).toBe(200) expect(await response.headerValue('Content-Type')).toBe('text/plain') expect(await response.text()).toBe('hello world') }) test('supports throwing an error response in a response resolver', async ({ loadExample, fetch, }) => { await loadExample(new URL('./throw-response.mocks.ts', import.meta.url)) const errorResponse = await fetch('/throw/error') expect(errorResponse.status()).toBe(400) expect(await errorResponse.headerValue('Content-Type')).toBe('text/plain') expect(await errorResponse.text()).toBe('invalid input') }) test('supports throwing a network error in a response resolver', async ({ loadExample, page, }) => { await loadExample(new URL('./throw-response.mocks.ts', import.meta.url)) const networkError = await page.evaluate(() => { return fetch('/throw/network-error') .then(() => null) .catch((error) => ({ name: error.name, message: error.message, stack: error.stack, cause: error.cause, })) }) expect(networkError?.name).toBe('TypeError') expect(networkError?.message).toBe('Failed to fetch') expect(networkError?.cause).toBeUndefined() }) test('supports middleware-style responses', async ({ loadExample, fetch }) => { await loadExample(new URL('./throw-response.mocks.ts', import.meta.url)) const response = await fetch('/middleware?id=1') expect(response.status()).toBe(200) expect(await response.text()).toBe('ok') const errorResponse = await fetch('/middleware') expect(errorResponse.status()).toBe(400) expect(await errorResponse.text()).toBe('must have id') }) test('throws a non-Response error as-is', async ({ loadExample, fetch }) => { await loadExample(new URL('./throw-response.mocks.ts', import.meta.url)) // Unhandled exceptions in the response resolver in the browser // are coerces to 500 Internal Server Error responses by MSW. const networkError = await fetch('/throw/non-response-error') expect(networkError.status()).toBe(500) expect(await networkError.json()).toEqual({ name: 'Error', message: 'Oops!', stack: expect.any(String), }) }) ================================================ FILE: test/browser/rest-api/response-patching.mocks.ts ================================================ import { http, HttpResponse, bypass } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('*/user', async ({ request }) => { const originalResponse = await fetch(bypass(request.url)) const body = await originalResponse.json() return HttpResponse.json( { name: body.name, location: body.location, mocked: true, }, { headers: { 'X-Source': 'msw', }, }, ) }), http.get('*/repos/:owner/:repoName', async ({ request }) => { const originalResponse = await fetch(bypass(request)) const body = await originalResponse.json() return HttpResponse.json( { name: body.name, stargazers_count: 9999, }, { headers: { 'X-Source': 'msw', }, }, ) }), http.get('*/headers', async ({ request }) => { const proxyUrl = new URL('/headers-proxy', request.url) const originalResponse = await fetch( bypass(proxyUrl, { method: 'POST', headers: request.headers, }), ) const body = await originalResponse.json() return HttpResponse.json(body, { headers: { 'X-Source': 'msw', }, }) }), http.post('*/posts', async ({ request }) => { const originalResponse = await fetch(bypass(request)) const body = await originalResponse.json() return HttpResponse.json( { ...body, mocked: true, }, { headers: { 'X-Source': 'msw', 'X-Custom': originalResponse.headers.get('x-custom') || '', }, }, ) }), http.get('*/posts', async ({ request }) => { const originalResponse = await fetch(bypass(request)) const body = await originalResponse.json() return HttpResponse.json( { ...body, mocked: true, }, { headers: { 'X-Source': 'msw', }, }, ) }), http.head('*/posts', async ({ request }) => { const originalResponse = await fetch(bypass(request)) return HttpResponse.json( { mocked: true, }, { headers: { 'X-Source': 'msw', 'X-Custom': originalResponse.headers.get('x-custom') || '', }, }, ) }), ) worker.start() ================================================ FILE: test/browser/rest-api/response-patching.test.ts ================================================ import { matchRequestUrl } from 'msw' import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '../playwright.extend' const httpServer = new HttpServer((app) => { app.get('/user', (req, res) => { res.status(200).json({ name: 'The Octocat', location: 'San Francisco', }) }) app.get('/repos/:owner/:name', (req, res) => { res.status(200).json({ name: req.params.name }) }) app.post('/headers-proxy', (req, res) => { if (!req.headers.authorization) { return res.status(403).json({ message: 'error' }) } return res.status(200).json({ message: 'success' }) }) app.head('/posts', (req, res) => { res .status(200) .set({ 'Access-Control-Expose-Headers': 'X-Custom', 'X-Custom': 'HEAD REQUEST PATCHED', }) .end() }) app.get('/posts', (req, res) => { res.status(200).json({ id: 101 }) }) app.post('/posts', (req, res) => { res .status(200) .set({ 'Access-Control-Expose-Headers': 'X-Custom', 'X-Custom': 'POST REQUEST PATCHED', }) .json({ id: 101 }) }) }) test.beforeEach(async () => { await httpServer.listen() }) test.afterEach(async () => { await httpServer.close() }) test('responds with a combination of the mocked and original responses', async ({ loadExample, fetch, }) => { await loadExample(new URL('./response-patching.mocks.ts', import.meta.url)) const res = await fetch(httpServer.http.url('/user')) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ name: 'The Octocat', location: 'San Francisco', mocked: true, }) }) test('bypasses the original request when it equals the mocked request', async ({ loadExample, fetch, }) => { await loadExample(new URL('./response-patching.mocks.ts', import.meta.url)) const res = await fetch( httpServer.http.url('/repos/mswjs/msw?mocked=true'), undefined, { waitForResponse(res) { return ( // Await the response from MSW so that the original response // from the same URL would not interfere. matchRequestUrl(new URL(res.request().url()), res.url()).matches && res.headers()['x-source'] === 'msw' ) }, }, ) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ name: 'msw', stargazers_count: 9999, }) }) test('forwards custom request headers to the original request', async ({ loadExample, fetch, page, }) => { await loadExample(new URL('./response-patching.mocks.ts', import.meta.url)) const requestPromise = fetch(httpServer.http.url('/headers'), { headers: { Authorization: 'token', }, }) const req = await page.waitForRequest(httpServer.http.url('/headers')) const res = await requestPromise expect(req.headers()).toHaveProperty('authorization', 'token') expect(req.headers()).not.toHaveProperty('_headers') expect(req.headers()).not.toHaveProperty('_names') const status = res.status() const body = await res.json() expect(status).toEqual(200) expect(body).toEqual({ message: 'success' }) }) test('supports patching a HEAD request', async ({ loadExample, fetch }) => { await loadExample(new URL('./response-patching.mocks.ts', import.meta.url)) const res = await fetch( httpServer.http.url('/posts'), { method: 'HEAD', }, { waitForResponse(res) { const headers = res.headers() return ( headers['x-source'] === 'msw' && headers['x-msw-bypass'] !== 'true' ) }, }, ) const status = res.status() const headers = res.headers() expect(status).toBe(200) expect(headers).toEqual( expect.objectContaining({ 'x-source': 'msw', 'x-custom': 'HEAD REQUEST PATCHED', }), ) }) test('supports patching a GET request', async ({ loadExample, fetch, makeUrl, }) => { await loadExample(new URL('./response-patching.mocks.ts', import.meta.url)) const res = await fetch( httpServer.http.url('/posts'), { method: 'GET', headers: { 'Content-Type': 'application/json', }, }, { waitForResponse(res) { return ( matchRequestUrl(new URL(makeUrl(res.request().url())), res.url()) .matches && res.headers()['x-source'] === 'msw' ) }, }, ) const status = res.status() const body = await res.json() expect(status).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(body).toEqual({ id: 101, mocked: true }) }) test('supports patching a POST request', async ({ loadExample, fetch, makeUrl, }) => { await loadExample(new URL('./response-patching.mocks.ts', import.meta.url)) const res = await fetch( httpServer.http.url('/posts'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: 'foo', body: 'bar', userId: 1, }), }, { waitForResponse(res) { return ( matchRequestUrl(new URL(makeUrl(res.request().url())), res.url()) .matches && res.headers()['x-source'] === 'msw' ) }, }, ) const status = res.status() const headers = res.headers() const body = await res.json() expect(status).toBe(200) expect(res.fromServiceWorker()).toBe(true) expect(headers).toHaveProperty('x-custom', 'POST REQUEST PATCHED') expect(body).toEqual({ id: 101, mocked: true, }) }) ================================================ FILE: test/browser/rest-api/send-beacon.mocks.ts ================================================ import { http, bypass } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.post('/analytics', ({ request }) => { return new Response(request.body) }), http.post('*/analytics-bypass', ({ request }) => { const nextRequest = bypass(request) return fetch(nextRequest) }), ) worker.start() ================================================ FILE: test/browser/rest-api/send-beacon.test.ts ================================================ import { test, expect } from '../playwright.extend' test('supports mocking a response to a "sendBeacon" request', async ({ loadExample, page, }) => { await loadExample(new URL('./send-beacon.mocks.ts', import.meta.url)) const isQueuedPromise = page.evaluate(() => { return navigator.sendBeacon( '/analytics', JSON.stringify({ event: 'pageview' }), ) }) const response = await page.waitForResponse((response) => { return response.url().endsWith('/analytics') }) expect(response.status()).toBe(200) // Technically, "sendBeacon" responses don't send any body back. // We use this body only to verify that the request body was accessible // in the request handlers. await expect(response.text()).resolves.toBe('{"event":"pageview"}') // Must return true, indicating that the server has queued the sent data. await expect(isQueuedPromise).resolves.toBe(true) }) test('supports bypassing "sendBeacon" requests', async ({ loadExample, page, }) => { const { compilation } = await loadExample( new URL('./send-beacon.mocks.ts', import.meta.url), { beforeNavigation(compilation) { compilation.use((router) => { router.post('/analytics-bypass', (_req, res) => { res.status(200).end() }) }) }, }, ) const url = new URL('./analytics-bypass', compilation.previewUrl).href const isQueuedPromise = page.evaluate((url) => { return navigator.sendBeacon(url, JSON.stringify({ event: 'pageview' })) }, url) const response = await page.waitForResponse(url) expect(response.status()).toBe(200) await expect(isQueuedPromise).resolves.toBe(true) }) ================================================ FILE: test/browser/rest-api/status.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/posts', () => { // Setting response status code without status text // implicitly sets the correct status text. return HttpResponse.text(null, { status: 403 }) }), http.get('/user', () => { // Response status text can be overridden // to an arbitrary string value. return HttpResponse.text(null, { status: 401, statusText: 'Custom text', }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/status.test.ts ================================================ import { test, expect } from '../playwright.extend' const EXAMPLE_PATH = new URL('./status.mocks.ts', import.meta.url) test('sets given status code on the mocked response', async ({ loadExample, fetch, }) => { await loadExample(EXAMPLE_PATH) const res = await fetch('/posts') const status = res.status() const statusText = res.statusText() expect(status).toBe(403) expect(statusText).toBe('Forbidden') }) test('supports custom status text on the mocked response', async ({ loadExample, fetch, }) => { await loadExample(EXAMPLE_PATH) const res = await fetch('/user') const status = res.status() const statusText = res.statusText() expect(status).toBe(401) expect(statusText).toBe('Custom text') }) ================================================ FILE: test/browser/rest-api/xhr.mocks.ts ================================================ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('https://api.github.com/users/octocat', () => { return HttpResponse.json({ mocked: true }) }), ) worker.start() ================================================ FILE: test/browser/rest-api/xhr.test.ts ================================================ import { test, expect } from '../playwright.extend' test('mocks a response to an XMLHttpRequest', async ({ loadExample, page }) => { await loadExample(new URL('./xhr.mocks.ts', import.meta.url)) const REQUEST_URL = 'https://api.github.com/users/octocat' page.evaluate((url) => { const req = new XMLHttpRequest() req.open('GET', url) req.send() }, REQUEST_URL) const res = await page.waitForResponse(REQUEST_URL) const body = await res.json() expect(res.status()).toBe(200) expect(body).toEqual({ mocked: true, }) }) ================================================ FILE: test/browser/setup/webpackHttpServer.ts ================================================ import fs from 'node:fs' import { WebpackHttpServer } from 'webpack-http-server' // @ts-expect-error Importing a JavaScript module. import { SERVICE_WORKER_BUILD_PATH } from '../../../config/constants.js' declare global { var webpackServerPromise: Promise | null } globalThis.webpackServerPromise = null export async function getWebpackServer(): Promise { if (globalThis.webpackServerPromise) { return globalThis.webpackServerPromise } globalThis.webpackServerPromise = startWebpackServer() return globalThis.webpackServerPromise } async function startWebpackServer(): Promise { const server = new WebpackHttpServer({ before(app) { // Prevent Express from responding with cached 304 responses. app.set('etag', false) app.get('/mockServiceWorker.js', (req, res) => { res.set('Content-Type', 'application/javascript; charset=utf8') res.set('Content-Encoding', 'chunked') const readable = fs.createReadStream(SERVICE_WORKER_BUILD_PATH) // Apply the worker script patch to forward console messages // from the worker to all its clients. // readable.push(workerScriptPatch, 'utf8') readable.pipe(res) }) }, webpackConfig: { module: { rules: [ { test: /\.ts$/, use: [ { loader: 'esbuild-loader', options: { loader: 'ts', tsconfigRaw: fs.readFileSync( new URL('../../tsconfig.json', import.meta.url), ), }, }, ], }, ], }, resolve: { alias: { 'msw/browser': new URL( '../../../lib/browser/index.mjs', import.meta.url, ).pathname, msw: new URL('../../../lib/core/index.mjs', import.meta.url).pathname, }, extensions: ['.ts', '.js', '.mjs', '.cjs'], }, }, }) await server.listen() return server } ================================================ FILE: test/browser/setup/workerConsole.ts ================================================ import { format } from 'outvariant' import { Emitter } from 'strict-event-emitter' import { type Page } from '@playwright/test' type WorkerConsoleMessageType = | 'log' | 'info' | 'warn' | 'error' | 'debug' | 'trace' | 'dir' | 'dirxml' | 'table' | 'clear' | 'startGroup' | 'startGroupCollapsed' | 'endGroup' | 'assert' | 'count' | 'countReset' | 'time' | 'timeLog' | 'timeEnd' | 'group' | 'groupCollapsed' | 'groupEnd' | 'profile' | 'profileEnd' | 'timeline' | 'timelineEnd' | 'timeStamp' | 'context' | 'memory' type WorkerConsoleEventMap = { [MessageType in WorkerConsoleMessageType]: [message: string] } type InternalWorkerConsoleMessageData = { type: 'internal/console' payload: { messageType: WorkerConsoleMessageType args: Array } } declare global { interface Window { handleIncomingWorkerConsoleMessage: ( messageType: WorkerConsoleMessageType, args: Array, ) => void } } export class WorkerConsole extends Emitter { public messages: Map> = new Map() private addMessage( messageType: WorkerConsoleMessageType, message: string, ): void { const messages = this.messages.get(messageType) || [] this.messages.set(messageType, messages.concat(message)) } public async init(page: Page): Promise { // Ensure the worker is activated before establishing the listener. await page.evaluate(() => { if (navigator.serviceWorker.controller) { // Only await the worker if it actually exists. // This prevents the WorkerConsole from throwing // when initialized in the scenarios where MSW is // expected to fail/skip registration. Respectively, // no console message forwarding will be established. return navigator.serviceWorker.ready } }) await page.exposeFunction( 'handleIncomingWorkerConsoleMessage', (messageType: WorkerConsoleMessageType, args: Array) => { const [template, ...positionals] = args const formattedMessage = format(template, ...positionals) this.addMessage(messageType, formattedMessage) this.emit(messageType, formattedMessage) }, ) await page.evaluate(() => { navigator.serviceWorker.addEventListener( 'message', (event: MessageEvent) => { const { data } = event if (data.type === 'internal/console') { const { messageType, args } = data.payload window.handleIncomingWorkerConsoleMessage(messageType, args) } }, ) }) } public removeAllListeners(...args: Array) { this.messages.clear() return super.removeAllListeners(...args) } } /** * @why Service Worker lives in a separate thread and doesn't * trigger the Playwright's page console events. No way to access * the console events issued in the worker. * @see https://github.com/microsoft/playwright/issues/6559 * @see https://stackoverflow.com/questions/54339039/puppeteer-can-not-listen-service-workers-console */ export function getWorkerScriptPatch(): string { return ` // EVERYTHING BELOW THIS LINE HAS BEEN APPENDED // TO FORWARD WORKER CONSOLE MESSAGES TO THE CLIENTS. async function sendToAll(payload) { const clients = await self.clients.matchAll() for (const client of clients) { client.postMessage(payload) } } function interceptConsoleCalls(listener) { globalThis.console = new Proxy(globalThis.console, { get(target, property) { if (property in target) { return (...args) => { listener(property, args) return Reflect.get(target, property) } } }, }) } interceptConsoleCalls((messageType, args) => { sendToAll({ type: 'internal/console', payload: { messageType, args } }) }) ` } ================================================ FILE: test/browser/sse-api/sse.client.send.extraneous.test.ts ================================================ import { http, sse, type ServerSentEventMessage } from 'msw' import { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { setupWorker: typeof setupWorker http: typeof http sse: typeof sse } } test('supports triggerring events from another handlers', async ({ loadExample, page, }) => { await loadExample(new URL('./sse.mocks.ts', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, http, sse } = window.msw const target = new EventTarget() const worker = setupWorker( sse('/stream', ({ client }) => { // Listen to the event target dispatching message events // to trigger server-sent events to the client. target.addEventListener('message', (event) => { if (event instanceof MessageEvent) { client.send(event.data) } }) }), http.get('/trigger', () => { // Dispatch a message event onto the event target // to trigger a server-sent event to the client. target.dispatchEvent( new MessageEvent('message', { data: { data: 'hello world', } satisfies ServerSentEventMessage, }), ) return new Response() }), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise(async (resolve, reject) => { const source = new EventSource('/stream') source.addEventListener('message', (event) => { resolve(event.data) }) source.addEventListener('error', () => { reject(new Error('EventSource errored')) }) source.addEventListener('open', async () => { await fetch('/trigger').catch((error) => reject(error)) }) }) }) expect(message).toBe('hello world') }) ================================================ FILE: test/browser/sse-api/sse.client.send.multiline.test.ts ================================================ import { sse } from 'msw' import { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { setupWorker: typeof setupWorker sse: typeof sse } } const EXAMPLE_URL = new URL('./sse.mocks.ts', import.meta.url) test('sends data with LF newline', async ({ loadExample, page }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ data: 'line1\nline2' }) }), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.onerror = () => reject(new Error('EventSource error')) source.addEventListener('message', (event) => { resolve(event.data) }) }) }) expect(message).toBe('line1\nline2') }) test('sends data with double LF (blank line in middle)', async ({ loadExample, page, }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ data: 'before\n\nafter' }) }), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.onerror = () => reject(new Error('EventSource error')) source.addEventListener('message', (event) => { resolve(event.data) }) }) }) expect(message).toBe('before\n\nafter') }) test('sends data with CR newline', async ({ loadExample, page }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ data: 'line1\rline2' }) }), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.onerror = () => reject(new Error('EventSource error')) source.addEventListener('message', (event) => { resolve(event.data) }) }) }) expect(message, 'Normalizes CR to LF (via EventSource parser)').toBe( 'line1\nline2', ) }) test('sends data with CRLF newline', async ({ loadExample, page }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ data: 'line1\r\nline2' }) }), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.onerror = () => reject(new Error('EventSource error')) source.addEventListener('message', (event) => { resolve(event.data) }) }) }) expect(message, 'Normalizes CRLF to LF (via EventSource parser)').toBe( 'line1\nline2', ) }) test('sends data with mixed line endings', async ({ loadExample, page }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ // Mix of LF, CR, and CRLF data: 'a\nb\rc\r\nd', }) }), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.onerror = () => reject(new Error('EventSource error')) source.addEventListener('message', (event) => { resolve(event.data) }) }) }) expect(message, 'Normalizes line endings to LF').toBe('a\nb\nc\nd') }) test('sends data with multiple consecutive newlines', async ({ loadExample, page, }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ data: 'a\n\n\nb', }) }), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.onerror = () => reject(new Error('EventSource error')) source.addEventListener('message', (event) => { resolve(event.data) }) }) }) expect(message).toBe('a\n\n\nb') }) test('sends an empty string data', async ({ loadExample, page }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ data: '' }) }), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.onerror = () => reject(new Error('EventSource error')) source.addEventListener('message', (event) => { resolve(event.data) }) }) }) expect(message).toBe('') }) ================================================ FILE: test/browser/sse-api/sse.client.send.test.ts ================================================ import { sse } from 'msw' import { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { setupWorker: typeof setupWorker sse: typeof sse } } const EXAMPLE_URL = new URL('./sse.mocks.ts', import.meta.url) test('sends a mock message event', async ({ loadExample, page }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ data: { username: 'john' }, }) }), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.onerror = () => reject() source.addEventListener('message', (event) => { resolve(`${event.type}:${event.data}`) }) }) }) expect(message).toBe('message:{"username":"john"}') }) test('sends a mock custom event', async ({ loadExample, page }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse<{ userconnect: { username: string } }>( 'http://localhost/stream', ({ client }) => { client.send({ event: 'userconnect', data: { username: 'john' }, }) }, ), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.addEventListener('userconnect', (event) => { resolve(`${event.type}:${event.data}`) }) source.onerror = () => reject() }) }) expect(message).toEqual('userconnect:{"username":"john"}') }) test('sends a mock message event with custom id', async ({ loadExample, page, }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse<{ userconnect: { username: string } }>( 'http://localhost/stream', ({ client }) => { client.send({ id: 'abc-123', event: 'userconnect', data: { username: 'john' }, }) }, ), ) await worker.start() }) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.addEventListener('userconnect', (event) => { resolve(`${event.type}:${event.lastEventId}:${event.data}`) }) source.onerror = () => reject() }) }) expect(message).toBe('userconnect:abc-123:{"username":"john"}') }) test('errors the connected source', async ({ loadExample, page }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream-error', ({ client }) => { queueMicrotask(() => client.error()) }), ) await worker.start() }) const readyState = await page.evaluate(() => { return new Promise((resolve) => { const source = new EventSource('http://localhost/stream-error') source.onerror = () => resolve(source.readyState) source.onopen = () => console.log('OPEN?') }) }) // EventSource must be closed. expect(readyState).toBe(2) /** * @note That erroring the stream does not throw any errors. */ }) ================================================ FILE: test/browser/sse-api/sse.mocks.ts ================================================ import { http, sse } from 'msw' import { setupWorker } from 'msw/browser' window.msw = { setupWorker, // @ts-ignore http, sse, } ================================================ FILE: test/browser/sse-api/sse.quiet.test.ts ================================================ import { sse } from 'msw' import { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { setupWorker: typeof setupWorker sse: typeof sse } } const EXAMPLE_URL = new URL('./sse.mocks.ts', import.meta.url) test('does not log anything if the "quiet" option is set to true', async ({ loadExample, spyOnConsole, page, }) => { const consoleSpy = spyOnConsole() await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ data: { username: 'john' }, }) }), ) await worker.start({ quiet: true }) }) await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.onerror = () => reject() source.addEventListener('message', (event) => { resolve(`${event.type}:${event.data}`) }) }) }) expect(consoleSpy.get('startGroupCollapsed')).toBeUndefined() }) ================================================ FILE: test/browser/sse-api/sse.retry.test.ts ================================================ import { sse } from 'msw' import { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { setupWorker: typeof setupWorker sse: typeof sse } } const EXAMPLE_URL = new URL('./sse.mocks.ts', import.meta.url) test('supports mocking the retry time', async ({ loadExample, page }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ retry: 2000, }) // Simulate the connection closure. // This will trigger the client to reconnect. queueMicrotask(() => client.close()) }), ) await worker.start() }) const reconnectionTime = await page.evaluate(() => { return new Promise((resolve) => { const source = new EventSource('http://localhost/stream') source.onerror = () => { const errorAt = Date.now() console.assert( source.readyState === EventSource.CONNECTING, 'Expected the connection to be in CONNECTING state but got %d', source.readyState, ) source.addEventListener('open', () => { const reconnectAt = Date.now() resolve(reconnectAt - errorAt) }) } }) }) expect(reconnectionTime).toBeGreaterThanOrEqual(2000) }) ================================================ FILE: test/browser/sse-api/sse.server.connect.test.ts ================================================ import { sse } from 'msw' import { setupWorker } from 'msw/browser' import { createTestHttpServer } from '@epic-web/test-server/http' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { setupWorker: typeof setupWorker sse: typeof sse } } const EXAMPLE_URL = new URL('./sse.mocks.ts', import.meta.url) globalThis.console = new Proxy(globalThis.console, { apply(target, thisArg, argArray) { process.stdout.write('console called!') process.stdout.write(new Error().stack!) return Reflect.apply(target as any, thisArg, argArray) }, }) test('makes the actual request when called "server.connect()"', async ({ loadExample, page, }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await using server = await createTestHttpServer({ defineRoutes(routes) { routes.get('/stream', () => { const stream = new ReadableStream({ start(controller) { controller.enqueue( new TextEncoder().encode('data: {"message": "hello"}\n\n'), ) controller.close() }, }) return new Response(stream, { headers: { 'access-control-allow-origin': '*', 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }, }) }) }, }) const url = server.http.url('/stream').href await page.evaluate(async (url) => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse(url, ({ server }) => { // Calling "server.connect()" establishes the actual connection. server.connect() }), ) await worker.start() }, url) const openPromise = page.evaluate((url) => { return new Promise((resolve, reject) => { const source = new EventSource(url) source.onopen = () => resolve() source.onerror = () => reject(new Error('EventSource connection failed')) }) }, url) await expect(openPromise).resolves.toBeUndefined() }) test('forwards message event from the server to the client automatically', async ({ loadExample, page, }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await using server = await createTestHttpServer({ defineRoutes(routes) { routes.get('/stream', () => { const stream = new ReadableStream({ start(controller) { controller.enqueue( new TextEncoder().encode('data: {"message": "hello"}\n\n'), ) }, }) return new Response(stream, { headers: { 'access-control-allow-origin': '*', 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }, }) }) }, }) const url = server.http.url('/stream').href await page.evaluate(async (url) => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse(url, ({ server }) => { server.connect() }), ) await worker.start() }, url) const message = await page.evaluate((url) => { return new Promise<{ message: string }>((resolve, reject) => { const source = new EventSource(url) source.addEventListener('message', (event) => { resolve(JSON.parse(event.data)) }) source.onerror = () => reject(new Error('EventSource connection failed')) }) }, url) expect(message).toEqual({ message: 'hello' }) }) test('forwards custom event from the server to the client automatically', async ({ loadExample, page, }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await using server = await createTestHttpServer({ defineRoutes(routes) { routes.get('/stream', () => { const stream = new ReadableStream({ start(controller) { controller.enqueue( new TextEncoder().encode( 'event: custom\ndata: {"message": "hello"}\n\n', ), ) }, }) return new Response(stream, { headers: { 'access-control-allow-origin': '*', 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }, }) }) }, }) const url = server.http.url('/stream').href await page.evaluate(async (url) => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse(url, ({ server }) => { server.connect() }), ) await worker.start() }, url) const message = await page.evaluate((url) => { return new Promise<{ message: string }>((resolve, reject) => { const source = new EventSource(url) source.addEventListener('custom', (event) => { resolve(JSON.parse(event.data)) }) source.onerror = () => reject(new Error('EventSource connection failed')) }) }, url) expect(message).toEqual({ message: 'hello' }) }) test('forwards error event from the server to the client automatically', async ({ loadExample, page, }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await using server = await createTestHttpServer({ defineRoutes(routes) { routes.get('/stream', () => { const stream = new ReadableStream({ start(controller) { controller.error() }, }) return new Response(stream, { headers: { 'access-control-allow-origin': '*', 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }, }) }) }, }) const url = server.http.url('/stream').href await page.evaluate(async (url) => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse(url, ({ server }) => { server.connect() }), ) await worker.start() }, url) const errorPromise = page.evaluate((url) => { return new Promise((resolve, reject) => { const source = new EventSource(url) source.onerror = () => resolve() source.onmessage = () => reject(new Error('Must not receive a message')) }) }, url) await expect(errorPromise).resolves.toBeUndefined() }) test('forward custom stream errors from the original server to the client automatically', async ({ loadExample, page, }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) await using server = await createTestHttpServer({ defineRoutes(routes) { routes.get('/stream', () => { const stream = new ReadableStream({ start(controller) { controller.error(new Error('Custom stream error')) }, }) return new Response(stream, { headers: { 'access-control-allow-origin': '*', 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive', }, }) }) }, }) const url = server.http.url('/stream').href await page.evaluate(async (url) => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse(url, ({ server }) => { server.connect() }), ) await worker.start() }, url) const errorPromise = page.evaluate((url) => { return new Promise((resolve, reject) => { const source = new EventSource(url) source.onerror = () => resolve() source.onmessage = () => { reject(new Error('Must not receive a message')) } }) }, url) await expect(errorPromise).resolves.toBeUndefined() }) ================================================ FILE: test/browser/sse-api/sse.use.test.ts ================================================ import { sse } from 'msw' import { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { setupWorker: typeof setupWorker sse: typeof sse } } const EXAMPLE_URL = new URL('./sse.mocks.ts', import.meta.url) test('supports server-sent event handler overrides', async ({ loadExample, page, }) => { await loadExample(EXAMPLE_URL, { skipActivation: true, }) const workerRef = await page.evaluateHandle(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ client }) => { client.send({ data: 'happy-path' }) }), ) await worker.start() return worker }) await page.evaluate((worker) => { const { sse } = window.msw worker.use( // Adding this runtime request handler will make it // take precedence over the happy path handler above. sse('http://localhost/stream', ({ client }) => { // Queue the data for the next tick to rule out // the happy path handler from executing. queueMicrotask(() => { client.send({ data: 'override' }) }) }), ) }, workerRef) const message = await page.evaluate(() => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream') source.addEventListener('message', (event) => { resolve(event.data) }) source.onerror = reject }) }) expect(message).toBe('override') }) ================================================ FILE: test/browser/sse-api/sse.with-credentials.test.ts ================================================ import { sse } from 'msw' import { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare namespace window { export const msw: { setupWorker: typeof setupWorker sse: typeof sse } } const EXAMPLE_URL = new URL('./sse.mocks.ts', import.meta.url) test('forwards document cookies on the request when "withCredentials" is set to true', async ({ loadExample, page, spyOnConsole, }) => { const consoleSpy = spyOnConsole() await loadExample(EXAMPLE_URL, { skipActivation: true, }) await page.evaluate(() => { document.cookie = 'foo=bar' }) await page.evaluate(async () => { const { setupWorker, sse } = window.msw const worker = setupWorker( sse('http://localhost/stream', ({ cookies }) => { console.log(JSON.stringify({ cookies })) }), ) await worker.start() }) await page.evaluate((url) => { return new Promise((resolve, reject) => { const source = new EventSource('http://localhost/stream', { withCredentials: true, }) source.onopen = () => resolve() source.onerror = () => reject(new Error('EventSource connection failed')) }) }) expect(consoleSpy.get('log')).toContain( JSON.stringify({ cookies: { foo: 'bar', }, }), ) }) ================================================ FILE: test/browser/third-party/axios-upload.browser.test.ts ================================================ import type { AxiosResponse } from 'axios' import { test, expect } from '../playwright.extend' declare namespace window { export let upload: () => Promise< AxiosResponse<{ message: string; content: string }> > export let progressEvents: Array } test('responds with a mocked response to an upload request', async ({ loadExample, page, }) => { await loadExample(new URL('./axios-upload.runtime.js', import.meta.url)) const uploadResult = await page.evaluate(() => { return window.upload().then((response) => response.data) }) expect(uploadResult).toEqual({ message: 'Successfully uploaded "doc.txt"!', content: 'Helloworld', }) const progressEvents = await page.evaluate(() => { return window.progressEvents }) expect(progressEvents.length).toBeGreaterThan(0) expect(progressEvents[0]).toMatchObject({ bytes: expect.any(Number), loaded: expect.any(Number), }) }) ================================================ FILE: test/browser/third-party/axios-upload.runtime.js ================================================ import axios from 'axios' import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.post('/upload', async ({ request }) => { const data = await request.formData() const file = data.get('file') if (!file) { return new HttpResponse('Missing document upload', { status: 400 }) } if (!(file instanceof File)) { return new HttpResponse('Uploaded document is not a File', { status: 400, }) } return HttpResponse.json({ message: `Successfully uploaded "${file.name}"!`, content: await file.text(), }) }), ) worker.start() const progressEvents = [] const request = axios.create({ baseURL: '/', onDownloadProgress(event) { progressEvents.push(event) }, }) window.progressEvents = progressEvents window.upload = async function () { const formData = new FormData() const file = new Blob(['Hello', 'world'], { type: 'text/plain' }) formData.set('file', file, 'doc.txt') return request.post('/upload', formData) } ================================================ FILE: test/browser/ws-api/ws.apply.browser.test.ts ================================================ import type { ws } from 'msw' import type { SetupWorker, setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare global { interface Window { worker: SetupWorker msw: { ws: typeof ws setupWorker: typeof setupWorker } } } test('does not apply the interceptor until "worker.start()" is called', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(() => { const { setupWorker, ws } = window.msw const api = ws.link('wss://example.com') window.worker = setupWorker(api.addEventListener('connection', () => {})) }) await expect( page.evaluate(() => { return new WebSocket('wss://example.com').constructor.name }), ).resolves.toBe('WebSocket') await page.evaluate(async () => { await window.worker.start() }) await expect( page.evaluate(() => { return new WebSocket('wss://example.com').constructor.name }), ).resolves.not.toBe('WebSocket') }) ================================================ FILE: test/browser/ws-api/ws.client.send.test.ts ================================================ import type { ws } from 'msw' import type { setupWorker } from 'msw/browser' import type { Page } from '@playwright/test' import { test, expect } from '../playwright.extend' declare global { interface Window { msw: { ws: typeof ws setupWorker: typeof setupWorker } } } test('sends data to a single client on connection', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('wss://example.com') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { // Send a message to the client as soon as it connects. client.send('hello world') }), ) await worker.start() }) const clientMessage = await page.evaluate(async () => { const socket = new WebSocket('wss://example.com') return new Promise((resolve, reject) => { socket.onmessage = (event) => resolve(event.data) socket.onerror = () => reject(new Error('WebSocket error')) }).finally(() => socket.close()) }) expect(clientMessage).toBe('hello world') }) test('sends data to multiple clients on connection', async ({ loadExample, browser, page, }) => { const { compilation } = await loadExample( new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }, ) async function createSocketAndGetFirstMessage(page: Page) { await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('wss://example.com') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { // Send a message to the client as soon as it connects. client.send('hello world') }), ) await worker.start() }) return page.evaluate(async () => { const socket = new WebSocket('wss://example.com') return new Promise((resolve, reject) => { socket.onmessage = (event) => resolve(event.data) socket.onerror = () => reject(new Error('WebSocket error')) }).finally(() => socket.close()) }) } const secondPage = await browser.newPage() await secondPage.goto(compilation.previewUrl) const [firstClientMessage, secondClientMessage] = await Promise.all([ createSocketAndGetFirstMessage(page), createSocketAndGetFirstMessage(secondPage), ]) expect(firstClientMessage).toBe('hello world') expect(secondClientMessage).toBe('hello world') }) test('sends data in response to a client message', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('wss://example.com') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (typeof event.data === 'string' && event.data === 'hello') { client.send('hello world') } }) }), ) await worker.start() }) const clientMessage = await page.evaluate(async () => { const socket = new WebSocket('wss://example.com') socket.onopen = () => { socket.send('ignore this') socket.send('hello') } return new Promise((resolve, reject) => { socket.onmessage = (event) => resolve(event.data) socket.onerror = () => reject(new Error('WebSocket error')) }).finally(() => socket.close()) }) expect(clientMessage).toBe('hello world') }) ================================================ FILE: test/browser/ws-api/ws.clients.browser.test.ts ================================================ import type { WebSocketLink, ws } from 'msw' import type { SetupWorker, setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare global { interface Window { msw: { ws: typeof ws setupWorker: typeof setupWorker } worker: SetupWorker link: WebSocketLink ws: WebSocket messages: string[] } } test('returns the number of active clients in the same runtime', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://example.com') const worker = setupWorker(api.addEventListener('connection', () => {})) window.link = api await worker.start() }) // Must return 0 when no clients are present. expect( await page.evaluate(() => { return window.link.clients.size }), ).toBe(0) await page.evaluate(async () => { const ws = new WebSocket('wss://example.com') await new Promise((done) => (ws.onopen = done)) }) // Must return 1 after a single client joined. await expect( page.waitForFunction(() => window.link.clients.size === 1), ).resolves.toBeTruthy() await page.evaluate(async () => { const ws = new WebSocket('wss://example.com') await new Promise((done) => (ws.onopen = done)) }) // Must return 2 now that another client has joined. await expect( page.waitForFunction(() => window.link.clients.size === 2), ).resolves.toBeTruthy() }) test('returns the number of active clients across different runtimes', async ({ loadExample, context, }) => { const { compilation } = await loadExample( new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }, ) const pageOne = await context.newPage() const pageTwo = await context.newPage() for (const page of [pageOne, pageTwo]) { await page.goto(compilation.previewUrl) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://example.com') const worker = setupWorker(api.addEventListener('connection', () => {})) window.link = api await worker.start() }) } await pageOne.bringToFront() await pageOne.evaluate(async () => { const ws = new WebSocket('wss://example.com') await new Promise((done) => (ws.onopen = done)) }) await expect( pageOne.waitForFunction(() => window.link.clients.size === 1), ).resolves.toBeTruthy() await expect( pageTwo.waitForFunction(() => window.link.clients.size === 1), ).resolves.toBeTruthy() await pageTwo.bringToFront() await pageTwo.evaluate(async () => { const ws = new WebSocket('wss://example.com') await new Promise((done) => (ws.onopen = done)) }) await expect( pageTwo.waitForFunction(() => window.link.clients.size === 2), ).resolves.toBeTruthy() await expect( pageOne.waitForFunction(() => window.link.clients.size === 2), ).resolves.toBeTruthy() }) test('broadcasts messages across runtimes', async ({ loadExample, context, page, }) => { const { compilation } = await loadExample( new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }, ) const pageOne = await context.newPage() const pageTwo = await context.newPage() for (const page of [pageOne, pageTwo]) { await page.goto(compilation.previewUrl) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://example.com') // @ts-expect-error window.api = api const worker = setupWorker( api.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { api.broadcast(event.data) }) }), ) await worker.start() window.worker = worker }) await page.evaluate(() => { window.messages = [] const ws = new WebSocket('wss://example.com') window.ws = ws ws.onmessage = (event) => { window.messages.push(event.data) } }) } await pageOne.evaluate(() => { window.ws.send('hi from one') }) { await pageOne.waitForFunction(() => window.messages.length === 1) await expect(pageOne.evaluate(() => window.messages)).resolves.toEqual([ 'hi from one', ]) await pageTwo.waitForFunction(() => window.messages.length === 1) await expect(pageTwo.evaluate(() => window.messages)).resolves.toEqual([ 'hi from one', ]) } await pageTwo.evaluate(() => { window.ws.send('hi from two') }) { await pageTwo.waitForFunction(() => window.messages.length === 2) await expect(pageTwo.evaluate(() => window.messages)).resolves.toEqual([ 'hi from one', 'hi from two', ]) await pageOne.waitForFunction(() => window.messages.length === 2) await expect(pageOne.evaluate(() => window.messages)).resolves.toEqual([ 'hi from one', 'hi from two', ]) } }) test('clears the list of clients when the worker is stopped', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://example.com') const worker = setupWorker(api.addEventListener('connection', () => {})) window.link = api window.worker = worker await worker.start() }) await expect(page.evaluate(() => window.link.clients.size)).resolves.toBe(0) await page.evaluate(async () => { const ws = new WebSocket('wss://example.com') await new Promise((done) => (ws.onopen = done)) }) // Must return the number of joined clients. await expect( page.waitForFunction(() => window.link.clients.size === 1), ).resolves.toBeTruthy() await page.evaluate(() => { window.worker.stop() }) // Must purge the local storage on reload. // The worker has been started as a part of the test, not runtime, // so it will start with empty clients. await expect( page.waitForFunction(() => window.link.clients.size === 0), ).resolves.toBeTruthy() }) test('clears the list of clients when the page is reloaded', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) const enableMocking = async () => { await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://example.com') const worker = setupWorker(api.addEventListener('connection', () => {})) window.link = api window.worker = worker await worker.start() }) } await enableMocking() await expect( page.waitForFunction(() => window.link.clients.size === 0), ).resolves.toBeTruthy() await page.evaluate(async () => { const ws = new WebSocket('wss://example.com') await new Promise((done) => (ws.onopen = done)) }) // Must return the number of joined clients. await expect( page.waitForFunction(() => window.link.clients.size === 1), ).resolves.toBeTruthy() await page.reload() await enableMocking() // Must purge the local storage on reload. // The worker has been started as a part of the test, not runtime, // so it will start with empty clients. await expect( page.waitForFunction(() => window.link.clients.size === 0), ).resolves.toBeTruthy() }) ================================================ FILE: test/browser/ws-api/ws.intercept.client.browser.test.ts ================================================ import type { ws } from 'msw' import type { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare global { interface Window { msw: { ws: typeof ws setupWorker: typeof setupWorker } } } test('does not throw on connecting to a non-existing host', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('*') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { queueMicrotask(() => client.close()) }), ) await worker.start() }) const clientClosePromise = page.evaluate(() => { const socket = new WebSocket('ws://non-existing-host.com') return new Promise((resolve, reject) => { socket.onclose = () => resolve() socket.onerror = () => reject('WebSocket connection errored') }) }) await expect(clientClosePromise).resolves.toBeUndefined() }) test('intercepts outgoing client text message', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) const clientMessagePromise = page.evaluate(() => { const { setupWorker, ws } = window.msw const service = ws.link('wss://example.com') return new Promise(async (resolve) => { const worker = setupWorker( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (typeof event.data === 'string') { resolve(event.data) } }) }), ) await worker.start() }) }) await page.evaluate(() => { const socket = new WebSocket('wss://example.com') socket.onopen = () => socket.send('hello world') }) await expect(clientMessagePromise).resolves.toBe('hello world') }) test('intercepts outgoing client Blob message', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) const clientMessagePromise = page.evaluate(() => { const { setupWorker, ws } = window.msw const service = ws.link('wss://example.com') return new Promise(async (resolve) => { const worker = setupWorker( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data instanceof Blob) { resolve(event.data.text()) } }) }), ) await worker.start() }) }) await page.evaluate(() => { const socket = new WebSocket('wss://example.com') socket.onopen = () => socket.send(new Blob(['hello world'])) }) await expect(clientMessagePromise).resolves.toBe('hello world') }) test('intercepts outgoing client ArrayBuffer message', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) const clientMessagePromise = page.evaluate(() => { const { setupWorker, ws } = window.msw const service = ws.link('wss://example.com') return new Promise(async (resolve) => { const worker = setupWorker( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data instanceof Uint8Array) { resolve(new TextDecoder().decode(event.data)) } }) }), ) await worker.start() }) }) await page.evaluate(() => { const socket = new WebSocket('wss://example.com') socket.onopen = () => socket.send(new TextEncoder().encode('hello world')) }) await expect(clientMessagePromise).resolves.toBe('hello world') }) test('resolves relative link URL against the page origin', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('/api') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { console.log('HANDLER!') client.send('hello world') }), ) await worker.start() }) const messagePromise = page.evaluate(() => { const pendingMessage = Promise.withResolvers() const socket = new WebSocket('/api') socket.onmessage = (event) => pendingMessage.resolve(event.data) socket.onerror = () => pendingMessage.reject('Did not match the WebSocket connection') return pendingMessage.promise }) await expect(messagePromise).resolves.toBe('hello world') }) ================================================ FILE: test/browser/ws-api/ws.intercept.server.browser.test.ts ================================================ import type { ws } from 'msw' import type { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare global { interface Window { msw: { ws: typeof ws setupWorker: typeof setupWorker } } } test('intercepts incoming server text message', async ({ loadExample, page, defineWebSocketServer, }) => { const server = await defineWebSocketServer() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) server.on('connection', (client) => { client.send('hello') }) const serverMessagePromise = page.evaluate((serverUrl) => { const { setupWorker, ws } = window.msw const service = ws.link(serverUrl) return new Promise(async (resolve) => { const worker = setupWorker( service.addEventListener('connection', ({ server }) => { server.connect() server.addEventListener('message', (event) => { if (typeof event.data === 'string') { resolve(event.data) } }) }), ) await worker.start() }) }, server.url) const clientMessage = await page.evaluate((serverUrl) => { const socket = new WebSocket(serverUrl) return new Promise((resolve, reject) => { socket.onerror = () => reject(new Error('Socket error')) socket.addEventListener('message', (event) => { resolve(event.data) }) }) }, server.url) expect(clientMessage).toBe('hello') expect(await serverMessagePromise).toBe('hello') }) test('intercepts incoming server Blob message', async ({ loadExample, page, defineWebSocketServer, }) => { const server = await defineWebSocketServer() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) server.on('connection', async (client) => { /** * `ws` doesn't support sending Blobs. * @see https://github.com/websockets/ws/issues/2206 */ client.send(await new Blob(['hello']).arrayBuffer()) }) const serverMessagePromise = page.evaluate((serverUrl) => { const { setupWorker, ws } = window.msw const service = ws.link(serverUrl) return new Promise(async (resolve) => { const worker = setupWorker( service.addEventListener('connection', ({ server }) => { server.connect() server.addEventListener('message', (event) => { if (event.data instanceof Blob) { resolve(event.data.text()) } }) }), ) await worker.start() }) }, server.url) const clientMessage = await page.evaluate((serverUrl) => { const socket = new WebSocket(serverUrl) return new Promise((resolve, reject) => { socket.onerror = () => reject(new Error('Socket error')) socket.addEventListener('message', (event) => { resolve(event.data.text()) }) }) }, server.url) expect(clientMessage).toBe('hello') expect(await serverMessagePromise).toBe('hello') }) test('intercepts outgoing server ArrayBuffer message', async ({ loadExample, page, defineWebSocketServer, }) => { const server = await defineWebSocketServer() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) const encoder = new TextEncoder() server.on('connection', async (client) => { client.binaryType = 'arraybuffer' client.send(encoder.encode('hello')) }) const serverMessagePromise = page.evaluate((serverUrl) => { const { setupWorker, ws } = window.msw const service = ws.link(serverUrl) return new Promise(async (resolve) => { const worker = setupWorker( service.addEventListener('connection', ({ server }) => { server.connect() server.addEventListener('message', (event) => { resolve(new TextDecoder().decode(event.data as Uint8Array)) }) }), ) await worker.start() }) }, server.url) const clientMessage = await page.evaluate((serverUrl) => { const socket = new WebSocket(serverUrl) socket.binaryType = 'arraybuffer' return new Promise((resolve, reject) => { socket.onerror = () => reject(new Error('Socket error')) socket.addEventListener('message', (event) => { resolve(new TextDecoder().decode(event.data)) }) }) }, server.url) expect(clientMessage).toBe('hello') expect(await serverMessagePromise).toBe('hello') }) ================================================ FILE: test/browser/ws-api/ws.logging.browser.test.ts ================================================ import type { ws } from 'msw' import type { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare global { interface Window { msw: { ws: typeof ws setupWorker: typeof setupWorker } } } test('does not log anything if "quiet" was set to "true"', async ({ loadExample, page, spyOnConsole, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker(api.addEventListener('connection', () => {})) await worker.start({ quiet: true }) }) await page.evaluate(() => { const ws = new WebSocket('wss://localhost/path') ws.onopen = () => { ws.send('hello') ws.send('world') ws.close() } return new Promise((resolve, reject) => { ws.onclose = () => resolve() ws.onerror = () => reject(new Error('Client connection closed')) }) }) expect(consoleSpy.get('startGroupCollapsed')).toBeUndefined() }) test('logs the open event', async ({ loadExample, page, spyOnConsole, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker(api.addEventListener('connection', () => {})) await worker.start() }) await page.evaluate(() => { new WebSocket('wss://localhost/path') }) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2} %c▶%c wss:\/\/localhost\/path color:#3b82f6 color:inherit$/, ), ]), ) }) }) test('logs the close event initiated by the client', async ({ loadExample, page, spyOnConsole, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker(api.addEventListener('connection', () => {})) await worker.start() }) await page.evaluate(() => { const ws = new WebSocket('wss://localhost/path') ws.onopen = () => ws.close() }) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c■%c wss:\/\/localhost\/path color:#3b82f6 color:inherit$/, ), ]), ) }) }) test('logs the close event initiated by the original server', async ({ loadExample, spyOnConsole, page, waitFor, defineWebSocketServer, }) => { const server = await defineWebSocketServer() const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) server.on('connection', (ws) => { ws.close(1003) }) await page.evaluate(async (url) => { const { setupWorker, ws } = window.msw const api = ws.link(url) const worker = setupWorker( api.addEventListener('connection', ({ server }) => { server.connect() }), ) await worker.start() }, server.url) await page.evaluate((url) => { new WebSocket(url) }, server.url) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c■%c ws:\/\/(.+):\d{4,}\/ color:#3b82f6 color:inherit$/, ), ]), ) }) }) test('logs the close event initiated by the event handler', async ({ loadExample, page, spyOnConsole, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker( api.addEventListener('connection', ({ client }) => { client.close() }), ) await worker.start() }) await page.evaluate(() => { new WebSocket('wss://localhost/path') }) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c■%c wss:\/\/localhost\/path color:#3b82f6 color:inherit$/, ), ]), ) }) }) test('logs outgoing client message sending text', async ({ loadExample, page, spyOnConsole, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker(api.addEventListener('connection', () => {})) await worker.start() }) await page.evaluate(() => { const ws = new WebSocket('wss://localhost/path') ws.onopen = () => ws.send('hello world') }) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬆%c hello world %c11%c color:#22c55e color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('logs outgoing client message sending long text', async ({ loadExample, page, spyOnConsole, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker(api.addEventListener('connection', () => {})) await worker.start() }) await page.evaluate(() => { const ws = new WebSocket('wss://localhost/path') ws.onopen = () => ws.send('this is an extremely long sentence to log out') }) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬆%c this is an extremely lon… %c45%c color:#22c55e color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('logs outgoing client message sending Blob', async ({ loadExample, page, spyOnConsole, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker(api.addEventListener('connection', () => {})) await worker.start() }) await page.evaluate(() => { const ws = new WebSocket('wss://localhost/path') ws.onopen = () => ws.send(new Blob(['hello world'])) }) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬆%c Blob\(hello world\) %c11%c color:#22c55e color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('logs outgoing client message sending long Blob', async ({ loadExample, page, spyOnConsole, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker(api.addEventListener('connection', () => {})) await worker.start() }) await page.evaluate(() => { const ws = new WebSocket('wss://localhost/path') ws.onopen = () => ws.send(new Blob(['this is an extremely long sentence to log out'])) }) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬆%c Blob\(this is an extremely lon…\) %c45%c color:#22c55e color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('logs outgoing client message sending ArrayBuffer data', async ({ loadExample, page, spyOnConsole, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker(api.addEventListener('connection', () => {})) await worker.start() }) await page.evaluate(() => { const ws = new WebSocket('wss://localhost/path') ws.onopen = () => ws.send(new TextEncoder().encode('hello world')) }) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬆%c ArrayBuffer\(hello world\) %c11%c color:#22c55e color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('logs outgoing client message sending long ArrayBuffer', async ({ loadExample, page, spyOnConsole, waitFor, }) => { const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const api = ws.link('wss://localhost/*') const worker = setupWorker(api.addEventListener('connection', () => {})) await worker.start() }) await page.evaluate(() => { const ws = new WebSocket('wss://localhost/path') ws.onopen = () => ws.send( new TextEncoder().encode( 'this is an extremely long sentence to log out', ), ) }) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬆%c ArrayBuffer\(this is an extremely lon…\) %c45%c color:#22c55e color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('logs incoming server messages', async ({ loadExample, page, spyOnConsole, waitFor, defineWebSocketServer, }) => { const server = await defineWebSocketServer() const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) server.addListener('connection', (ws) => { ws.send('hello from server') ws.addEventListener('message', (event) => { if (event.data === 'how are you, server?') { ws.send('thanks, not bad') } }) }) await page.evaluate(async (url) => { const { setupWorker, ws } = window.msw const api = ws.link(url) const worker = setupWorker( api.addEventListener('connection', ({ client, server }) => { server.connect() }), ) await worker.start() }, server.url) await page.evaluate((url) => { const ws = new WebSocket(url) ws.addEventListener('message', (event) => { if (event.data === 'hello from server') { ws.send('how are you, server?') } }) }, server.url) // Initial message sent to every connected client. await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬇%c hello from server %c17%c color:#ef4444 color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) // Message sent in response to a client message. await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬇%c thanks, not bad %c15%c color:#ef4444 color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('logs raw incoming server events', async ({ loadExample, page, spyOnConsole, waitFor, defineWebSocketServer, }) => { const server = await defineWebSocketServer() const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) server.addListener('connection', (ws) => { ws.send('hello from server') }) await page.evaluate(async (url) => { const { setupWorker, ws } = window.msw const api = ws.link(url) const worker = setupWorker( api.addEventListener('connection', ({ client, server }) => { server.connect() server.addEventListener('message', (event) => { event.preventDefault() // This is the only data the client will receive // but we should still print the raw server message. client.send('intercepted server event') }) }), ) await worker.start() }, server.url) await page.evaluate((url) => { new WebSocket(url) }, server.url) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ // The actual (raw) message received from the server. // The arrow is dotted because the message's default has been prevented. expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⇣%c hello from server %c17%c color:#ef4444 color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), // The mocked message sent from the event handler (client.send()). expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬇%c intercepted server event %c24%c color:#ff6a33 color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('logs mocked outgoing client message (server.send)', async ({ loadExample, page, spyOnConsole, waitFor, defineWebSocketServer, }) => { const server = await defineWebSocketServer() const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async (url) => { const { setupWorker, ws } = window.msw const api = ws.link(url) const worker = setupWorker( api.addEventListener('connection', ({ server }) => { server.connect() server.send('hello from handler') }), ) await worker.start() }, server.url) await page.evaluate((url) => { new WebSocket(url) }, server.url) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬆%c hello from handler %c18%c color:#ff6a33 color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('logs mocked incoming server message (client.send)', async ({ loadExample, page, spyOnConsole, waitFor, defineWebSocketServer, }) => { const server = await defineWebSocketServer() const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async (url) => { const { setupWorker, ws } = window.msw const api = ws.link(url) const worker = setupWorker( api.addEventListener('connection', ({ client }) => { client.send('hello from handler') }), ) await worker.start() }, server.url) await page.evaluate((url) => { new WebSocket(url) }, server.url) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⬇%c hello from handler %c18%c color:#ff6a33 color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('marks the prevented outgoing client event as dashed', async ({ loadExample, page, spyOnConsole, waitFor, defineWebSocketServer, }) => { const server = await defineWebSocketServer() const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async (url) => { const { setupWorker, ws } = window.msw const api = ws.link(url) const worker = setupWorker( api.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { event.preventDefault() }) }), ) await worker.start() }, server.url) await page.evaluate((url) => { const socket = new WebSocket(url) socket.onopen = () => socket.send('hello world') }, server.url) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⇡%c hello world %c11%c color:#22c55e color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) test('marks the prevented incoming server event as dashed', async ({ loadExample, page, spyOnConsole, waitFor, defineWebSocketServer, }) => { const server = await defineWebSocketServer() const consoleSpy = spyOnConsole() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) server.addListener('connection', (ws) => { ws.send('hello from server') }) await page.evaluate(async (url) => { const { setupWorker, ws } = window.msw const api = ws.link(url) const worker = setupWorker( api.addEventListener('connection', ({ server }) => { server.connect() server.addEventListener('message', (event) => { event.preventDefault() }) }), ) await worker.start() }, server.url) await page.evaluate((url) => { new WebSocket(url) }, server.url) await waitFor(() => { expect(consoleSpy.get('raw')!.get('startGroupCollapsed')).toEqual( expect.arrayContaining([ expect.stringMatching( /^\[MSW\] \d{2}:\d{2}:\d{2}\.\d{3} %c⇣%c hello from server %c17%c color:#ef4444 color:inherit color:gray;font-weight:normal color:inherit;font-weight:inherit$/, ), ]), ) }) }) ================================================ FILE: test/browser/ws-api/ws.runtime.js ================================================ import { ws } from 'msw' import { setupWorker } from 'msw/browser' window.msw = { ws, setupWorker, } ================================================ FILE: test/browser/ws-api/ws.server.connect.browser.test.ts ================================================ import type { ws } from 'msw' import type { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare global { interface Window { msw: { ws: typeof ws setupWorker: typeof setupWorker } } } test('does not connect to the actual server by default', async ({ loadExample, page, defineWebSocketServer, }) => { const server = await defineWebSocketServer() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) server.once('connection', (client) => { client.send('must not receive this') }) await page.evaluate(async (serverUrl) => { const { setupWorker, ws } = window.msw const service = ws.link(serverUrl) const worker = setupWorker( service.addEventListener('connection', ({ client }) => { queueMicrotask(() => client.send('mock')) }), ) await worker.start() }, server.url) const clientMessage = await page.evaluate((serverUrl) => { const socket = new WebSocket(serverUrl) return new Promise((resolve, reject) => { socket.onmessage = (event) => resolve(event.data) socket.onerror = () => reject(new Error('WebSocket error')) }).finally(() => socket.close()) }, server.url) expect(clientMessage).toBe('mock') }) test('forwards incoming server events to the client once connected', async ({ loadExample, page, defineWebSocketServer, }) => { const server = await defineWebSocketServer() await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) server.once('connection', (client) => { client.send('hello from server') }) await page.evaluate(async (serverUrl) => { const { setupWorker, ws } = window.msw const service = ws.link(serverUrl) const worker = setupWorker( service.addEventListener('connection', ({ server }) => { // Calling "connect()" establishes the connection // to the actual WebSocket server. server.connect() }), ) await worker.start() }, server.url) const clientMessage = await page.evaluate((serverUrl) => { const socket = new WebSocket(serverUrl) return new Promise((resolve, reject) => { socket.onmessage = (event) => { resolve(event.data) socket.close() } socket.onerror = reject }) }, server.url) expect(clientMessage).toBe('hello from server') }) test('throws an error when connecting to a non-existing server', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) const error = await page.evaluate((serverUrl) => { const { setupWorker, ws } = window.msw const service = ws.link(serverUrl) return new Promise(async (resolve) => { const worker = setupWorker( service.addEventListener('connection', ({ server }) => { server.connect() }), ) await worker.start() const socket = new WebSocket(serverUrl) socket.onerror = () => resolve('Connection failed') }) }, 'ws://non-existing-websocket-address.com') expect(error).toMatch('Connection failed') }) ================================================ FILE: test/browser/ws-api/ws.use.browser.test.ts ================================================ import type { ws } from 'msw' import type { setupWorker } from 'msw/browser' import { test, expect } from '../playwright.extend' declare global { interface Window { msw: { ws: typeof ws setupWorker: typeof setupWorker } } } test('resolves outgoing events using initial handlers', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('*') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { client.send('hello from mock') } }) }), ) await worker.start() }) const clientMessage = await page.evaluate(() => { const socket = new WebSocket('wss://example.com') return new Promise((resolve, reject) => { socket.onopen = () => socket.send('hello') socket.onmessage = (event) => resolve(event.data) socket.onerror = reject }) }) expect(clientMessage).toBe('hello from mock') }) test('overrides an outgoing event listener', async ({ loadExample, page }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('*') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { client.send('must not be sent') } }) }), ) await worker.start() worker.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { event.stopImmediatePropagation() client.send('howdy, client!') } }) }), ) }) const clientMessage = await page.evaluate(() => { const socket = new WebSocket('wss://example.com') return new Promise((resolve, reject) => { socket.onopen = () => socket.send('hello') socket.onmessage = (event) => resolve(event.data) socket.onerror = reject }) }) expect(clientMessage).toBe('howdy, client!') }) test('combines initial and override listeners', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('*') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { // This will be sent the last since the initial // event listener is attached the first. client.send('hello from mock') queueMicrotask(() => client.close()) } }) }), ) await worker.start() worker.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { // This will be sent first since the override listener // is attached the last. client.send('override data') } }) }), ) }) const clientMessages = await page.evaluate(() => { const messages: Array = [] const socket = new WebSocket('wss://example.com') return new Promise>((resolve, reject) => { socket.onopen = () => socket.send('hello') socket.onmessage = (event) => messages.push(event.data) socket.onclose = () => resolve(messages) socket.onerror = reject }) }) expect(clientMessages).toEqual(['override data', 'hello from mock']) }) test('combines initial and override listeners in the opposite order', async ({ loadExample, page, }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('*') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { client.send('hello from mock') } }) }), ) await worker.start() worker.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { // Queue this send to the next tick so it // happens after the initial listener's send. queueMicrotask(() => { client.send('override data') queueMicrotask(() => client.close()) }) } }) }), ) }) const clientMessages = await page.evaluate(() => { const messages: Array = [] const socket = new WebSocket('wss://example.com') return new Promise>((resolve, reject) => { socket.onopen = () => socket.send('hello') socket.onmessage = (event) => messages.push(event.data) socket.onclose = () => resolve(messages) socket.onerror = reject }) }) expect(clientMessages).toEqual(['hello from mock', 'override data']) }) test('does not affect unrelated events', async ({ loadExample, page }) => { await loadExample(new URL('./ws.runtime.js', import.meta.url), { skipActivation: true, }) await page.evaluate(async () => { const { setupWorker, ws } = window.msw const service = ws.link('*') const worker = setupWorker( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { client.send('must not be sent') } if (event.data === 'fallthrough') { client.send('ok') queueMicrotask(() => client.close()) } }) }), ) await worker.start() worker.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { event.stopImmediatePropagation() client.send('howdy, client!') } }) }), ) }) const clientMessages = await page.evaluate(() => { const messages: Array = [] const socket = new WebSocket('wss://example.com') return new Promise>((resolve, reject) => { socket.onopen = () => socket.send('hello') socket.onmessage = (event) => { messages.push(event.data) if (event.data === 'howdy, client!') { socket.send('fallthrough') } } socket.onclose = () => resolve(messages) socket.onerror = reject }) }) expect(clientMessages).toEqual(['howdy, client!', 'ok']) }) ================================================ FILE: test/e2e/auto-update-worker.node.test.ts ================================================ import fs from 'node:fs' import { inject } from 'vitest' import { createTeardown } from 'fs-teardown' import { fromTemp } from '../support/utils.js' const TARBALL_PATH = inject('tarballPath') const fsMock = createTeardown({ rootDir: fromTemp('worker-script-auto-update'), }) describe( 'worker script auto-update', { sequential: true, }, () => { beforeAll(async () => { await fsMock.prepare() }) afterEach(async () => { await fsMock.reset() }) afterAll(async () => { await fsMock.cleanup() }) it('updates the worker script on the "postinstall" hook', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'example', msw: { workerDirectory: 'public', }, }), }) // Install "msw" from the tarball into the dummy project. const installCommand = await fsMock.exec(`npm install ${TARBALL_PATH}`) expect(installCommand.stderr).toBe('') // Asset the worker script has been created/updated. expect( fs.existsSync(fsMock.resolve('public/mockServiceWorker.js')), ).toEqual(true) }) it('updates multiple directories on the "postinstall" hook', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'example-multiple-dirs', msw: { workerDirectory: ['./packages/one', './packages/two'], }, }), }) const installCommand = await fsMock.exec(`npm install ${TARBALL_PATH}`) /** * @note Cannot assert on the empty stderr because npm * writes to stderr if there's a new version of npm available. */ // expect(installCommand.stderr).toBe('') expect( fs.existsSync(fsMock.resolve('packages/one/mockServiceWorker.js')), ).toEqual(true) expect( fs.existsSync(fsMock.resolve('packages/two/mockServiceWorker.js')), ).toEqual(true) }) }, ) ================================================ FILE: test/e2e/cli-init.node.test.ts ================================================ import fs from 'node:fs' import path from 'node:path' import url from 'node:url' import { spawnSync } from 'node:child_process' import { createTeardown } from 'fs-teardown' import { fromTemp } from '../support/utils' const fsMock = createTeardown({ rootDir: fromTemp('cli/init'), }) const CLI_PATH = url.fileURLToPath( new URL('../../cli/index.js', import.meta.url), ) function readJson(filePath: string) { const rawContent = fs.readFileSync(filePath, 'utf8') try { const content = JSON.parse(rawContent) return content } catch (error) { return rawContent } } beforeAll(async () => { spawnSync('pnpm', ['build']) await fsMock.prepare() }) beforeEach(async () => { await fsMock.reset() }) afterEach(() => { vi.restoreAllMocks() }) afterAll(async () => { await fsMock.cleanup() }) async function init(inlineArgs: Array): ReturnType { const result = await fsMock.exec( `node ${CLI_PATH} init ${inlineArgs.join(' ')}`, ) return { ...result, // Strip stdout from color unicode characters: stdout: result.stdout.replace(/\x1b\[\d+m/gi, ''), stderr: result.stderr.replace(/\x1b\[\d+m/gi, ''), } } test('copies the script to a given path without saving', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'test', }), }) const initCommand = await init([ fsMock.resolve('./public'), /** * @note Pass the "--no-save" flag to prevent the "init" command * from spawning a "Would you like to save the path" prompt. */ '--no-save', ]) expect(initCommand.stderr).toBe('') expect(initCommand.stdout).toContain( `Worker script successfully copied!\n - ${fsMock.resolve('public')}`, ) expect(fs.existsSync(fsMock.resolve('public/mockServiceWorker.js'))).toBe( true, ) // Must not write anything to package.json expect(readJson(fsMock.resolve('package.json'))).toEqual({ name: 'test', }) }) test('saves the path in "msw.workerDirectory" if the "--save" flag was provided', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'test', }), }) const initCommand = await init([fsMock.resolve('./public'), '--save']) expect(initCommand.stderr).toBe('') expect(initCommand.stdout).toContain( `Worker script successfully copied!\n - ${fsMock.resolve('public')}`, ) expect(fs.existsSync(fsMock.resolve('public/mockServiceWorker.js'))).toBe( true, ) expect(readJson(fsMock.resolve('package.json'))).toMatchObject({ msw: { workerDirectory: ['public'], }, }) }) test('deduplicates saved paths in "msw.workerDirectory" (using plain string)', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'test', msw: { /** * @note Use the plain string value that most users have. */ workerDirectory: 'public', }, }), }) const initCommand = await init(['./public', '--save']) expect(initCommand.stderr).toBe('') expect(fs.existsSync(fsMock.resolve('public/mockServiceWorker.js'))).toBe( true, ) expect(readJson(fsMock.resolve('package.json'))).toMatchObject({ msw: { // The plain string value will be left as-is because no // updates are necessary to package.json workerDirectory: 'public', }, }) }) test('deduplicates saved paths in "msw.workerDirectory" (using array)', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'test', msw: { workerDirectory: ['public', 'another'], }, }), }) const initCommand = await init(['./public', '--save']) expect(initCommand.stderr).toBe('') expect(fs.existsSync(fsMock.resolve('public/mockServiceWorker.js'))).toBe( true, ) expect(readJson(fsMock.resolve('package.json'))).toMatchObject({ msw: { workerDirectory: ['public', 'another'], }, }) }) test('does not save the path if the "--no-save" flag was provided', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'test', msw: { workerDirectory: ['public'], }, }), }) const initCommand = await init(['./static', '--no-save']) expect(initCommand.stderr).toBe('') expect(fs.existsSync(fsMock.resolve('static/mockServiceWorker.js'))).toBe( true, ) expect(readJson(fsMock.resolve('package.json'))).toMatchObject({ msw: { workerDirectory: ['public'], }, }) }) test('adds multiple paths to "msw.workerDirectory"', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'example', }), }) await init(['one', '--save']) expect(fs.existsSync(fsMock.resolve('one/mockServiceWorker.js'))).toBe(true) expect(readJson(fsMock.resolve('package.json'))).toMatchObject({ msw: { workerDirectory: ['one'], }, }) await init(['two', '--save']) expect(fs.existsSync(fsMock.resolve('two/mockServiceWorker.js'))).toBe(true) expect(readJson(fsMock.resolve('package.json'))).toMatchObject({ msw: { workerDirectory: ['one', 'two'], }, }) }) test('throws if creating a directory under path failed', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'example', }), }) /** * @note Require the "init" command source * so that the "fs" mocks could apply. */ // @ts-expect-error const { init } = await import('../../cli/init.js') // Mock the "mkdir" method throwing an error. const error = new Error('Failed to create directory') vi.spyOn(fs.promises, 'mkdir').mockRejectedValue(error) const exitSpy = vi.spyOn(process, 'exit').mockImplementationOnce(() => { throw error }) const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementationOnce(() => void 0) const initCommandPromise = init({ _: [null, './does/not/exist'], save: false, }) await expect(initCommandPromise).rejects.toThrowError(error) expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining(`Failed to copy the worker script at`), // The absolute public directory path will be resolved // against CWD, and not "fsMock". No need to assert it. expect.any(String), error, ) expect(exitSpy).toHaveBeenCalledWith(1) }) test('does not copy the script to saved paths if public directory was provided', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'example', msw: { workerDirectory: ['one', 'two'], }, }), }) const initCommand = await init(['three', '--save']) expect(initCommand.stderr).toBe('') expect(initCommand.stdout).toContain('- three') expect(initCommand.stdout).not.toContain('- one') expect(initCommand.stdout).not.toContain('- two') // Must not copy the worker script to stored paths // when the "init" command is called with a new path. expect(fs.existsSync(fsMock.resolve('one/mockServiceWorker.js'))).toBe(false) expect(fs.existsSync(fsMock.resolve('two/mockServiceWorker.js'))).toBe(false) // Must copy the worker script only to the provided path. expect(fs.existsSync(fsMock.resolve('three/mockServiceWorker.js'))).toBe(true) }) test('copies the script to all saved paths if the public directory was not provided', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'test', msw: { workerDirectory: ['packages/one/public', 'packages/two/static'], }, }), }) const initCommand = await init([]) expect(initCommand.stderr).toBe('') expect( fs.existsSync(fsMock.resolve('packages/one/public/mockServiceWorker.js')), ).toBe(true) expect( fs.existsSync(fsMock.resolve('packages/two/static/mockServiceWorker.js')), ).toBe(true) expect(readJson(fsMock.resolve('package.json'))).toMatchObject({ msw: { workerDirectory: ['packages/one/public', 'packages/two/static'], }, }) }) test('throws when called with "--save" flag and without the public directory', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'test', msw: { workerDirectory: ['public'], }, }), }) const initCommandPromise = init(['--save']) // Must throw a meaningful arrow about a no-op "--save" flag. await expect(initCommandPromise).rejects.toThrowError( `Failed to copy the worker script: cannot call the "init" command without a public directory but with the "--save" flag. Either drop the "--save" flag to copy the worker script to all paths listed in "msw.workerDirectory", or add an explicit public directory to the command, like "npx msw init ./public".`, ) // Must not modify the package.json. expect(readJson(fsMock.resolve('package.json'))).toEqual({ name: 'test', msw: { workerDirectory: ['public'], }, }) // Must not copy the worker script. expect( expect(fs.existsSync(fsMock.resolve('public/mockServiceWorker.js'))).toBe( false, ), ) }) test('prints the list of failed paths to copy', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'example', msw: { workerDirectory: ['one', 'problematic'], }, }), }) vi.spyOn(fs, 'copyFileSync').mockImplementation((_, dest) => { // Only fail one of the stored paths. if (dest.toString().includes('/problematic/')) { throw copyFileError } }) // @ts-expect-error const { init } = await import('../../cli/init.js') const copyFileError = new Error('Failed to copy file') const consoleLogSpy = vi .spyOn(console, 'log') .mockImplementation(() => void 0) const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementationOnce(() => void 0) await init({ // Calling "init" without the public directory // to trigger it to copy the worker script to all // stored paths at "msw.workerDirectory". _: [null], // Use a custom CWD so that the direct call to "init" // resolves the "package.json" from the "fsMock". cwd: fsMock.resolve('.'), }) // Must still print the successful message if any paths succeeded. expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining('Worker script successfully copied!'), ) expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining( ` - ${fsMock.resolve('one/mockServiceWorker.js')}`, ), ) // Must print the failed paths. expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining( `Copying the worker script failed at following paths:`, ), ) expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining(` - ${fsMock.resolve('problematic')}`), ) expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining(copyFileError.message), ) }) test('supports a mix of unix/windows paths in "workerDirectory"', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'example', msw: { // Use a mix of different path styles to emulate multiple developers // working from different operating systems. workerDirectory: [ path.win32.join('public', 'windows-style'), 'unix/style', ], }, }), }) const initCommand = await init(['']) expect(initCommand.stderr).toBe('') expect( fs.existsSync(fsMock.resolve('public/windows-style/mockServiceWorker.js')), ).toBe(true) expect(fs.existsSync(fsMock.resolve('unix/style/mockServiceWorker.js'))).toBe( true, ) const normalizedPaths = readJson(fsMock.resolve('package.json')).msw .workerDirectory // Expect normalized paths expect(normalizedPaths).toContain('public\\windows-style') expect(normalizedPaths).toContain('unix/style') }) test('copies the script only to provided windows path in args', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'example', msw: { workerDirectory: ['unix/style'], }, }), }) const initCommand = await init([ `"${path.win32.join('.', 'windows-style', 'new-folder')}"`, '--save', ]) expect(initCommand.stderr).toBe('') expect( fs.existsSync( fsMock.resolve('windows-style/new-folder/mockServiceWorker.js'), ), ).toBe(true) expect(fs.existsSync(fsMock.resolve('unix/style/mockServiceWorker.js'))).toBe( false, ) }) test('copies the script only to provided unix path in args', async () => { await fsMock.create({ 'package.json': JSON.stringify({ name: 'example', msw: { workerDirectory: [path.win32.join('windows-style', 'new-folder')], }, }), }) const initCommand = await init(['./unix/style', '--save']) expect(initCommand.stderr).toBe('') expect(fs.existsSync(fsMock.resolve('unix/style/mockServiceWorker.js'))).toBe( true, ) expect( fs.existsSync( fsMock.resolve('windows-style/new-folder/mockServiceWorker.js'), ), ).toBe(false) }) ================================================ FILE: test/e2e/tsconfig.json ================================================ { "extends": "../tsconfig.json", "include": ["./**/*.test.ts", "./vitest.d.ts", "./vitest.global.setup.ts"] } ================================================ FILE: test/e2e/vitest.config.ts ================================================ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { dir: './test/e2e', globals: true, environment: 'node', globalSetup: './test/e2e/vitest.global.setup.ts', testTimeout: 60_000, hookTimeout: 60_000, }, }) ================================================ FILE: test/e2e/vitest.d.ts ================================================ declare module 'vitest' { export interface ProvidedContext { tarballPath: string } } export {} ================================================ FILE: test/e2e/vitest.global.setup.ts ================================================ import fs from 'node:fs' import { fileURLToPath } from 'node:url' import { spawnSync } from 'node:child_process' import { invariant } from 'outvariant' import type { GlobalSetupContext } from 'vitest/node' import * as packageJson from '../../package.json' export default function setup({ provide }: GlobalSetupContext) { const tarballPath = fileURLToPath( new URL(`../../msw-${packageJson.version}.tgz`, import.meta.url), ) if (fs.existsSync(tarballPath)) { return } // Pack the library before all E2E tests. spawnSync('pnpm', ['pack'], { stdio: 'inherit', }) invariant( fs.existsSync(tarballPath), 'Failed to set up e2e tests: library tarball not found at "%s"', tarballPath, ) console.log('Library built at "%s"!', tarballPath) provide('tarballPath', tarballPath) } ================================================ FILE: test/global.d.ts ================================================ declare module 'url-loader!*' { const content: string export default content } ================================================ FILE: test/modules/browser/esm-browser.test.ts ================================================ import url from 'node:url' import { invariant } from 'outvariant' import { createTeardown } from 'fs-teardown' import * as express from 'express' import { HttpServer } from '@open-draft/test-server/lib/http.js' import { test, expect } from '@playwright/test' import { spyOnConsole } from 'page-with' import { startDevServer } from '@web/dev-server' import { installLibrary } from '../module-utils' type DevServer = Awaited> const fsMock = createTeardown({ rootDir: url.fileURLToPath(new URL('esm-browser-tests', import.meta.url)), paths: { 'package.json': JSON.stringify({ type: 'module' }), }, }) let devServer: DevServer function getDevServerUrl(): string { const address = devServer.server?.address() invariant(address, 'Failed to retrieve dev server url: null') if (typeof address === 'string') { return new URL(address).href } return new URL(`http://localhost:${address.port}`).href } const httpServer = new HttpServer((app) => { app.use(express.static(fsMock.resolve('.'))) }) test.beforeAll(async () => { devServer = await startDevServer({ config: { rootDir: fsMock.resolve('.'), port: 0, nodeResolve: { exportConditions: ['browser'], }, }, logStartMessage: false, }) await httpServer.listen() await fsMock.prepare() await installLibrary(fsMock.resolve('.')) }) test.afterAll(async () => { await devServer?.stop() await httpServer.close() await fsMock.cleanup() }) test('runs in an ESM browser project', async ({ page }) => { await fsMock.create({ 'entry.mjs': ` import { http,HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' const worker = setupWorker( http.get('/resource', () => new Response()), http.post('/login', () => HttpResponse.json([1, 2, 3])) ) console.log(typeof worker.start) `, 'index.html': ` `, }) const consoleSpy = spyOnConsole(page) const pageErrors: Array = [] page.on('pageerror', (error) => pageErrors.push(`${error.message}\n${error.stack}`), ) await page.goto(getDevServerUrl(), { waitUntil: 'networkidle' }) expect(pageErrors).toEqual([]) expect(consoleSpy.get('error')).toBeUndefined() expect(consoleSpy.get('log')).toEqual(expect.arrayContaining(['function'])) }) ================================================ FILE: test/modules/browser/playwright.config.ts ================================================ import url from 'node:url' import { type Config } from '@playwright/test' const TEST_DIR = url.fileURLToPath(new URL('./', import.meta.url)) export default { testDir: TEST_DIR, use: { launchOptions: { devtools: !process.env.CI, }, }, fullyParallel: true, } satisfies Config ================================================ FILE: test/modules/module-utils.ts ================================================ import fs from 'node:fs' import url from 'node:url' import { spawnSync } from 'node:child_process' import { invariant } from 'outvariant' import packageJson from '../../package.json' assert { type: 'json' } async function getLibraryTarball(): Promise { const ROOT_PATH = new URL('../..', import.meta.url) const packFilename = `msw-${packageJson.version}.tgz` const packPath = url.fileURLToPath(new URL(packFilename, ROOT_PATH)) /** * @note Beware that you need to remove the tarball after * the test run is done. Don't want to use a stale tgarball, do you? */ if (fs.existsSync(packPath)) { return packPath } const out = spawnSync('pnpm', ['pack'], { cwd: ROOT_PATH }) if (out.error) { console.error(out.error) } invariant( fs.existsSync(packPath), 'Failed to pack the library at "%s"', packPath, ) return packPath } export async function installLibrary(projectPath: string) { const TARBALL_PATH = await getLibraryTarball() const output = spawnSync('pnpm', ['install', TARBALL_PATH], { cwd: projectPath, }) if (output.error) { console.error(output.error) return Promise.reject( 'Failed to install the library. See the stderr output above.', ) } /** * @todo Assert that pnpm printed success: * + msw 0.0.0-fetch.rc-11 */ } ================================================ FILE: test/modules/node/esm-node.test.ts ================================================ import url from 'node:url' import { createTeardown } from 'fs-teardown' import { installLibrary } from '../module-utils' const fsMock = createTeardown({ rootDir: url.fileURLToPath(new URL('node-esm-tests', import.meta.url)), paths: { 'package.json': JSON.stringify({ type: 'module' }), }, }) beforeAll(async () => { await fsMock.prepare() await installLibrary(fsMock.resolve('.')) }) afterAll(async ({ result }) => { if (result?.state === 'fail') { return } await fsMock.cleanup() }) it('runs in a ESM Node.js project', async () => { await fsMock.create({ 'resolve.mjs': ` console.log('msw:', await import.meta.resolve('msw')) console.log('msw/node:', await import.meta.resolve('msw/node')) console.log('msw/native:', await import.meta.resolve('msw/native')) `, 'runtime.mjs': ` import { http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('/resource', () => new Response()) ) console.log(typeof server.listen) `, }) const resolveStdio = await fsMock.exec( /** * @note Using the import meta resolve flag * to enable the "import.meta.resolve" API to see * what library imports resolve to in Node.js ESM. */ 'node --experimental-import-meta-resolve ./resolve.mjs', ) expect(resolveStdio.stderr).toBe('') /** * @todo Take these expected export paths from package.json. * That should be the source of truth. */ expect(resolveStdio.stdout).toMatch( /^msw: (.+?)\/node_modules\/msw\/lib\/core\/index\.mjs/m, ) expect(resolveStdio.stdout).toMatch( /^msw\/node: (.+?)\/node_modules\/msw\/lib\/node\/index\.mjs/m, ) expect(resolveStdio.stdout).toMatch( /^msw\/native: (.+?)\/node_modules\/msw\/lib\/native\/index\.mjs/m, ) /** * @todo Also test the "msw/browser" import that throws, * saying that the "./browser" export is not defined. * That's correct, it's explicitly set as "browser: null" for Node.js. */ const runtimeStdio = await fsMock.exec('node ./runtime.mjs') expect(runtimeStdio.stderr).toBe('') expect(runtimeStdio.stdout).toMatch(/function/m) }) it('runs in a CJS Node.js project', async () => { await fsMock.create({ 'resolve.cjs': ` console.log('msw:', require.resolve('msw')) console.log('msw/node:', require.resolve('msw/node')) console.log('msw/native:', require.resolve('msw/native')) `, 'runtime.cjs': ` const { http } = require('msw') const { setupServer } = require('msw/node') const server = setupServer( http.get('/resource', () => new Response()) ) console.log(typeof server.listen) `, }) const resolveStdio = await fsMock.exec('node ./resolve.cjs') expect(resolveStdio.stderr).toBe('') /** * @todo Take these expected export paths from package.json. * That should be the source of truth. */ /** * @note Although the test requires the package in CJS, * the "module-sync" condition allows loading the ESM build. * This is supported in Node.js v20+. */ expect(resolveStdio.stdout).toMatch( /^msw: (.+?)\/node_modules\/msw\/lib\/core\/index\.mjs/m, ) expect(resolveStdio.stdout).toMatch( /^msw\/node: (.+?)\/node_modules\/msw\/lib\/node\/index\.mjs/m, ) // Must load regular CJS build for React Native. expect(resolveStdio.stdout).toMatch( /^msw\/native: (.+?)\/node_modules\/msw\/lib\/native\/index\.js/m, ) const runtimeStdio = await fsMock.exec('node ./runtime.mjs') expect(runtimeStdio.stderr).toBe('') expect(runtimeStdio.stdout).toMatch(/function/m) }) ================================================ FILE: test/modules/node/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./**/*.test.ts"] } ================================================ FILE: test/modules/node/vitest.config.ts ================================================ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { globals: true, dir: __dirname, environment: 'node', testTimeout: 60_000, hookTimeout: 60_000, }, }) ================================================ FILE: test/native/node-events.native.test.ts ================================================ /** * @see https://github.com/mswjs/msw/pull/1858 * @see https://github.com/mswjs/msw/issues/1868 */ import { setupServer } from 'msw/native' test('calls "setupServer" without errors in React Native', async () => { /** * @note Asserting that mocking works is not possible with * the current testing setup. We force Vitest to alias "msw" * imports to their ESM build, which is a hack. * * We need Vitest to load the ESM build here in order to * use "vi.mock()" to mock the "node:events" imports to throw. * Vitest doesn't support module mocking in CommonJS. * * But aliasing the build isn't enough for it to function. * The root-level package.json is still CJS, which, I suspects, * resolves any subsequent in-build imports to their CJS counterparts. */ expect(() => setupServer()).not.toThrow() }) ================================================ FILE: test/native/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "resolveJsonModule": true, // Support default imports for modules that have no default exports. // This way "http" imports stay "import http from 'http'". // Using wildcard there breaks request interception since it // encapsulates the values under the ".default" key. "allowSyntheticDefaultImports": true, "types": ["node", "vitest/globals"] }, "include": ["../../global.d.ts", "./**/*.test.ts"] } ================================================ FILE: test/native/vitest.config.ts ================================================ import { defineConfig } from 'vitest/config' import { mswExports } from '../support/alias.js' export default defineConfig({ test: { root: './test/native', include: ['**/*.native.test.ts'], globals: true, setupFiles: ['./vitest.setup.ts'], alias: { /** * @note Force Vitest load ESM targets of MSW. * If we run ESM in tests, we can use "vi.mock()" to * emulate certain standard Node.js modules missing * (like "node:events") in React Native. * * Vitest won't pick up the ESM targets because * the root-level "package.json" is not "module". */ ...mswExports, }, }, }) ================================================ FILE: test/native/vitest.setup.ts ================================================ import { vi } from 'vitest' /** * @note The list of standard Node.js modules missing * in the React Native runtime. */ const reactNativeMissingModules = ['events', 'node:events'] reactNativeMissingModules.forEach((moduleName) => { vi.doMock(moduleName, () => { throw new Error( `Failed to import module "${moduleName}": it does not exist in React Native. This likely means MSW tries to import something too optimistically in that environment.`, ) }) }) ================================================ FILE: test/node/graphql-api/anonymous-operations.test.ts ================================================ // @vitest-environment node import { HttpServer } from '@open-draft/test-server/http' import { HttpResponse, graphql } from 'msw' import { setupServer } from 'msw/node' import { createGraphQLClient } from '../../support/graphql' const httpServer = new HttpServer((app) => { app.post('/graphql', (req, res) => { res.json({ data: { user: { id: 'abc-123' }, }, }) }) }) const server = setupServer(graphql.query('GetUser', () => {})) beforeAll(async () => { server.listen() await httpServer.listen() vi.spyOn(console, 'warn').mockImplementation(() => {}) }) afterEach(() => { server.resetHandlers() vi.clearAllMocks() }) afterAll(async () => { vi.restoreAllMocks() server.close() await httpServer.close() }) it('warns on unhandled anonymous GraphQL operations', async () => { const endpointUrl = httpServer.http.url('/graphql') const client = createGraphQLClient({ uri: endpointUrl }) const result = await client({ query: ` query { user { id } } `, }) expect.soft(result.data).toEqual({ user: { id: 'abc-123' }, }) expect(console.warn, 'Warns about an anonymous operation') .toHaveBeenCalledWith(`\ [MSW] Failed to intercept a GraphQL request at "POST ${endpointUrl}": anonymous GraphQL operations are not supported. Consider naming this operation or using "graphql.operation()" request handler to intercept GraphQL requests regardless of their operation name/type. Read more: https://mswjs.io/docs/api/graphql/#graphqloperationresolver`) }) it('does not print a warning when using anonymous operation with "graphql.operation()"', async () => { server.use( graphql.operation(async () => { return HttpResponse.json({ data: { pets: [{ name: 'Tom' }, { name: 'Jerry' }], }, }) }), ) const endpointUrl = httpServer.http.url('/graphql') const client = createGraphQLClient({ uri: endpointUrl }) const result = await client({ query: ` query { pets { name } } `, }) expect.soft(result.data).toEqual({ pets: [{ name: 'Tom' }, { name: 'Jerry' }], }) expect(console.warn, 'Must not print any warnings').not.toHaveBeenCalled() }) ================================================ FILE: test/node/graphql-api/batched-queries.apollo.test.ts ================================================ /** * @vitest-environment node * Example of mocking batched GraphQL queries via Apollo. * @see https://github.com/mswjs/msw/issues/510 * @see https://www.apollographql.com/docs/router/executing-operations/query-batching */ import { http, graphql, bypass, HttpResponse, getResponse, RequestHandler, } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' const httpServer = new HttpServer((app) => { app.post('/graphql', (req, res) => { res.json({ data: { server: { url: httpServer.http.address.href }, }, }) }) }) /** * A higher-order request handler function that resolves any * batched GraphQL queries to the given URL against the list * of request handlers. */ function batchedGraphQLQuery(url: string, handlers: Array) { return http.post(url, async ({ request }) => { const data = await request.json() // Ignore GraphQL queries that are not batched queries. if (!Array.isArray(data)) { return } const responses = await Promise.all( // Resolve each individual query against the existing // array of GraphQL request handlers. data.map(async (operation) => { const scopedRequest = new Request(request, { body: JSON.stringify(operation), }) const response = await getResponse(handlers, scopedRequest) return response || fetch(bypass(scopedRequest)) }), ) // Read the mocked response JSON bodies to use // in the response to the entire batched query. const queryData = await Promise.all( responses.map((response) => response?.json()), ) return HttpResponse.json(queryData) }) } const graphqlHandlers = [ graphql.query('GetUser', () => { return HttpResponse.json({ data: { user: { id: 1 }, }, }) }), graphql.query('GetProduct', () => { return HttpResponse.json({ data: { product: { name: 'Hoover 2000' }, }, }) }), ] const server = setupServer(...graphqlHandlers) beforeAll(async () => { await httpServer.listen() server.listen() /** * @note This handler doesn't have to be a runtime handler. * This is a prerequisite of how MSW spawns a test server * for this particular test. Move it to the top of the rest * of your request handlers. */ server.use( batchedGraphQLQuery(httpServer.http.url('/graphql'), graphqlHandlers), ) }) afterAll(async () => { server.close() await httpServer.close() }) it('sends a mocked response to a batched GraphQL query', async () => { const response = await fetch(httpServer.http.url('/graphql'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify([ { query: ` query GetUser { user { id } } `, }, { query: ` query GetProduct { product { name } } `, }, ]), }) expect(await response.json()).toEqual([ { data: { user: { id: 1 } }, }, { data: { product: { name: 'Hoover 2000' } }, }, ]) }) it('combines mocked and original responses in a single batched query', async () => { const response = await fetch(httpServer.http.url('/graphql'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify([ { query: ` query GetUser { user { id } } `, }, { query: ` query UnmockedQuery { server { url } } `, }, ]), }) expect(await response.json()).toEqual([ { data: { user: { id: 1 } }, }, { data: { server: { url: httpServer.http.address.href } }, }, ]) }) ================================================ FILE: test/node/graphql-api/batched-queries.batched-execute.test.ts ================================================ /** * @vitest-environment node * Example of mocking batched GraphQL queries via "batched-execute". * @see https://github.com/mswjs/msw/issues/510 * @see https://the-guild.dev/graphql/stitching/handbook/appendices/batching-arrays-and-queries */ import { buildSchema, graphql as executeGraphQL, print, defaultFieldResolver, } from 'graphql' import { http, HttpResponse, GraphQLVariables, bypass } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' import { createGraphQLClient } from '../../support/graphql' const httpServer = new HttpServer((app) => { app.post('/graphql', (req, res) => { res.json({ data: { server: { url: httpServer.http.address.href }, }, }) }) }) const schema = buildSchema(` type User { id: ID! } type Product { name: String! } type Server { url: String! } type Query { user: User product: Product server: Server } `) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.listen() server.use( http.post( httpServer.http.url('/graphql'), async ({ request }) => { const data = await request.json() // "batched-execute" produces a standard-compliant GraphQL query // so you can resolve it against the mocked schema as-is! const result = await executeGraphQL({ source: data.query, variableValues: data.variables, schema, rootValue: { // Since "batched-execute" produces a single query // with individual queries as fields, you have to // go with a field-based mocking. user: () => ({ id: 'abc-123' }), product: () => ({ name: 'Hoover 2000' }), }, async fieldResolver(source, args, context, info) { // Resolve known fields in "rootValue" as usual. if (source[info.fieldName]) { return defaultFieldResolver(source, args, context, info) } // Proxy the unknown fields to the actual GraphQL server. const compiledQuery = info.fieldNodes .map((node) => print(node)) .join('\n') const query = `${info.operation.operation} { ${compiledQuery} }` const queryRequest = new Request(request, { body: JSON.stringify({ query }), }) const response = await fetch(bypass(queryRequest)) const { error, data } = await response.json() if (error) { throw error } return data[info.fieldName] }, }) return HttpResponse.json(result) }, ), ) }) afterAll(async () => { server.close() await httpServer.close() }) it('sends a mocked response to a batched GraphQL query', async () => { const client = createGraphQLClient({ uri: httpServer.http.url('/graphql'), }) const result = await client({ query: ` query { user_0: user { id } product_0: product { name } } `, }) expect.soft(result.data).toEqual({ user_0: { id: 'abc-123' }, product_0: { name: 'Hoover 2000' }, }) expect.soft(result.errors).toBeUndefined() }) it('combines mocked and original responses in a single batched query', async () => { const client = createGraphQLClient({ uri: httpServer.http.url('/graphql'), }) const result = await client({ query: ` query { user_0: user { id } server_0: server { url } } `, }) expect.soft(result.data).toEqual({ user_0: { id: 'abc-123' }, server_0: { url: httpServer.http.address.href }, }) expect.soft(result.errors).toEqual(undefined) }) ================================================ FILE: test/node/graphql-api/compatibility.node.test.ts ================================================ // @vitest-environment node import { graphql as executeGraphql, buildSchema } from 'graphql' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { createGraphQLClient, gql } from '../../support/graphql' const schema = gql` type User { firstName: String! } type Query { user: User! } ` const server = setupServer( graphql.query('GetUser', async ({ query }) => { const executionResult = await executeGraphql({ schema: buildSchema(schema), source: query, rootValue: { user: { firstName: 'John', }, }, }) return HttpResponse.json({ data: executionResult.data, errors: executionResult.errors, }) }), ) const client = createGraphQLClient({ uri: 'http://localhost:3000/graphql', }) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('fetches the data from a GraphQL schema', async () => { const result = await client({ query: gql` query GetUser { user { firstName } } `, }) expect.soft(result.data).toEqual({ user: { firstName: 'John', }, }) expect.soft(result.errors).toBeUndefined() }) it('propagates execution errors', async () => { const result = await client({ query: gql` query GetUser { user { firstName # Intentionally querying a non-existing field # to cause a GraphQL error upon execution. lastName } } `, }) expect.soft(result.data).toBeUndefined() expect.soft(result.errors).toEqual([ expect.objectContaining({ message: 'Cannot query field "lastName" on type "User". Did you mean "firstName"?', }), ]) }) ================================================ FILE: test/node/graphql-api/content-type.test.ts ================================================ // @vitest-environment node import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { createGraphQLClient } from '../../support/graphql' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('responds with the "application/json" content type by default', async () => { server.use( graphql.query('GetUser', () => { return HttpResponse.json({ data: { user: { name: 'John Maverick' } }, }) }), ) const client = createGraphQLClient({ uri: 'http://any.host.here/irrelevant', }) const result = await client({ query: ` query GetUser { user { name } } `, }) expect .soft(result.response.headers.get('content-type')) .toBe('application/json') expect.soft(result.data).toEqual({ user: { name: 'John Maverick' } }) expect.soft(result.errors).toBeUndefined() }) it('responds with the "application/graphql-response+json" content type if the client accepts it', async () => { server.use( graphql.mutation('CreatePost', () => { return HttpResponse.json({ data: { post: { id: 'abc-123' } }, }) }), ) const client = createGraphQLClient({ uri: 'http://any.host.here/irrelevant', }) { const result = await client({ query: ` mutation CreatePost { post { id } } `, headers: { accept: 'application/graphql-response+json', }, }) expect .soft( result.response.headers.get('content-type'), 'Supports the new accept type exclusively', ) .toBe('application/graphql-response+json') expect.soft(result.data).toEqual({ post: { id: 'abc-123' } }) expect.soft(result.errors).toBeUndefined() } { const result = await client({ query: ` mutation CreatePost { post { id } } `, headers: { accept: 'application/graphql-response+json, application/json;q=0.9', }, }) expect .soft( result.response.headers.get('content-type'), 'Supports a mix of the new and old accept types', ) .toBe('application/graphql-response+json') expect.soft(result.data).toEqual({ post: { id: 'abc-123' } }) expect.soft(result.errors).toBeUndefined() } }) it('respects the "Accept" request header quality', async () => { server.use( graphql.mutation('CreatePost', () => { return HttpResponse.json({ data: { post: { id: 'abc-123' } }, }) }), ) const client = createGraphQLClient({ uri: 'http://any.host.here/irrelevant', }) { const result = await client({ query: ` mutation CreatePost { post { id } } `, headers: { accept: ' application/graphql-response+json;q=0.5, application/json', }, }) expect .soft(result.response.headers.get('content-type')) .toBe('application/json') expect.soft(result.data).toEqual({ post: { id: 'abc-123' } }) expect.soft(result.errors).toBeUndefined() } }) it('responds with the "application/graphql-response+json" in generator responses', async () => { server.use( graphql.query('GetForecast', function* () { yield HttpResponse.json({ data: { forecast: { degrees: 25 } }, }) }), ) const client = createGraphQLClient({ uri: 'http://any.host.here/irrelevant', }) const result = await client({ query: ` query GetForecast { forecast { degrees } } `, headers: { accept: 'application/graphql-response+json', }, }) expect .soft(result.response.headers.get('content-type')) .toBe('application/graphql-response+json') expect.soft(result.data).toEqual({ forecast: { degrees: 25 } }) expect.soft(result.errors).toBeUndefined() }) it('ignores request "Accept" preferences if an explicit "content-type" is set on the mocked response', async () => { server.use( graphql.query('GetUser', () => { return HttpResponse.json( { data: { user: { name: 'John Maverick' } }, }, { headers: { // Simulating an old server that doesn't support the modern mime type. 'content-type': 'application/json', }, }, ) }), ) const client = createGraphQLClient({ uri: 'http://any.host.here/irrelevant', }) const result = await client({ query: ` query GetUser { user { name } } `, headers: { accept: 'application/graphql-response+json', }, }) expect .soft(result.response.headers.get('content-type')) .toBe('application/json') expect.soft(result.data).toEqual({ user: { name: 'John Maverick' } }) expect.soft(result.errors).toBeUndefined() }) ================================================ FILE: test/node/graphql-api/cookies.test.ts ================================================ // @vitest-environment node import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { createGraphQLClient, gql } from '../../support/graphql' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('intercepts request cookies', async () => { server.use( graphql.query('GetSession', ({ cookies }) => { return HttpResponse.json({ data: { session: { id: cookies.sessionId, }, }, }) }), ) const client = createGraphQLClient({ uri: 'http://localhost:3000/graphql', }) const result = await client({ query: gql` query GetSession { session { id } } `, headers: { cookie: 'sessionId=abc123', }, }) expect.soft(result.data).toEqual({ session: { id: 'abc123' }, }) expect.soft(result.errors).toBeUndefined() }) it('mocks a single response cookie', async () => { server.use( graphql.mutation<{ user: { email: string } }, { email: string }>( 'SignIn', ({ variables }) => { return HttpResponse.json( { data: { user: { email: variables.email, }, }, }, { headers: { 'set-cookie': 'sessionId=abc-123', }, }, ) }, ), ) const client = createGraphQLClient({ uri: 'http://localhost:3000/graphql', }) const result = await client({ query: gql` mutation SignIn($email: String!) { user { email } } `, variables: { email: 'test@example.com', }, }) expect .soft(result.response.headers.get('set-cookie')) .toBe('sessionId=abc-123') expect.soft(result.data).toEqual({ user: { email: 'test@example.com' }, }) expect.soft(result.errors).toBeUndefined() }) it('mocks a multi-value response cookie', async () => { server.use( graphql.mutation<{ user: { email: string } }, { email: string }>( 'SignIn', ({ variables }) => { return HttpResponse.json( { data: { user: { email: variables.email, }, }, }, { headers: [ ['set-cookie', 'sessionId=abc-123'], ['set-cookie', 'userId=123'], ], }, ) }, ), ) const client = createGraphQLClient({ uri: 'http://localhost:3000/graphql', }) const result = await client({ query: gql` mutation SignIn($email: String!) { user { email } } `, variables: { email: 'test@example.com', }, }) expect .soft(result.response.headers.get('set-cookie')) .toBe('sessionId=abc-123, userId=123') expect.soft(result.data).toEqual({ user: { email: 'test@example.com' }, }) expect.soft(result.errors).toBeUndefined() }) ================================================ FILE: test/node/graphql-api/custom-predicate.node.test.ts ================================================ // @vitest-environment node import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { createGraphQLClient, gql } from '../../support/graphql' const server = setupServer() beforeAll(() => { server.listen({ onUnhandledRequest: 'bypass', }) }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('matches requests when the predicate function returns true', async () => { server.use( graphql.query( ({ operationName }) => { return operationName.toLowerCase().includes('user') }, () => { return HttpResponse.json({ data: { user: 1 } }) }, ), ) const client = createGraphQLClient({ uri: 'http://localhost:3000/graphql', }) const result = await client({ query: gql` query GetUser { user { firstName } } `, }) expect.soft(result.data).toEqual({ user: 1 }) expect.soft(result.errors).toBeUndefined() }) it('does not match requests when the predicate function returns false', async () => { server.use( graphql.query( ({ operationName }) => { return operationName.toLowerCase().includes('user') }, () => { return HttpResponse.json({ data: { user: 1 } }) }, ), ) const client = createGraphQLClient({ uri: 'http://localhost:3000/graphql', }) const requestError = await client({ query: gql` query GetCart { cart { id } } `, }).catch((error) => error) expect.soft(requestError).toBeInstanceOf(Error) expect.soft(requestError.message).toEqual('fetch failed') }) ================================================ FILE: test/node/graphql-api/extensions.node.test.ts ================================================ // @vitest-environment node import { buildSchema, graphql as executeGraphql } from 'graphql' import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { createGraphQLClient, gql } from '../../support/graphql' const schema = gql` type User { firstName: String! } type Query { user: User! } ` const server = setupServer( graphql.query('GetUser', async ({ query }) => { const { data, errors } = await executeGraphql({ schema: buildSchema(schema), source: query, rootValue: { user: { firstName: 'John', }, }, }) return HttpResponse.json({ data, errors, extensions: { tracking: { version: 1, page: '/test', }, }, }) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('adds extensions to the original response data', async () => { const client = createGraphQLClient({ uri: 'http://localhost:3000/graphql', }) const result = await client({ query: gql` query GetUser { user { firstName } } `, }) expect.soft(result.data).toEqual({ user: { firstName: 'John', }, }) expect.soft(result.errors).toBeUndefined() expect.soft(result.extensions).toEqual({ tracking: { version: 1, page: '/test', }, }) }) ================================================ FILE: test/node/graphql-api/response-patching.node.test.ts ================================================ // @vitest-environment node import { bypass, graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { graphql as executeGraphql, buildSchema } from 'graphql' import { HttpServer } from '@open-draft/test-server/http' import { createGraphQLClient, gql } from '../../support/graphql' const server = setupServer( graphql.query('GetUser', async ({ request }) => { const originalResponse = await fetch(bypass(request)) const { requestHeaders, queryResult } = await originalResponse.json() return HttpResponse.json({ data: { user: { firstName: 'Christian', lastName: queryResult.data?.user?.lastName, }, // Setting the request headers on the response data on purpose // to access them in the response of the Apollo client. requestHeaders, }, errors: queryResult.errors, }) }), ) const httpServer = new HttpServer((app) => { app.post('/graphql', async (req, res) => { const result = await executeGraphql({ schema: buildSchema(gql` type User { firstName: String! lastName: String! } # Describing an additional type to return # the request headers back to the request handler. # Apollo will strip off any extra data that # doesn't match the query. type RequestHeader { name: String! value: String! } type Query { user: User! requestHeaders: [RequestHeader!] } `), operationName: 'GetUser', source: req.body.query, rootValue: { user: { firstName: 'John', lastName: 'Maverick', }, }, }) return res.status(200).json({ requestHeaders: req.headers, queryResult: result, }) }) }) beforeAll(async () => { server.listen() // This test server acts as a production server MSW will be hitting // when performing a request patching with `ctx.fetch()`. await httpServer.listen() }) afterAll(async () => { server.close() await httpServer.close() }) test('patches a GraphQL response', async () => { const client = createGraphQLClient({ uri: httpServer.http.url('/graphql'), }) const res = await client<{ user: { firstName: string lastName: string } requestHeaders: Record }>({ query: gql` query GetUser { user { firstName lastName } requestHeaders { name value } } `, }) expect(res.errors).toBeUndefined() expect(res.data).toHaveProperty('user', { firstName: 'Christian', lastName: 'Maverick', }) expect(res.data?.requestHeaders).toHaveProperty('accept', '*/*') expect(res.data?.requestHeaders).not.toHaveProperty('_headers') expect(res.data?.requestHeaders).not.toHaveProperty('_names') }) ================================================ FILE: test/node/graphql-api/typed-document-node.test.ts ================================================ // @vitest-environment node import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { gql, createTypedDocumentNode, createGraphQLClient, } from '../../support/graphql' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('supports TypedDocumentNode as request predicate', async () => { const GET_USER = gql` query GetUser($id: ID!) { user(id: $id) { id name } } ` const documentNode = createTypedDocumentNode< { user: { id: string; name: string } }, { id: string } >(GET_USER) server.use( graphql.query(documentNode, ({ variables }) => { return HttpResponse.json({ data: { user: { id: variables.id, name: 'John Doe', }, }, }) }), ) const client = createGraphQLClient({ uri: 'http://localhost:3000/graphql', }) const result = await client({ query: GET_USER, variables: { id: 'abc-123', }, }) expect .soft(result.data) .toEqual({ user: { id: 'abc-123', name: 'John Doe' } }) expect.soft(result.errors).toBeUndefined() }) ================================================ FILE: test/node/graphql-api/typed-document-string.test.ts ================================================ // @vitest-environment node import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { gql, createGraphQLClient, createTypedDocumentString, } from '../../support/graphql' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('supports TypedDocumentString as request predicate', async () => { const GET_USER = gql` query GetUser($id: ID!) { user(id: $id) { id name } } ` const documentString = createTypedDocumentString< { user: { id: string; name: string } }, { id: string } >(GET_USER) server.use( graphql.query(documentString, ({ variables }) => { return HttpResponse.json({ data: { user: { id: variables.id, name: 'John Doe', }, }, }) }), ) const client = createGraphQLClient({ uri: 'http://localhost:3000/graphql', }) const result = await client({ query: GET_USER, variables: { id: 'abc-123' }, }) expect.soft(result.data).toEqual({ user: { id: 'abc-123', name: 'John Doe', }, }) expect.soft(result.errors).toBeUndefined() }) ================================================ FILE: test/node/msw-api/context/delay-infinite.fixture.js ================================================ import { delay, HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('http://any.host.here/irrelevant', async () => { await delay('infinite') }), ) server.listen() fetch('http://any.host.here/irrelevant') setTimeout(() => { server.close() }, 20) ================================================ FILE: test/node/msw-api/context/delay.node.test.ts ================================================ // @vitest-environment node import { spawnSync } from 'node:child_process' import { delay, HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { performance } from 'perf_hooks' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) async function makeRequest(url: string) { const requestStart = performance.now() const res = await fetch(url) const requestEnd = performance.now() const responseTime = requestEnd - requestStart return { res, responseTime } } test('uses explicit server response time', async () => { server.use( http.get('http://localhost/user', async () => { await delay(500) return HttpResponse.text('john') }), ) const { res, responseTime } = await makeRequest('http://localhost/user') expect(responseTime).toBeGreaterThanOrEqual(500) await expect(res.text()).resolves.toBe('john') }) test('uses realistic server response time when no duration is provided', async () => { server.use( http.get('http://localhost/user', async () => { await delay() return HttpResponse.text('john') }), ) const { res, responseTime } = await makeRequest('http://localhost/user') // Realistic server response time in Node.js is set to 5ms. expect(responseTime).toBeGreaterThan(5) await expect(res.text()).resolves.toBe('john') }) test('uses realistic server response time when "real" mode is provided', async () => { server.use( http.get('http://localhost/user', async () => { await delay('real') return HttpResponse.text('john') }), ) const { res, responseTime } = await makeRequest('http://localhost/user') // Realistic server response time in Node.js is set to 5ms. expect(responseTime).toBeGreaterThan(5) await expect(res.text()).resolves.toBe('john') }) test('does not keep the process alive when using "infinite" delay', () => { const result = spawnSync( process.execPath, [new URL('./delay-infinite.fixture.js', import.meta.url).pathname], { stdio: 'inherit', timeout: 1000, }, ) expect.soft(result.status).toBe(0) expect.soft(result.error).toBeUndefined() }) ================================================ FILE: test/node/msw-api/req/passthrough.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, passthrough, http } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' const httpServer = new HttpServer((app) => { app.post('/user', (req, res) => { res.json({ name: 'John' }) }) app.post('/code/:code', (req, res) => { res.status(parseInt(req.params.code)).send() }) }) const server = setupServer() interface ResponseBody { name: string } beforeAll(async () => { await httpServer.listen() server.listen() }) beforeEach(() => { vi.spyOn(console, 'warn').mockImplementation(() => void 0) }) afterEach(() => { server.resetHandlers() vi.restoreAllMocks() }) afterAll(async () => { server.close() await httpServer.close() }) it('performs request as-is when returning "req.passthrough" call in the resolver', async () => { const endpointUrl = httpServer.http.url('/user') server.use( http.post(endpointUrl, () => { return passthrough() }), ) const res = await fetch(endpointUrl, { method: 'POST' }) const json = await res.json() expect(json).toEqual({ name: 'John', }) expect(console.warn).not.toHaveBeenCalled() }) it('does not allow fall-through when returning "req.passthrough" call in the resolver', async () => { const endpointUrl = httpServer.http.url('/user') server.use( http.post(endpointUrl, () => { return passthrough() }), http.post(endpointUrl, () => { return HttpResponse.json({ name: 'Kate' }) }), ) const res = await fetch(endpointUrl, { method: 'POST' }) const json = await res.json() expect(json).toEqual({ name: 'John', }) expect(console.warn).not.toHaveBeenCalled() }) it('performs a request as-is if nothing was returned from the resolver', async () => { const endpointUrl = httpServer.http.url('/user') server.use( http.post(endpointUrl, () => { return }), ) const res = await fetch(endpointUrl, { method: 'POST' }) const json = await res.json() expect(json).toEqual({ name: 'John', }) }) for (const code of [204, 205, 304]) { it(`performs a ${code} request as-is if nothing was returned from the resolver`, async () => { const endpointUrl = httpServer.http.url(`/code/${code}`) server.use( http.post(endpointUrl, () => { return }), ) const res = await fetch(endpointUrl, { method: 'POST' }) expect(res.status).toEqual(code) }) it(`performs a ${code} request as-is if passthrough was returned from the resolver`, async () => { const endpointUrl = httpServer.http.url(`/code/${code}`) server.use( http.post(endpointUrl, () => { return passthrough() }), ) const res = await fetch(endpointUrl, { method: 'POST' }) expect(res.status).toEqual(code) }) } ================================================ FILE: test/node/msw-api/res/network-error.node.test.ts ================================================ /** * @vitest-environment node */ import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('http://example.com/user', () => { return HttpResponse.error() }), ) beforeAll(() => server.listen()) afterAll(() => server.close()) test('throws a network error when used with fetch', async () => { await expect(fetch('http://example.com/user')).rejects.toThrow( 'Failed to fetch', ) }) ================================================ FILE: test/node/msw-api/setup-server/boundary/boundary.args.test.ts ================================================ /** * @vitest-environment node */ import { setupServer } from 'msw/node' const server = setupServer() beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it.concurrent('forwards arguments to the callback function', async () => { server.boundary((...args) => { expect(args).toEqual([1, { 2: true }, [3]]) })(1, { 2: true }, [3]) }) it.concurrent('returns the result of the callback function', async () => { const result = server.boundary((number: number) => { return number * 10 })(2) expect(result).toBe(20) }) ================================================ FILE: test/node/msw-api/setup-server/boundary/boundary.concurrency.test.ts ================================================ /** * @vitest-environment jsdom */ import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('/initial', () => { return HttpResponse.text('initial') }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) describe.concurrent('concurrent tests', () => { it( 'resolves request against the initial handlers', server.boundary(async () => { const response = await fetch('/initial') expect(response.status).toBe(200) expect(await response.text()).toBe('initial') }), ) it( 'resolves request against the in-test handler override', server.boundary(async () => { server.use( http.get('/initial', () => { return HttpResponse.text('override') }), ) const response = await fetch('/initial') expect(response.status).toBe(200) expect(await response.text()).toBe('override') }), ) it( 'resolves requests against the initial handlers again', server.boundary(async () => { const response = await fetch('/initial') expect(response.status).toBe(200) expect(await response.text()).toBe('initial') }), ) }) ================================================ FILE: test/node/msw-api/setup-server/boundary/boundary.handlers.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('https://example.com', () => { return HttpResponse.json({ name: 'John' }) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it.concurrent( 'treats higher scope handlers as initial handlers', server.boundary(async () => { expect( await await fetch('https://example.com').then((response) => response.json(), ), ).toEqual({ name: 'John' }) server.use( http.get('https://example.com', () => { return HttpResponse.json({ override: true }) }), ) expect( await await fetch('https://example.com').then((response) => response.json(), ), ).toEqual({ override: true }) }), ) it.concurrent( 'resets the runtime handlers to the initial handlers', server.boundary(async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ override: true }) }), ) expect( await await fetch('https://example.com').then((response) => response.json(), ), ).toEqual({ override: true }) server.resetHandlers() expect( await await fetch('https://example.com').then((response) => response.json(), ), ).toEqual({ name: 'John' }) }), ) it.concurrent( 'treats the higher boundary handlers as initial handlers for nested boundary', server.boundary(async () => { server.use( http.get('https://example.com', () => { return HttpResponse.json({ override: true }) }), ) await server.boundary(async () => { expect( await await fetch('https://example.com').then((response) => response.json(), ), ).toEqual({ override: true }) server.resetHandlers() // Reset does nothing at this point because no runtime // request handlers were added within this boundary. expect( await await fetch('https://example.com').then((response) => response.json(), ), ).toEqual({ override: true }) server.use( http.get('https://example.com', () => { return HttpResponse.json({ nested: true }) }), ) expect( await await fetch('https://example.com').then((response) => response.json(), ), ).toEqual({ nested: true }) }) }), ) ================================================ FILE: test/node/msw-api/setup-server/input-validation.node.test.ts ================================================ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' test('throws an error given an Array of request handlers to "setupServer"', () => { const createServer = () => { // @ts-expect-error intentionally invalid parameters for setupServer to force it to throw return setupServer([ http.get('https://test.mswjs.io/book/:bookId', () => { return HttpResponse.json({ title: 'Original title' }) }), ]) } expect(createServer).toThrow( `[MSW] Failed to apply given request handlers: invalid input. Did you forget to spread the request handlers Array?`, ) }) ================================================ FILE: test/node/msw-api/setup-server/life-cycle-events/on.node.test.ts ================================================ // @vitest-environment node import { HttpResponse, http } from 'msw' import { SetupServerApi, setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' const httpServer = new HttpServer((app) => { app.get('/user', (req, res) => res.status(500).end()) app.post('/no-response', (req, res) => res.send('original-response')) app.get('/unknown-route', (req, res) => res.send('majestic-unknown')) }) const server = setupServer() function spyOnEvents(server: SetupServerApi) { const listener = vi.fn() const wrapListener = (eventName: string, listener: any) => { return (...args) => listener(eventName, ...args) } server.events.on('request:start', wrapListener('request:start', listener)) server.events.on('request:match', wrapListener('request:match', listener)) server.events.on( 'request:unhandled', wrapListener('request:unhandled', listener), ) server.events.on('request:end', wrapListener('request:end', listener)) server.events.on('response:mocked', wrapListener('response:mocked', listener)) server.events.on('response:bypass', wrapListener('response:bypass', listener)) server.events.on( 'unhandledException', wrapListener('unhandledException', listener), ) return listener } beforeAll(async () => { // Supress "Expected a mocking resolver function to return a mocked response" // warnings when hitting intentionally empty resolver. vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) await httpServer.listen() server.use( http.get(httpServer.http.url('/user'), () => { return HttpResponse.text('response-body') }), http.post(httpServer.http.url('/no-response'), () => { return }), http.get(httpServer.http.url('/unhandled-exception'), () => { throw new Error('Unhandled resolver error') }), ) server.listen() }) afterAll(async () => { server.close() await httpServer.close() }) test('emits events for a handled request and mocked response', async () => { const listener = spyOnEvents(server) const url = httpServer.http.url('/user') await fetch(url) expect(listener).toHaveBeenNthCalledWith( 1, 'request:start', expect.objectContaining({ request: expect.objectContaining({ method: 'GET', url, }), requestId: expect.any(String), }), ) const { requestId } = listener.mock.calls[0][1] expect(listener).toHaveBeenNthCalledWith( 2, 'request:match', expect.objectContaining({ request: expect.objectContaining({ method: 'GET', url, }), requestId, }), ) expect(listener).toHaveBeenNthCalledWith( 3, 'request:end', expect.objectContaining({ request: expect.objectContaining({ method: 'GET', url, }), requestId, }), ) expect(listener).toHaveBeenNthCalledWith( 4, 'response:mocked', expect.objectContaining({ request: expect.objectContaining({ method: 'GET', url, }), requestId, response: expect.any(Response), }), ) const { response } = listener.mock.calls[3][1] expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(await response.text()).toBe('response-body') expect(listener).toHaveBeenCalledTimes(4) }) test('emits events for a handled request with no response', async () => { const listener = spyOnEvents(server) const url = httpServer.http.url('/no-response') await fetch(url, { method: 'POST' }) expect(listener).toHaveBeenNthCalledWith( 1, 'request:start', expect.objectContaining({ request: expect.objectContaining({ method: 'POST', url, }), requestId: expect.any(String), }), ) const { requestId } = listener.mock.calls[0][1] expect(listener).toHaveBeenNthCalledWith( 2, 'request:end', expect.objectContaining({ request: expect.objectContaining({ method: 'POST', url, }), requestId, }), ) expect(listener).toHaveBeenNthCalledWith( 3, 'response:bypass', expect.objectContaining({ request: expect.objectContaining({ method: 'POST', url, }), requestId, response: expect.any(Response), }), ) const { response } = listener.mock.calls[2][1] expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(await response.text()).toBe('original-response') expect(listener).toHaveBeenCalledTimes(3) }) test('emits events for an unhandled request', async () => { const listener = spyOnEvents(server) const url = httpServer.http.url('/unknown-route') await fetch(url) expect(listener).toHaveBeenNthCalledWith( 1, 'request:start', expect.objectContaining({ request: expect.any(Request), requestId: expect.any(String), }), ) const { requestId } = listener.mock.calls[0][1] expect(listener).toHaveBeenNthCalledWith( 2, 'request:unhandled', expect.objectContaining({ request: expect.objectContaining({ method: 'GET', url, }), requestId, }), ) expect(listener).toHaveBeenNthCalledWith( 3, 'request:end', expect.objectContaining({ request: expect.objectContaining({ method: 'GET', url, }), requestId, }), ) expect(listener).toHaveBeenNthCalledWith( 4, 'response:bypass', expect.objectContaining({ request: expect.objectContaining({ method: 'GET', url, }), requestId, response: expect.any(Response), }), ) const { response } = listener.mock.calls[3][1] expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(await response.text()).toBe('majestic-unknown') expect(listener).toHaveBeenCalledTimes(4) }) test('emits unhandled exceptions in the request handler', async () => { const listener = spyOnEvents(server) const url = httpServer.http.url('/unhandled-exception') await fetch(url).catch(() => undefined) expect(listener).toHaveBeenNthCalledWith( 1, 'request:start', expect.objectContaining({ request: expect.any(Request), requestId: expect.any(String), }), ) const { requestId } = listener.mock.calls[0][1] expect(listener).toHaveBeenNthCalledWith( 2, 'unhandledException', expect.objectContaining({ request: expect.objectContaining({ method: 'GET', url, }), requestId, error: new Error('Unhandled resolver error'), }), ) /** * @note The fallback 500 response still counts as a mocked response. * I'm torn on this but I believe we should indicate that the request * (a) received a response; (b) that response was mocked. */ expect(listener).toHaveBeenNthCalledWith( 3, 'response:mocked', expect.objectContaining({ request: expect.objectContaining({ method: 'GET', url, }), requestId, response: expect.any(Response), }), ) const { response } = listener.mock.calls[2][1] expect(response.status).toBe(500) expect(response.statusText).toBe('Unhandled Exception') expect(listener).toHaveBeenCalledTimes(3) }) test('stops emitting events once the server is stopped', async () => { const listener = spyOnEvents(server) server.close() await fetch(httpServer.http.url('/user')) expect(listener).not.toHaveBeenCalled() }) ================================================ FILE: test/node/msw-api/setup-server/life-cycle-events/removeAllListeners.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' const httpServer = new HttpServer((app) => { app.get('/user', (req, res) => { res.status(500).end() }) }) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.use( http.get(httpServer.http.url('/user'), () => { return HttpResponse.json({ firstName: 'John' }) }), ) server.listen() }) afterEach(() => { vi.restoreAllMocks() }) afterAll(async () => { server.close() await httpServer.close() }) test('removes all listeners attached to the server instance', async () => { const listeners = { requestStart: vi.fn(), requestEnd: vi.fn(), } server.events.on('request:start', listeners.requestStart) server.events.on('request:end', listeners.requestEnd) await fetch(httpServer.http.url('/user')) expect(listeners.requestStart).toHaveBeenCalledTimes(1) expect(listeners.requestEnd).toHaveBeenCalledTimes(1) listeners.requestStart.mockReset() listeners.requestEnd.mockReset() server.events.removeAllListeners() await fetch(httpServer.http.url('/user')) expect(listeners.requestStart).not.toHaveBeenCalled() expect(listeners.requestEnd).not.toHaveBeenCalled() }) test('removes all the listeners by the event name', async () => { const listeners = { requestStart: vi.fn(), requestEnd: vi.fn(), } server.events.on('request:start', listeners.requestStart) server.events.on('request:start', listeners.requestStart) server.events.on('request:start', listeners.requestStart) server.events.on('request:end', listeners.requestEnd) server.events.removeAllListeners('request:start') await fetch(httpServer.http.url('/user')) expect(listeners.requestStart).not.toHaveBeenCalled() expect(listeners.requestEnd).toHaveBeenCalledTimes(1) }) test('does not remove the internal listeners', async () => { const listeners = { requestStart: vi.fn(), responseMocked: vi.fn(), } server.events.on('request:start', listeners.requestStart) server.events.removeAllListeners() // The "response:*" events in Node.js are propagated from the interceptors library. // MSW adds an internal listener to react to those events from the interceptors. server.events.on('response:mocked', listeners.responseMocked) await fetch(httpServer.http.url('/user')) expect(listeners.requestStart).not.toHaveBeenCalled() expect(listeners.responseMocked).toHaveBeenCalledTimes(1) }) ================================================ FILE: test/node/msw-api/setup-server/life-cycle-events/removeListener.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' const httpServer = new HttpServer((app) => { app.get('/user', (req, res) => { res.status(500).end() }) }) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.use( http.get(httpServer.http.url('/user'), () => { return HttpResponse.json({ firstName: 'John' }) }), ) server.listen() }) afterAll(async () => { server.close() await httpServer.close() }) test('removes a listener by the event name', async () => { const listeners = { requestStart: vi.fn(), requestEnd: vi.fn(), } server.events.on('request:start', listeners.requestStart) server.events.on('request:end', listeners.requestEnd) server.events.removeListener('request:start', listeners.requestStart) await fetch(httpServer.http.url('/user')) expect(listeners.requestStart).not.toHaveBeenCalled() expect(listeners.requestEnd).toHaveBeenCalledTimes(1) }) ================================================ FILE: test/node/msw-api/setup-server/listHandlers.node.test.ts ================================================ /** * @vitest-environment node */ import { http, graphql } from 'msw' import { setupServer } from 'msw/node' const resolver = () => null const github = graphql.link('https://api.github.com') const server = setupServer( http.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( http.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)', ]) }) ================================================ FILE: test/node/msw-api/setup-server/resetHandlers.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('https://test.mswjs.io/books', () => { return HttpResponse.json({ title: 'Original title' }) }), ) beforeAll(() => { vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) server.listen() }) afterAll(() => { vi.restoreAllMocks() server.close() }) test('removes all runtime request handlers when resetting without explicit next handlers', async () => { server.use( http.post('https://test.mswjs.io/login', () => { return HttpResponse.json({ accepted: true }) }), ) // Request handlers added on runtime affect the network communication. const loginResponse = await fetch('https://test.mswjs.io/login', { method: 'POST', }) const loginBody = await loginResponse.json() expect(loginResponse.status).toBe(200) expect(loginBody).toEqual({ accepted: true }) // Once reset, all the runtime request handlers are removed. server.resetHandlers() const secondLoginResponse = await fetch('https://test.mswjs.io/login', { method: 'POST', }) expect(secondLoginResponse.status).toBe(404) // Initial request handlers (given to `setupServer`) are not affected. const booksResponse = await fetch('https://test.mswjs.io/books') const booksBody = await booksResponse.json() expect(booksResponse.status).toBe(200) expect(booksBody).toEqual({ title: 'Original title' }) }) test('replaces all handlers with the explicit next runtime handlers upon reset', async () => { server.use( http.post('https://test.mswjs.io/login', () => { return HttpResponse.json({ accepted: true }) }), ) // Once reset with explicit next request handlers, // replaces all present request handlers with those. server.resetHandlers( http.get('https://test.mswjs.io/products', () => { return HttpResponse.json([1, 2, 3]) }), ) const loginResponse = await fetch('https://test.mswjs.io/login') expect(loginResponse.status).toBe(404) const booksResponse = await fetch('https://test.mswjs.io/books') expect(booksResponse.status).toBe(404) const productsResponse = await fetch('https://test.mswjs.io/products') const productsBody = await productsResponse.json() expect(productsResponse.status).toBe(200) expect(productsBody).toEqual([1, 2, 3]) }) ================================================ FILE: test/node/msw-api/setup-server/restoreHandlers.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('https://test.mswjs.io/book/:bookId', () => { return HttpResponse.json({ title: 'Original title' }) }), ) beforeAll(() => server.listen()) afterAll(() => server.close()) test('returns a mocked response from the used one-time request handler when restored', async () => { server.use( http.get( 'https://test.mswjs.io/book/:bookId', () => { return HttpResponse.json({ title: 'Overridden title' }) }, { once: true }, ), ) const firstResponse = await fetch('https://test.mswjs.io/book/abc-123') const firstBody = await firstResponse.json() expect(firstResponse.status).toBe(200) expect(firstBody).toEqual({ title: 'Overridden title' }) const secondResponse = await fetch('https://test.mswjs.io/book/abc-123') const secondBody = await secondResponse.json() expect(secondResponse.status).toBe(200) expect(secondBody).toEqual({ title: 'Original title' }) server.restoreHandlers() const thirdResponse = await fetch('https://test.mswjs.io/book/abc-123') const thirdBody = await thirdResponse.json() expect(firstResponse.status).toBe(200) expect(thirdBody).toEqual({ title: 'Overridden title' }) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/cookies-request.node.test.ts ================================================ // @vitest-environment node import https from 'https' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { httpsAgent, HttpServer } from '@open-draft/test-server/http' import { waitForClientRequest } from '../../../../support/utils' const httpServer = new HttpServer((app) => { app.get('/user', (req, res) => { res.json({ works: false }) }) }) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.listen() }) afterAll(async () => { server.close() await httpServer.close() }) test('exposes request cookies', async () => { const endpointUrl = httpServer.https.url('/user') server.use( http.get(endpointUrl, ({ cookies }) => { return HttpResponse.json({ cookies }) }), ) const url = new URL(endpointUrl) const request = https.get({ protocol: url.protocol, hostname: url.hostname, path: url.pathname, port: url.port, headers: { Cookie: 'auth-token=abc-123', }, agent: httpsAgent, }) const { responseText } = await waitForClientRequest(request) expect(responseText).toBe('{"cookies":{"auth-token":"abc-123"}}') }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/custom-interceptors.node.test.ts ================================================ // @vitest-environment node import nodeHttp from 'node:http' import { HttpResponse, http } from 'msw' import { SetupServerApi } from 'msw/node' import { FetchInterceptor } from '@mswjs/interceptors/fetch' import { waitForClientRequest } from '../../../../support/utils' const server = new SetupServerApi( [ http.get('http://localhost', () => { return HttpResponse.text('hello world') }), ], [new FetchInterceptor()], ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) test('uses only the provided interceptors', async () => { { const response = await fetch('http://localhost') // Must receive a mocked response per the defined interceptor + handler. expect(response.status).toBe(200) await expect(response.text()).resolves.toBe('hello world') } { const request = nodeHttp.get('http://localhost') const requestPromise = waitForClientRequest(request) // Must receive a connection error since no intereceptor handles this client. await expect(requestPromise).rejects.toThrow('ECONNREFUSED') } }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/custom-transformers.node.test.ts ================================================ import * as JSONbig from 'json-bigint' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('http://test.mswjs.io/me', () => { return new HttpResponse( JSONbig.stringify({ username: 'john.maverick', balance: BigInt(1597928668063727616), }), { headers: { 'Content-Tpye': 'application/json', }, }, ) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) test('uses custom response transformer to stringify response body', async () => { const res = await fetch('http://test.mswjs.io/me') const body = await res.text() expect(body).toEqual( JSONbig.stringify({ username: 'john.maverick', balance: BigInt(1597928668063727616), }), ) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/fake-timers.node.test.ts ================================================ // @vitest-environment node import { setupServer } from 'msw/node' import { HttpResponse, http } from 'msw' const server = setupServer( http.get('https://test.mswjs.io/pull', () => { return HttpResponse.json({ status: 'pulled' }) }), ) beforeAll(() => server.listen()) afterAll(() => server.close()) test('tolerates fake timers', async () => { vi.useFakeTimers() const res = await fetch('https://test.mswjs.io/pull') const body = await res.json() vi.useRealTimers() expect(body).toEqual({ status: 'pulled' }) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/fall-through.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const log = vi.fn() const server = setupServer( http.get('https://test.mswjs.io/*', () => log('[get] first')), http.get('https://test.mswjs.io/us*', () => log('[get] second')), http.get('https://test.mswjs.io/user', () => { return HttpResponse.json({ firstName: 'John' }) }), http.get('https://test.mswjs.io/user', () => log('[get] third')), http.post('https://test.mswjs.io/blog/*', () => log('[post] first')), http.post('https://test.mswjs.io/blog/article', () => log('[post] second')), ) beforeAll(() => { server.listen() }) afterEach(() => { vi.resetAllMocks() }) afterAll(() => { server.close() }) test('falls through all relevant request handlers until response is returned', async () => { const res = await fetch('https://test.mswjs.io/user') const body = await res.json() expect(body).toEqual({ firstName: 'John', }) expect(log).toHaveBeenNthCalledWith(1, '[get] first') expect(log).toHaveBeenNthCalledWith(2, '[get] second') expect(log).not.toBeCalledWith('[get] third') }) test('falls through all relevant handlers even if none return response', async () => { const res = await fetch('https://test.mswjs.io/blog/article', { method: 'POST', }) const { status } = res expect(status).toBe(404) expect(log).toHaveBeenNthCalledWith(1, '[post] first') expect(log).toHaveBeenNthCalledWith(2, '[post] second') }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/fetch.node.test.ts ================================================ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('http://test.mswjs.io', () => { return HttpResponse.json( { firstName: 'John', age: 32, }, { status: 401, headers: { 'X-Header': 'yes', }, }, ) }), http.post('https://test.mswjs.io', async ({ request }) => { return HttpResponse.json(await request.json(), { status: 403, headers: { 'X-Header': 'yes', }, }) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('returns a mocked response to a GET request using fetch', async () => { const res = await fetch('http://test.mswjs.io') expect(res.status).toEqual(401) expect(res.headers.get('content-type')).toEqual('application/json') expect(res.headers.get('x-header')).toEqual('yes') expect(await res.json()).toEqual({ firstName: 'John', age: 32, }) }) it('returns a mocked response to a POST request using fetch', async () => { const res = await fetch('https://test.mswjs.io', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ payload: 'info', }), }) expect(res.status).toEqual(403) expect(res.headers.get('content-type')).toEqual('application/json') expect(res.headers.get('x-header')).toEqual('yes') expect(await res.json()).toEqual({ payload: 'info', }) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/generator.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get<{ maxCount: string }>( 'https://example.com/polling/:maxCount', function* ({ params }) { const maxCount = parseInt(params.maxCount) let count = 0 while (count < maxCount) { count += 1 yield HttpResponse.json({ status: 'pending', count, }) } return HttpResponse.json({ status: 'complete', count, }) }, ), http.get<{ maxCount: string }>( 'https://example.com/polling/once/:maxCount', function* ({ params }) { const maxCount = parseInt(params.maxCount) let count = 0 while (count < maxCount) { count += 1 yield HttpResponse.json({ status: 'pending', count, }) } return HttpResponse.json({ status: 'complete', count, }) }, { once: true }, ), http.get<{ maxCount: string }>( 'https://example.com/polling/once/:maxCount', () => { return HttpResponse.json({ status: 'done' }) }, ), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) test('supports generator as the response resolver', async () => { type ExpectedResponseBody = { status: 'pending' | 'complete' count: number } const assertRequest = async (expectedBody: ExpectedResponseBody) => { const res = await fetch('https://example.com/polling/3') const body = await res.json() expect(res.status).toBe(200) expect(body).toEqual(expectedBody) } await assertRequest({ status: 'pending', count: 1 }) await assertRequest({ status: 'pending', count: 2 }) await assertRequest({ status: 'pending', count: 3 }) // Once the generator is done, any subsequent requests // return the last mocked response. await assertRequest({ status: 'complete', count: 3 }) await assertRequest({ status: 'complete', count: 3 }) await assertRequest({ status: 'complete', count: 3 }) }) test('supports one-time handlers with the generator as the response resolver', async () => { type ExpectedResponseBody = | { status: 'pending' | 'complete' count: number } | { status: 'done' } const assertRequest = async (expectedBody: ExpectedResponseBody) => { const res = await fetch('https://example.com/polling/once/3') const body = await res.json() expect(res.status).toBe(200) expect(body).toEqual(expectedBody) } await assertRequest({ status: 'pending', count: 1 }) await assertRequest({ status: 'pending', count: 2 }) await assertRequest({ status: 'pending', count: 3 }) await assertRequest({ status: 'complete', count: 3 }) // Since the last response from the one-time handler // has been returned, it falls through to the next one. await assertRequest({ status: 'done' }) await assertRequest({ status: 'done' }) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/graphql.node.test.ts ================================================ // @vitest-environment node import { graphql, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { createGraphQLClient, gql } from '../../../../support/graphql' const apolloClient = createGraphQLClient({ uri: 'http://localhost:3000', }) const GET_USER_DETAIL = gql` query GetUserDetail($userId: String!) { user { id firstName age } } ` const LOGIN = gql` mutation Login($username: String!) { user { id } } ` const server = setupServer( graphql.query('GetUserDetail', ({ variables }) => { const { userId } = variables return HttpResponse.json({ data: { user: { id: userId, firstName: 'John', age: 32, }, }, }) }), graphql.mutation('Login', ({ variables }) => { const { username } = variables return HttpResponse.json({ errors: [ { message: `User "${username}" is not found`, locations: [ { line: 12, column: 4, }, ], }, ], }) }), ) beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('returns the mocked response for a GraphQL query', async () => { const res = await apolloClient({ query: GET_USER_DETAIL, variables: { userId: 'abc-123', }, }) expect(res.errors).toBeUndefined() expect(res.data).toEqual({ user: { firstName: 'John', age: 32, id: 'abc-123', }, }) }) it('returns the mocked response for a GraphQL mutation', async () => { const res = await apolloClient({ query: LOGIN, variables: { username: 'john', }, }) expect(res.data).toBeUndefined() expect(res.errors).toEqual([ { message: `User "john" is not found`, locations: [ { line: 12, column: 4, }, ], }, ]) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/http.node.test.ts ================================================ /** * @vitest-environment node */ import nodeHttp from 'http' import { HttpServer } from '@open-draft/test-server/http' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { waitForClientRequest } from '../../../../support/utils' const httpServer = new HttpServer((app) => { app.get('/resource', (_, res) => { return res.status(500).send('original-response') }) }) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.listen() }) beforeEach(() => { server.use( http.get(httpServer.http.url('/resource'), () => { return HttpResponse.json( { firstName: 'John' }, { status: 401, headers: { 'x-header': 'yes', }, }, ) }), ) }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() await httpServer.close() }) it('returns a mocked response to an "http.get" request', async () => { const request = nodeHttp.get(httpServer.http.url('/resource')) const { response, responseText } = await waitForClientRequest(request) expect(response.statusCode).toBe(401) expect(response.headers).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-header': 'yes', }), ) expect(responseText).toBe('{"firstName":"John"}') }) it('returns a mocked response to an "http.request" request', async () => { const request = nodeHttp.request(httpServer.http.url('/resource')) request.end() const { response, responseText } = await waitForClientRequest(request) expect(response.statusCode).toBe(401) expect(response.headers).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-header': 'yes', }), ) expect(responseText).toBe('{"firstName":"John"}') }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/https.node.test.ts ================================================ /** * @vitest-environment node */ import https from 'https' import { HttpServer, httpsAgent } from '@open-draft/test-server/http' import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { waitForClientRequest } from '../../../../support/utils' const httpServer = new HttpServer((app) => { app.get('/resource', (_, res) => { return res.status(500).send('original-response') }) }) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.listen() }) beforeEach(() => { server.use( http.get(httpServer.https.url('/resource'), () => { return HttpResponse.json( { firstName: 'John', }, { status: 401, headers: { 'X-Header': 'yes', }, }, ) }), ) }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() await httpServer.close() }) it('returns a mocked response to an "https.get" request', async () => { const request = https.get(httpServer.https.url('/resource'), { agent: httpsAgent, }) const { response, responseText } = await waitForClientRequest(request) expect(response.statusCode).toBe(401) expect(response.headers).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-header': 'yes', }), ) expect(responseText).toBe('{"firstName":"John"}') }) it('returns a mocked response to an "https.request" request', async () => { const request = https.request(httpServer.https.url('/resource'), { agent: httpsAgent, }) request.end() const { response, responseText } = await waitForClientRequest(request) expect(response.statusCode).toBe(401) expect(response.headers).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-header': 'yes', }), ) expect(responseText).toBe('{"firstName":"John"}') }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/on-unhandled-request/bypass.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpServer } from '@open-draft/test-server/http' import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const httpServer = new HttpServer((app) => { app.get('/', (req, res) => { res.send('root') }) app.get('/user', (req, res) => { res.json({ firstName: 'Miranda' }) }) }) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.use( http.get(httpServer.http.url('/user'), () => { return HttpResponse.json({ firstName: 'John' }) }), ) server.listen({ onUnhandledRequest: 'bypass' }) vi.spyOn(global.console, 'error').mockImplementation(() => void 0) vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) }) afterAll(async () => { vi.restoreAllMocks() server.close() await httpServer.close() }) test('bypasses unhandled requests', async () => { const res = await fetch(httpServer.http.url('/')) // Request should be performed as-is expect(res.status).toBe(200) expect(await res.text()).toEqual('root') // No warnings/errors should be printed expect(console.error).not.toBeCalled() expect(console.warn).not.toBeCalled() }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback-throws.node.test.ts ================================================ /** * @vitest-environment node */ import { setupServer } from 'msw/node' import { HttpResponse, http } from 'msw' const server = setupServer( http.get('https://test.mswjs.io/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) beforeAll(() => server.listen({ onUnhandledRequest(request) { /** * @fixme @todo For some reason, the exception from the "onUnhandledRequest" * callback doesn't propagate to the intercepted request but instead is thrown * in this test's context. */ throw new Error(`Custom error for ${request.method} ${request.url}`) }, }), ) afterAll(() => { server.close() }) test('handles exceptions in "onUnhandledRequest" callback as 500 responses', async () => { const response = await fetch('https://example.com') expect(response.status).toBe(500) expect(response.statusText).toBe('Unhandled Exception') expect(await response.json()).toEqual({ name: 'Error', message: 'Custom error for GET https://example.com/', stack: expect.any(String), }) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback.node.test.ts ================================================ /** * @vitest-environment node */ import { setupServer } from 'msw/node' import { HttpResponse, http } from 'msw' const server = setupServer( http.get('https://test.mswjs.io/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) const unhandledListener = vi.fn() beforeAll(() => { server.listen({ onUnhandledRequest: unhandledListener, }) }) afterEach(() => { vi.resetAllMocks() }) afterAll(() => { server.close() }) it('calls the given callback function on un unhandled request', async () => { const response = await fetch('https://test.mswjs.io') // Request should be performed as-is, since the callback didn't throw. expect(response).toHaveProperty('status', 404) expect(unhandledListener).toHaveBeenCalledTimes(1) const [request, print] = unhandledListener.mock.calls[0] expect(request.method).toBe('GET') expect(request.url).toBe('https://test.mswjs.io/') expect(print).toEqual({ error: expect.any(Function), warning: expect.any(Function), }) }) it('calls the given callback on unhandled "file://" requests', async () => { await fetch('file:///does/not/exist').catch(() => void 0) expect(unhandledListener).toHaveBeenCalledTimes(1) const [request, print] = unhandledListener.mock.calls[0] expect(request.method).toBe('GET') expect(request.url).toBe('file:///does/not/exist') expect(print).toEqual({ error: expect.any(Function), warning: expect.any(Function), }) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/on-unhandled-request/default.node.test.ts ================================================ /** * @vitest-environment node */ import { setupServer } from 'msw/node' import { HttpResponse, http } from 'msw' const server = setupServer( http.get('https://test.mswjs.io/user', () => { return HttpResponse.json({ firstName: 'John' }) }), ) beforeAll(() => { server.listen() vi.spyOn(global.console, 'error').mockImplementation(() => void 0) vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) }) afterEach(() => { vi.resetAllMocks() }) afterAll(() => { server.close() vi.restoreAllMocks() }) it('warns on unhandled requests by default', async () => { const response = await fetch('https://test.mswjs.io') // Request should be performed as-is expect(response).toHaveProperty('status', 404) expect(console.error).not.toBeCalled() expect(console.warn).toBeCalledWith(`\ [MSW] Warning: intercepted a request without a matching request handler: • GET https://test.mswjs.io/ If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`) }) it('does not warn on unhandled "file://" requests', async () => { // This request is expected to fail: // Fetching non-existing file URL. await fetch('file:///file/does/not/exist').catch(() => void 0) expect(console.error).not.toBeCalled() expect(console.warn).not.toBeCalled() }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpServer } from '@open-draft/test-server/http' import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const httpServer = new HttpServer((app) => { app.get('/user', (req, res) => { res.status(200).json({ original: true }) }) app.post('/explicit-return', (req, res) => { res.status(500).end() }) app.post('/implicit-return', (req, res) => { res.status(500).end() }) }) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.use( http.get(httpServer.http.url('/user'), () => { return HttpResponse.json({ mocked: true }) }), http.post(httpServer.http.url('/explicit-return'), () => { // Short-circuiting in a handler makes it perform the request as-is, // but still treats this request as handled. return }), http.post(httpServer.http.url('/implicit-return'), () => { // The handler that has no return value so it falls through any // other matching handlers (whicbh are none). In the end, // the request is performed as-is and is still considered handled. }), ) server.listen({ onUnhandledRequest: 'error' }) }) beforeEach(() => { vi.spyOn(global.console, 'error').mockImplementation(() => void 0) vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) }) afterEach(() => { vi.clearAllMocks() }) afterAll(async () => { vi.restoreAllMocks() server.close() await httpServer.close() }) test('errors on unhandled request when using the "error" strategy', async () => { const endpointUrl = httpServer.http.url('/') const makeRequest = () => { return fetch(endpointUrl) .then(() => { throw new Error('Must not resolve') }) .catch((error) => { return error }) } const requestError = await makeRequest() expect.soft(requestError).toBeInstanceOf(Error) expect .soft(requestError.message) .toBe( '[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.', ) expect(console.error) .toHaveBeenCalledWith(`[MSW] Error: intercepted a request without a matching request handler: • GET ${endpointUrl} If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`) expect(console.warn).not.toHaveBeenCalled() }) test('does not error on request which handler explicitly returns no mocked response', async () => { const makeRequest = () => { return fetch(httpServer.http.url('/explicit-return'), { method: 'POST', }) } await makeRequest() expect(console.error).not.toHaveBeenCalled() }) test('does not error on request which handler implicitly returns no mocked response', async () => { const makeRequest = () => { return fetch(httpServer.http.url('/implicit-return'), { method: 'POST', }) } await makeRequest() expect(console.error).not.toHaveBeenCalled() }) test( 'ignores common static assets when using the "error" strategy', { timeout: 8000 }, async () => { await fetch('http://localhost:3000/styles/main.css').catch(() => void 0) expect(console.error).not.toHaveBeenCalled() }, ) ================================================ FILE: test/node/msw-api/setup-server/scenarios/on-unhandled-request/warn.node.test.ts ================================================ // @vitest-environment node import { setupServer } from 'msw/node' const server = setupServer() beforeAll(() => { server.listen({ onUnhandledRequest: 'warn' }) vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) }) afterEach(() => { vi.clearAllMocks() }) afterAll(() => { server.close() vi.restoreAllMocks() }) test('warns on unhandled request when using the "warn" strategy', async () => { await fetch('http://localhost:3000/user').catch(() => void 0) expect(console.warn).toBeCalledWith(`\ [MSW] Warning: intercepted a request without a matching request handler: • GET http://localhost:3000/user If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`) }) test( 'ignores common static assets when using the "warn" strategy', { timeout: 10_000 }, async () => { await fetch('http://localhost:3000/styles/main.css').catch(() => void 0) expect(console.warn).not.toHaveBeenCalled() }, ) ================================================ FILE: test/node/msw-api/setup-server/scenarios/relative-url.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('/books', () => { return HttpResponse.json([1, 2, 3]) }), http.get('https://api.backend.com/path', () => { return HttpResponse.json({ success: true }) }), ) beforeAll(() => server.listen()) afterAll(() => server.close()) test('tolerates relative request handlers on the server', async () => { const res = await fetch('https://api.backend.com/path') const body = await res.json() expect(res.status).toBe(200) expect(body).toEqual({ success: true }) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/response-patching.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpServer } from '@open-draft/test-server/http' import { HttpResponse, http, bypass } from 'msw' import { setupServer } from 'msw/node' const httpServer = new HttpServer((app) => { app.get('/user', (req, res) => { res.status(200).json({ id: 101 }).end() }) app.post('/user', (req, res) => { res.status(200).json({ id: 202 }).end() }) }) interface ResponseBody { id: number mocked: boolean } const server = setupServer( http.get('https://test.mswjs.io/user', async () => { const originalResponse = await fetch(bypass(httpServer.http.url('/user'))) const body = await originalResponse.json() return HttpResponse.json({ id: body.id, mocked: true, }) }), http.get('https://test.mswjs.io/complex-request', async ({ request }) => { const url = new URL(request.url) const shouldBypass = url.searchParams.get('bypass') === 'true' const performRequest = shouldBypass ? () => fetch( bypass( new Request(httpServer.http.url('/user'), { method: 'POST', }), ), ).then((res) => res.json()) : () => fetch('https://httpbin.org/post', { method: 'POST' }).then((res) => res.json(), ) const originalResponse = await performRequest() return HttpResponse.json({ id: originalResponse.id, mocked: true, }) }), http.post('https://httpbin.org/post', () => { return HttpResponse.json({ id: 303 }) }), ) beforeAll(async () => { await httpServer.listen() server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() await httpServer.close() }) test('returns a combination of mocked and original responses', async () => { const res = await fetch('https://test.mswjs.io/user') const { status } = res const body = await res.json() expect(status).toBe(200) expect(body).toEqual({ id: 101, mocked: true, }) }) test('bypasses a mocked request when using "bypass()"', async () => { const res = await fetch('https://test.mswjs.io/complex-request?bypass=true') expect(res.status).toBe(200) expect(await res.json()).toEqual({ id: 202, mocked: true, }) }) test('falls into the mocked request when using "fetch" directly', async () => { const res = await fetch('https://test.mswjs.io/complex-request') expect(res.status).toBe(200) expect(await res.json()).toEqual({ id: 303, mocked: true, }) }) ================================================ FILE: test/node/msw-api/setup-server/scenarios/xhr.node.test.ts ================================================ /** * @vitest-environment jsdom */ import { http } from 'msw' import { setupServer } from 'msw/node' import { stringToHeaders } from 'headers-polyfill' const server = setupServer( http.get('http://localhost:3001/resource', ({ request }) => { return new Response( JSON.stringify({ firstName: 'John', age: 32, }), { status: 401, statusText: 'Unauthorized', headers: { 'Content-Type': 'application/json', 'X-Header': 'yes', }, }, ) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) describe('given I perform an XMLHttpRequest', () => { let statusCode: number let headers: Headers let body: string beforeAll(async () => { const req = new XMLHttpRequest() req.open('GET', 'http://localhost:3001/resource') const requestFinishPromise = new Promise((resolve, reject) => { req.onload = function () { statusCode = this.status body = JSON.parse(this.response) headers = stringToHeaders(this.getAllResponseHeaders()) resolve() } req.onerror = reject.bind(req) }) req.send() await requestFinishPromise }) test('returns mocked status code', () => { expect(statusCode).toEqual(401) }) test('returns mocked headers', () => { expect(headers.get('content-type')).toEqual('application/json') expect(headers.get('x-header')).toEqual('yes') }) test('returns mocked body', () => { expect(body).toEqual({ firstName: 'John', age: 32, }) }) }) ================================================ FILE: test/node/msw-api/setup-server/use.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { SetupServer, setupServer } from 'msw/node' import { RequestHandler as ExpressRequestHandler } from 'express' import { HttpServer } from '@open-draft/test-server/http' const httpServer = new HttpServer((app) => { const handler: ExpressRequestHandler = (req, res) => { res.status(500).send('') } app.get('/book/:bookId', handler) app.post('/login', handler) }) let server: SetupServer beforeAll(async () => { await httpServer.listen() server = setupServer( http.get<{ bookId: string }>(httpServer.http.url('/book/:bookId'), () => { return HttpResponse.json({ title: 'Original title' }) }), ) server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() await httpServer.close() }) test('returns a mocked response from a runtime request handler upon match', async () => { server.use( http.post(httpServer.http.url('/login'), () => { return HttpResponse.json({ accepted: true }) }), ) // Request handlers added on runtime affect network communication as usual. const loginResponse = await fetch(httpServer.http.url('/login'), { method: 'POST', }) const loginBody = await loginResponse.json() expect(loginResponse.status).toBe(200) expect(loginBody).toEqual({ accepted: true }) // Other request handlers are preserved, if there are no overlaps. const bookResponse = await fetch(httpServer.http.url('/book/abc-123')) expect(bookResponse.status).toBe(200) expect(await bookResponse.json()).toEqual({ title: 'Original title' }) }) test('returns a mocked response from a persistent request handler override', async () => { server.use( http.get<{ bookId: string }>(httpServer.http.url('/book/:bookId'), () => { return HttpResponse.json({ title: 'Permanent override' }) }), ) const bookResponse = await fetch(httpServer.http.url('/book/abc-123')) const bookBody = await bookResponse.json() expect(bookResponse.status).toBe(200) expect(bookBody).toEqual({ title: 'Permanent override' }) const anotherBookResponse = await fetch(httpServer.http.url('/book/abc-123')) expect(anotherBookResponse.status).toBe(200) expect(await anotherBookResponse.json()).toEqual({ title: 'Permanent override', }) }) test('returns a mocked response from a one-time request handler override only upon first request match', async () => { server.use( http.get<{ bookId: string }>( httpServer.http.url('/book/:bookId'), () => { return HttpResponse.json({ title: 'One-time override' }) }, { once: true }, ), ) const bookResponse = await fetch(httpServer.http.url('/book/abc-123')) const bookBody = await bookResponse.json() expect(bookResponse.status).toBe(200) expect(bookBody).toEqual({ title: 'One-time override' }) const anotherBookResponse = await fetch(httpServer.http.url('/book/abc-123')) expect(anotherBookResponse.status).toBe(200) expect(await anotherBookResponse.json()).toEqual({ title: 'Original title' }) }) test('returns a mocked response from a one-time request handler override only upon first request match with parallel requests', async () => { server.use( http.get<{ bookId: string }>( httpServer.http.url('/book/:bookId'), ({ params }) => { return HttpResponse.json({ title: 'One-time override', bookId: params.bookId, }) }, { once: true }, ), ) const bookRequestPromise = fetch(httpServer.http.url('/book/abc-123')) const anotherBookRequestPromise = fetch(httpServer.http.url('/book/abc-123')) const bookResponse = await bookRequestPromise expect(bookResponse.status).toBe(200) expect(await bookResponse.json()).toEqual({ title: 'One-time override', bookId: 'abc-123', }) const anotherBookResponse = await anotherBookRequestPromise expect(anotherBookResponse.status).toBe(200) expect(await anotherBookResponse.json()).toEqual({ title: 'Original title' }) }) test('throws if provided the invalid handlers array', async () => { expect(() => server.use( // @ts-expect-error Intentionally invalid input. [http.get('*', () => new Response())], ), ).toThrow( '[MSW] Failed to call "use()" with the given request handlers: invalid input. Did you forget to spread the array of request handlers?', ) }) ================================================ FILE: test/node/regressions/2129-server-use.test.ts ================================================ /** * @vitest-environment node * @see https://github.com/mswjs/msw/issues/2129 */ import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() beforeAll(async () => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() }) it('does not override existing handlers when adding override for a different method', async () => { server.use( http.get('http://localhost/v1/issues', () => { return HttpResponse.text('get-body') }), ) server.use( http.post('http://localhost/v1/issues', () => { return HttpResponse.text('post-body') }), ) const geetResponse = await fetch('http://localhost/v1/issues') expect(await geetResponse.text()).toBe('get-body') const postResponse = await fetch('http://localhost/v1/issues', { method: 'POST', }) expect(await postResponse.text()).toBe('post-body') }) ================================================ FILE: test/node/regressions/2370-listen-after-close.test.ts ================================================ /** * @see https://github.com/mswjs/msw/issues/2370 */ // @vitest-environment node import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' const server = setupServer() const httpServer = new HttpServer((app) => { app.get('/resource', (_req, res) => { res.send('original') }) }) beforeAll(async () => { server.listen() await httpServer.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() await httpServer.close() }) it('intercepts a request once `server.listen()` is called after `server.close()`', async () => { const requestUrl = httpServer.http.url('/resource') server.use( http.get(requestUrl, () => { return HttpResponse.text('mocked') }), ) // Must respond with a mocked response while MSW is active. { const response = await fetch(requestUrl) await expect(response.text()).resolves.toBe('mocked') } server.close() // Must respond with the original response once MSW is closed. { const response = await fetch(requestUrl) await expect(response.text()).resolves.toBe('original') } server.listen() // Must respond with the mocked response once MSW is active again. { const response = await fetch(requestUrl) await expect(response.text()).resolves.toBe('mocked') } }) ================================================ FILE: test/node/regressions/many-request-handlers-jsdom.test.ts ================================================ // @vitest-environment jsdom import { HttpServer } from '@open-draft/test-server/http' import { graphql, http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const httpServer = new HttpServer((app) => { app.post('/graphql', (_, res) => { return res.status(500).send('original-response') }) app.post('/resource/not-defined', (_, res) => { return res.status(500).send('original-response') }) }) const server = setupServer() const requestCloneSpy = vi.spyOn(Request.prototype, 'clone') const processErrorSpy = vi.spyOn(process.stderr, 'write') const NUMBER_OF_REQUEST_HANDLERS = 100 beforeAll(async () => { await httpServer.listen() server.listen() }) afterEach(() => { server.resetHandlers() vi.clearAllMocks() }) afterAll(async () => { server.close() vi.restoreAllMocks() await httpServer.close() }) describe('http handlers', () => { beforeEach(() => { server.use( ...new Array(NUMBER_OF_REQUEST_HANDLERS).fill(null).map((_, index) => { return http.post( httpServer.http.url(`/resource/${index}`), async ({ request }) => { const text = await request.text() return HttpResponse.text(text + index.toString()) }, ) }), ) }) it('does not print a memory leak warning for the last handler', async () => { const httpResponse = await fetch( `${httpServer.http.url(`/resource/${NUMBER_OF_REQUEST_HANDLERS - 1}`)}`, { method: 'POST', body: 'request-body-', }, ).then((response) => response.text()) // Each clone is a new AbortSignal listener which needs to be registered expect(requestCloneSpy).toHaveBeenCalledTimes(1) expect(httpResponse).toBe(`request-body-${NUMBER_OF_REQUEST_HANDLERS - 1}`) expect(processErrorSpy).not.toHaveBeenCalled() }) it('does not print a memory leak warning for onUnhandledRequest', async () => { const httpResponse = await fetch( `${httpServer.http.url(`/resource/not-defined`)}`, { method: 'POST', body: 'request-body-', }, ) // Each clone is a new AbortSignal listener which needs to be registered. // One clone is `onUnhandledRequest` reading the request body to print. expect(requestCloneSpy).toHaveBeenCalledTimes(3) expect(httpResponse.status).toBe(500) expect(processErrorSpy).not.toHaveBeenCalled() }) }) describe('graphql handlers', () => { beforeEach(() => { server.use( ...new Array(NUMBER_OF_REQUEST_HANDLERS).fill(null).map((_, index) => { return graphql.query(`Get${index}`, () => { return HttpResponse.json({ data: { index } }) }) }), ) }) it('does not print a memory leak warning', async () => { const graphqlResponse = await fetch(httpServer.http.url('/graphql'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: `query Get${NUMBER_OF_REQUEST_HANDLERS - 1} { index }`, }), }).then((response) => response.json()) expect(requestCloneSpy).toHaveBeenCalledTimes(2) expect(graphqlResponse).toEqual({ data: { index: NUMBER_OF_REQUEST_HANDLERS - 1 }, }) expect(processErrorSpy).not.toHaveBeenCalled() }) it('does not print a memory leak warning for onUnhandledRequest', async () => { const unhandledResponse = await fetch(httpServer.http.url('/graphql'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: `query NotDefinedAtAll { index }`, }), }) expect(unhandledResponse.status).toEqual(500) expect(requestCloneSpy).toHaveBeenCalledTimes(4) // Must not print any memory leak warnings. expect(processErrorSpy).not.toHaveBeenCalled() }) }) ================================================ FILE: test/node/regressions/many-request-handlers.test.ts ================================================ // @vitest-environment node import { HttpServer } from '@open-draft/test-server/http' import { graphql, http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const httpServer = new HttpServer((app) => { app.post('/graphql', (_, res) => { return res.status(500).send('original-response') }) app.post('/resource/not-defined', (_, res) => { return res.status(500).send('original-response') }) }) const server = setupServer() const requestCloneSpy = vi.spyOn(Request.prototype, 'clone') const stdErrSpy = vi.spyOn(process.stderr, 'write') const NUMBER_OF_REQUEST_HANDLERS = 100 beforeAll(async () => { await httpServer.listen() server.listen() }) afterEach(() => { server.resetHandlers() vi.clearAllMocks() }) afterAll(async () => { server.close() vi.restoreAllMocks() await httpServer.close() }) describe('http handlers', () => { beforeEach(() => { server.use( ...new Array(NUMBER_OF_REQUEST_HANDLERS).fill(null).map((_, index) => { return http.post( httpServer.http.url(`/resource/${index}`), async ({ request }) => { const text = await request.text() return HttpResponse.text(text + index.toString()) }, ) }), ) }) it('does not print a memory leak warning for the last handler', async () => { const httpResponse = await fetch( `${httpServer.http.url(`/resource/${NUMBER_OF_REQUEST_HANDLERS - 1}`)}`, { method: 'POST', body: 'request-body-', }, ).then((response) => response.text()) // Each clone is a new AbortSignal listener which needs to be registered expect(requestCloneSpy).toHaveBeenCalledTimes(1) expect(httpResponse).toBe(`request-body-${NUMBER_OF_REQUEST_HANDLERS - 1}`) expect(stdErrSpy).not.toHaveBeenCalled() }) it('does not print a memory leak warning for onUnhandledRequest', async () => { const httpResponse = await fetch( `${httpServer.http.url(`/resource/not-defined`)}`, { method: 'POST', body: 'request-body-', }, ) // Each clone is a new AbortSignal listener which needs to be registered. // One clone is `onUnhandledRequest` reading the request body to print. expect(requestCloneSpy).toHaveBeenCalledTimes(3) expect(httpResponse.status).toBe(500) expect(stdErrSpy).not.toHaveBeenCalled() }) }) describe('graphql handlers', () => { beforeEach(() => { server.use( ...new Array(NUMBER_OF_REQUEST_HANDLERS).fill(null).map((_, index) => { return graphql.query(`Get${index}`, () => { return HttpResponse.json({ data: { index } }) }) }), ) }) it('does not print a memory leak warning', async () => { const graphqlResponse = await fetch(httpServer.http.url('/graphql'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: `query Get${NUMBER_OF_REQUEST_HANDLERS - 1} { index }`, }), }).then((response) => response.json()) // Each clone is a new AbortSignal listener which needs to be registered expect(requestCloneSpy).toHaveBeenCalledTimes(2) expect(graphqlResponse).toEqual({ data: { index: NUMBER_OF_REQUEST_HANDLERS - 1 }, }) expect(stdErrSpy).not.toHaveBeenCalled() }) it('does not print a memory leak warning for onUnhandledRequest', async () => { const unhandledResponse = await fetch(httpServer.http.url('/graphql'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: `query NotDefinedAtAll { index }`, }), }) expect(unhandledResponse.status).toEqual(500) expect(requestCloneSpy).toHaveBeenCalledTimes(4) // Must not print any memory leak warnings. expect(stdErrSpy).not.toHaveBeenCalled() }) }) ================================================ FILE: test/node/regressions/miniflare.node.test.ts ================================================ /** * @vitest-environment miniflare */ import nodeHttp from 'http' import { HttpServer } from '@open-draft/test-server/http' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { waitForClientRequest } from '../../support/utils' const httpServer = new HttpServer((app) => { app.get('/resource', (_, res) => { return res.status(500).send('original-response') }) }) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.listen() }) beforeEach(() => { server.use( http.get(httpServer.http.url('/resource'), () => { return HttpResponse.json( { firstName: 'John' }, { status: 401, headers: { 'x-header': 'yes', }, }, ) }), ) }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() await httpServer.close() }) it('returns a mocked response to an "http.get" request', async () => { const request = nodeHttp.get(httpServer.http.url('/resource')) const { response, responseText } = await waitForClientRequest(request) expect(response.statusCode).toBe(401) expect(response.headers).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-header': 'yes', }), ) expect(responseText).toBe('{"firstName":"John"}') }) it('returns a mocked response to an "http.request" request', async () => { const request = nodeHttp.request(httpServer.http.url('/resource')) request.end() const { response, responseText } = await waitForClientRequest(request) expect(response.statusCode).toBe(401) expect(response.headers).toEqual( expect.objectContaining({ 'content-type': 'application/json', 'x-header': 'yes', }), ) expect(responseText).toBe('{"firstName":"John"}') }) ================================================ FILE: test/node/regressions/mixed-graphql-http-with-query-in-body.node.test.ts ================================================ /** * @vitest-environment jsdom */ import { graphql, http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const mswGraphql = graphql.link('https://mswjs.com/graphql') const server = setupServer() beforeAll(async () => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() }) test('no console error occurs when the http handler is first', async () => { server.use( http.post('https://mswjs.com/example', () => { return HttpResponse.text('http route') }), mswGraphql.query('GetData', () => { return HttpResponse.json({ data: { data: 'graphql route', }, }) }), graphql.query('GetOtherUser', () => { return HttpResponse.json({ data: { data: 'graphql route', }, }) }), ) const consoleError = vi.spyOn(console, 'error') const response = await fetch('https://mswjs.com/example', { method: 'POST', body: JSON.stringify({ query: 'some query', }), }) await expect(response.text()).resolves.toEqual('http route') const graphqlResponse = await fetch('https://mswjs.com/graphql', { method: 'POST', body: JSON.stringify({ query: 'query GetData { id }', }), }) await expect(graphqlResponse.json()).resolves.toEqual({ data: { data: 'graphql route' }, }) expect(consoleError).not.toHaveBeenCalled() }) test('no console error occurs when the http handler is first, but we apply a use after it', async () => { server.use( http.post('https://mswjs.com/example', () => { return HttpResponse.text('http route') }), mswGraphql.query('GetData', () => { return HttpResponse.json({ data: { data: 'graphql route' }, }) }), ) server.use( mswGraphql.query('GetData', () => { return HttpResponse.json({ data: { data: 'graphql route', }, }) }), ) const consoleError = vi.spyOn(console, 'error') const response = await fetch('https://mswjs.com/example', { method: 'POST', body: JSON.stringify({ query: 'some query', }), }) await expect(response.text()).resolves.toEqual('http route') const graphqlResponse = await fetch('https://mswjs.com/graphql', { method: 'POST', body: JSON.stringify({ query: 'query GetData { id }', }), }) await expect(graphqlResponse.json()).resolves.toEqual({ data: { data: 'graphql route' }, }) expect(consoleError).not.toHaveBeenCalled() }) test('no console error occurs when the http handler is second to the graphql handler', async () => { server.use( mswGraphql.query('GetData', () => { return HttpResponse.json({ data: { data: 'graphql route', }, }) }), http.post('https://mswjs.com/example', () => { return HttpResponse.text('http route') }), ) const consoleError = vi.spyOn(console, 'error') const response = await fetch('https://mswjs.com/example', { method: 'POST', body: JSON.stringify({ query: 'some query', }), }) await expect(response.text()).resolves.toEqual('http route') const graphqlResponse = await fetch('https://mswjs.com/graphql', { method: 'POST', body: JSON.stringify({ query: 'query GetData { id }', }), }) await expect(graphqlResponse.json()).resolves.toEqual({ data: { data: 'graphql route' }, }) expect(consoleError).not.toHaveBeenCalled() }) test("a console error occurs when the http handler is second to the graphql handler, and we don't use link", async () => { server.use( graphql.query('GetData', () => { return HttpResponse.json({ data: { data: 'graphql route', }, }) }), http.post('https://mswjs.com/example', () => { return HttpResponse.text('http route') }), ) const consoleError = vi.spyOn(console, 'error') const response = await fetch('https://mswjs.com/example', { method: 'POST', body: JSON.stringify({ query: 'some query', }), }) await expect(response.text()).resolves.toEqual('http route') expect(consoleError).not.toHaveBeenNthCalledWith( 1, expect.stringContaining('[MSW] Failed to intercept a GraphQL request'), ) const graphqlResponse = await fetch('https://mswjs.com/graphql', { method: 'POST', body: JSON.stringify({ query: 'query GetData { id }', }), }) await expect(graphqlResponse.json()).resolves.toEqual({ data: { data: 'graphql route' }, }) }) ================================================ FILE: test/node/rest-api/cookies-inheritance.node.test.ts ================================================ /** * @vitest-environment jsdom */ import { HttpResponse, http } from 'msw' import { setupServer, SetupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' import { RequestHandler as ExpressRequestHandler } from 'express' let server: SetupServer const httpServer = new HttpServer((app) => { const handler: ExpressRequestHandler = (req, res) => { res.status(500).end() } app.post('/login', handler) app.get('/user', handler) }) beforeAll(async () => { await httpServer.listen() server = setupServer( http.post(httpServer.https.url('/login'), () => { return new HttpResponse(null, { headers: { 'Set-Cookie': `authToken=${encodeURIComponent('abc-123-甲乙丙')}`, }, }) }), http.get< never, never, { firstName: string; lastName: string } | { error: string } >(httpServer.https.url('/user'), ({ cookies }) => { if (cookies.authToken == null) { return HttpResponse.json( { error: 'Auth token not found', }, { status: 403 }, ) } return HttpResponse.json({ firstName: 'John', lastName: 'Maverick', }) }), ) server.listen() }) afterAll(async () => { server.close() await httpServer.close() }) test('inherits cookies set from a preceding request', async () => { const res = await fetch(httpServer.https.url('/login'), { method: 'POST', }).then(() => { // Fetch the user after requesting login to see // if the response cookies set in the login request handler // are automatically forwarded to the "GET /user" request. return fetch(httpServer.https.url('/user')) }) expect(res.status).toBe(200) const json = await res.json() expect(json).toEqual({ firstName: 'John', lastName: 'Maverick', }) }) ================================================ FILE: test/node/rest-api/https.node.test.ts ================================================ /** * @vitest-environment node */ import https from 'https' import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) test('intercepts and mocks a request made via "https"', async () => { server.use( http.get('https://api.example.com/resource', () => { return HttpResponse.text('Hello, world!') }), ) const request = https.get('https://api.example.com/resource') await new Promise((resolve, reject) => { request.on('response', (response) => { const chunks: Array = [] response.on('data', (chunk) => chunks.push(Buffer.from(chunk))) response.on('error', (error) => reject(error)) response.once('end', () => { expect(chunks).toHaveLength(1) const responseText = Buffer.concat(chunks).toString('utf8') expect(responseText).toBe('Hello, world!') resolve() }) }) request.on('error', (error) => reject(error)) }) }) ================================================ FILE: test/node/rest-api/request/body/body-arraybuffer.node.test.ts ================================================ /** * @vitest-environment node */ import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' function encodeBuffer(value: unknown) { return Buffer.from(JSON.stringify(value)).buffer } const server = setupServer( http.post('http://localhost/arrayBuffer', async ({ request }) => { const requestBodyBuffer = await request.arrayBuffer() return HttpResponse.arrayBuffer(requestBodyBuffer) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) test('reads text request body as array buffer', async () => { const res = await fetch('http://localhost/arrayBuffer', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: 'foo bar', }) const body = await res.arrayBuffer() expect(res.status).toBe(200) expect(body).toEqual(encodeBuffer('foo bar')) }) test('reads array buffer request body as array buffer', async () => { const res = await fetch('http://localhost/arrayBuffer', { method: 'POST', body: encodeBuffer('foo bar'), }) const body = await res.arrayBuffer() expect(res.status).toBe(200) expect(body).toEqual(encodeBuffer('foo bar')) }) test('reads null request body as empty array buffer', async () => { const res = await fetch('http://localhost/arrayBuffer', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: undefined, }) const body = await res.arrayBuffer() expect(res.status).toBe(200) expect(body).toEqual(encodeBuffer('')) }) test('reads undefined request body as empty array buffer', async () => { const res = await fetch('http://localhost/arrayBuffer', { method: 'POST', body: undefined, }) const body = await res.arrayBuffer() expect(res.status).toBe(200) expect(body).toEqual(encodeBuffer('')) }) ================================================ FILE: test/node/rest-api/request/body/body-form-data.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.post('http://localhost/resource', async ({ request }) => { const formData = await request.formData() return HttpResponse.json(Array.from(formData.entries())) }), http.post('http://localhost/file', async ({ request }) => { const formData = await request.formData() const file = formData.get('file') as File | null if (!file) { throw HttpResponse.text('Missing file', { status: 400 }) } return HttpResponse.json({ name: file.name, size: file.size, content: await file.text(), }) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('supports FormData request body', async () => { // Note that creating a `FormData` instance in Node/JSDOM differs // from the same instance in a real browser. Follow the instructions // of your `fetch` polyfill to learn more. const formData = new FormData() formData.append('username', 'john.maverick') formData.append('password', 'secret123') const res = await fetch('http://localhost/resource', { method: 'POST', body: formData, }) const json = await res.json() expect(res.status).toBe(200) expect(json).toEqual([ ['username', 'john.maverick'], ['password', 'secret123'], ]) }) it('respects Blob size in request body', async () => { const blob = new Blob([JSON.stringify({ data: 1 })], { type: 'application/json', }) const formData = new FormData() formData.set('file', blob, 'data.json') const response = await fetch('http://localhost/file', { method: 'POST', body: formData, }) expect(response.status).toBe(200) expect(await response.json()).toEqual({ name: 'data.json', size: blob.size, content: await blob.text(), }) }) ================================================ FILE: test/node/rest-api/request/body/body-json.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { encodeBuffer } from '@mswjs/interceptors' const server = setupServer( http.post('http://localhost/json', async ({ request }) => { return HttpResponse.json(await request.json()) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) test('reads request body using json() method', async () => { const res = await fetch('http://localhost/json', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ firstName: 'John' }), }) const json = await res.json() expect(res.status).toBe(200) expect(json).toEqual({ firstName: 'John' }) }) test('reads array buffer request body using json() method', async () => { const res = await fetch('http://localhost/json', { method: 'POST', body: encodeBuffer(JSON.stringify({ firstName: 'John' })), }) const json = await res.json() expect(res.status).toBe(200) expect(json).toEqual({ firstName: 'John' }) }) ================================================ FILE: test/node/rest-api/request/body/body-protobuf.node.test.ts ================================================ // @vitest-environment node import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.post('https://example.com/protobuf', async ({ request }) => { const buffer = await request.arrayBuffer() return new HttpResponse(new Uint8Array(buffer), { headers: { 'Content-Type': 'application/protobuf', }, }) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('responds with a "application/protobuf" mocked response', async () => { const payload = new Uint8Array([138, 1, 6, 10, 4, 10, 2, 32, 1]) const response = await fetch('https://example.com/protobuf', { method: 'POST', headers: { 'Content-Type': 'application/protobuf', }, body: payload, }) const body = await response.arrayBuffer() expect(new Uint8Array(body)).toEqual(payload) }) ================================================ FILE: test/node/rest-api/request/body/body-text.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import { encodeBuffer } from '@mswjs/interceptors' const server = setupServer( http.post('http://localhost/resource', async ({ request }) => { return HttpResponse.text(await request.text()) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) test('reads plain text request body as text', async () => { const res = await fetch('http://localhost/resource', { method: 'POST', headers: { 'Content-Type': 'text/plain', }, body: 'hello-world', }) const body = await res.text() expect(res.status).toBe(200) expect(body).toBe('hello-world') }) test('reads json request body as text', async () => { const res = await fetch('http://localhost/resource', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ firstName: 'John' }), }) const body = await res.text() expect(res.status).toBe(200) expect(body).toBe(`{"firstName":"John"}`) }) test('reads array buffer request body as text', async () => { const res = await fetch('http://localhost/resource', { method: 'POST', body: encodeBuffer('hello-world'), }) const body = await res.text() expect(res.status).toBe(200) expect(body).toBe('hello-world') }) test('reads null request body as empty text', async () => { const res = await fetch('http://localhost/resource', { method: 'POST', body: null as any, }) const body = await res.text() expect(res.status).toBe(200) expect(body).toBe('') }) test('reads undefined request body as empty text', async () => { const res = await fetch('http://localhost/resource', { method: 'POST', }) const body = await res.text() expect(res.status).toBe(200) expect(body).toBe('') }) ================================================ FILE: test/node/rest-api/request/body/body-used.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' import * as express from 'express' import { HttpServer } from '@open-draft/test-server/http' const httpServer = new HttpServer((app) => { app.post('/resource', express.json(), (req, res) => { res.json({ response: `received: ${req.body.message}` }) }) }) const server = setupServer() beforeAll(async () => { server.listen() await httpServer.listen() }) afterEach(() => { server.resetHandlers() vi.restoreAllMocks() }) afterAll(async () => { server.close() await httpServer.close() }) it('does not read the body while parsing an unhandled request', async () => { // Expecting an unhandled request warning in this test. vi.spyOn(console, 'warn').mockImplementation(() => {}) const requestUrl = httpServer.http.url('/resource') const response = await fetch(requestUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: 'Hello server', }), }) expect(await response.json()).toEqual({ response: `received: Hello server` }) }) it('does not read the body while parsing an unhandled request', async () => { const requestUrl = httpServer.http.url('/resource') server.use( http.post(requestUrl, () => { return HttpResponse.json({ mocked: true }) }), ) const response = await fetch(requestUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: 'Hello server', }), }) expect(await response.json()).toEqual({ mocked: true }) }) ================================================ FILE: test/node/rest-api/request/matching/all.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpServer } from '@open-draft/test-server/http' import { HttpMethods, http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const httpServer = new HttpServer((app) => { // Responding with "204 No Content" because the "OPTIONS" // request returns 204 without an obvious way to override that. app.all('*', (req, res) => res.status(204).end()) }) const server = setupServer() beforeAll(async () => { await httpServer.listen() server.listen({ onUnhandledRequest: 'bypass', }) }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() await httpServer.close() }) async function forEachMethod(callback: (method: HttpMethods) => unknown) { for (const method of Object.values(HttpMethods)) { await callback(method) } } test('matches all requests given no custom path', async () => { server.use( http.all('*', () => { return HttpResponse.text('welcome to the jungle') }), ) const responses = await Promise.all( Object.values(HttpMethods).reduce[]>((all, method) => { return all.concat( [ httpServer.http.url('/'), httpServer.http.url('/foo'), 'https://example.com', ].map((url) => fetch(url, { method })), ) }, []), ) for (const response of responses) { expect(response.status).toBe(200) expect(await response.text()).toEqual('welcome to the jungle') } }) test('respects custom path when matching requests', async () => { server.use( http.all(httpServer.http.url('/api/*'), () => { return HttpResponse.text('hello world') }), ) // Root requests. await forEachMethod(async (method) => { const response = await fetch(httpServer.http.url('/api/'), { method }) expect(response.status).toBe(200) expect(await response.text()).toEqual('hello world') }) // Nested requests. await forEachMethod(async (method) => { const response = await fetch(httpServer.http.url('/api/foo'), { method, }) expect(response.status).toBe(200) expect(await response.text()).toEqual('hello world') }) // Mismatched requests. await forEachMethod(async (method) => { const response = await fetch(httpServer.http.url('/foo'), { method }) expect(response.status).toEqual(204) }) }) ================================================ FILE: test/node/rest-api/request/matching/custom-predicate.node.test.ts ================================================ // @vitest-environment node import { http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() beforeAll(() => { server.listen({ onUnhandledRequest: 'bypass' }) }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('matches requests when the predicate function returns true', async () => { server.use( http.post( async ({ request }) => { const requestBody = await request.clone().text() return requestBody === 'hello world' }, async ({ request }) => { return new Response(request.clone().body, request) }, ), ) const response = await fetch('http://localhost/irrelevant', { method: 'POST', body: 'hello world', }) expect.soft(response.status).toBe(200) await expect.soft(response.text()).resolves.toBe('hello world') }) it('does not match requests when the predicate function returns false', async () => { server.use( http.post( async ({ request }) => { const requestBody = await request.clone().text() return requestBody === 'hello world' }, async ({ request }) => { return new Response(request.clone().body, request) }, ), ) await expect( fetch('http://localhost/irrelevant', { method: 'POST', body: 'non-matching-request', }), ).rejects.toThrow('fetch failed') }) ================================================ FILE: test/node/rest-api/request/matching/path-params-decode.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get<{ url: string }>( 'https://test.mswjs.io/reflect-url/:url', ({ params }) => { return HttpResponse.json({ url: params.url }) }, ), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) test('decodes url componets', async () => { const url = 'http://example.com:5001/example' const res = await fetch( `https://test.mswjs.io/reflect-url/${encodeURIComponent(url)}`, ) expect(res.status).toBe(200) expect(await res.json()).toEqual({ url, }) }) ================================================ FILE: test/node/rest-api/request/matching/path-params-optional.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('intercepts the request that fully matches the path', async () => { server.use( http.get('http://localhost/user/:id?', () => HttpResponse.json({ mocked: true }), ), ) const response = await fetch('http://localhost/user/123') expect(response.status).toBe(200) expect(await response.json()).toEqual({ mocked: true }) }) it('intercepts the request that partially matches the path', async () => { server.use( http.get('http://localhost/user/:id?', () => HttpResponse.json({ mocked: true }), ), ) const response = await fetch('http://localhost/user') expect(response.status).toBe(200) expect(await response.json()).toEqual({ mocked: true }) }) ================================================ FILE: test/node/rest-api/request/matching/relative-url.node.test.ts ================================================ /** * @vitest-environment jsdom */ import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('/api/movies', () => { return HttpResponse.json([ { title: 'The Lord of the Rings' }, { title: 'The Matrix' }, ]) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('responds to a relative URL in jsdom', async () => { const response = await fetch('/api/movies') expect(response.status).toBe(200) expect(await response.json()).toEqual([ { title: 'The Lord of the Rings' }, { title: 'The Matrix' }, ]) }) ================================================ FILE: test/node/rest-api/response/body-binary.node.test.ts ================================================ /** * @vitest-environment node */ import * as path from 'path' import * as fs from 'fs' import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' function getImageBuffer() { return fs.readFileSync(path.resolve(__dirname, '../../../fixtures/image.jpg')) } const server = setupServer( http.get('http://test.mswjs.io/image', () => { const imageBuffer = getImageBuffer() return HttpResponse.arrayBuffer(imageBuffer, { headers: { 'Content-Type': 'image/jpeg', 'Content-Length': imageBuffer.byteLength.toString(), }, }) }), ) beforeAll(() => server.listen()) afterAll(() => server.close()) test('returns given buffer in the mocked response', async () => { const response = await fetch('http://test.mswjs.io/image') const actualImageBuffer = await response.arrayBuffer() const expectedImageBuffer = getImageBuffer() expect(response.status).toBe(200) expect(response.headers.get('content-length')).toBe( actualImageBuffer.byteLength.toString(), ) expect( Buffer.compare(Buffer.from(actualImageBuffer), expectedImageBuffer), ).toBe(0) }) test('returns given blob in the mocked response', async () => { const response = await fetch('http://test.mswjs.io/image') const blob = await response.blob() const expectedImageBuffer = getImageBuffer() expect(response.status).toBe(200) expect(blob.type).toBe('image/jpeg') expect(blob.size).toBe(Number(response.headers.get('content-length'))) expect( Buffer.compare(Buffer.from(await blob.arrayBuffer()), expectedImageBuffer), ).toBe(0) }) ================================================ FILE: test/node/rest-api/response/body-html.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('http://localhost/html', () => { return HttpResponse.html(`

Jane Doe

`) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) test('responds with an HTML response body', async () => { const res = await fetch('http://localhost/html') const text = await res.text() expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('text/html') expect(text).toEqual(`

Jane Doe

`) }) ================================================ FILE: test/node/rest-api/response/body-json.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('http://localhost/json', () => { return HttpResponse.json({ firstName: 'John' }) }), http.get('http://localhost/number', () => { return HttpResponse.json(123) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('responds with a JSON response body', async () => { const response = await fetch('http://localhost/json') expect(response.headers.get('Content-Type')).toBe('application/json') expect(await response.json()).toEqual({ firstName: 'John' }) }) it('responds with a single number JSON response body', async () => { const response = await fetch('http://localhost/number') expect(response.headers.get('Content-Type')).toBe('application/json') expect(await response.json()).toEqual(123) }) it('implicitly sets "Content-Length" header on the mocked response', async () => { const response = await fetch('http://localhost/json') expect(response.headers.get('Content-Length')).toBe('20') }) it('implicitly sets "Content-Length" to 0 if the mocked response body is empty', async () => { server.use( http.get('http://localhost/json', () => { return HttpResponse.json() }), ) const response = await fetch('http://localhost/json') expect(response.headers.get('Content-Length')).toBe('0') }) it('respects custom "Content-Length" mocked response header', async () => { server.use( http.get('http://localhost/json', () => { return HttpResponse.json( { firstName: 'John' }, { headers: { 'Content-Length': '32', }, }, ) }), ) const response = await fetch('http://localhost/json') expect(response.headers.get('Content-Length')).toBe('32') }) ================================================ FILE: test/node/rest-api/response/body-stream.node.test.ts ================================================ // @vitest-environment node import https from 'node:https' import { HttpResponse, http, delay } from 'msw' import { setupServer } from 'msw/node' interface CustomMatchers { toRoughlyEqual: (actual: number, deviation: number) => R } declare module 'vitest' { interface Matchers extends CustomMatchers {} } expect.extend({ /** * Asserts a given actual number to roughly equal to the expected number, * taking the maximum allowed delta `deviation` into account. */ toRoughlyEqual(actual, expected, deviation) { const diff = Math.abs(actual - expected) const passes = diff <= deviation if (passes) { return { pass: true, message: () => `expected ${actual} not to be roughly equal to ${expected} (deviation: ${deviation})`, } } return { pass: false, message: () => `expected ${actual} to be roughly equal to ${expected} (deviation: ${deviation})`, } }, }) const encoder = new TextEncoder() const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) test('responds with a ReadableStream', async () => { server.use( http.get('https://api.example.com/stream', () => { const stream = new ReadableStream({ start(controller) { controller.enqueue(encoder.encode('hello')) controller.enqueue(encoder.encode('world')) controller.close() }, }) return new HttpResponse(stream, { headers: { 'Content-Type': 'text/event-stream', }, }) }), ) const response = await fetch('https://api.example.com/stream') expect(response.status).toBe(200) expect(response.statusText).toBe('OK') expect(response.body).toBeInstanceOf(ReadableStream) expect(response.body!.locked).toBe(false) expect(await response.text()).toBe('helloworld') }) test('supports delays when enqueuing chunks', async () => { server.use( http.get('https://api.example.com/stream', () => { const stream = new ReadableStream({ async start(controller) { controller.enqueue(encoder.encode('first')) await delay(250) controller.enqueue(encoder.encode('second')) await delay(250) controller.enqueue(encoder.encode('third')) await delay(250) controller.close() }, }) return new HttpResponse(stream, { headers: { 'Content-Type': 'text/event-stream', }, }) }), ) await new Promise((resolve, reject) => { const request = https.get('https://api.example.com/stream', (response) => { const chunks: Array<{ buffer: Buffer; timestamp: number }> = [] response.on('data', (data) => { chunks.push({ buffer: Buffer.from(data), timestamp: performance.now(), }) }) response.once('end', () => { const textChunks = chunks.map((chunk) => chunk.buffer.toString('utf8')) expect(textChunks).toEqual(['first', 'second', 'third']) // Ensure that the chunks were sent over time, // respecting the delay set in the mocked stream. const chunkTimings = chunks.map((chunk) => chunk.timestamp) expect(chunkTimings[1] - chunkTimings[0]).toRoughlyEqual(250, 50) expect(chunkTimings[2] - chunkTimings[1]).toRoughlyEqual(250, 50) resolve() }) }) request.on('error', (error) => reject(error)) }) }) ================================================ FILE: test/node/rest-api/response/body-text.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('http://localhost/text', () => { return HttpResponse.text('hello world') }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('responds with a text response body', async () => { const res = await fetch('http://localhost/text') const text = await res.text() expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('text/plain') expect(text).toBe('hello world') }) it('implicitly sets "Content-Length" header on a text response', async () => { const response = await fetch('http://localhost/text') expect(response.headers.get('Content-Length')).toBe('11') }) it('implicitly sets "Content-Length" header to 0 on empty text response', async () => { server.use( http.get('http://localhost/text', () => { return HttpResponse.text('') }), ) const response = await fetch('http://localhost/text') expect(response.headers.get('Content-Length')).toBe('0') }) it('respects custom "Content-Length" mocked response header', async () => { server.use( http.get('http://localhost/text', () => { return HttpResponse.text('hello-world', { headers: { 'Content-Length': '32', }, }) }), ) const response = await fetch('http://localhost/text') expect(response.headers.get('Content-Length')).toBe('32') }) ================================================ FILE: test/node/rest-api/response/body-xml.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('http://localhost/xml', () => { return HttpResponse.xml(` abc-123 John Maverick `) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) test('responds with an XML response body', async () => { const res = await fetch('http://localhost/xml') const text = await res.text() expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('text/xml') expect(text).toEqual(` abc-123 John Maverick `) }) ================================================ FILE: test/node/rest-api/response/generator.test.ts ================================================ /** * @vitest-environment node */ import { http, HttpResponse, delay } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() async function fetchJson(input: string | URL | Request, init?: RequestInit) { return fetch(input, init).then((response) => response.json()) } beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('supports generator function as response resolver', async () => { server.use( http.get('https://example.com/weather', function* () { let degree = 10 while (degree < 13) { degree++ yield HttpResponse.json(degree) } degree++ return HttpResponse.json(degree) }), ) // Must respond with yielded responses. await expect(fetchJson('https://example.com/weather')).resolves.toEqual(11) await expect(fetchJson('https://example.com/weather')).resolves.toEqual(12) await expect(fetchJson('https://example.com/weather')).resolves.toEqual(13) // Must respond with the final "done" response. await expect(fetchJson('https://example.com/weather')).resolves.toEqual(14) // Must keep responding with the final "done" response. await expect(fetchJson('https://example.com/weather')).resolves.toEqual(14) }) it('supports async generator function as response resolver', async () => { server.use( http.get('https://example.com/weather', async function* () { await delay(20) let degree = 10 while (degree < 13) { degree++ yield HttpResponse.json(degree) } degree++ return HttpResponse.json(degree) }), ) await expect(fetchJson('https://example.com/weather')).resolves.toEqual(11) await expect(fetchJson('https://example.com/weather')).resolves.toEqual(12) await expect(fetchJson('https://example.com/weather')).resolves.toEqual(13) await expect(fetchJson('https://example.com/weather')).resolves.toEqual(14) await expect(fetchJson('https://example.com/weather')).resolves.toEqual(14) }) it('supports generator function as one-time response resolver', async () => { server.use( http.get( 'https://example.com/weather', function* () { let degree = 10 while (degree < 13) { degree++ yield HttpResponse.json(degree) } degree++ return HttpResponse.json(degree) }, { once: true }, ), http.get('*', () => { return HttpResponse.json('fallback') }), ) // Must respond with the yielded incrementing responses. await expect(fetchJson('https://example.com/weather')).resolves.toEqual(11) await expect(fetchJson('https://example.com/weather')).resolves.toEqual(12) await expect(fetchJson('https://example.com/weather')).resolves.toEqual(13) // Must respond with the "done" final response from the iterator. await expect(fetchJson('https://example.com/weather')).resolves.toEqual(14) // Must respond with the other handler since the generator one is used. await expect(fetchJson('https://example.com/weather')).resolves.toEqual( 'fallback', ) await expect(fetchJson('https://example.com/weather')).resolves.toEqual( 'fallback', ) }) ================================================ FILE: test/node/rest-api/response/response-cookies.test.ts ================================================ /** * @vitest-environment node */ import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('supports mocking a response cookie', async () => { server.use( http.get('*/resource', () => { return new HttpResponse(null, { headers: { 'Set-Cookie': 'a=1', }, }) }), ) const response = await fetch('http://localhost/resource') expect(response.headers.get('Set-Cookie')).toBe('a=1') }) it('supports mocking multiple response cookies', async () => { server.use( http.get('*/resource', () => { return new HttpResponse(null, { headers: [ ['Set-Cookie', 'a=1'], ['Set-Cookie', 'b=2'], ], }) }), ) const response = await fetch('http://localhost/resource') expect(response.headers.get('Set-Cookie')).toBe('a=1, b=2') }) ================================================ FILE: test/node/rest-api/response/response-error.test.ts ================================================ /** * @vitest-environment node */ import { http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('responds with a mocked error response using "Response.error" shorthand', async () => { server.use( http.get('https://api.example.com/resource', () => { return Response.error() }), ) const responseError = await fetch('https://api.example.com/resource') .then(() => null) .catch((error) => error) expect(responseError.name).toBe('TypeError') expect(responseError.message).toBe('Failed to fetch') // Guard against false positives due to exceptions arising from the library. expect(responseError.cause).toBeInstanceOf(Response) expect(responseError.cause.type).toBe('error') expect(responseError.cause.status).toBe(0) }) ================================================ FILE: test/node/rest-api/response/throw-response.node.test.ts ================================================ /** * @vitest-environment node */ import { HttpResponse, http } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it('supports throwing a plain Response in a response resolver', async () => { server.use( http.get('https://example.com/', () => { // You can throw a Response instance in a response resolver // to short-circuit its execution and respond "early". throw new Response('hello world') }), ) const response = await fetch('https://example.com') expect(response.status).toBe(200) expect(await response.text()).toBe('hello world') }) it('supports throwing an HttpResponse instance in a response resolver', async () => { server.use( http.get('https://example.com/', () => { throw HttpResponse.text('hello world') }), ) const response = await fetch('https://example.com') expect(response.status).toBe(200) expect(response.headers.get('Content-Type')).toBe('text/plain') expect(await response.text()).toBe('hello world') }) it('supports throwing an error response in a response resolver', async () => { server.use( http.get('https://example.com/', () => { throw HttpResponse.text('not found', { status: 400 }) }), ) const response = await fetch('https://example.com') expect(response.status).toBe(400) expect(response.headers.get('Content-Type')).toBe('text/plain') expect(await response.text()).toBe('not found') }) it('supports throwing a network error in a response resolver', async () => { server.use( http.get('https://example.com/', () => { throw HttpResponse.error() }), ) await expect(fetch('https://example.com')).rejects.toThrow('Failed to fetch') }) it('supports middleware-style responses', async () => { server.use( http.get('https://example.com/', ({ request }) => { const url = new URL(request.url) if (!url.searchParams.has('id')) { throw HttpResponse.text('must have id', { status: 400 }) } return HttpResponse.text('ok') }), ) const response = await fetch('https://example.com/?id=1') expect(response.status).toBe(200) expect(response.headers.get('Content-Type')).toBe('text/plain') expect(await response.text()).toBe('ok') const errorResponse = await fetch('https://example.com/') expect(errorResponse.status).toBe(400) expect(errorResponse.headers.get('Content-Type')).toBe('text/plain') expect(await errorResponse.text()).toBe('must have id') }) it('handles non-response errors as 500 error responses', async () => { server.use( http.get('https://example.com/', () => { throw new Error('Custom error') }), ) const response = await fetch('https://example.com') expect(response.status).toBe(500) expect(response.statusText).toBe('Unhandled Exception') expect(await response.json()).toEqual({ name: 'Error', message: 'Custom error', stack: expect.any(String), }) }) ================================================ FILE: test/node/rest-api/response-patching.node.test.ts ================================================ // @vitest-environment node import { http, bypass } from 'msw' import { setupServer } from 'msw/node' import express from 'express' import { HttpServer } from '@open-draft/test-server/http' const httpServer = new HttpServer((app) => { app.use('/resource', (_req, res, next) => { res.setHeader('access-control-allow-headers', '*') next() }) app.post('/resource', express.text(), (req, res) => { res.json({ text: req.body, requestHeaders: req.headers, }) }) }) const server = setupServer() beforeAll(async () => { server.listen() await httpServer.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(async () => { server.close() await httpServer.close() }) it('supports patching an original HTTP response', async () => { server.use( http.post(httpServer.http.url('/resource'), async ({ request }) => { const originalResponse = await fetch(bypass(request)) const { text, requestHeaders } = await originalResponse.json() return new Response(text.toUpperCase(), { headers: requestHeaders }) }), ) const response = await fetch(httpServer.http.url('/resource'), { method: 'POST', body: 'world', }) await expect(response.text()).resolves.toBe('WORLD') // Must not contain the internal bypass request header. expect(Object.fromEntries(response.headers)).toHaveProperty('accept', '*/*') }) it('preserves request "accept" header when patching a response', async () => { server.use( http.post(httpServer.http.url('/resource'), async ({ request }) => { const originalResponse = await fetch(bypass(request)) const { text, requestHeaders } = await originalResponse.json() return new Response(text.toUpperCase(), { headers: requestHeaders }) }), ) const response = await fetch(httpServer.http.url('/resource'), { method: 'POST', headers: { accept: 'application/json', }, body: 'world', }) await expect(response.text()).resolves.toBe('WORLD') // Must not contain the internal bypass request header. expect(Object.fromEntries(response.headers)).toHaveProperty( 'accept', 'application/json', ) }) ================================================ FILE: test/node/third-party/axios-error-response.test.ts ================================================ // @vitest-environment node import axios, { AxiosError } from 'axios' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.get('https://example.com/resource', () => { return HttpResponse.json({ errorMessage: 'Custom error' }, { status: 400 }) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('responds with an error response to axios request', async () => { const error = await axios('https://example.com/resource') .then(() => { throw new Error('Must reject the request Promise') }) .catch((error) => error as AxiosError) expect(error.response?.status).toBe(400) expect(error.response?.data).toEqual({ errorMessage: 'Custom error' }) }) ================================================ FILE: test/node/third-party/axios-timeout.node.test.ts ================================================ // @vitest-environment node import axios, { AxiosError } from 'axios' import { http, HttpResponse, delay } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() beforeAll(() => { server.listen() }) afterEach(() => { server.resetHandlers() }) afterAll(() => { server.close() }) it("axios times out when the handler's delay is greater than axios timeout", async () => { server.use( http.get('http://localhost/slow', async () => { await delay(500) return HttpResponse.json({ ok: true }) }), ) const error = await axios('http://localhost/slow', { timeout: 50 }) .then(() => { expect.fail('Request must not succeed') }) .catch((error) => error as AxiosError) expect(error).toBeInstanceOf(AxiosError) expect(error).toMatchObject>({ code: 'ECONNABORTED', message: expect.stringMatching(/timeout/i), }) }) it("axios does not time out when the handler's delay is less than axios timeout", async () => { server.use( http.get('http://localhost/fast', async () => { await delay(50) return HttpResponse.json({ ok: true }) }), ) const response = await axios('http://localhost/fast', { timeout: 200 }) expect(response.data).toEqual({ ok: true }) }) ================================================ FILE: test/node/third-party/axios-upload.node.test.ts ================================================ // @vitest-environment node import { File } from 'node:buffer' import axios from 'axios' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' const server = setupServer( http.post('https://example.com/upload', async ({ request }) => { const data = await request.formData() const file = data.get('file') if (!file) { return new HttpResponse('Missing document upload', { status: 400 }) } if (!(file instanceof File)) { return new HttpResponse('Uploaded document is not a File', { status: 400, }) } return HttpResponse.json({ message: `Successfully uploaded "${file.name}"!`, content: await file.text(), }) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it('responds with a mocked response to an upload request', async () => { const onUploadProgress = vi.fn() const request = axios.create({ baseURL: 'https://example.com', onUploadProgress, }) const formData = new FormData() const file = new Blob(['Hello', 'world'], { type: 'text/plain' }) formData.set('file', file, 'doc.txt') const response = await request.post('/upload', formData).catch((error) => { throw error.response.data }) expect(response.data).toEqual({ message: 'Successfully uploaded "doc.txt"!', content: 'Helloworld', }) expect(onUploadProgress.mock.calls.length).toBeGreaterThan(0) expect(onUploadProgress).toHaveBeenNthCalledWith( 1, expect.objectContaining({ loaded: expect.any(Number), total: expect.any(Number), bytes: expect.any(Number), }), ) }) ================================================ FILE: test/node/tsconfig.json ================================================ { "extends": "../tsconfig.json", "include": ["../../global.d.ts", "./**/*.test.ts"] } ================================================ FILE: test/node/vitest.config.ts ================================================ import { defineConfig } from 'vitest/config' import { mswExports, customViteEnvironments } from '../support/alias' export default defineConfig({ test: { dir: './test/node', globals: true, alias: { ...mswExports, ...customViteEnvironments, }, environmentOptions: { jsdom: { url: 'http://localhost/', }, }, }, }) ================================================ FILE: test/node/ws-api/on-unhandled-request/callback.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' const service = ws.link('wss://localhost:4321') const server = setupServer() const onUnhandledRequest = vi.fn() beforeAll(() => { server.listen({ onUnhandledRequest }) vi.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { server.resetHandlers() vi.resetAllMocks() }) afterAll(() => { server.close() vi.restoreAllMocks() }) it('calls a custom callback on an unhandled WebSocket connection', async () => { const socket = new WebSocket('wss://localhost:4321') await vi.waitFor(() => { return new Promise((resolve, reject) => { socket.onopen = resolve socket.onerror = reject }) }) expect(onUnhandledRequest).toHaveBeenCalledOnce() const [request] = onUnhandledRequest.mock.calls[0] expect(request).toBeInstanceOf(Request) expect(request.method).toBe('GET') expect(request.url).toBe('wss://localhost:4321/') expect(Array.from(request.headers)).toEqual([ ['connection', 'upgrade'], ['upgrade', 'websocket'], ]) }) it('does not call a custom callback for a handled WebSocket connection', async () => { server.use(service.addEventListener('connection', () => {})) const socket = new WebSocket('wss://localhost:4321') await vi.waitFor(() => { return new Promise((resolve, reject) => { socket.onopen = resolve socket.onerror = reject }) }) expect(onUnhandledRequest).not.toHaveBeenCalled() }) ================================================ FILE: test/node/ws-api/on-unhandled-request/error.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' const service = ws.link('wss://localhost:4321') const server = setupServer() beforeAll(() => { server.listen({ onUnhandledRequest: 'error' }) vi.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { server.resetHandlers() vi.resetAllMocks() }) afterAll(() => { server.close() vi.restoreAllMocks() }) it( 'errors on unhandled WebSocket connection', server.boundary(async () => { const socket = new WebSocket('wss://localhost:4321') const errorListener = vi.fn() await vi.waitUntil(() => { return new Promise((resolve, reject) => { // These are intentionally swapped. The connection MUST error. socket.addEventListener('error', errorListener) socket.addEventListener('error', resolve) socket.onopen = () => { reject(new Error('WebSocket connection opened unexpectedly')) } }) }) expect(console.error).toHaveBeenCalledWith( `\ [MSW] Error: intercepted a request without a matching request handler: • GET wss://localhost:4321/ If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`, ) expect(errorListener).toHaveBeenCalledOnce() // Must forward the original `onUnhandledRequest` error as the // `cause` property of the error event emitted on the connection. const [event] = errorListener.mock.calls[0] expect(event).toBeInstanceOf(Event) expect(event.type).toBe('error') expect(event.cause).toBeInstanceOf(Error) expect(event.cause.message).toBe( '[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.', ) }), ) it( 'does not error on handled WebSocket connection', server.boundary(async () => { server.use(service.addEventListener('connection', () => {})) const socket = new WebSocket('wss://localhost:4321') await vi.waitFor(() => { return new Promise((resolve, reject) => { socket.onopen = resolve socket.onerror = reject }) }) expect(console.error).not.toHaveBeenCalled() }), ) ================================================ FILE: test/node/ws-api/on-unhandled-request/warn.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' const service = ws.link('wss://localhost:4321') const server = setupServer() beforeAll(() => { server.listen({ onUnhandledRequest: 'warn' }) vi.spyOn(console, 'warn').mockImplementation(() => {}) }) afterEach(() => { server.resetHandlers() vi.resetAllMocks() }) afterAll(() => { server.close() vi.restoreAllMocks() }) it( 'warns on unhandled WebSocket connection', server.boundary(async () => { const socket = new WebSocket('wss://localhost:4321') await vi.waitFor(() => { return new Promise((resolve, reject) => { socket.onopen = resolve socket.onerror = reject }) }) expect(console.warn).toHaveBeenCalledWith( `\ [MSW] Warning: intercepted a request without a matching request handler: • GET wss://localhost:4321/ If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/http/intercepting-requests`, ) }), ) it( 'does not warn on handled WebSocket connection', server.boundary(async () => { server.use(service.addEventListener('connection', () => {})) const socket = new WebSocket('wss://localhost:4321') await vi.waitFor(() => { return new Promise((resolve, reject) => { socket.onopen = resolve socket.onerror = reject }) }) expect(console.warn).not.toHaveBeenCalled() }), ) ================================================ FILE: test/node/ws-api/ws.apply.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() afterEach(() => { server.close() }) it('patches WebSocket class even if no event handlers were defined', () => { server.listen() const raw = new WebSocket('wss://example.com') expect(raw.constructor.name).toBe('WebSocketOverride') expect(raw).toBeInstanceOf(EventTarget) }) it('does not patch WebSocket class until server.listen() is called', () => { const api = ws.link('wss://example.com') server.use(api.addEventListener('connection', () => {})) const raw = new WebSocket('wss://example.com') expect(raw.constructor.name).toBe('WebSocket') expect(raw).toBeInstanceOf(EventTarget) server.listen() const mocked = new WebSocket('wss://example.com') expect(mocked.constructor.name).not.toBe('WebSocket') expect(mocked).toBeInstanceOf(EventTarget) }) ================================================ FILE: test/node/ws-api/ws.event-patching.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' import { WebSocketServer } from '../../support/WebSocketServer' const service = ws.link('ws://*') const originalServer = new WebSocketServer() const server = setupServer( service.addEventListener('connection', ({ server }) => { server.connect() }), ) beforeAll(async () => { server.listen() await originalServer.listen() }) afterEach(() => { server.resetHandlers() originalServer.resetState() }) afterAll(async () => { server.close() await originalServer.close() }) it('patches incoming server message', async () => { originalServer.once('connection', (client) => { client.send('hi from John') }) server.use( service.addEventListener('connection', ({ client, server }) => { /** * @note Since the initial handler connects to the server, * there's no need to call `server.connect()` again. */ server.addEventListener('message', (event) => { // Preventing the default stops the server-to-client forwarding. // It means that the WebSocket client won't receive the // actual server message. event.preventDefault() client.send(event.data.replace('John', 'Sarah')) }) }), ) const messageListener = vi.fn() const ws = new WebSocket(originalServer.url) ws.onmessage = (event) => messageListener(event.data) await vi.waitFor(() => { expect(messageListener).toHaveBeenNthCalledWith(1, 'hi from Sarah') expect(messageListener).toHaveBeenCalledTimes(1) }) }) it('combines original and mock server messages', async () => { originalServer.once('connection', (client) => { client.send('original message') }) server.use( service.addEventListener('connection', ({ client, server }) => { server.addEventListener('message', () => { client.send('mocked message') }) }), ) const messageListener = vi.fn() const ws = new WebSocket(originalServer.url) ws.onopen = () => ws.send('hello') ws.onmessage = (event) => messageListener(event.data) await vi.waitFor(() => { /** * @note That the server will send the message as soon as the client * connects. This happens before the event handler is called. */ expect(messageListener).toHaveBeenNthCalledWith(1, 'original message') expect(messageListener).toHaveBeenNthCalledWith(2, 'mocked message') expect(messageListener).toHaveBeenCalledTimes(2) }) }) it('combines original and mock server messages in the different order', async () => { originalServer.once('connection', (client) => { client.send('original message') }) server.use( service.addEventListener('connection', ({ client, server }) => { server.addEventListener('message', (event) => { /** * @note To change the incoming server events order, * prevent the default, send a mocked message, and * then send the original message as-is. */ event.preventDefault() client.send('mocked message') client.send(event.data) }) }), ) const messageListener = vi.fn() const ws = new WebSocket(originalServer.url) ws.onmessage = (event) => messageListener(event.data) await vi.waitFor(() => { expect(messageListener).toHaveBeenNthCalledWith(1, 'mocked message') expect(messageListener).toHaveBeenNthCalledWith(2, 'original message') expect(messageListener).toHaveBeenCalledTimes(2) }) }) ================================================ FILE: test/node/ws-api/ws.intercept.client.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' import { WebSocketServer } from '../../support/WebSocketServer' const server = setupServer() const wsServer = new WebSocketServer() const service = ws.link('ws://*') beforeAll(async () => { server.listen() await wsServer.listen() }) afterEach(() => { server.resetHandlers() wsServer.resetState() }) afterAll(async () => { server.close() await wsServer.close() }) it('intercepts outgoing client text message', async () => { const mockMessageListener = vi.fn() const realConnectionListener = vi.fn() server.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', mockMessageListener) }), ) wsServer.on('connection', realConnectionListener) const socket = new WebSocket(wsServer.url) socket.onopen = () => socket.send('hello') await vi.waitFor(() => { // Must intercept the outgoing client message event. expect(mockMessageListener).toHaveBeenCalledTimes(1) const messageEvent = mockMessageListener.mock.calls[0][0] as MessageEvent expect(messageEvent.type).toBe('message') expect(messageEvent.data).toBe('hello') expect(messageEvent.target).toBe(socket) // Must not connect to the actual server by default. expect(realConnectionListener).not.toHaveBeenCalled() }) }) it('intercepts outgoing client Blob message', async () => { const mockMessageListener = vi.fn() const realConnectionListener = vi.fn() server.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', mockMessageListener) }), ) wsServer.on('connection', realConnectionListener) const socket = new WebSocket(wsServer.url) socket.onopen = () => socket.send(new Blob(['hello'])) await vi.waitFor(() => { expect(mockMessageListener).toHaveBeenCalledTimes(1) const messageEvent = mockMessageListener.mock.calls[0][0] as MessageEvent expect(messageEvent.type).toBe('message') expect(messageEvent.data.size).toBe(5) expect(messageEvent.target).toEqual(socket) // Must not connect to the actual server by default. expect(realConnectionListener).not.toHaveBeenCalled() }) }) it('intercepts outgoing client ArrayBuffer message', async () => { const mockMessageListener = vi.fn() const realConnectionListener = vi.fn() server.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', mockMessageListener) }), ) wsServer.on('connection', realConnectionListener) const socket = new WebSocket(wsServer.url) socket.binaryType = 'arraybuffer' socket.onopen = () => socket.send(new TextEncoder().encode('hello')) await vi.waitFor(() => { expect(mockMessageListener).toHaveBeenCalledTimes(1) const messageEvent = mockMessageListener.mock.calls[0][0] as MessageEvent expect(messageEvent.type).toBe('message') expect(messageEvent.data).toEqual(new TextEncoder().encode('hello')) expect(messageEvent.target).toEqual(socket) // Must not connect to the actual server by default. expect(realConnectionListener).not.toHaveBeenCalled() }) }) ================================================ FILE: test/node/ws-api/ws.intercept.server.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' import { WebSocketServer } from '../../support/WebSocketServer' const server = setupServer() const originalServer = new WebSocketServer() const service = ws.link('ws://*') beforeAll(async () => { server.listen() await originalServer.listen() }) afterEach(() => { server.resetHandlers() originalServer.resetState() }) afterAll(async () => { server.close() await originalServer.close() }) it('intercepts incoming server text message', async () => { const serverMessageListener = vi.fn() const clientMessageListener = vi.fn() originalServer.on('connection', (client) => { client.send('hello') }) server.use( service.addEventListener('connection', ({ server }) => { server.connect() server.addEventListener('message', serverMessageListener) }), ) const socket = new WebSocket(originalServer.url) socket.addEventListener('message', clientMessageListener) await vi.waitFor(() => { expect(serverMessageListener).toHaveBeenCalledTimes(1) const serverMessage = serverMessageListener.mock.calls[0][0] as MessageEvent expect(serverMessage.type).toBe('message') expect(serverMessage.data).toBe('hello') expect(clientMessageListener).toHaveBeenCalledTimes(1) const clientMessage = clientMessageListener.mock.calls[0][0] as MessageEvent expect(clientMessage.type).toBe('message') expect(clientMessage.data).toBe('hello') }) }) it('intercepts incoming server Blob message', async () => { const serverMessageListener = vi.fn() const clientMessageListener = vi.fn() originalServer.on('connection', async (client) => { /** * @note You should use plain `Blob` instead. * For some reason, the "ws" package has trouble accepting * it as an input (expects a Buffer). */ client.send(await new Blob(['hello']).arrayBuffer()) }) server.use( service.addEventListener('connection', ({ server }) => { server.connect() server.addEventListener('message', serverMessageListener) }), ) const socket = new WebSocket(originalServer.url) socket.addEventListener('message', clientMessageListener) await vi.waitFor(() => { expect(serverMessageListener).toHaveBeenCalledTimes(1) const serverMessage = serverMessageListener.mock.calls[0][0] as MessageEvent expect(serverMessage.type).toBe('message') expect(serverMessage.data).toEqual(new Blob(['hello'])) expect(clientMessageListener).toHaveBeenCalledTimes(1) const clientMessage = clientMessageListener.mock.calls[0][0] as MessageEvent expect(clientMessage.type).toBe('message') expect(clientMessage.data).toEqual(new Blob(['hello'])) }) }) it('intercepts incoming ArrayBuffer message', async () => { const encoder = new TextEncoder() const serverMessageListener = vi.fn() const clientMessageListener = vi.fn() originalServer.on('connection', async (client) => { client.binaryType = 'arraybuffer' client.send(encoder.encode('hello world')) }) server.use( service.addEventListener('connection', ({ server }) => { server.connect() server.addEventListener('message', serverMessageListener) }), ) const socket = new WebSocket(originalServer.url) socket.binaryType = 'arraybuffer' socket.addEventListener('message', clientMessageListener) await vi.waitFor(() => { expect(serverMessageListener).toHaveBeenCalledTimes(1) const serverMessage = serverMessageListener.mock.calls[0][0] as MessageEvent expect(serverMessage.type).toBe('message') expect(new TextDecoder().decode(serverMessage.data)).toBe('hello world') expect(clientMessageListener).toHaveBeenCalledTimes(1) const clientMessage = clientMessageListener.mock.calls[0][0] as MessageEvent expect(clientMessage.type).toBe('message') expect(new TextDecoder().decode(clientMessage.data)).toBe('hello world') }) }) ================================================ FILE: test/node/ws-api/ws.server.connect.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' import { WebSocketServer } from '../../support/WebSocketServer' const service = ws.link('ws://*') const originalServer = new WebSocketServer() const server = setupServer() beforeAll(async () => { server.listen() await originalServer.listen() }) afterEach(() => { server.resetHandlers() originalServer.resetState() }) afterAll(async () => { server.close() await originalServer.close() }) it('does not connect to the actual server by default', async () => { const serverConnectionListener = vi.fn() const mockConnectionListener = vi.fn() originalServer.once('connection', serverConnectionListener) server.use(service.addEventListener('connection', mockConnectionListener)) new WebSocket(originalServer.url) await vi.waitFor(() => { expect(mockConnectionListener).toHaveBeenCalledTimes(1) expect(serverConnectionListener).not.toHaveBeenCalled() }) }) it('connects to the actual server after calling "server.connect()"', async () => { const serverConnectionListener = vi.fn() const mockConnectionListener = vi.fn() originalServer.once('connection', serverConnectionListener) server.use( service.addEventListener('connection', ({ server }) => { mockConnectionListener() server.connect() }), ) new WebSocket(originalServer.url) await vi.waitFor(() => { expect(mockConnectionListener).toHaveBeenCalledTimes(1) expect(serverConnectionListener).toHaveBeenCalledTimes(1) }) }) it('forwards incoming server events to the client once connected', async () => { originalServer.once('connection', (client) => client.send('hello')) server.use( service.addEventListener('connection', ({ server }) => { server.connect() }), ) const messageListener = vi.fn() const ws = new WebSocket(originalServer.url) ws.onmessage = (event) => messageListener(event.data) await vi.waitFor(() => { expect(messageListener).toHaveBeenNthCalledWith(1, 'hello') expect(messageListener).toHaveBeenCalledTimes(1) }) }) it('throws an error when connecting to a non-existing server', async () => { server.use( service.addEventListener('connection', ({ server }) => { server.connect() }), ) const errorListener = vi.fn() const ws = new WebSocket('ws://localhost:9876') ws.onerror = errorListener await vi.waitFor(() => { expect(errorListener).toHaveBeenCalledTimes(1) }) }) ================================================ FILE: test/node/ws-api/ws.stop-propagation.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' import { WebSocketServer } from '../../support/WebSocketServer' const server = setupServer() const service = ws.link('ws://*') const originalServer = new WebSocketServer() beforeAll(async () => { server.listen({ // We are intentionally connecting to non-existing WebSocket URLs. // Skip the unhandled request warnings, they are intentional. onUnhandledRequest: 'bypass', }) await originalServer.listen() }) afterEach(() => { server.resetHandlers() originalServer.resetState() }) afterAll(async () => { server.close() await originalServer.close() }) it('stops propagation for client "message" event', async () => { const clientMessageListener = vi.fn<(input: number) => void>() server.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { // Calling `stopPropagation` will prevent this event from being // dispatched on the `client` belonging to a different event handler. event.stopPropagation() clientMessageListener(1) }) client.addEventListener('message', () => { clientMessageListener(2) }) }), service.addEventListener('connection', ({ client }) => { client.addEventListener('message', () => { clientMessageListener(3) }) }), service.addEventListener('connection', ({ client }) => { client.addEventListener('message', () => { clientMessageListener(4) }) process.nextTick(() => { client.close() }) }), ) const ws = new WebSocket('ws://localhost') ws.onopen = () => ws.send('hello world') await vi.waitFor(() => { expect(ws.readyState).toBe(WebSocket.CLOSED) }) expect(clientMessageListener).toHaveBeenNthCalledWith(1, 1) expect(clientMessageListener).toHaveBeenNthCalledWith(2, 2) expect(clientMessageListener).toHaveBeenCalledTimes(2) }) it('stops immediate propagation for client "message" event', async () => { const clientMessageListener = vi.fn<(input: number) => void>() server.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { // Calling `stopPropagation` will prevent this event from being // dispatched on the `client` belonging to a different event handler. event.stopImmediatePropagation() clientMessageListener(1) }) client.addEventListener('message', () => { clientMessageListener(2) }) client.addEventListener('message', () => { clientMessageListener(3) }) }), service.addEventListener('connection', ({ client }) => { client.addEventListener('message', () => { clientMessageListener(4) }) process.nextTick(() => { client.close() }) }), ) const ws = new WebSocket('ws://localhost') ws.onopen = () => ws.send('hello world') await vi.waitFor(() => { expect(ws.readyState).toBe(WebSocket.CLOSED) }) expect(clientMessageListener).toHaveBeenNthCalledWith(1, 1) expect(clientMessageListener).toHaveBeenCalledOnce() }) it('stops propagation for server "open" event', async () => { const serverOpenListener = vi.fn<(input: number) => void>() originalServer.addListener('connection', () => {}) server.use( service.addEventListener('connection', ({ client, server }) => { server.connect() server.addEventListener('open', (event) => { // Calling `stopPropagation` will prevent this event from being // dispatched on the `server` belonging to a different event handler. event.stopPropagation() serverOpenListener(1) process.nextTick(() => client.close()) }) server.addEventListener('open', () => { serverOpenListener(2) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('open', () => { serverOpenListener(3) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('open', () => { serverOpenListener(4) }) }), ) const ws = new WebSocket(originalServer.url) await vi.waitFor(() => { expect(ws.readyState).toBe(WebSocket.CLOSED) }) expect(serverOpenListener).toHaveBeenNthCalledWith(1, 1) expect(serverOpenListener).toHaveBeenNthCalledWith(2, 2) expect(serverOpenListener).toHaveBeenCalledTimes(2) }) it('stops immediate propagation for server "open" event', async () => { const serverOpenListener = vi.fn<(input: number) => void>() originalServer.addListener('connection', () => {}) server.use( service.addEventListener('connection', ({ client, server }) => { server.connect() server.addEventListener('open', (event) => { event.stopImmediatePropagation() serverOpenListener(1) process.nextTick(() => client.close()) }) server.addEventListener('open', () => { serverOpenListener(2) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('open', () => { serverOpenListener(3) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('open', () => { serverOpenListener(4) }) }), ) const ws = new WebSocket(originalServer.url) await vi.waitFor(() => { expect(ws.readyState).toBe(WebSocket.CLOSED) }) expect(serverOpenListener).toHaveBeenNthCalledWith(1, 1) expect(serverOpenListener).toHaveBeenCalledOnce() }) it('stops propagation for server "message" event', async () => { const serverMessageListener = vi.fn<(input: number) => void>() originalServer.addListener('connection', (ws) => { // Send data from the original server to trigger the "message" event. ws.send('hello') }) server.use( service.addEventListener('connection', ({ client, server }) => { server.connect() server.addEventListener('message', (event) => { // Calling `stopPropagation` will prevent this event from being // dispatched on the `server` belonging to a different event handler. event.stopPropagation() serverMessageListener(1) process.nextTick(() => client.close()) }) server.addEventListener('message', () => { serverMessageListener(2) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('message', () => { serverMessageListener(3) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('message', () => { serverMessageListener(4) }) }), ) const ws = new WebSocket(originalServer.url) await vi.waitFor(() => { expect(ws.readyState).toBe(WebSocket.CLOSED) }) expect(serverMessageListener).toHaveBeenNthCalledWith(1, 1) expect(serverMessageListener).toHaveBeenNthCalledWith(2, 2) expect(serverMessageListener).toHaveBeenCalledTimes(2) }) it('stops immediate propagation for server "message" event', async () => { const serverMessageListener = vi.fn<(input: number) => void>() originalServer.addListener('connection', (ws) => { // Send data from the original server to trigger the "message" event. ws.send('hello') }) server.use( service.addEventListener('connection', ({ client, server }) => { server.connect() server.addEventListener('message', (event) => { event.stopImmediatePropagation() serverMessageListener(1) process.nextTick(() => client.close()) }) server.addEventListener('message', () => { serverMessageListener(2) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('message', () => { serverMessageListener(3) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('message', () => { serverMessageListener(4) }) }), ) const ws = new WebSocket(originalServer.url) await vi.waitFor(() => { expect(ws.readyState).toBe(WebSocket.CLOSED) }) expect(serverMessageListener).toHaveBeenNthCalledWith(1, 1) expect(serverMessageListener).toHaveBeenCalledOnce() }) it('stops propagation for server "error" event', async () => { const serverErrorListener = vi.fn<(input: number) => void>() server.use( service.addEventListener('connection', ({ client, server }) => { server.connect() server.addEventListener('error', (event) => { event.stopPropagation() serverErrorListener(1) }) server.addEventListener('error', () => { serverErrorListener(2) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('error', () => { serverErrorListener(3) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('error', () => { serverErrorListener(4) }) }), ) const ws = new WebSocket('ws://localhost/non-existing-path') await vi.waitFor(() => { /** * @note Ideally, await the "CLOSED" ready state, * but Node.js doesn't dispatch it correctly. * @see https://github.com/nodejs/undici/issues/3697 */ return new Promise((resolve) => { ws.onerror = () => resolve() }) }) expect(serverErrorListener).toHaveBeenNthCalledWith(1, 1) expect(serverErrorListener).toHaveBeenNthCalledWith(2, 2) expect(serverErrorListener).toHaveBeenCalledTimes(2) }) it('stops immediate propagation for server "error" event', async () => { const serverErrorListener = vi.fn<(input: number) => void>() server.use( service.addEventListener('connection', ({ client, server }) => { server.connect() server.addEventListener('error', (event) => { event.stopImmediatePropagation() serverErrorListener(1) }) server.addEventListener('error', () => { serverErrorListener(2) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('error', () => { serverErrorListener(3) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('error', () => { serverErrorListener(4) }) }), ) const ws = new WebSocket('ws://localhost/non-existing-path') await vi.waitFor(() => { /** * @note Ideally, await the "CLOSED" ready state, but Node.js doesn't dispatch it correctly. * @see https://github.com/nodejs/undici/issues/3697 */ return new Promise((resolve) => { ws.onerror = () => resolve() }) }) expect(serverErrorListener).toHaveBeenNthCalledWith(1, 1) expect(serverErrorListener).toHaveBeenCalledOnce() }) it('stops propagation for server "close" event', async () => { const serverCloseListener = vi.fn<(input: number) => void>() originalServer.addListener('connection', (ws) => { ws.close() }) server.use( service.addEventListener('connection', ({ client, server }) => { server.connect() server.addEventListener('close', (event) => { event.stopPropagation() serverCloseListener(1) process.nextTick(() => client.close()) }) server.addEventListener('close', () => { serverCloseListener(2) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('close', () => { serverCloseListener(3) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('close', () => { serverCloseListener(4) }) }), ) const ws = new WebSocket(originalServer.url) await vi.waitFor(() => { expect(ws.readyState).toBe(WebSocket.CLOSED) }) expect(serverCloseListener).toHaveBeenNthCalledWith(1, 1) expect(serverCloseListener).toHaveBeenNthCalledWith(2, 2) expect(serverCloseListener).toHaveBeenCalledTimes(2) }) it('stops immediate propagation for server "close" event', async () => { const serverCloseListener = vi.fn<(input: number) => void>() originalServer.addListener('connection', (ws) => { ws.close() }) server.use( service.addEventListener('connection', ({ client, server }) => { server.connect() server.addEventListener('close', (event) => { event.stopImmediatePropagation() serverCloseListener(1) process.nextTick(() => client.close()) }) server.addEventListener('close', () => { serverCloseListener(2) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('close', () => { serverCloseListener(3) }) }), service.addEventListener('connection', ({ server }) => { server.addEventListener('close', () => { serverCloseListener(4) }) }), ) const ws = new WebSocket(originalServer.url) await vi.waitFor(() => { expect(ws.readyState).toBe(WebSocket.CLOSED) }) expect(serverCloseListener).toHaveBeenNthCalledWith(1, 1) expect(serverCloseListener).toHaveBeenCalledOnce() }) ================================================ FILE: test/node/ws-api/ws.unhandled-exception.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' const server = setupServer() const service = ws.link('ws://*') beforeAll(() => { server.listen() vi.spyOn(console, 'error').mockImplementation(() => void 0) }) afterEach(() => { server.resetHandlers() vi.clearAllMocks() }) afterAll(() => { server.close() vi.restoreAllMocks() }) it('dispatches a WebSocket error event on handler exception', async () => { server.use( service.addEventListener('connection', () => { throw new Error('Handler exception') }), ) const socket = new WebSocket('ws://localhost:3000') const closeListener = vi.fn() const errorListener = vi.fn() socket.onclose = closeListener socket.onerror = errorListener socket.onopen = () => { expect.fail('Must not open the WebSocket connection') } await expect.poll(() => errorListener).toHaveBeenCalledOnce() expect(closeListener).toHaveBeenCalledOnce() expect(closeListener).toHaveBeenCalledWith( expect.objectContaining({ code: 1011, reason: 'Handler exception', }), ) expect(socket.readyState).toBe(WebSocket.CLOSED) expect(console.error, 'Prints the error for the user').toHaveBeenCalledWith( new Error('Handler exception'), ) }) ================================================ FILE: test/node/ws-api/ws.use.test.ts ================================================ // @vitest-environment node-websocket import { ws } from 'msw' import { setupServer } from 'msw/node' const service = ws.link('wss://*') const server = setupServer( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { client.send('hello, client!') } if (event.data === 'fallthrough') { client.send('ok') } }) }), ) beforeAll(() => { server.listen() }) afterAll(() => { server.close() }) it.concurrent( 'resolves outgoing events using initial handlers', server.boundary(async () => { const messageListener = vi.fn() const ws = new WebSocket('wss://example.com') ws.onmessage = (event) => messageListener(event.data) ws.onopen = () => ws.send('hello') await vi.waitFor(() => { expect(messageListener).toHaveBeenCalledWith('hello, client!') expect(messageListener).toHaveBeenCalledTimes(1) }) }), ) it.concurrent( 'overrides an outgoing event listener', server.boundary(async () => { server.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { // Stopping immediate event propagation will prevent // the same message listener in the initial handler // from being called. event.stopImmediatePropagation() client.send('howdy, client!') } }) }), ) const messageListener = vi.fn() const ws = new WebSocket('wss://example.com') ws.onmessage = (event) => messageListener(event.data) ws.onopen = () => ws.send('hello') await vi.waitFor(() => { expect(messageListener).toHaveBeenCalledWith('howdy, client!') expect(messageListener).toHaveBeenCalledTimes(1) }) }), ) it.concurrent( 'combines initial and override listeners', server.boundary(async () => { server.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { // Not stopping the event propagation will result in both // the override handler and the runtime handler sending // data to the client in order. The override handler is // prepended, so it will send data first. client.send('override data') } }) }), ) const messageListener = vi.fn() const ws = new WebSocket('wss://example.com') ws.onmessage = (event) => messageListener(event.data) ws.onopen = () => ws.send('hello') await vi.waitFor(() => { // The runtime handler is executed first, so it sends its message first. expect(messageListener).toHaveBeenNthCalledWith(1, 'override data') // The initial handler will send its message next. expect(messageListener).toHaveBeenNthCalledWith(2, 'hello, client!') expect(messageListener).toHaveBeenCalledTimes(2) }) }), ) it.concurrent( 'combines initial and override listeners in the opposite order', async () => { server.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { // Queuing the send to the next tick will ensure // that the initial handler sends data first, // and this override handler sends data next. queueMicrotask(() => { client.send('override data') }) } }) }), ) const messageListener = vi.fn() const ws = new WebSocket('wss://example.com') ws.onmessage = (event) => messageListener(event.data) ws.onopen = () => ws.send('hello') await vi.waitFor(() => { expect(messageListener).toHaveBeenNthCalledWith(1, 'hello, client!') expect(messageListener).toHaveBeenNthCalledWith(2, 'override data') expect(messageListener).toHaveBeenCalledTimes(2) }) }, ) it.concurrent( 'does not affect unrelated events', server.boundary(async () => { server.use( service.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { // Stopping immediate event propagation will prevent // the same message listener in the initial handler // from being called. event.stopImmediatePropagation() client.send('howdy, client!') } }) }), ) const messageListener = vi.fn() const ws = new WebSocket('wss://example.com') ws.onmessage = (event) => { messageListener(event.data) if (event.data === 'howdy, client!') { ws.send('fallthrough') } } ws.onopen = () => ws.send('hello') await vi.waitFor(() => { expect(messageListener).toHaveBeenNthCalledWith(1, 'howdy, client!') }) await vi.waitFor(() => { // The initial handler still sends data to unrelated events. expect(messageListener).toHaveBeenNthCalledWith(2, 'ok') expect(messageListener).toHaveBeenCalledTimes(2) }) }), ) ================================================ FILE: test/package.json ================================================ { "type": "module" } ================================================ FILE: test/support/WebSocketServer.ts ================================================ import { invariant } from 'outvariant' import { Emitter } from 'strict-event-emitter' import fastify, { FastifyInstance } from 'fastify' import fastifyWebSocket, { type WebSocket as FastifySocket, } from '@fastify/websocket' type WebSocketEventMap = { connection: [client: FastifySocket] } export class WebSocketServer extends Emitter { private _url?: string private app: FastifyInstance private clients: Set constructor() { super() this.clients = new Set() this.app = fastify() this.app.register(fastifyWebSocket) this.app.register(async (fastify) => { fastify.get('/', { websocket: true }, (socket) => { this.clients.add(socket) socket.once('close', () => this.clients.delete(socket)) this.emit('connection', socket) }) }) } get url(): string { invariant( this._url, 'Failed to get "url" on WebSocketServer: server is not running. Did you forget to "await server.listen()"?', ) return this._url } public async listen(port = 0): Promise { const address = await this.app.listen({ host: '127.0.0.1', port, }) const url = new URL(address) url.protocol = url.protocol.replace(/^http/, 'ws') this._url = url.href } public resetState(): void { this.closeAllClients() this.removeAllListeners() } public closeAllClients(): void { this.clients.forEach((client) => { client.close() }) } public async close(): Promise { return this.app.close() } } ================================================ FILE: test/support/alias.ts ================================================ import url from 'node:url' import path from 'node:path' const ROOT_URL = new URL('../../', import.meta.url) export function fromRoot(...paths: Array): string { return url.fileURLToPath(new URL(path.join(...paths), ROOT_URL)) } export const mswExports = { 'msw/node': fromRoot('./lib/node/index.mjs'), 'msw/native': fromRoot('./lib/native/index.mjs'), 'msw/browser': fromRoot('./lib/browser/index.mjs'), msw: fromRoot('./lib/core/index.mjs'), } export const customViteEnvironments = { 'vitest-environment-node-websocket': fromRoot( './test/support/environments/vitest-environment-node-websocket', ), } ================================================ FILE: test/support/environments/vitest-environment-node-websocket.ts ================================================ /** * Node.js environment superset that has a global WebSocket API. */ import type { Environment } from 'vitest' import { builtinEnvironments } from 'vitest/environments' import { WebSocket } from 'undici' export default { name: 'node-with-websocket', transformMode: 'ssr', async setup(global, options) { /** * @note It's crucial this extend the Node.js environment. * JSDOM polyfills the global "Event", making it unusable * with Node's "EventTarget". */ const { teardown } = await builtinEnvironments.node.setup(global, options) Reflect.set(globalThis, 'WebSocket', WebSocket) return { teardown, } }, } ================================================ FILE: test/support/graphql.ts ================================================ import { parse, type ExecutionResult } from 'graphql' import { DocumentTypeDecoration, TypedDocumentNode, } from '@graphql-typed-document-node/core' /** * Identity function that returns a given template string array. * Provides GraphQL syntax highlighting without any extra transformations. */ export const gql = (str: TemplateStringsArray) => { return str.join('') } interface GraphQLClientOPtions { uri: string fetch?: (input: any, init?: any) => Promise } interface GraphQLOperationInput { query: TemplateStringsArray | string variables?: Record headers?: Record } /** * Create a new GraphQL client. Uses `fetch` to dispatch a * specification-compliant GraphQL request. */ export function createGraphQLClient(options: GraphQLClientOPtions) { return async >( input: GraphQLOperationInput, ): Promise & { response: Response }> => { const response = await fetch(options.uri, { method: 'POST', headers: { accept: '*/*', 'content-type': 'application/json', ...(input.headers || {}), }, body: JSON.stringify(input), }) // No need to transform the JSON into `ExecutionResult`, // because that's the responsibility of an actual server // or an MSW request handler. const { data, errors, extensions } = await response.json() return { data, errors, extensions, response, } } } export class TypedDocumentString extends String implements DocumentTypeDecoration { __apiType?: DocumentTypeDecoration['__apiType'] constructor( private value: string, public __meta__?: { hash: string }, ) { super(value) } toString(): string & DocumentTypeDecoration { return this.value } } export function createTypedDocumentString( source: string, ) { return new TypedDocumentString(source) } export function createTypedDocumentNode( source: string, ): TypedDocumentNode { const doc = typeof source === 'string' ? parse(source) : source return doc as TypedDocumentNode } ================================================ FILE: test/support/msw-esm/package.json ================================================ { "type": "module", "exports": { ".": { "module-sync": { "types": "./lib/core/index.d.mts", "default": "./lib/core/index.mjs" }, "module": { "types": "./lib/core/index.d.mts", "default": "./lib/core/index.mjs" }, "react-native": { "import": { "types": "./lib/core/index.d.mts", "default": "./lib/core/index.mjs" }, "default": { "types": "./lib/core/index.d.ts", "default": "./lib/core/index.js" } }, "import": { "types": "./lib/core/index.d.mts", "default": "./lib/core/index.mjs" }, "default": { "types": "./lib/core/index.d.ts", "default": "./lib/core/index.js" } }, "./browser": { "module-sync": { "types": "./lib/browser/index.d.mts", "default": "./lib/browser/index.mjs" }, "module": { "types": "./lib/browser/index.d.mts", "default": "./lib/browser/index.mjs" }, "browser": { "types": "./lib/browser/index.d.mts", "default": "./lib/browser/index.mjs" }, "import": { "types": "./lib/browser/index.d.mts", "default": "./lib/browser/index.mjs" }, "node": null, "react-native": null, "default": { "types": "./lib/browser/index.d.ts", "default": "./lib/browser/index.js" } }, "./node": { "module-sync": { "types": "./lib/node/index.d.mts", "default": "./lib/node/index.mjs" }, "module": { "types": "./lib/node/index.d.mts", "default": "./lib/node/index.mjs" }, "node": { "require": "./lib/node/index.js", "import": "./lib/node/index.mjs" }, "import": { "types": "./lib/node/index.d.mts", "default": "./lib/node/index.mjs" }, "browser": null, "react-native": null, "default": { "types": "./lib/node/index.d.ts", "default": "./lib/node/index.js" } }, "./native": { "react-native": { "import": { "types": "./lib/native/index.d.mts", "default": "./lib/native/index.mjs" }, "default": { "types": "./lib/native/index.d.ts", "default": "./lib/native/index.js" } }, "browser": null, "import": { "types": "./lib/native/index.d.mts", "default": "./lib/native/index.mjs" }, "default": { "types": "./lib/native/index.d.ts", "default": "./lib/native/index.js" } }, "./core/http": { "module-sync": { "types": "./lib/core/http.d.mts", "default": "./lib/core/http.mjs" }, "module": { "types": "./lib/core/http.d.mts", "default": "./lib/core/http.mjs" }, "import": { "types": "./lib/core/http.d.mts", "default": "./lib/core/http.mjs" }, "default": { "types": "./lib/core/http.d.ts", "default": "./lib/core/http.js" } }, "./core/graphql": { "module-sync": { "types": "./lib/core/graphql.d.mts", "default": "./lib/core/graphql.mjs" }, "module": { "types": "./lib/core/graphql.d.mts", "default": "./lib/core/graphql.mjs" }, "import": { "types": "./lib/core/graphql.d.mts", "default": "./lib/core/graphql.mjs" }, "default": { "types": "./lib/core/graphql.d.ts", "default": "./lib/core/graphql.js" } }, "./core/ws": { "module-sync": { "types": "./lib/core/ws.d.mts", "default": "./lib/core/ws.mjs" }, "module": { "types": "./lib/core/ws.d.mts", "default": "./lib/core/ws.mjs" }, "import": { "types": "./lib/core/ws.d.mts", "default": "./lib/core/ws.mjs" }, "default": { "types": "./lib/core/ws.d.ts", "default": "./lib/core/ws.js" } }, "./package.json": "./package.json" } } ================================================ FILE: test/support/utils.ts ================================================ import url from 'node:url' import path from 'node:path' import { ClientRequest, IncomingMessage } from 'http' export function sleep(duration: number) { return new Promise((resolve) => { setTimeout(resolve, duration) }) } export function fromTemp(...segments: string[]) { return url.fileURLToPath( new URL(path.join('../..', 'tmp', ...segments), import.meta.url), ) } export async function waitForClientRequest(request: ClientRequest): Promise<{ response: IncomingMessage responseText: string }> { return new Promise((resolve, reject) => { request.once('error', (error) => { /** * @note Since Node.js v20, Node.js may throw an AggregateError * that doesn't have the `message` property and thus won't be handled * here correctly. Instead, use the error's `code` as the rejection reason. * The code stays consistent across Node.js versions. */ reject('code' in error ? error.code : error) }) request.once('abort', () => reject(new Error('Request was aborted'))) request.on('response', (response) => { response.once('error', reject) const responseChunks: Array = [] response.on('data', (chunk) => { responseChunks.push(Buffer.from(chunk)) }) response.once('end', () => { resolve({ response, responseText: Buffer.concat(responseChunks).toString('utf8'), }) }) }) }) } ================================================ FILE: test/support/waitFor.ts ================================================ import { sleep } from './utils' const RETRY_INTERVAL = 500 const MAX_RETRIES = 5 export async function waitFor(fn: () => unknown): Promise { for (let retryCount = 1; retryCount <= MAX_RETRIES; retryCount++) { try { await fn() return } catch (error) { if (retryCount === MAX_RETRIES) { throw error } await sleep(RETRY_INTERVAL) } } } ================================================ FILE: test/tsconfig.json ================================================ { "compilerOptions": { "strict": true, "skipLibCheck": true, "module": "esnext", "target": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, // Support default imports for modules that have no default exports. // This way "http" imports stay "import http from 'http'". // Using wildcard there breaks request interception since it // encapsulates the values under the ".default" key. "allowSyntheticDefaultImports": true, "esModuleInterop": true, "declaration": false, "noEmit": true, "types": ["node", "vitest/globals"] }, "include": ["**/*.ts"] } ================================================ FILE: test/typings/custom-handler.test-d.ts ================================================ import { it } from 'vitest' import { http, HttpRequestHandler, GraphQLRequestHandler, graphql } from 'msw' import { setupWorker } from 'msw/browser' import { setupServer } from 'msw/node' const generateHttpHandler: HttpRequestHandler = (path, resolver, options) => { return http.get(path, resolver, options) } const generateGraphQLHandler: GraphQLRequestHandler = ( operationName, resolver, ) => { return graphql.query(operationName, resolver) } it('accepts custom request handler (setupWorker)', () => { setupWorker( generateHttpHandler('/', () => {}), generateGraphQLHandler('GetResource', () => {}), ) }) it('accepts custom request handler (setupServer)', () => { setupServer( generateHttpHandler('/', () => {}), generateGraphQLHandler('GetResource', () => {}), ) }) ================================================ FILE: test/typings/custom-resolver.test-d.ts ================================================ import { it, expectTypeOf } from 'vitest' import { http, HttpResponseResolver, delay, PathParams, DefaultBodyType, HttpResponse, graphql, GraphQLQuery, GraphQLVariables, GraphQLResponseResolver, } from 'msw' it('custom http resolver has correct parameters type', () => { /** * A higher-order resolver that injects a fixed * delay before calling the provided resolver. */ function withDelay< // Recreate the generic signature of the default resolver // so the arguments passed to "http.get" propagate here. Params extends PathParams, RequestBodyType extends DefaultBodyType, ResponseBodyType extends DefaultBodyType, >( delayMs: number, resolver: HttpResponseResolver, ): HttpResponseResolver { return async (...args) => { await delay(delayMs) return resolver(...args) } } http.get<{ id: string }, never, 'hello'>( '/user/:id', withDelay(250, ({ params }) => { expectTypeOf(params).toEqualTypeOf<{ id: string }>() return HttpResponse.text( // @ts-expect-error Response body doesn't match the response type. 'non-matching', ) }), ) }) function identityGraphQLResolver< Query extends GraphQLQuery, Variables extends GraphQLVariables, >( resolver: GraphQLResponseResolver, ): GraphQLResponseResolver { return async (...args) => { return resolver(...args) } } it('custom graphql resolver has correct variables and response type', () => { graphql.query<{ number: number }, { id: string }>( 'GetUser', identityGraphQLResolver(({ variables }) => { expectTypeOf(variables).toEqualTypeOf<{ id: string }>() return HttpResponse.json({ data: { number: 1, }, }) }), ) }) it('custom graphql resolver does not accept unknown variables', () => { graphql.query<{ number: number }, { id: string }>( 'GetUser', identityGraphQLResolver(({ variables }) => { expectTypeOf(variables).toEqualTypeOf<{ id: string }>() return HttpResponse.json({ data: { // @ts-expect-error Incompatible response query type. user: { id: variables.id, }, }, }) }), ) }) ================================================ FILE: test/typings/graphql-custom-predicate.test-d.ts ================================================ import { graphql, type GraphQLOperationType, type GraphQLVariables, type GraphQLResponseResolver, } from 'msw' const resolver: GraphQLResponseResolver = () => void 0 it('supports custom predicate', () => { graphql.query<{ user: null }, { a: string }>( ({ request, cookies, operationType, operationName, query, variables }) => { expectTypeOf(request).toEqualTypeOf() expectTypeOf(cookies).toEqualTypeOf>() expectTypeOf(operationType).toEqualTypeOf expectTypeOf(operationName).toEqualTypeOf() /** * @note Both query and variables do not infer the narrow type from the handler * because this is the matching phase and values might be arbitrary. */ expectTypeOf(query).toEqualTypeOf() expectTypeOf(variables).toEqualTypeOf() return operationName === 'MyQuery' }, resolver, ) graphql.query(() => true, resolver) graphql.query(() => false, resolver) graphql.query( // @ts-expect-error Invalid return type. () => {}, resolver, ) graphql.query( // @ts-expect-error Invalid return type. () => ({}), resolver, ) graphql.query( // @ts-expect-error Invalid return type. () => undefined, resolver, ) graphql.query( // @ts-expect-error Invalid return type. () => null, resolver, ) }) it('supports returning extended match result from a custom predicate', () => { graphql.query(() => ({ matches: true }), resolver) graphql.query(() => ({ matches: false }), resolver) graphql.query( // @ts-expect-error Invalid return type. () => ({ matches: 2 }), resolver, ) graphql.query( // @ts-expect-error Invalid return type. () => ({ matches: undefined }), resolver, ) graphql.query( // @ts-expect-error Invalid return type. () => ({ matches: null }), resolver, ) }) ================================================ FILE: test/typings/graphql-typed-document-node.test-d.ts ================================================ import { graphql, HttpResponse } from 'msw' import { TypedDocumentNode } from '@graphql-typed-document-node/core' const GetUserQuery = {} as TypedDocumentNode< { user: { name: 'John' } }, { userId: string } > it('infers the result type', () => { graphql.query(GetUserQuery, () => { if (Math.random()) { return HttpResponse.json({ data: { user: { // @ts-expect-error Invalid result type. name: 123, }, }, }) } return HttpResponse.json({ data: { user: { name: 'John' }, }, }) }) }) it('infers the query variables type', () => { graphql.query(GetUserQuery, ({ query, variables }) => { expectTypeOf(query).toBeString() expectTypeOf(variables).toEqualTypeOf<{ userId: string }>() return HttpResponse.json({ data: { user: { name: 'John' }, }, }) }) }) ================================================ FILE: test/typings/graphql-typed-document-string.test-d.ts ================================================ import { graphql, HttpResponse } from 'msw' import { DocumentTypeDecoration } from '@graphql-typed-document-node/core' declare function createTypedDocumentString( query: string, ): DocumentTypeDecoration it('infers the result type', () => { graphql.query( createTypedDocumentString<{ user: { id: string; name: string } }>(''), () => { if (Math.random()) { return HttpResponse.json({ data: { user: { // @ts-expect-error Invalid result type. id: 123, name: 'John Doe', }, }, }) } return HttpResponse.json({ data: { user: { id: '1', name: 'John Doe' } }, }) }, ) }) it('infers the variables type', () => { graphql.query( createTypedDocumentString(''), ({ variables }) => { expectTypeOf(variables).toEqualTypeOf<{ id: string }>() }, ) }) ================================================ FILE: test/typings/graphql.test-d.ts ================================================ import { it, expectTypeOf } from 'vitest' import { parse } from 'graphql' import { graphql, HttpResponse, passthrough } from 'msw' it('graphql mutation can be used without variables generic type', () => { graphql.mutation('GetUser', () => { return HttpResponse.json({ data: { id: '2' } }) }) }) it('graphql mutation accepts inline generic variables type', () => { graphql.mutation('GetUser', ({ variables }) => { expectTypeOf(variables).toEqualTypeOf<{ id: string }>() }) }) it('graphql mutation accepts inline generic variables never type', () => { graphql.mutation('CreateUser', ({ variables }) => { expectTypeOf(variables).toEqualTypeOf() }) }) it("graphql mutation does not accept null as variables' generic mutation type", () => { graphql.mutation< { key: string }, // @ts-expect-error `null` is not a valid variables type. null >('', () => {}) }) it('graphql mutation allows explicit null as the response body type for the mutation', () => { graphql.mutation<{ key: string }>('MutateData', () => { return HttpResponse.json({ // Explicit null in mutations must also be allowed. data: null, }) }) }) it('graphql mutation does not allow mismatched mutation response', () => { graphql.mutation<{ key: string }>('MutateData', () => { return HttpResponse.json({ // @ts-expect-error Response data doesn't match the query type. data: {}, }) }) }) it("graphql query does not accept null as variables' generic query type ", () => { graphql.query< { key: string }, // @ts-expect-error `null` is not a valid variables type. null >('', () => {}) }) it("graphql query accepts the correct type for the variables' generic query type", () => { /** * Response body type (GraphQL query type). */ // Returned mocked response body must satisfy the // GraphQL query generic. graphql.query<{ id: string }>('GetUser', () => { return HttpResponse.json({ data: { id: '2' }, }) }) }) it('graphql query allows explicit null as the response body type for the query', () => { graphql.query<{ id: string }>('GetUser', () => { return HttpResponse.json({ // Explicit null must be allowed. data: null, }) }) }) it('supports nullable queries', () => { graphql.query<{ id: string } | null>('GetUser', () => { return HttpResponse.json({ data: null, }) }) }) it('supports nullable mutations', () => { graphql.mutation<{ id: string } | null>('GetUser', () => { return HttpResponse.json({ data: null, }) }) }) it('graphql query does not accept invalid data type for the response body type for the query', () => { graphql.query<{ id: string }>('GetUser', () => { return HttpResponse.json({ data: { // @ts-expect-error "id" type is incorrect id: 123, }, }) }) }) it('graphql query does not allow empty response when the query type is defined', () => { graphql.query<{ id: string }>( 'GetUser', // @ts-expect-error response json is empty () => HttpResponse.json({ data: {} }), ) }) it('graphql query does not allow incompatible response body type', () => { graphql.query<{ id: string }>( 'GetUser', // @ts-expect-error incompatible response body type () => HttpResponse.text('hello'), ) }) it('graphql operation does not accept null as variables type', () => { graphql.operation< { key: string }, // @ts-expect-error `null` is not a valid variables type. null >(() => { return HttpResponse.json({ data: { key: 'a' } }) }) }) it('graphql operation does not allow mismatched operation response', () => { graphql.operation<{ key: string }>(() => { return HttpResponse.json({ // @ts-expect-error Response data doesn't match the query type. data: {}, }) }) }) it('graphql operation allows explicit null as the response body type for the operation', () => { graphql.operation<{ key: string }>(() => { return HttpResponse.json({ data: null }) }) }) it('graphql handlers allow passthrough responses', () => { // Passthrough responses. graphql.query('GetUser', () => passthrough()) graphql.mutation('AddPost', () => passthrough()) graphql.operation(() => passthrough()) graphql.query('GetUser', ({ request }) => { if (request.headers.has('cookie')) { return passthrough() } return HttpResponse.json({ data: {} }) }) }) it('supports Response.error()', () => { graphql.query<{ id: string }>('GetUser', () => HttpResponse.error()) graphql.mutation('UpdatePost', () => HttpResponse.error()) graphql.operation(() => HttpResponse.error()) graphql.query('GetUser', async () => HttpResponse.error()) graphql.query('GetUser', function* () { return HttpResponse.error() }) graphql.query('GetUser', () => Response.error()) graphql.query('GetUser', async () => Response.error()) graphql.query('GetUser', function* () { return Response.error() }) }) it("graphql variables cannot extract type from the runtime 'DocumentNode'", () => { /** * Supports `DocumentNode` as the GraphQL operation name. */ const getUser = parse(` query GetUser { user { firstName } } `) graphql.query(getUser, () => { return HttpResponse.json({ // Cannot extract query type from the runtime `DocumentNode`. data: { arbitrary: true }, }) }) }) it('graphql query cannot extract variable and response types', () => { const getUserById = parse(` query GetUserById($userId: String!) { user(id: $userId) { firstName } } `) graphql.query(getUserById, ({ variables }) => { // Cannot extract variables type from a DocumentNode. expectTypeOf(variables).toEqualTypeOf>() return HttpResponse.json({ data: { user: { firstName: 'John', // Extracting a query body type from the "DocumentNode" is impossible. lastName: 'Maverick', }, }, }) }) }) it('graphql mutation cannot extract variable and response types', () => { const createUser = parse(` mutation CreateUser { user { id } } `) graphql.mutation(createUser, () => { return HttpResponse.json({ data: { arbitrary: true }, }) }) }) it('graphql query allows extensions in the response body', () => { graphql.query<{ id: string }>('GetUser', () => { return HttpResponse.json({ data: { id: '2' }, extensions: { requestId: '3', runtime: 'foo', }, }) }) }) ================================================ FILE: test/typings/http-custom-predicate.test-d.ts ================================================ import { http, HttpResponseResolver } from 'msw' const resolver: HttpResponseResolver = () => void 0 it('supports custom predicate', () => { http.get(({ request, cookies }) => { expectTypeOf(request).toEqualTypeOf() expectTypeOf(cookies).toEqualTypeOf>() return request.url.includes('user') }, resolver) http.get(() => true, resolver) http.get(() => false, resolver) // @ts-expect-error Invalid return type. http.get(() => {}, resolver) // @ts-expect-error Invalid return type. http.get(() => ({}), resolver) // @ts-expect-error Invalid return type. http.get(() => undefined, resolver) // @ts-expect-error Invalid return type. http.get(() => null, resolver) }) it('supports returning path parameters from the custom predicate', () => { // Implicit path parameters type. http.get( () => ({ matches: true, params: { user: 'hello' }, }), ({ params }) => { expectTypeOf(params).toEqualTypeOf<{ user: string }>() }, ) // Explicit path parameters type. http.get<{ inferred: string }>( () => ({ matches: true, params: { inferred: '1' }, }), ({ params }) => { expectTypeOf(params).toEqualTypeOf<{ inferred: string }>() }, ) }) it('supports returning extended match result from a custom predicate', () => { http.get(() => ({ matches: true, params: {} }), resolver) http.get(() => ({ matches: false, params: {} }), resolver) // @ts-expect-error Invalid return type. http.get(() => ({ matches: true }), resolver) // @ts-expect-error Invalid return type. http.get(() => ({ params: {} }), resolver) }) ================================================ FILE: test/typings/http.test-d.ts ================================================ import { it, expectTypeOf } from 'vitest' import { http, HttpResponse, passthrough } from 'msw' it('supports a single path parameter', () => { http.get<{ id: string }>('/user/:id', ({ params }) => { expectTypeOf(params).toEqualTypeOf<{ id: string }>() }) }) it('supports a repeating path parameter', () => { http.get<{ id?: string }>('/user/id*', ({ params }) => { expectTypeOf(params).toEqualTypeOf<{ id?: string }>() }) }) it('supports an optional path parameter', () => { http.get<{ id?: string }>('/user/:id?', ({ params }) => { expectTypeOf(params).toEqualTypeOf<{ id?: string }>() }) }) it('supports optional repeating path parameter', () => { /** * @note This is the newest "path-to-regexp" syntax. * MSW doesn't support this quite yet. */ http.get<{ path?: string[] }>('/user{/*path}', ({ params }) => { expectTypeOf(params).toEqualTypeOf<{ path?: string[] }>() }) }) it('supports multiple path parameters', () => { type Params = { a: string; b: string[] } http.get('/user/:a/:b/:b', ({ params }) => { expectTypeOf(params).toEqualTypeOf() }) }) it('supports path parameters declared via type', () => { type Params = { id: string } http.get('/user/:id', ({ params }) => { expectTypeOf(params).toEqualTypeOf() }) }) it('supports path parameters declared via interface', () => { interface PostPathParameters { id: string } http.get('/user/:id', ({ params }) => { expectTypeOf(params).toEqualTypeOf() }) }) it('supports json as a request body type argument', () => { http.post('/user', async ({ request }) => { const data = await request.json() expectTypeOf(data).toEqualTypeOf<{ id: string }>() const text = await request.text() expectTypeOf(text).toEqualTypeOf() expectTypeOf(text).toEqualTypeOf() }) }) it('supports null as the request body type argument', () => { http.get('/user', async ({ request }) => { const data = await request.json() expectTypeOf(data).toEqualTypeOf() }) }) it('returns the same request type when cloning', () => { http.post('/user', async ({ request }) => { const data = await request.clone().json() expectTypeOf(data).toEqualTypeOf<{ id: string }>() }) http.post('/user', async ({ request }) => { const data = await request.clone().json() expectTypeOf(data).toEqualTypeOf() }) }) it('returns plain Response without explicit response body type argument', () => { http.get('/user', () => { return new Response('hello') }) }) it('supports a text response without explicit response body type argument', () => { http.get('/resource', () => { return HttpResponse.text('hello world') }) }) it('supports a json response without explicit response body type argument', () => { http.get('/resource', () => { return HttpResponse.json({ id: 1 }) }) }) it('supports an xml response without explicit response body type argument', () => { http.get('/resource', () => { return HttpResponse.xml('world') }) }) it('supports a form data response without explicit response body type argument', () => { http.get('/resource', () => { return HttpResponse.formData(new FormData()) }) }) it('supports a stream response without explicit response body type argument', () => { http.get('/resource', () => { return new HttpResponse(new ReadableStream()) }) }) it('returns HttpResponse with URLSearchParams as response body', () => { http.get('/', () => { return new HttpResponse(new URLSearchParams()) }) }) it('returns HttpResponse with FormData as response body', () => { http.get('/', () => { return new HttpResponse(new FormData()) }) }) it('returns HttpResponse with ReadableStream as response body', () => { http.get('/', () => { return new HttpResponse(new ReadableStream()) }) }) it('returns HttpResponse with Blob as response body', () => { http.get('/', () => { return new HttpResponse(new Blob(['hello'])) }) }) it('returns HttpResponse with ArrayBuffer as response body', () => { http.get('/', () => { return new HttpResponse(new ArrayBuffer(5)) }) }) it('supports HttpResponse.arrayBuffer shorthand method', () => { http.get('/', () => { return HttpResponse.arrayBuffer(new ArrayBuffer(5)) }) http.get('/', async () => { return HttpResponse.arrayBuffer( await fetch('/image').then((response) => response.arrayBuffer()), ) }) http.get('/', () => { return HttpResponse.arrayBuffer(new ArrayBuffer(5)) }) }) it('supports null as a response body type argument', () => { http.get('/', () => { return new HttpResponse() }) http.get('/', () => { return new HttpResponse( // @ts-expect-error Expected null, got a string. 'hello', ) }) http.get('/', () => { return HttpResponse.json( // @ts-expect-error Expected null, got an object. { id: 1 }, ) }) }) it('supports string as a response body type argument', () => { http.get('/', ({ request }) => { if (request.headers.has('x-foo')) { return HttpResponse.text('conditional') } return HttpResponse.text('hello') }) }) it('supports exact string as a response body type argument', () => { http.get('/', () => { return HttpResponse.text('hello') }) http.get('/', () => { // @ts-expect-error Non-matching response body type. return HttpResponse.text('unexpected') }) }) it('supports object as a response body type argument', () => { http.get('/user', () => { return HttpResponse.json({ id: 1 }) }) }) it('supports narrow object as a response body type argument', () => { http.get('/user', () => { return HttpResponse.json({ id: 123 }) }) http.get('/user', () => { return HttpResponse.json({ // @ts-expect-error Non-matching response body type. id: 456, }) }) }) it('supports object with extra keys as a response body type argument', () => { type ResponseBody = { [key: string]: number | string id: 123 } http.get('/user', () => { return HttpResponse.json({ id: 123, // Extra keys are allowed if they satisfy the index signature. name: 'John', }) }) http.get('/user', () => { return HttpResponse.json({ // @ts-expect-error Must be 123. id: 456, name: 'John', }) }) http.get('/user', () => { return HttpResponse.json({ id: 123, // @ts-expect-error Must satisfy the index signature. name: { a: 1 }, }) }) }) it('supports response body type argument declared via type', () => { type ResponseBodyType = { id: number } http.get('/user', () => { const data: ResponseBodyType = { id: 1 } return HttpResponse.json(data) }) }) it('supports response body type argument declared via interface', () => { interface ResponseBodyInterface { id: number } http.get('/user', () => { const data: ResponseBodyInterface = { id: 1 } return HttpResponse.json(data) }) }) it('throws when returning a json response not matching the response body type argument', () => { http.get( '/user', // @ts-expect-error String not assignable to number () => HttpResponse.json({ id: 'invalid' }), ) }) it('throws when returning an empty json response not matching the response body type argument', () => { http.get( '/user', // @ts-expect-error Missing property "id" () => HttpResponse.json({}), ) }) it('accepts narrower type for response body', () => { http.get('/user', () => HttpResponse.json(['value']), ) }) it('accepts more specific type for response body', () => { http.get('/user', () => HttpResponse.json({ label: true }), ) }) it("accepts passthrough in HttpResponse's body", () => { // Passthrough responses. http.all('/', () => passthrough()) http.get('/', () => passthrough()) http.get('/', ({ request }) => { if (request.headers.has('cookie')) { return passthrough() } return HttpResponse.json({ id: 1 }) }) }) it('infers a narrower json response type', () => { type ResponseBody = { a: number } http.get('/', () => { // @ts-expect-error Unknown property "b". return HttpResponse.json({ a: 1, b: 2 }) }) }) it('errors when returning non-Response data from resolver', () => { http.get( '/resource', // @ts-expect-error () => 123, ) http.get( '/resource', // @ts-expect-error () => 'foo', ) http.get( '/resource', // @ts-expect-error () => ({}), ) }) it('treats non-typed HttpResponse body type as matching', () => { http.get('/resource', () => { /** * @note When constructing a Response/HttpResponse instance, * its body type must effectively be treated as `any`. You * cannot provide or infer a narrower type because these classes * operate on streams or strings, none of which are type-safe. */ return new HttpResponse(null, { status: 500 }) }) }) it('supports returning Response.error()', () => { http.get('/resource', () => Response.error()) http.get('/resource', async () => Response.error()) http.get('/resource', function* () { return Response.error() }) http.get('/resource', () => HttpResponse.error()) http.get('/resource', async () => HttpResponse.error()) http.get('/resource', function* () { return HttpResponse.error() }) }) ================================================ FILE: test/typings/regressions/default-resolver-type.test-d.ts ================================================ /** * @see https://github.com/mswjs/msw/issues/2506 */ import { http, HttpResponse, HttpResponseResolver } from 'msw' it('supports a union of the matching explicit and implicit response resolvers', () => { function handle(resolver?: HttpResponseResolver) { const defaultResolver = () => HttpResponse.html('
test
') http.get('/path', resolver || defaultResolver) } }) ================================================ FILE: test/typings/regressions/request-handler-type.test-d.ts ================================================ /** * @see https://github.com/mswjs/msw/discussions/498 */ import { http, HttpResponse } from 'msw' import { setupWorker } from 'msw/browser' it('does not conflict when passing request handlers to `setupWorker`', () => { const resourceHandler = http.get('/resource', () => { return HttpResponse.json({ data: 'abc-123' }) }) const handlers = [resourceHandler] setupWorker(...handlers) }) ================================================ FILE: test/typings/regressions/response-body-type.test-d.ts ================================================ /** * @see https://github.com/mswjs/msw/issues/1823 */ import { http, Path, HttpResponse, DefaultBodyType } from 'msw' it('accepts custom response body generic argument', () => { function myHandler( path: Path, ) { return (response: CustomResponseBodyType) => { http.get(path, () => HttpResponse.json(response)) } } }) ================================================ FILE: test/typings/resolver-generator.test-d.ts ================================================ import { it } from 'vitest' import { http, HttpResponse } from 'msw' it('supports generator function as response resolver', () => { http.get('/', function* () { yield HttpResponse.json({ value: 1 }) yield HttpResponse.json({ value: 2 }) return HttpResponse.json({ value: 3 }) }) http.get('/', function* () { yield HttpResponse.json({ value: 'one' }) yield HttpResponse.json({ // @ts-expect-error Expected string, got number. value: 2, }) return HttpResponse.json({ value: 'three' }) }) }) it('supports async generator function as response resolver', () => { http.get('/', async function* () { yield HttpResponse.json({ value: 1 }) yield HttpResponse.json({ value: 2 }) return HttpResponse.json({ value: 3 }) }) http.get('/', async function* () { yield HttpResponse.json({ value: 'one' }) yield HttpResponse.json({ // @ts-expect-error Expected string, got number. value: 2, }) return HttpResponse.json({ value: 'three' }) }) }) it('supports returning nothing from generator resolvers', () => { http.get('/', function* () {}) http.get('/', async function* () {}) }) it('supports returning undefined from generator resolvers', () => { http.get('/', function* () { return undefined }) http.get('/', async function* () { return undefined }) }) ================================================ FILE: test/typings/server.boundary.test-d.ts ================================================ import { it, expectTypeOf } from 'vitest' import { setupServer } from 'msw/node' const fn = (args: { a: number }): string => 'hello' const server = setupServer() const bound = server.boundary(fn) it('infers the argument type of the callback', () => { expectTypeOf(bound).toEqualTypeOf<(args: { a: number }) => string>() }) it('infers the return type of the callback', () => { expectTypeOf(bound({ a: 1 })).toEqualTypeOf() }) ================================================ FILE: test/typings/setup-server.test-d.ts ================================================ import { http, HttpResponse, graphql } from 'msw' import { setupServer } from 'msw/node' it('does not produce a type error when called without arguments', () => { setupServer() }) it('accepts a single HTTP request handler', () => { setupServer( http.get('/user', () => { return HttpResponse.json({ name: 'John Doe' }) }), ) setupServer( http.get('/user', async () => { return HttpResponse.json({ name: 'John Doe' }) }), ) }) it('accepts a single GraphQL request handler', () => { setupServer( graphql.query('GetUser', () => { return HttpResponse.json({ data: { name: 'John Doe' } }) }), ) setupServer( graphql.query('GetUser', async () => { return HttpResponse.json({ data: { name: 'John Doe' } }) }), ) }) it('supports a list of request handlers defined elsewhere', () => { const handlers = [ http.get('/user', () => { return HttpResponse.json({ name: 'John Doe' }) }), ] setupServer(...handlers) }) ================================================ FILE: test/typings/setup-worker.test-d.ts ================================================ import { http, HttpResponse, graphql } from 'msw' import { setupWorker } from 'msw/browser' it('does not produce a type error when called without arguments', () => { setupWorker() }) it('accepts a single HTTP request handler', () => { setupWorker( http.get('/user', () => { return HttpResponse.json({ name: 'John Doe' }) }), ) setupWorker( http.get('/user', async () => { return HttpResponse.json({ name: 'John Doe' }) }), ) }) it('accepts a single GraphQL request handler', () => { setupWorker( graphql.query('GetUser', () => { return HttpResponse.json({ data: { name: 'John Doe' } }) }), ) setupWorker( graphql.query('GetUser', async () => { return HttpResponse.json({ data: { name: 'John Doe' } }) }), ) }) it('supports a list of request handlers defined elsewhere', () => { const handlers = [ http.get('/user', () => { return HttpResponse.json({ name: 'John Doe' }) }), ] setupWorker(...handlers) }) ================================================ FILE: test/typings/sse.test-d.ts ================================================ import { it } from 'vitest' import { sse } from 'msw' /** * @note Define the global property to simulate an EventSource-compatible environment. * MSW checks for that and throws to prevent incorrect usage. */ Object.defineProperty(global, 'EventSource', { value: () => {} }) it('supports sending anything without an explicit event map type', () => { sse('/stream', ({ client }) => { client.send({ data: 123 }) client.send({ data: 'hello' }) }) }) it('supports an optional "id" property', () => { sse('/stream', ({ client }) => { client.send({ id: '1', data: 'hello' }) }) sse<{ message: 'greeting' }>('/stream', ({ client }) => { client.send({ id: '2', data: 'greeting' }) }) sse<{ custom: 'goodbye' }>('/stream', ({ client }) => { client.send({ id: '2', event: 'custom', data: 'goodbye', }) }) }) it('supports custom event map type', () => { sse<{ myevent: string }>('/stream', ({ client }) => { client.send({ event: 'myevent', data: 'hello', }) client.send({ // @ts-expect-error Unknown event type "unknown". event: 'unknown', data: 'hello', }) client.send({ /** * @note Sending anonymous events ("message" events) * must still accept any data type unless narrowed down * in the event map by the "message" key. */ data: 'anything', }) }) }) it('supports event map type argument for unnamed events', () => { sse<{ message: number; custom: string; other: boolean }>( '/stream', ({ client }) => { /** * @note TS 5.0 reports errors at different locations * to satisfy a wide range of TS versions in type tests formatting gets disabled here * so that @ts-expect-error can assert the reported errors regardless of the location * see: https://github.com/mswjs/msw/pull/2521#issuecomment-2931733565 */ // prettier-ignore-start client.send({ data: 123, }) // When no explicit "event" key is provided, // threat it as if event was set to "message". // @ts-expect-error Unexpected data type for "message" event. client.send({ data: 'goodbye' }) client.send({ event: 'message', data: 123, }) // @ts-expect-error Unexpected data type for "message" event. client.send({ event: 'message', data: 'invalid' }) // @ts-expect-error Unexpected data type for "message" event. client.send({ event: 'message', data: 'goodbye' }) client.send({ event: 'custom', data: 'goodbye', }) // @ts-expect-error Unexpected data type for "custom" event client.send({ event: 'custom', data: 123 }) // Sending unknown events must be forbidden // if the EventMap type argument was provided. // @ts-expect-error Unknown event type "unknown". client.send({ event: 'invalid', data: 123 }) // boolean is only allowed for "other" event // @ts-expect-error client.send({ event: 'custom', data: true }) // boolean is only allowed for "other" event // @ts-expect-error client.send({ event: 'custom' as 'custom' | 'other', data: true }) // should error when required data is missing // @ts-expect-error client.send({ event: 'other' }) // prettier-ignore-end }, ) }) it('supports optional mnessage data', () => { sse<{ message?: string }>('/stream', ({ client }) => { // No data is fine because data is optional. client.send({}) // Allows explicit undefined as optional data. client.send({ data: undefined }) // Data is still validated even if optional. client.send({ data: 'hello' }) client.send({ // @ts-expect-error Invalid data type. data: 123, }) // No data is fine because data is optional. client.send({ event: 'message' }) // Allows explicit undefined as optional data. client.send({ event: 'message', data: undefined }) // Data is still validated even if optional. client.send({ event: 'message', data: 'hello' }) client.send({ event: 'message', // @ts-expect-error Invalid data type. data: 123, }) }) }) it('supports optional event data', () => { sse<{ maybe?: string }>('/stream', ({ client }) => { // No data is fine because data is optional. client.send({ event: 'maybe' }) // Allows explicit undefined as optional data. client.send({ event: 'maybe', data: undefined }) // Data is still validated even if optional. client.send({ event: 'maybe', data: 'hello' }) client.send({ event: 'maybe', // @ts-expect-error Invalid data type. data: 123, }) }) }) it('supports sending custom retry duration', () => { sse<{ custom: 'goodbye' }>('/stream', ({ client }) => { /** * @note TS 5.0 reports errors at different locations * to satisfy a wide range of TS versions in type tests formatting gets disabled here * so that @ts-expect-error can assert the reported errors regardless of the location * see: https://github.com/mswjs/msw/pull/2521#issuecomment-2931733565 */ // prettier-ignore-start client.send({ retry: 1000 }) // @ts-expect-error Cannot use message properties with "retry". client.send({ retry: 1000, id: '1' }) // @ts-expect-error Cannot use message properties with "retry". client.send({ retry: 1000, data: 'hello' }) // @ts-expect-error Cannot use message properties with "retry". client.send({ retry: 1000, event: 'custom', data: 'goodbye' }) // prettier-ignore-end }) }) ================================================ FILE: test/typings/tsconfig.5.0.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "esnext", "moduleResolution": "Node16" } } ================================================ FILE: test/typings/tsconfig.5.1.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "esnext", "moduleResolution": "Node16" } } ================================================ FILE: test/typings/tsconfig.5.2.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" } } ================================================ FILE: test/typings/tsconfig.5.3.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" } } ================================================ FILE: test/typings/tsconfig.5.4.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" } } ================================================ FILE: test/typings/tsconfig.5.5.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" } } ================================================ FILE: test/typings/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { // Use non-strict TypeScript configuration because Vitest only ever // generates a single "tsconfig.json" during the typecheck run // but we are running against different versions of TypeScript. // Keeping strict mode will error with "Unknown compiler option" errors. "strict": false, "strictNullChecks": true, "strictBindCallApply": true, "strictFunctionTypes": true, "rootDir": "../..", "lib": ["dom"] }, "include": ["../../global.d.ts", "**/*.test-d.ts"], "exclude": ["node_modules"] } ================================================ FILE: test/typings/vitest.config.ts ================================================ import fs from 'node:fs' import url from 'node:url' import path from 'node:path' import { defineConfig } from 'vitest/config' import { invariant } from 'outvariant' import tsPackageJson from 'typescript/package.json' assert { type: 'json' } import { mswExports } from '../support/alias' const TEST_ROOT = url.fileURLToPath(new URL('./', import.meta.url)) export default defineConfig({ resolve: { alias: { ...mswExports, }, }, test: { root: TEST_ROOT, globals: true, typecheck: { enabled: true, checker: 'tsc', include: ['**/*.test-d.ts'], tsconfig: (() => { const tsInstalledVersion = tsPackageJson.version invariant( tsInstalledVersion, 'Failed to run typings tests: unable to determine TypeScript version', ) const tsVersionMajorMinor = tsInstalledVersion.substring( 0, tsInstalledVersion.lastIndexOf('.'), ) const tsConfigPaths = [ url.fileURLToPath( new URL(`./tsconfig.${tsVersionMajorMinor}.json`, import.meta.url), ), url.fileURLToPath(new URL('./tsconfig.json', import.meta.url)), ] const tsConfigPath = tsConfigPaths.find((path) => fs.existsSync(path), ) as string const relativeTsConfigPath = path.relative(TEST_ROOT, tsConfigPath) console.log('Using tsconfig at: %s', relativeTsConfigPath) return relativeTsConfigPath })(), }, }, }) ================================================ FILE: test/typings/ws.test-d.ts ================================================ import { it, expectTypeOf } from 'vitest' import { WebSocketData, WebSocketLink, WebSocketHandlerConnection, ws, } from 'msw' import { WebSocketClientConnectionProtocol } from '@mswjs/interceptors/WebSocket' it('supports URL as the link argument', () => { expectTypeOf(ws.link('ws://localhost')).toEqualTypeOf() }) it('supports RegExp as the link argument', () => { expectTypeOf(ws.link(/\/ws$/)).toEqualTypeOf() }) it('exposes root-level link APIs', () => { const link = ws.link('ws://localhost') expectTypeOf(link.addEventListener).toBeFunction() expectTypeOf(link.broadcast).toBeFunction() expectTypeOf(link.broadcastExcept).toBeFunction() expectTypeOf(link.clients).toEqualTypeOf< Set >() }) it('supports "connection" event listener', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', (connection) => { expectTypeOf(connection).toEqualTypeOf() }) }) it('errors on arbitrary event names passed to the link', () => { const link = ws.link('ws://localhost') link.addEventListener( // @ts-expect-error Unknown event name "abc". 'abc', () => {}, ) }) /** * Client API. */ it('exposes root-level "client" APIs', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', ({ client }) => { expectTypeOf(client.id).toBeString() expectTypeOf(client.url).toEqualTypeOf() expectTypeOf(client.addEventListener).toBeFunction() expectTypeOf(client.send).toBeFunction() expectTypeOf(client.removeEventListener).toBeFunction() expectTypeOf(client.close).toBeFunction() }) }) it('supports "message" event listener on the client', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { expectTypeOf(event).toEqualTypeOf>() }) }) }) it('supports "close" event listener on the client', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', ({ client }) => { client.addEventListener('close', (event) => { expectTypeOf(event).toMatchTypeOf() }) }) }) it('errors on arbitrary event names passed to the client', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', ({ client }) => { client.addEventListener( // @ts-expect-error Unknown event name "abc". 'abc', () => {}, ) }) }) /** * Server API. */ it('exposes root-level "server" APIs', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', ({ server }) => { expectTypeOf(server.connect).toEqualTypeOf<() => void>() expectTypeOf(server.addEventListener).toBeFunction() expectTypeOf(server.send).toBeFunction() expectTypeOf(server.removeEventListener).toBeFunction() expectTypeOf(server.close).toBeFunction() }) }) it('supports "message" event listener on the server', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', ({ server }) => { server.addEventListener('message', (event) => { expectTypeOf(event).toEqualTypeOf>() }) }) }) it('supports "open" event listener on the server', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', ({ server }) => { server.addEventListener('open', (event) => { expectTypeOf(event).toMatchTypeOf() }) }) }) it('supports "close" event listener on the server', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', ({ server }) => { server.addEventListener('close', (event) => { expectTypeOf(event).toMatchTypeOf() }) }) }) it('errors on arbitrary event names passed to the server', () => { const link = ws.link('ws://localhost') link.addEventListener('connection', ({ server }) => { server.addEventListener( // @ts-expect-error Unknown event name "abc". 'abc', () => {}, ) }) }) ================================================ FILE: tsconfig.base.json ================================================ { "compilerOptions": { "strict": true, "target": "esnext", "module": "esnext", "moduleResolution": "node", "strictNullChecks": true, "skipLibCheck": true, "allowSyntheticDefaultImports": false, "baseUrl": "./", "paths": { "~/core": ["src/core"], "~/core/*": ["src/core/*"] } }, "include": ["./global.d.ts"], "exclude": ["node_modules", "lib"] } ================================================ FILE: tsconfig.json ================================================ { "extends": "./tsconfig.base.json", "references": [ // Source. { "path": "./src/browser/tsconfig.browser.json" }, { "path": "./src/tsconfig.node.json" }, { "path": "./src/tsconfig.worker.json" }, // Tests. { "path": "./tsconfig.test.unit.json" } ] } ================================================ FILE: tsconfig.test.unit.json ================================================ { // Configuration for the unit tests living // next to the source code. "extends": "./tsconfig.base.json", "compilerOptions": { "composite": true, "target": "esnext", "module": "esnext", "types": ["vitest/globals"] }, "include": ["./src/**/*.test.ts", "./test/support"], "references": [ { "path": "./src/tsconfig.src.json" } ] } ================================================ FILE: tsup.config.ts ================================================ import * as path from 'node:path' import { defineConfig, Options } from 'tsup' import * as glob from 'glob' import { getWorkerChecksum, copyWorkerPlugin, } from './config/plugins/esbuild/copyWorkerPlugin' import { resolveCoreImportsPlugin } from './config/plugins/esbuild/resolveCoreImportsPlugin' import { forceEsmExtensionsPlugin } from './config/plugins/esbuild/forceEsmExtensionsPlugin' import { graphqlImportPlugin } from './config/plugins/esbuild/graphQLImportPlugin' import packageJson from './package.json' // Externalize the in-house dependencies so that the user // would get the latest published version automatically. const ecosystemDependencies = /^@mswjs\/(.+)$/ // Externalize the core functionality (reused across environments) // so that it can be shared between the environments. const mswCore = /\/core(\/.+)?$/ const SERVICE_WORKER_CHECKSUM = getWorkerChecksum() /** * A designated configuration for CJS shims. * This bundles the shims so CJS modules could be used * in the browser. */ const shimConfig: Options = { name: 'shims', platform: 'neutral', entry: glob.sync('./src/shims/**/*.ts'), format: ['esm', 'cjs'], noExternal: Object.keys(packageJson.dependencies), outDir: './lib/shims', bundle: true, splitting: false, sourcemap: false, dts: true, } const coreConfig: Options = { name: 'core', platform: 'neutral', entry: glob.sync('./src/core/**/*.ts', { ignore: '**/*.test.ts', }), external: [ecosystemDependencies], noExternal: ['cookie'], format: ['esm', 'cjs'], outDir: './lib/core', bundle: false, splitting: false, sourcemap: true, dts: true, tsconfig: path.resolve(__dirname, 'src/tsconfig.core.build.json'), esbuildPlugins: [graphqlImportPlugin(), forceEsmExtensionsPlugin()], } const nodeConfig: Options = { name: 'node', platform: 'node', entry: ['./src/node/index.ts'], inject: ['./config/polyfills-node.ts'], external: [mswCore, ecosystemDependencies], format: ['esm', 'cjs'], outDir: './lib/node', bundle: true, splitting: false, sourcemap: true, dts: true, tsconfig: path.resolve(__dirname, 'src/tsconfig.node.build.json'), esbuildPlugins: [resolveCoreImportsPlugin(), forceEsmExtensionsPlugin()], } const browserConfig: Options = { name: 'browser', platform: 'browser', entry: ['./src/browser/index.ts'], external: [mswCore, ecosystemDependencies], format: ['esm', 'cjs'], outDir: './lib/browser', bundle: true, splitting: false, sourcemap: true, dts: true, noExternal: Object.keys(packageJson.dependencies).filter((packageName) => { /** * @note Never bundle MSW core so all builds reference the *same* * JavaScript and TypeScript core files. This way types across * export paths remain compatible: * import { http } from 'msw' // <- core * import { setupWorker } from 'msw/browser' // <- /browser * setupWorker(http.get(path, resolver)) // OK */ return !mswCore.test(packageName) }), /** * @note Use a proxy TypeScript configuration where the "compilerOptions.composite" * option is set to false. * @see https://github.com/egoist/tsup/issues/571 */ tsconfig: path.resolve(__dirname, 'src/browser/tsconfig.browser.build.json'), define: { SERVICE_WORKER_CHECKSUM: JSON.stringify(SERVICE_WORKER_CHECKSUM), }, esbuildPlugins: [ resolveCoreImportsPlugin(), forceEsmExtensionsPlugin(), copyWorkerPlugin(SERVICE_WORKER_CHECKSUM), ], } const reactNativeConfig: Options = { name: 'react-native', platform: 'node', entry: ['./src/native/index.ts'], external: ['picocolors', 'util', 'events', mswCore, ecosystemDependencies], format: ['esm', 'cjs'], outDir: './lib/native', bundle: true, splitting: false, sourcemap: true, dts: true, tsconfig: path.resolve(__dirname, 'src/tsconfig.node.build.json'), esbuildPlugins: [resolveCoreImportsPlugin(), forceEsmExtensionsPlugin()], } const iifeConfig: Options = { name: 'iife', platform: 'browser', globalName: 'MockServiceWorker', entry: ['./src/iife/index.ts'], /** * @note Legacy output format will automatically create * a "iife" directory under the "outDir". */ outDir: './lib', format: ['iife'], legacyOutput: true, bundle: true, splitting: false, sourcemap: true, dts: false, tsconfig: path.resolve(__dirname, 'src/browser/tsconfig.browser.build.json'), define: { // Sign the IIFE build as well because any bundle containing // the worker API must have the the integrity checksum defined. SERVICE_WORKER_CHECKSUM: JSON.stringify(SERVICE_WORKER_CHECKSUM), }, } export default defineConfig([ shimConfig, coreConfig, nodeConfig, reactNativeConfig, browserConfig, iifeConfig, ]) ================================================ FILE: vitest.config.mts ================================================ import { defineConfig } from 'vitest/config' import { mswExports, customViteEnvironments, fromRoot, } from './test/support/alias' export default defineConfig({ test: { globals: true, // Lookup the unit tests in the "src" directory because // they are located next to the source code they are testing. dir: './src', alias: { ...mswExports, ...customViteEnvironments, '~/core': fromRoot('src/core'), }, typecheck: { // Load the TypeScript configuration to the unit tests. // Otherwise, Vitest will use the root-level "tsconfig.json", // which includes way too more than the tests need. tsconfig: './tsconfig.test.unit.json', }, environmentOptions: { jsdom: { // Drop the 3000 port from the default "location.href" // for backward-compatibility with the existing tests. url: 'http://localhost/', }, }, }, })