From 0fa55c43b2514e852771ce8df0f12e0cdd97a608 Mon Sep 17 00:00:00 2001 From: nanami-0713 <312767031+nanami-0713@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:05:28 +0800 Subject: [PATCH] fix(jsdom): unwrap the Blob implementation via implForWrapper to support jsdom 30.1 jsdom 30.1 moved the implementation object from the hidden Symbol(impl) own property into a private class field, so the symbol scan in createCompatUtils finds nothing and every makeCompatBlob caller (URL.createObjectURL, Request/FormData bodies) throws "Cannot read properties of undefined (reading '_bytes')". Resolve the implementation with jsdom's own implForWrapper instead (jsdom has no exports map, the subpath can be required directly), keeping the symbol scan as a fallback for older layouts. --- packages/vitest/src/integrations/env/jsdom.ts | 29 ++++++++++++++++++- test/unit/test/environments/jsdom.spec.ts | 2 ++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/integrations/env/jsdom.ts b/packages/vitest/src/integrations/env/jsdom.ts index ffa661180d86..15a073bc80e0 100644 --- a/packages/vitest/src/integrations/env/jsdom.ts +++ b/packages/vitest/src/integrations/env/jsdom.ts @@ -1,6 +1,7 @@ import type { ConstructorOptions, DOMWindow, VirtualConsole as IVirtualConsole } from 'jsdom' import type { Environment } from '../../types/environment' import type { JSDOMOptions } from '../../types/jsdom-options' +import { createRequire } from 'node:module' import { URL as NodeURL } from 'node:url' import { populateGlobal } from './utils' @@ -306,6 +307,11 @@ function createCompatUtils(window: DOMWindow): CompatUtils { const implSymbol = Object.getOwnPropertySymbols( Object.getOwnPropertyDescriptors(new window.Blob()), )[0] + // jsdom 30.1 moved the implementation from the `Symbol(impl)` own property + // into a private class field, so `implSymbol` is `undefined` there and + // `blob[implSymbol]` cannot unwrap the wrapper anymore; + // jsdom's own `implForWrapper` can unwrap both layouts + const implForWrapper = getImplForWrapper() const utils = { window, makeCompatFormData(formData: FormData) { @@ -321,7 +327,7 @@ function createCompatUtils(window: DOMWindow): CompatUtils { return nodeFormData }, makeCompatBlob(blob: Blob) { - const impl = (blob as any)[implSymbol] + const impl = (implForWrapper?.(blob) ?? (blob as any)[implSymbol]) as any // jsdom 28 renamed `_buffer` to `_bytes` return new NodeBlob_([impl._bytes ?? impl._buffer], { type: blob.type }) }, @@ -329,6 +335,27 @@ function createCompatUtils(window: DOMWindow): CompatUtils { return utils } +function getImplForWrapper(): ((wrapper: object) => unknown) | undefined { + const require = createRequire(import.meta.url) + // jsdom 30.1+ exports `implForWrapper` from the new subpath, older + // versions from the old one; jsdom has no `exports` map, so both + // subpaths can be required directly + const subpaths = [ + 'jsdom/lib/generated/idl/utils.js', + 'jsdom/lib/jsdom/living/generated/utils.js', + ] + for (const subpath of subpaths) { + try { + const utils = require(subpath) + if (typeof utils?.implForWrapper === 'function') { + return utils.implForWrapper + } + } + catch {} + } + return undefined +} + function patchAddEventListener(window: DOMWindow) { const abortControllers = new WeakMap() const JSDOMAbortSignal = window.AbortSignal diff --git a/test/unit/test/environments/jsdom.spec.ts b/test/unit/test/environments/jsdom.spec.ts index b61a0af842db..ae348b798aa1 100644 --- a/test/unit/test/environments/jsdom.spec.ts +++ b/test/unit/test/environments/jsdom.spec.ts @@ -324,6 +324,8 @@ test('URL.createObjectUrl works properly', () => { expect(() => { URL.createObjectURL(new File([], 'name.js')) }).not.toThrow() + expect(URL.createObjectURL(new Blob(['hello']))).toMatch(/^blob:/) + expect(URL.createObjectURL(new File(['hello'], 'name.js'))).toMatch(/^blob:/) }) test('compat classes preserve their .name property', () => {