msw: Undici 6.x - Request/Response/TextEncoder is not defined (Jest) no longer works

Prerequisites

Environment check

  • I’m using the latest msw version
  • I’m using Node.js version 18 or higher

Node.js version

v18.19.0

Reproduction repository

http://

Reproduction steps

  1. Install latest version of Undici (6.x)
  2. Try to run tests

Current behavior

The documentation suggestion to create a jest.polyfills.js no longer works in Undici version 6.x

All tests are returning the same error:

 ● Test suite failed to run

   ReferenceError: ReadableStream is not defined

     27 |   Headers: { value: Headers },
     28 |   FormData: { value: FormData },
   > 29 |   Request: { value: Request },
        |                 ^
     30 |   Response: { value: Response },
     31 | });
     32 |

Expected behavior

An update version of jest.polyfills.js

About this issue

  • Original URL
  • State: closed
  • Created 7 months ago
  • Reactions: 17
  • Comments: 25 (7 by maintainers)

Commits related to this issue

Most upvoted comments

A polyfill of a polyfill? 😄

I hope there is clear solution.

I’ve gotten to the point where I don’t personally think it’s worth it to migrate to v2 if you’re using Jest. Part of the amazing thing about this library on v1 is that it just worked. It was a magical DX when you compared it to the alternatives. But now, I’m highly advising anyone using Jest (which many of us are) to just continue using v1. I’ve followed the official migration docs with various levels of success but never able to reproduce the just works factor.

The library authors have taken a pretty clear stance that they consider Jest antiquated even though it’s currently 20x more popular than vitest according to npm downloads. And for the record, I do respect their right to take the library in that direction as the maintainers.

Conclusion

You are experiencing issues because Jest+JSDOM take away Node.js globals from you (the structuredClone global function in this case). For more info on this, see this and this.

Solution

There is nothing we can or should do to fix legacy tooling that doesn’t embrace the platform. The issue here isn’t caused by MSW, so it’s not MSW that should be fixing it.

I suggest you migrate to a modern test framework, like Vitest. You are also welcome to tackle this in whichever fashion you choose, using custom Jest environment, downgrading to undici@5 and other workarounds. I will not be recommending those because you don’t need them. The only reason you resort to them is because Jest forces you to. Naturally, I recommend migrating away from Jest.

@azangru Thanks for the suggestion. I like it better than installing Undici and dealing with incompatibilities with each new version.

This is my jsdom-extended.js file if anyone wants to use the same approach:

const JSDOMEnvironment = require("jest-environment-jsdom").default; // or import JSDOMEnvironment from 'jest-environment-jsdom' if you are using ESM modules

class JSDOMEnvironmentExtended extends JSDOMEnvironment {
  constructor(...args) {
    super(...args);

    this.global.ReadableStream = ReadableStream;
    this.global.TextDecoder = TextDecoder;
    this.global.TextEncoder = TextEncoder;

    this.global.Blob = Blob;
    this.global.File = File;
    this.global.Headers = Headers;
    this.global.FormData = FormData;
    this.global.Request = Request;
    this.global.Response = Response;
    this.global.Request = Request;
    this.global.Response = Response;
    this.global.fetch = fetch;
    this.global.structuredClone = structuredClone;
  }
}

module.exports = JSDOMEnvironmentExtended;

And then in the jest.config.js file:

  testEnvironment: "<rootDir>/src/test-helpers/jsdom-extended.js",

@azangru Thanks for the suggestion. I like it better than installing Undici and dealing with incompatibilities with each new version.

This is my jsdom-extended.js file if anyone wants to use the same approach:

const JSDOMEnvironment = require("jest-environment-jsdom").default; // or import JSDOMEnvironment from 'jest-environment-jsdom' if you are using ESM modules

class JSDOMEnvironmentExtended extends JSDOMEnvironment {
  constructor(...args) {
    super(...args);

    this.global.ReadableStream = ReadableStream;
    this.global.TextDecoder = TextDecoder;
    this.global.TextEncoder = TextEncoder;

    this.global.Blob = Blob;
    this.global.File = File;
    this.global.Headers = Headers;
    this.global.FormData = FormData;
    this.global.Request = Request;
    this.global.Response = Response;
    this.global.Request = Request;
    this.global.Response = Response;
    this.global.fetch = fetch;
    this.global.structuredClone = structuredClone;
  }
}

module.exports = JSDOMEnvironmentExtended;

And then in the jest.config.js file:

  testEnvironment: "<rootDir>/src/test-helpers/jsdom-extended.js",

@joel-daros solution works when I combine it with some information from another issue thread. When I add his testEnvironment file jsdom-extended.js and the following configuration in jest.config.js then it works as expected with Webpack, jest, MSW and SWR.

File: jest.config.js

/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
    verbose: true,
    preset: 'ts-jest',
    testEnvironment: "<rootDir>/jsdom-extended.js",
    // Next Line(s) is important! https://github.com/mswjs/msw/issues/1786#issuecomment-1782559851
    testEnvironmentOptions: {
        customExportConditions: [''],
    },

File: jsdom-extended.js

