diff --git a/examples/with-jest-jsdom/README.md b/examples/with-jest-jsdom/README.md index 9cae012..abb9129 100644 --- a/examples/with-jest-jsdom/README.md +++ b/examples/with-jest-jsdom/README.md @@ -13,12 +13,11 @@ Using MSW with JSDOM requires additional configuration. Unfortunately, JSDOM (`j Please see the setup steps below to properly configure Jest when using in combination with JSDOM. -### Module resolution +## Custom Jest environment -Opt-out from the browser module resolution in JSDOM by setting the `testEnvironmentOptions.customExportConditions` option in [`jest.config.ts`](./jest.config.ts). This will force JSDOM to use Node.js module resolution, correctly resolving export conditions of third-party packages. +Use the [`jest-fixed-jsdom`](https://github.com/mswjs/jest-fixed-jsdom) custom environment for your JSDOM tests. That environment is a superset of JSDOM with a few important modifications: -> Despite JSDOM predenting to be a browser environment, your code _still runs in Node.js_. Using the browser module resolution can cause all sorts of import issues with third-party packages that depend on the standard Node.js API. +- Ensures the module resolution is set to Node.js, not the browser (`customExporConditions`); +- Restores the global functions and classes present in the browser (e.g. `fetch`, `structuredClone`, etc.). -### Polyfills - -Create a [jest.polyfills.ts](./jest.polyfills.ts) file in your project (you can copy it) and include it in the `setupFiles` option in `jest.config.ts`. This will re-add some of the Node.js globals (and browser) missing in JSDOM, like `fetch`, `Request`, `Response`, etc. +See [`jest.config.ts`](./jest.config.ts) for the configuration reference. diff --git a/examples/with-jest-jsdom/example.test.ts b/examples/with-jest-jsdom/example.test.ts index 5dc567f..a4580b6 100644 --- a/examples/with-jest-jsdom/example.test.ts +++ b/examples/with-jest-jsdom/example.test.ts @@ -1,7 +1,3 @@ -/** - * @jest-environment jsdom - */ - it('receives a mocked response to a REST API request', async () => { const response = await fetch('https://api.example.com/user') diff --git a/examples/with-jest-jsdom/jest.config.ts b/examples/with-jest-jsdom/jest.config.ts index 400e309..8984e62 100644 --- a/examples/with-jest-jsdom/jest.config.ts +++ b/examples/with-jest-jsdom/jest.config.ts @@ -2,25 +2,15 @@ import type { Config } from 'jest' export default { rootDir: __dirname, - setupFilesAfterEnv: ['/jest.setup.ts'], - /** - * @note Include the polyfills in the "setupFiles" - * to apply them BEFORE the test environment. - */ - setupFiles: ['/jest.polyfills.ts'], + + // Use a custom environment to fix missing globals in jsdom. + testEnvironment: 'jest-fixed-jsdom', + + // Provide a setup file to enable MSW. + setupFilesAfterEnv: ['./jest.setup.ts'], + + // (Optional) Add suppor for TypeScript in Jest. transform: { '^.+\\.tsx?$': '@swc/jest', }, - testEnvironmentOptions: { - /** - * @note Opt-out from JSDOM using browser-style resolution - * for dependencies. This is simply incorrect, as JSDOM is - * not a browser, and loading browser-oriented bundles in - * Node.js will break things. - * - * Consider migrating to a more modern test runner if you - * don't want to deal with this. - */ - customExportConditions: [''], - }, } satisfies Config diff --git a/examples/with-jest-jsdom/jest.polyfills.ts b/examples/with-jest-jsdom/jest.polyfills.ts deleted file mode 100644 index f78c19c..0000000 --- a/examples/with-jest-jsdom/jest.polyfills.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @note The block below contains polyfills for Node.js globals - * required for Jest to function when running JSDOM tests. - * These HAVE to be require's and HAVE to be in this exact - * order, since "undici" depends on the "TextEncoder" global API. - * - * Consider migrating to a more modern test runner if - * you don't want to deal with this. - */ - -const { TextDecoder, TextEncoder } = require('node:util') - -Object.defineProperties(globalThis, { - TextDecoder: { value: TextDecoder }, - TextEncoder: { value: TextEncoder }, -}) - -const { Blob } = require('node:buffer') -const { fetch, Headers, FormData, Request, Response } = require('undici') - -Object.defineProperties(globalThis, { - fetch: { value: fetch, writable: true }, - Blob: { value: Blob }, - Headers: { value: Headers }, - FormData: { value: FormData }, - Request: { value: Request }, - Response: { value: Response }, -}) diff --git a/examples/with-jest-jsdom/package.json b/examples/with-jest-jsdom/package.json index c40a89e..d95becd 100644 --- a/examples/with-jest-jsdom/package.json +++ b/examples/with-jest-jsdom/package.json @@ -10,9 +10,9 @@ "@types/node": "^18", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", - "msw": "2.2.2", + "jest-fixed-jsdom": "^0.0.9", + "msw": "2.6.4", "ts-node": "^10.9.2", - "typescript": "^5.0.4", - "undici": "^5.22.0" + "typescript": "^5.0.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82ebc5d..6552c66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -165,18 +165,18 @@ importers: jest-environment-jsdom: specifier: ^29.5.0 version: 29.7.0 + jest-fixed-jsdom: + specifier: ^0.0.9 + version: 0.0.9(jest-environment-jsdom@29.7.0) msw: - specifier: 2.2.2 - version: 2.2.2(typescript@5.3.3) + specifier: 2.6.4 + version: 2.6.4(@types/node@18.19.7)(typescript@5.3.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.3.103)(@types/node@18.19.7)(typescript@5.3.3) typescript: specifier: ^5.0.4 version: 5.3.3 - undici: - specifier: ^5.22.0 - version: 5.28.2 examples/with-karma: devDependencies: @@ -3151,12 +3151,25 @@ packages: cookie: 0.5.0 dev: true + /@bundled-es-modules/cookie@2.0.1: + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + dependencies: + cookie: 0.7.2 + dev: true + /@bundled-es-modules/statuses@1.0.1: resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} dependencies: statuses: 2.0.1 dev: true + /@bundled-es-modules/tough-cookie@0.1.6: + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + dev: true + /@cloudflare/workerd-darwin-64@1.20240129.0: resolution: {integrity: sha512-DfVVB5IsQLVcWPJwV019vY3nEtU88c2Qu2ST5SQxqcGivZ52imagLRK0RHCIP8PK4piSiq90qUC6ybppUsw8eg==} engines: {node: '>=16'} @@ -4145,6 +4158,34 @@ packages: '@inquirer/type': 1.2.0 dev: true + /@inquirer/confirm@5.0.2(@types/node@18.19.7): + resolution: {integrity: sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + dependencies: + '@inquirer/core': 10.1.0(@types/node@18.19.7) + '@inquirer/type': 3.0.1(@types/node@18.19.7) + '@types/node': 18.19.7 + dev: true + + /@inquirer/core@10.1.0(@types/node@18.19.7): + resolution: {integrity: sha512-I+ETk2AL+yAVbvuKx5AJpQmoaWhpiTFOg/UJb7ZkMAK4blmtG8ATh5ct+T/8xNld0CZG/2UhtkdMwpgvld92XQ==} + engines: {node: '>=18'} + dependencies: + '@inquirer/figures': 1.0.8 + '@inquirer/type': 3.0.1(@types/node@18.19.7) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + transitivePeerDependencies: + - '@types/node' + dev: true + /@inquirer/core@7.0.0: resolution: {integrity: sha512-g13W5yEt9r1sEVVriffJqQ8GWy94OnfxLCreNSOTw0HPVcszmc/If1KIf7YBmlwtX4klmvwpZHnQpl3N7VX2xA==} engines: {node: '>=18'} @@ -4165,11 +4206,25 @@ packages: wrap-ansi: 6.2.0 dev: true + /@inquirer/figures@1.0.8: + resolution: {integrity: sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==} + engines: {node: '>=18'} + dev: true + /@inquirer/type@1.2.0: resolution: {integrity: sha512-/vvkUkYhrjbm+RolU7V1aUFDydZVKNKqKHR5TsE+j5DXgXFwrsOPcoGUJ02K0O7q7O53CU2DOTMYCHeGZ25WHA==} engines: {node: '>=18'} dev: true + /@inquirer/type@3.0.1(@types/node@18.19.7): + resolution: {integrity: sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + dependencies: + '@types/node': 18.19.7 + dev: true + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -4530,6 +4585,18 @@ packages: strict-event-emitter: 0.5.1 dev: true + /@mswjs/interceptors@0.36.10: + resolution: {integrity: sha512-GXrJgakgJW3DWKueebkvtYgGKkxA7s0u5B0P5syJM5rvQUnrpLPigvci8Hukl7yEM+sU06l+er2Fgvx/gmiRgg==} + engines: {node: '>=18'} + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + dev: true + /@ngtools/webpack@15.2.10(@angular/compiler-cli@15.2.10)(typescript@4.9.5)(webpack@5.76.1): resolution: {integrity: sha512-ZExB4rKh/Saad31O/Ofd2XvRuILuCNTYs0+qJL697Be2pzeewvzBhE4Xe1Mm7Jg13aWSPeuIdzSGOqCdwxxxFQ==} engines: {node: ^14.20.0 || ^16.13.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -4695,7 +4762,7 @@ packages: resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} dependencies: is-node-process: 1.2.0 - outvariant: 1.4.2 + outvariant: 1.4.3 dev: true /@open-draft/until@2.1.0: @@ -8546,6 +8613,11 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + /cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + dev: true + /cookies@0.9.1: resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} engines: {node: '>= 0.8'} @@ -12154,6 +12226,15 @@ packages: jest-util: 29.7.0 dev: true + /jest-fixed-jsdom@0.0.9(jest-environment-jsdom@29.7.0): + resolution: {integrity: sha512-KPfqh2+sn5q2B+7LZktwDcwhCpOpUSue8a1I+BcixWLOQoEVyAjAGfH+IYZGoxZsziNojoHGRTC8xRbB1wDD4g==} + engines: {node: '>=18.0.0'} + peerDependencies: + jest-environment-jsdom: '>=28.0.0' + dependencies: + jest-environment-jsdom: 29.7.0 + dev: true + /jest-get-type@29.6.3: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -14169,6 +14250,40 @@ packages: yargs: 17.7.2 dev: true + /msw@2.6.4(@types/node@18.19.7)(typescript@5.3.3): + resolution: {integrity: sha512-Pm4LmWQeytDsNCR+A7gt39XAdtH6zQb6jnIKRig0FlvYOn8eksn3s1nXxUfz5KYUjbckof7Z4p2ewzgffPoCbg==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 5.0.2(@types/node@18.19.7) + '@mswjs/interceptors': 0.36.10 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.4 + chalk: 4.1.2 + graphql: 16.8.1 + headers-polyfill: 4.0.2 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + strict-event-emitter: 0.5.1 + type-fest: 4.26.1 + typescript: 5.3.3 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + dev: true + /muggle-string@0.3.1: resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} dev: true @@ -14195,6 +14310,11 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + dev: true + /nanocolors@0.2.13: resolution: {integrity: sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==} dev: true @@ -14661,6 +14781,10 @@ packages: resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} dev: true + /outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + dev: true + /p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -14943,6 +15067,10 @@ packages: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} dev: true + /path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + dev: true + /path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -17221,6 +17349,16 @@ packages: url-parse: 1.5.10 dev: true + /tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true @@ -17433,6 +17571,11 @@ packages: engines: {node: '>=16'} dev: true + /type-fest@4.26.1: + resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==} + engines: {node: '>=16'} + dev: true + /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -18864,6 +19007,11 @@ packages: engines: {node: '>=12.20'} dev: true + /yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + dev: true + /yoga-wasm-web@0.3.3: resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} dev: true