diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index d9369618c2a5..b8d9bb3bee68 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -759,9 +759,9 @@ it('the value is restored before running an other test', () => { ```ts // vitest.config.ts -export default { +export default defineConfig({ test: { unstubAllEnvs: true, } -} +}) ``` diff --git a/packages/web-worker/src/shared-worker.ts b/packages/web-worker/src/shared-worker.ts index d7954a1c9fbf..8da51396051f 100644 --- a/packages/web-worker/src/shared-worker.ts +++ b/packages/web-worker/src/shared-worker.ts @@ -2,20 +2,10 @@ import { MessageChannel, type MessagePort as NodeMessagePort, } from 'node:worker_threads' -import type { InlineWorkerContext, Procedure } from './types' +import type { Procedure } from './types' import { InlineWorkerRunner } from './runner' import { debug, getFileIdFromUrl, getRunnerOptions } from './utils' -interface SharedInlineWorkerContext - extends Omit< - InlineWorkerContext, - 'onmessage' | 'postMessage' | 'self' | 'global' - > { - onconnect: Procedure | null - self: SharedInlineWorkerContext - global: SharedInlineWorkerContext -} - function convertNodePortToWebPort(port: NodeMessagePort): MessagePort { if (!('addEventListener' in port)) { Object.defineProperty(port, 'addEventListener', { @@ -79,33 +69,55 @@ export function createSharedWorkerConstructor(): typeof SharedWorker { super() const name = typeof options === 'string' ? options : options?.name - - // should be equal to SharedWorkerGlobalScope - const context: SharedInlineWorkerContext = { - onconnect: null, - name, + let selfProxy: typeof globalThis + + const context = { + onmessage: null, + onmessageerror: null, + onerror: null, + onlanguagechange: null, + onoffline: null, + ononline: null, + onrejectionhandled: null, + onrtctransform: null, + onunhandledrejection: null, + origin: typeof location !== 'undefined' ? location.origin : 'http://localhost:3000', + importScripts: () => { + throw new Error( + '[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.', + ) + }, + crossOriginIsolated: false, + onconnect: null as ((e: MessageEvent) => void) | null, + name: name || '', close: () => this.port.close(), dispatchEvent: (event: Event) => { return this._vw_workerTarget.dispatchEvent(event) }, - addEventListener: (...args) => { - return this._vw_workerTarget.addEventListener(...args) + addEventListener: (...args: any[]) => { + return this._vw_workerTarget.addEventListener(...args as [any, any]) }, removeEventListener: this._vw_workerTarget.removeEventListener, get self() { - return context - }, - get global() { - return context + return selfProxy }, } + selfProxy = new Proxy(context, { + get(target, prop, receiver) { + if (Reflect.has(target, prop)) { + return Reflect.get(target, prop, receiver) + } + return Reflect.get(globalThis, prop, receiver) + }, + }) as any + const channel = new MessageChannel() this.port = convertNodePortToWebPort(channel.port1) this._vw_workerPort = convertNodePortToWebPort(channel.port2) this._vw_workerTarget.addEventListener('connect', (e) => { - context.onconnect?.(e) + context.onconnect?.(e as MessageEvent) }) const runner = new InlineWorkerRunner(runnerOptions, context) diff --git a/packages/web-worker/src/types.ts b/packages/web-worker/src/types.ts index 0bd3bfa7ddc0..df16ab9a6322 100644 --- a/packages/web-worker/src/types.ts +++ b/packages/web-worker/src/types.ts @@ -4,19 +4,3 @@ export type CloneOption = 'native' | 'ponyfill' | 'none' export interface DefineWorkerOptions { clone: CloneOption } - -export interface InlineWorkerContext { - onmessage: Procedure | null - name?: string - close: () => void - dispatchEvent: (e: Event) => void - addEventListener: (e: string, fn: Procedure) => void - removeEventListener: (e: string, fn: Procedure) => void - postMessage: ( - data: any, - transfer?: Transferable[] | StructuredSerializeOptions - ) => void - self: InlineWorkerContext - global: InlineWorkerContext - importScripts?: any -} diff --git a/packages/web-worker/src/worker.ts b/packages/web-worker/src/worker.ts index 1013214355d8..0d13bfa57de1 100644 --- a/packages/web-worker/src/worker.ts +++ b/packages/web-worker/src/worker.ts @@ -1,7 +1,6 @@ import type { CloneOption, DefineWorkerOptions, - InlineWorkerContext, Procedure, } from './types' import { InlineWorkerRunner } from './runner' @@ -45,22 +44,39 @@ export function createWorkerConstructor( constructor(url: URL | string, options?: WorkerOptions) { super() - // should be equal to DedicatedWorkerGlobalScope - const context: InlineWorkerContext = { - onmessage: null, - name: options?.name, + let selfProxy: typeof globalThis + + // should be in sync with DedicatedWorkerGlobalScope, but without globalThis + const context = { + onmessage: null as null | ((e: MessageEvent) => void), + onmessageerror: null, + onerror: null, + onlanguagechange: null, + onoffline: null, + ononline: null, + onrejectionhandled: null, + onrtctransform: null, + onunhandledrejection: null, + origin: typeof location !== 'undefined' ? location.origin : 'http://localhost:3000', + importScripts: () => { + throw new Error( + '[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.', + ) + }, + crossOriginIsolated: false, + name: options?.name || '', close: () => this.terminate(), dispatchEvent: (event: Event) => { return this._vw_workerTarget.dispatchEvent(event) }, - addEventListener: (...args) => { + addEventListener: (...args: any[]) => { if (args[1]) { this._vw_insideListeners.set(args[0], args[1]) } - return this._vw_workerTarget.addEventListener(...args) + return this._vw_workerTarget.addEventListener(...args as [any, any]) }, removeEventListener: this._vw_workerTarget.removeEventListener, - postMessage: (...args) => { + postMessage: (...args: any[]) => { if (!args.length) { throw new SyntaxError( '"postMessage" requires at least one argument.', @@ -76,15 +92,21 @@ export function createWorkerConstructor( this.dispatchEvent(event) }, get self() { - return context - }, - get global() { - return context + return selfProxy }, } + selfProxy = new Proxy(context, { + get(target, prop, receiver) { + if (Reflect.has(target, prop)) { + return Reflect.get(target, prop, receiver) + } + return globalThis[prop as 'crypto'] + }, + }) as any + this._vw_workerTarget.addEventListener('message', (e) => { - context.onmessage?.(e) + context.onmessage?.(e as MessageEvent) }) this.addEventListener('message', (e) => { diff --git a/packages/web-worker/tsconfig.json b/packages/web-worker/tsconfig.json index efb0628e6fcf..2099cdc01337 100644 --- a/packages/web-worker/tsconfig.json +++ b/packages/web-worker/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ESNext", "WebWorker"] + }, "exclude": ["./dist"] } diff --git a/test/core/src/web-worker/worker-globals.ts b/test/core/src/web-worker/worker-globals.ts new file mode 100644 index 000000000000..59d6b80d4790 --- /dev/null +++ b/test/core/src/web-worker/worker-globals.ts @@ -0,0 +1,8 @@ +self.onmessage = () => { + self.postMessage({ + crypto: !!self.crypto, + caches: !!self.caches, + location: !!self.location, + origin: self.origin, + }) +} diff --git a/test/core/test/web-worker-jsdom.test.ts b/test/core/test/web-worker-jsdom.test.ts index 5f740eadcd38..94197e66a816 100644 --- a/test/core/test/web-worker-jsdom.test.ts +++ b/test/core/test/web-worker-jsdom.test.ts @@ -3,6 +3,7 @@ import '@vitest/web-worker' import { expect, it } from 'vitest' +import GlobalsWorker from '../src/web-worker/worker-globals?worker' it('worker with invalid url throws an error', async () => { const url = import.meta.url @@ -35,3 +36,25 @@ it('throws an error on invalid path', async () => { } expect(event.error.message).toContain('Failed to load') }) + +it('returns globals on self correctly', async () => { + const worker = new GlobalsWorker() + await new Promise((resolve, reject) => { + worker.onmessage = (e) => { + try { + expect(e.data).toEqual({ + crypto: !!globalThis.crypto, + location: !!globalThis.location, + caches: !!globalThis.caches, + origin: 'http://localhost:3000', + }) + resolve() + } + catch (err) { + reject(err) + } + } + worker.onerror = reject + worker.postMessage(null) + }) +}) diff --git a/test/core/test/web-worker-node.test.ts b/test/core/test/web-worker-node.test.ts index 403bd9a7bbcd..5b4242b2c576 100644 --- a/test/core/test/web-worker-node.test.ts +++ b/test/core/test/web-worker-node.test.ts @@ -10,6 +10,7 @@ import MyObjectWorker from '../src/web-worker/objectWorker?worker' import MyEventListenerWorker from '../src/web-worker/eventListenerWorker?worker' import MySelfWorker from '../src/web-worker/selfWorker?worker' import MySharedWorker from '../src/web-worker/sharedWorker?sharedworker' +import GlobalsWorker from '../src/web-worker/worker-globals?worker' const major = Number(version.split('.')[0].slice(1)) @@ -269,3 +270,25 @@ it('doesn\'t trigger events, if closed', async () => { setTimeout(resolve, 100) }) }) + +it('returns globals on self correctly', async () => { + const worker = new GlobalsWorker() + await new Promise((resolve, reject) => { + worker.onmessage = (e) => { + try { + expect(e.data).toEqual({ + crypto: !!globalThis.crypto, + location: !!globalThis.location, + caches: !!globalThis.caches, + origin: 'http://localhost:3000', + }) + resolve() + } + catch (err) { + reject(err) + } + } + worker.onerror = reject + worker.postMessage(null) + }) +})