msw: TypeError: performance.markResourceTiming is not a function

Prerequisites

Environment check

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

Node.js version

20.9.0

Reproduction steps

  • Follow the 1.x -> 2.x migration steps
  • Create a jest.polyfills.js file to work around #1796 (docs)
  • Install undici as instructed (currently v5.27.2)
  • Set the undici global origin to work around #1625 (example)

Current behavior

undici will throw an error: TypeError: performance.markResourceTiming is not a function. I’m guessing this is because performance.markResourceTiming is not a browser API, but the jest.polyfills.js file replaces fetch with undici which attempts to call this Node API here.

Expected behavior

fetch should work

I worked around the issue by adding performance to my jest.polyfills.js.

/* global globalThis */

/**
 * @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 { performance } = require('node:perf_hooks')
const { TextDecoder, TextEncoder } = require('node:util')

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

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

Apologies for not creating a reproduction repository. Hopefully this is the correct workaround and it helps someone.

About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Reactions: 1
  • Comments: 18 (7 by maintainers)

Most upvoted comments

@samanthaburboz Here’s what my jest.polyfills.js looks like. I think performance and clearImmediate need to be imported and added to globalThis before undici is imported.

/**
 * @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 { performance } = require("node:perf_hooks");
const { TextDecoder, TextEncoder } = require("node:util");
const { clearImmediate } = require("node:timers");

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

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

Yeah, if the assumption is correct—that undici is trying to use missing globals in JSDom—then I’m wondering:

  • Should the migration guide docs on jest.polyfills.js be updated to include these two missing globals? I’m happy to submit a PR for that if folks agree that it’s the right direction.
  • Should the migration guide recommend pinning a specific version of undici in package.json. Otherwise developers might need to keep adding polyfills if future versions of undici attempt to use other functions not defined in JSDom

@kettanaito what do you think?

It’s possible that something is wrong with my setup, but logging process.version during the test setup gives me v20.9.0

console.log
    process.version v20.9.0

      at Object.log (admin/setupTests.js:11:9)

  console.warn
    [MSW] Warning: intercepted a request without a matching request handler:
    
      • GET /profile/user
    
    If you still wish to intercept this unhandled request, please create a request handler for it.
    Read more: https://mswjs.io/docs/getting-started/mocks

      at Object.warn (node_modules/msw/lib/core/utils/internal/devUtils.js:31:11)
      at applyStrategy (node_modules/msw/lib/core/utils/request/onUnhandledRequest.js:170:36)
      at node_modules/msw/lib/core/utils/request/onUnhandledRequest.js:191:5
      at fulfilled (node_modules/msw/lib/core/utils/request/onUnhandledRequest.js:33:24)

  console.error
    Error: Uncaught [TypeError: performance.markResourceTiming is not a function]

I’ll see if I can put together a minimal reproduction at some point. It’s a little tough because our app is an ejected create-react-app that has been modified a fair bit.

By polyfilling with node versions of these, we’re removing the ability to use browser like parts of them in jsdom - using node apis only.

Yes I think this is the core issue, not the lack of Node v18 support in RTL (that might be a separate issue, but I don’t think it’s the one I’m hitting in this ticket).

In my test I’m attempting to use the Clipboard API that RTL stubs out:

expect(await navigator.clipboard.readText()).toBe(expectedUrl.href)

Which throws this error from the convert() function in JSDom

TypeError: Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'.
  at Object.exports.convert (node_modules/jsdom/lib/jsdom/living/generated/Blob.js:23:9)
  at FileReader.readAsText (node_modules/jsdom/lib/jsdom/living/generated/FileReader.js:163:23)

Here is the convert() function in JSDom’s Blob.js.

// node_modules/jsdom/lib/jsdom/living/generated/Blob.js

exports.is = value => {
  return utils.isObject(value) && utils.hasOwn(value, implSymbol) && value[implSymbol] instanceof Impl.implementation;
};
exports.isImpl = value => {
  return utils.isObject(value) && value instanceof Impl.implementation;
};
exports.convert = (globalObject, value, { context = "The provided value" } = {}) => {
  if (exports.is(value)) {  // <-- We are failing this check 🔴 
    return utils.implForWrapper(value);
  }
  throw new globalObject.TypeError(`${context} is not of type 'Blob'.`);
};

If I console.log the steps from is() then it shows that we are failing the second and third steps where it checks for the existence of a Symbol on the Blob

utils.isObject(value) // true
utils.hasOwn(value, implSymbol) // false
value[implSymbol] instanceof Impl.implementation // false

My guess is that when we replace JSDom’s Blob with Node’s Blob in jest.polyfills.js, it allows for the creation of Blobs that are missing a Symbol that JSDom normally attaches to them. When code tries to interact with the Blob in JSDom, it fails this identity check.

If I remove Blob from my jest.polyfills.js then the test passes. I suspect this happens because I’m not yet using any part of undici that depends on a Node-specific version of Blob.

Do/can we support browser based versions of these when running in jsdom? (I’m not actually sure offhand) but this might help avoid some issues generally.

I’m really curious to see how Vitest works around this? I’m going to try to recreate the issue with RTL in a Vitest app to see if everything “just works”.

I’m starting to think this might be a bigger issue and this polyfills file might make msw incompatible with Jest and libraries designed to work with Jest.

Here are some examples from our tests that I don’t know how to work around:

  • React Testing Library user-event’s clipboard API breaks because it attempts to use FileReader readAsText() which expects a Blob. Blob is defined in JSDom but we are replacing it with a version from Node that is not compatible.
TypeError: Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'.
Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'
  • jest.useFakeTimers({ legacyFakeTimers: true }) breaks because it attempts to use clearImmediate
TypeError: Cannot assign to read only property 'clearImmediate' of object '[object Window]'

If I comment out the polyfills then all of the above issues go away and the tests pass again.

edit:

Maybe a possible fix for the clearImmediate issue would be to make it writeable?

clearImmediate: { value: clearImmediate, writable: true },

But i’m not sure how to work around the other issues with FormData and Blob

Forgot to mention - in the repo, I specifically commented out my msw handlers to make the test fail. If you comment them back in, everything will work. So I’m guessing we’re hitting a code path in undici when we 404 the request and it causes it to try to call clearImmediate

interesting! definitely looks like that 404 / clearImmediate relates to node apis that jsdom doesn’t implement.

The issue of “node” and “jsdom” implementations and how they kind of leak between eachother is…fun

It looks like we are using "jest-environment-jsdom": "^29.7.0" and we have testEnvironment: 'jest-environment-jsdom', set in our jest config.

jest-environment-jsdom v29.7.0 uses jsdom v20

jsdom v20.0.0 does have the performance object defined on window, but it does not implement markResourceTiming. Neither does the current version of jsdom.

I think markResourceTiming is a Node API, not a browser API, so it would make sense that jsdom would not implement it.

In jest.polyfills.js we set fetch to be undici’s version but I’m a little confused as to where undici is looking when it tries to access performance.markResourceTiming — is it trying to access the performance object defined in jsdom or Node itself?

Hi, @robdodson. This error implies you are running a version of Node.js older than 18.2.0 (see this). Please upgrade your Node.js version and that will fix the issue.