Web Worker support for testing in Vitest without requiring JSDom
npx @tessl/cli install tessl/npm-vitest--web-worker@3.2.0@vitest/web-worker provides Web Worker support for Vitest testing without requiring JSDom. It simulates Web Worker functionality in the same thread, enabling developers to test web worker code in unit tests while maintaining full compatibility with the Web Worker API.
npm install -D @vitest/web-workerMain entry (side-effect only, no exports):
import '@vitest/web-worker';Pure entry for manual setup:
import { defineWebWorkers } from '@vitest/web-worker/pure';CommonJS (pure entry only):
const { defineWebWorkers } = require('@vitest/web-worker/pure');Add to your Vitest configuration for global Web Worker support:
import { defineConfig } from 'vitest/node';
export default defineConfig({
test: {
setupFiles: ['@vitest/web-worker'],
},
});import '@vitest/web-worker';
import MyWorker from './worker?worker';
const worker = new MyWorker();
worker.postMessage('hello');
worker.onmessage = (e) => {
console.log(e.data); // 'hello world'
};
// Or with URL-based workers
const worker2 = new Worker(new URL('./worker.ts', import.meta.url));@vitest/web-worker is built around these key components:
defineWebWorkers functionConfigure Web Worker behavior and message cloning strategies.
/**
* Defines Worker and SharedWorker constructors on globalThis
* @param options - Optional configuration for worker behavior
*/
function defineWebWorkers(options?: DefineWorkerOptions): void;
interface DefineWorkerOptions {
/** Cloning strategy for message passing in Worker communication */
clone: CloneOption;
}
type CloneOption = 'native' | 'ponyfill' | 'none';Create simulated Web Workers for testing.
/**
* Web Worker constructor that simulates worker behavior in same thread
* Available globally after importing '@vitest/web-worker' or calling defineWebWorkers()
*/
declare class Worker extends EventTarget {
/** Message event handler */
onmessage: ((event: MessageEvent) => void) | null;
/** Message error event handler */
onmessageerror: ((event: MessageEvent) => void) | null;
/** Error event handler */
onerror: ((event: ErrorEvent) => void) | null;
/**
* Create a new Worker instance
* @param url - Worker script URL or path
* @param options - Worker options including name
*/
constructor(url: URL | string, options?: WorkerOptions);
/**
* Send a message to the worker
* @param data - Data to send
* @param transferOrOptions - Transfer options or transferable objects
*/
postMessage(
data: any,
transferOrOptions?: StructuredSerializeOptions | Transferable[]
): void;
/** Terminate the worker and clean up resources */
terminate(): void;
}
interface WorkerOptions {
/** Optional name for the worker */
name?: string;
}Create simulated Shared Web Workers for testing.
/**
* SharedWorker constructor that simulates shared worker behavior
* Available globally after importing '@vitest/web-worker' or calling defineWebWorkers()
*/
declare class SharedWorker extends EventTarget {
/** Communication port for the shared worker */
readonly port: MessagePort;
/** Error event handler */
onerror: ((event: ErrorEvent) => void) | null;
/**
* Create a new SharedWorker instance
* @param url - SharedWorker script URL or path
* @param options - SharedWorker options or name string
*/
constructor(url: URL | string, options?: WorkerOptions | string);
}API available within worker execution context.
/**
* Dedicated Worker Global Scope - available as 'self' within worker context
* Compatible with standard DedicatedWorkerGlobalScope interface
*/
interface DedicatedWorkerGlobalScope {
/** Message event handler from main thread */
onmessage: ((event: MessageEvent) => void) | null;
/** Message error event handler */
onmessageerror: ((event: MessageEvent) => void) | null;
/** Error event handler */
onerror: ((event: ErrorEvent) => void) | null;
/** Worker origin */
readonly origin: string;
/** Worker name */
readonly name: string;
/** Cross-origin isolation status */
readonly crossOriginIsolated: boolean;
/**
* Post message to main thread
* @param data - Data to send
* @param transferOrOptions - Transfer options or transferable objects
*/
postMessage(
data: any,
transferOrOptions?: StructuredSerializeOptions | Transferable[]
): void;
/** Close/terminate the worker */
close(): void;
/** Import scripts (not supported in Vite - throws error) */
importScripts(): never;
/** Reference to worker global scope */
readonly self: DedicatedWorkerGlobalScope;
}
/**
* Shared Worker Global Scope - available as 'self' within shared worker context
* Compatible with standard SharedWorkerGlobalScope interface
*/
interface SharedWorkerGlobalScope {
/** Connect event handler when new clients connect */
onconnect: ((event: MessageEvent) => void) | null;
/** Worker name */
readonly name: string;
/** Worker origin */
readonly origin: string;
/** Cross-origin isolation status */
readonly crossOriginIsolated: boolean;
/** Close the worker port */
close(): void;
/** Import scripts (not supported in Vite - throws error) */
importScripts(): never;
/** Reference to shared worker global scope */
readonly self: SharedWorkerGlobalScope;
}Types used by the defineWebWorkers function.
/** Message cloning strategy options */
type CloneOption = 'native' | 'ponyfill' | 'none';
/** Configuration options for defining web workers */
interface DefineWorkerOptions {
/** Message cloning strategy for Worker communication */
clone: CloneOption;
}Configure worker behavior through environment variables:
VITEST_WEB_WORKER_CLONE: Set default cloning strategy ('native' | 'ponyfill' | 'none')Enable debug logging with the environment variable:
DEBUG=vitest:web-workerThe package supports various worker import patterns:
// Vite worker imports with ?worker suffix
import MyWorker from './worker?worker';
const worker = new MyWorker();
// Vite shared worker imports with ?sharedworker suffix
import MySharedWorker from './worker?sharedworker';
const sharedWorker = new MySharedWorker();
// URL-based worker creation
const worker = new Worker(new URL('./worker.ts', import.meta.url));
const sharedWorker = new SharedWorker(new URL('./worker.ts', import.meta.url));importScripts is not supported in Vite workersimport '@vitest/web-worker';
// worker.ts
self.onmessage = (e) => {
self.postMessage(`${e.data} world`);
};
// test file
import MyWorker from './worker?worker';
const worker = new MyWorker();
worker.postMessage('hello');
worker.onmessage = (e) => {
expect(e.data).toBe('hello world');
};import '@vitest/web-worker';
// shared-worker.ts
self.onconnect = (e) => {
const port = e.ports[0];
port.onmessage = (event) => {
port.postMessage(event.data);
};
};
// test file
import MySharedWorker from './shared-worker?sharedworker';
const worker = new MySharedWorker();
worker.port.postMessage('test');
worker.port.onmessage = (e) => {
expect(e.data).toBe('test');
};import { defineWebWorkers } from '@vitest/web-worker/pure';
// Configure with no message cloning for performance
defineWebWorkers({ clone: 'none' });
// Configure with ponyfill cloning for compatibility
defineWebWorkers({ clone: 'ponyfill' });
// Configure with native cloning (requires Node 17+)
defineWebWorkers({ clone: 'native' });import { defineWebWorkers } from '@vitest/web-worker/pure';
if (process.env.SUPPORT_WORKERS) {
defineWebWorkers({ clone: 'none' });
}onmessage = () => {}. Use self.onmessage = () => {} insteadonconnect = () => {}. Use self.onconnect = () => {} insteadbyteLengthstructuredClone, otherwise falls back to polyfill or no cloning