Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
4 tasks done
joel-daros opened this issue Dec 7, 2023 · 28 comments
Closed
4 tasks done
Labels
bug Something isn't working needs:triage Issues that have not been investigated yet. scope:node Related to MSW running in Node

Comments

@joel-daros
Copy link

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

@joel-daros joel-daros added bug Something isn't working needs:triage Issues that have not been investigated yet. scope:node Related to MSW running in Node labels Dec 7, 2023
@joel-daros joel-daros changed the title Undici 6.x - Request/Response/TextEncoder is not defined (Jest) doesn’t work anymore Undici 6.x - Request/Response/TextEncoder is not defined (Jest) no longer works Dec 7, 2023
@blift
Copy link

blift commented Dec 7, 2023

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 },
})

@joel-daros
Copy link
Author

joel-daros commented Dec 7, 2023

A polyfill of a polyfill? 😄

I hope there is clear solution.

@azangru
Copy link

azangru commented Dec 11, 2023

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

@joel-daros
Copy link
Author

@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",

@Scott-Fischer
Copy link

Scott-Fischer commented Dec 16, 2023

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.

@denchen
Copy link

denchen commented Dec 17, 2023

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.

@m-nathani
Copy link

@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",

i tried import this to setupTests.js and still doesnt work

@pedroSoaresll
Copy link

ReadableStream: { value: ReadableStream },

ReadableStream was important to make work my local tests with Jest, I believe it should be included in the documentation at this part: https://mswjs.io/docs/migrations/1.x-to-2.x#requestresponsetextencoder-is-not-defined-jest

@DeividZavala
Copy link

DeividZavala commented Jan 10, 2024

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 },
});

@mattcosta7
Copy link
Contributor

@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",

i tried import this to setupTests.js and still doesnt work

this is a test environment configuration, importing that in setup tests won't do anything, so i think that's expected. the test environment solution seems reasonable enough to use though

dylants added a commit to dylants/bookstore that referenced this issue Jan 23, 2024
- Add msw as a test dependency. Use v1 since v2 has jsdom issues (see mswjs/msw#1916 and jsdom/jsdom#2524)
- Resolve fetch polyfill for the test environment
dylants added a commit to dylants/bookstore that referenced this issue Jan 23, 2024
- Add msw as a test dependency. Use v1 since v2 has jsdom issues (see mswjs/msw#1916 and jsdom/jsdom#2524)
- Resolve fetch polyfill for the test environment
@kettanaito
Copy link
Member

kettanaito commented Jan 24, 2024

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.

@joel-daros
Copy link
Author

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

@kettanaito
Copy link
Member

@joel-daros, I cannot fix issues in others' tools. You are encouraged to report this to Jest and JSDOM and to have them address this. You cannot bring a tool that depends on JavaScript into another tool that throws JavaScript out of the window and expect things to work.

@tobiashochguertel
Copy link

tobiashochguertel commented Jan 31, 2024

@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

@kettanaito
Copy link
Member

@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!).

@tobiashochguertel
Copy link

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.

Yes, it solved the problem with the structuredClone issue after solving the ReadableStream issue. I first worked with the polyfill solution which was provided in another issue and is now also available in the FAQ from MSWjs, but then you run into the issue that structuredClone is not available and there I didn't have a polyfill for - and didn't know from where I can get one.

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!).

@kettanaito I didn't think on that. I don't understand the solution completely, I just was good at puzzling everything together.

I didn't want to migrate to Vite now.

@Aaron-Pool
Copy link

Just wanted to add here that I'm on node 18, and I had to remove the this.global.File = File from the environment provided above, since apparently that API doesn't exist in node 18. Which for me was fine, since I didn't need the File api to run my tests.

@piyushchauhan2011
Copy link

I tried the jsdom-extended.js solution, it seems to work better than using jest-polyfills.js as mentioned in msw docs for troubleshooting fetch

@piyushchauhan2011
Copy link

It would be nice if docs can reflect this in FAQ https://mswjs.io/docs/faq#requestresponsetextencoder-is-not-defined-jest rather than suggesting only one options of jest polyfills

@kettanaito
Copy link
Member

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.

@robbieaverill
Copy link

Hi @kettanaito, just wanted to say that as a spectator of this issue for some time, we appreciate your time and effort spent on solving this problem, even though it's not an issue caused by msw. I've enjoyed removing my polyfills and undici. Thank you.

@piyushchauhan2011
Copy link

Thank you for the great effort and time @kettanaito 🚀

@kettanaito
Copy link
Member

@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.

@azangru
Copy link

azangru commented Mar 22, 2024

I hope even more that we migrate away from JSDOM and test browser code in the browser—that is the actual solution.

😃

Me too. But what does the landscape of test runners currently look like? Last time I checked, there was Web Test Runner, by the Modern Web project; but I think it is targeting primarily browser-native code, which something like React is not. And there was also Playwright component testing, but it has remained experimental for over a year, and is emphatically not a priority for the Playwright team. Have there been any developments among test runners that give you hope that we can move to testing our UI components, which at this point are predominantly React, in the browser?

@kettanaito
Copy link
Member

@azangru, there's a Browser mode in Vitest, which looks promising. What Playwright is doing with component-level testing for React is also good and I hope it finds the proper love it needs from the team.

In the RSC era, unless the frameworks expose the rendering/hydration pipelines, there will be no reliable way to test RSC on an integration level. That's concerning. I really hope the frameworks will work on this.

@alimemz
Copy link

alimemz commented Jul 24, 2024

Importing ReadableStream from node:stream/web fixed my tests.

// jest.polyfills.js

const { TextDecoder, TextEncoder } = require('node:util');
const { ReadableStream } = require('node:stream/web'); // <--- this did the magic

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

const { Blob, File } = require('node:buffer');
const { fetch, Response, Request, FormData, Headers } = 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 },
});

@kettanaito
Copy link
Member

Please note that the recommended way forward is to use the https://github.com/mswjs/jest-fixed-jsdom package in Jest. Then you don't have to create the setup file at all. It also correctly remaps the globals on the test environment level, where you don't have to import them from undici or other sources.

@marcuslindfeldt
Copy link

Importing ReadableStream from node:stream/web fixed my tests.

// jest.polyfills.js

const { TextDecoder, TextEncoder } = require('node:util');
const { ReadableStream } = require('node:stream/web'); // <--- this did the magic

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

const { Blob, File } = require('node:buffer');
const { fetch, Response, Request, FormData, Headers } = 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 worked for me. I'm using vitest with jsdom so it seems this issue is not only related to jest.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working needs:triage Issues that have not been investigated yet. scope:node Related to MSW running in Node
Projects
None yet
Development

No branches or pull requests