const JSDOMEnvironment = require("jest-environment-jsdom").default; // or import JSDOMEnvironment from 'jest-environment-jsdom' if you are using ESM modules

class JSDOMEnvironmentExtended extends JSDOMEnvironment {
    constructor(...args) {
        super(...args);

        this.global.ReadableStream = ReadableStream;
        this.global.TextDecoder = TextDecoder;
        this.global.TextEncoder = TextEncoder;

        this.global.Blob = Blob;
        this.global.File = File;
        this.global.Headers = Headers;
        this.global.FormData = FormData;
        this.global.Request = Request;
        this.global.Response = Response;
        this.global.Request = Request;
        this.global.Response = Response;
        this.global.fetch = fetch;
        this.global.structuredClone = structuredClone;
    }
}

module.exports = JSDOMEnvironmentExtended;

I have written an article on medium about it ⇾ Jest + React + MSW + Webpack — Solution for Errors

Update

Thank you so much everyone for battling your way through this. I think we should publish that custom Jest environment to NPM and recommend it officially for everyone using MSW (and beyond, really).

I’ve created the jest-fixed-jsdom repo to host that environment. I’ve invited @joel-daros, @tobiashochguertel, and @JamesZoft. You have the write access to that repo now. Please, push the source for that Jest environment and I will help you get it published to NPM! Will update the docs afterward too. Let’s solve these Jest issues once and for all.

Thanks.

I had the same issue, but I’ve solved it with install web-streams-polyfill and modify jest.polyfills.js as bellow:

const { TextDecoder, TextEncoder } = require('node:util')
const { ReadableStream } = require('web-streams-polyfill/ponyfill/es2018')
 
Object.defineProperties(globalThis, {
  ReadableStream: { value: ReadableStream },
  TextDecoder: { value: TextDecoder },
  TextEncoder: { value: TextEncoder },
})
 
const { Blob, File } = require('node:buffer')
const { fetch, Headers, FormData, Request, Response } = require('undici')
 
Object.defineProperties(globalThis, {
  fetch: { value: fetch, writable: true },
  Blob: { value: Blob },
  File: { value: File },
  Headers: { value: Headers },
  FormData: { value: FormData },
  Request: { value: Request },
  Response: { value: Response },
})

This is the classic answer: “It’s working on my machine”. 😄

I just want to chime in that the proposed solution using web-stream-polyfill did not work for me, but the alternate solution extending JSDOMEnvironment did work… sorta.

I had to modify the solution as such:

const JSDOMEnvironment = require('jest-environment-jsdom').default;

class MyJSDOMEnvironment extends JSDOMEnvironment {
  constructor(...args) {
    super(...args);

    this.global.Request = Request;
    this.global.Response = Response;
    this.global.TextEncoder = TextEncoder; // Had to add this
    this.global.TextDecoder = TextDecoder; // Had to add this
    this.global.fetch = fetch;
    this.global.structuredClone = structuredClone;
  }
}

module.exports = MyJSDOMEnvironment;

Even though my tests did pass, I got a billion of these messages in my console:

(node:49550) MaxListenersExceededWarning: Possible EventTarget memory leak detected. 15 abort listeners
added to [AbortSignal]. Use events.setMaxListeners() to increase limit

As such, I’m gonna stick with using the previous major version of undici until either a more robust solution is presented, or I migrate to vitest.

@robbieaverill @piyushchauhan2011 means a lot to me to hear this. I know issues like this suck. I also understand how natural it is to blame the tool that seemingly causes the issue when, in fact, it just helps it surface. I hope jest-fixed-jsdom will provide a better developer experience for everyone blocked by this. I hope even more that we migrate away from JSDOM and test browser code in the browser—that is the actual solution.

@tobiashochguertel, thanks for putting the custom environment up!

So, because in the environment class’ context we’re still in regular Node.js, we can grab the globals without having to explicitly import them from anywhere? This is interesting. Sounds like it solves the ReadableStream/structuredClone issue.

My only concern with this is that it’s a workaround. I feel uncomfortable recommending workarounds. Granted, what we recommend in the docs right now is also a workaround, albeit with a bit smaller effect area. I’d much prefer to see those changes you propose to be a part of jest-environment-jsdom because without them, that environment is quite broken.

Did you consider raising this as an issue/pull request to Jest? That would be a nice solution for everyone (and a contribution opportunity for you too!).

I solve it this way

// jest.polyfills.js

const { TextDecoder, TextEncoder, ReadableStream } = require('node:util');

Object.defineProperties(globalThis, {
  TextDecoder: { value: TextDecoder },
  TextEncoder: { value: TextEncoder },
  ReadableStream: { value: ReadableStream },
});

const { Blob, File } = require('node:buffer');
const { fetch, Headers, FormData, Request, Response } = require('undici');

Object.defineProperties(globalThis, {
  fetch: { value: fetch, writable: true },
  Blob: { value: Blob },
  File: { value: File },
  Headers: { value: Headers },
  FormData: { value: FormData },
  Request: { value: Request },
  Response: { value: Response },
});

I know this solution was frowned upon; but it’s been working well for me so far, and it does not require Undici.