A WebdriverIO runner to run tests locally within isolated worker processes
—
Individual worker process management with lifecycle control, message passing, and browser session handling.
Individual worker process manager that extends EventEmitter to handle test execution in isolated child processes.
/**
* WorkerInstance manages individual worker processes
* Extends EventEmitter for message and event handling
*/
class WorkerInstance extends EventEmitter implements Workers.Worker {
cid: string;
config: WebdriverIO.Config;
configFile: string;
caps: WebdriverIO.Capabilities;
capabilities: WebdriverIO.Capabilities;
specs: string[];
execArgv: string[];
retries: number;
stdout: WritableStreamBuffer;
stderr: WritableStreamBuffer;
childProcess?: ChildProcess;
sessionId?: string;
server?: Record<string, string>;
instances?: Record<string, { sessionId: string }>;
isMultiremote?: boolean;
isBusy: boolean;
isKilled: boolean;
isReady: Promise<boolean>;
isSetup: Promise<boolean>;
}Creates a new WorkerInstance with configuration and stream buffers.
/**
* Create a WorkerInstance
* @param config - WebdriverIO configuration
* @param payload - Worker run payload with cid, specs, capabilities, etc.
* @param stdout - Stdout stream buffer for aggregated output
* @param stderr - Stderr stream buffer for aggregated output
* @param xvfbManager - Xvfb manager for virtual display handling
*/
constructor(
config: WebdriverIO.Config,
payload: Workers.WorkerRunPayload,
stdout: WritableStreamBuffer,
stderr: WritableStreamBuffer,
xvfbManager: XvfbManager
);Usage Example:
import { WritableStreamBuffer } from 'stream-buffers';
import { XvfbManager } from '@wdio/xvfb';
import WorkerInstance from '@wdio/local-runner';
const stdout = new WritableStreamBuffer();
const stderr = new WritableStreamBuffer();
const xvfbManager = new XvfbManager({ enabled: true });
const worker = new WorkerInstance(
config,
{
cid: "0-0",
configFile: "/path/to/wdio.conf.js",
caps: { browserName: "chrome" },
specs: ["./test/example.spec.js"],
execArgv: [],
retries: 0
},
stdout,
stderr,
xvfbManager
);Spawn the child process for the worker with proper environment setup.
/**
* Spawn worker child process
* @returns Promise resolving to the spawned ChildProcess
*/
startProcess(): Promise<ChildProcess>;Usage Example:
// Process is started automatically, but can be started manually
const childProcess = await worker.startProcess();
console.log(`Worker process PID: ${childProcess.pid}`);
// Listen for process events
childProcess.on('exit', (code) => {
console.log(`Worker exited with code ${code}`);
});Send commands and messages to the worker process via IPC.
/**
* Send command to worker process
* @param command - Command to execute in worker
* @param args - Arguments for the command
* @param requiresSetup - Whether command requires session setup
* @returns Promise that resolves when message is sent
*/
postMessage(
command: string,
args: Workers.WorkerMessageArgs,
requiresSetup?: boolean
): Promise<void>;Usage Example:
// Send run command to start test execution
await worker.postMessage('run', {
sessionId: 'abc123',
config: { baseUrl: 'https://example.com' }
});
// Send end session command
await worker.postMessage('endSession', {
sessionId: 'abc123'
});
// Command that requires session setup
await worker.postMessage('workerRequest', {
action: 'screenshot'
}, true);WorkerInstance emits various events during its lifecycle.
interface WorkerEvents {
'message': (payload: Workers.WorkerMessage & { cid: string }) => void;
'error': (error: Error & { cid: string }) => void;
'exit': (data: { cid: string; exitCode: number; specs: string[]; retries: number }) => void;
}Usage Examples:
// Listen for worker messages
worker.on('message', (payload) => {
console.log(`Worker ${payload.cid} sent:`, payload.name);
switch (payload.name) {
case 'ready':
console.log('Worker is ready to receive commands');
break;
case 'sessionStarted':
console.log('Browser session started:', payload.content.sessionId);
break;
case 'finishedCommand':
console.log('Command completed:', payload.content.command);
break;
}
});
// Listen for worker errors
worker.on('error', (error) => {
console.error(`Worker ${error.cid} error:`, error.message);
});
// Listen for worker exit
worker.on('exit', ({ cid, exitCode, specs, retries }) => {
console.log(`Worker ${cid} exited with code ${exitCode}`);
console.log(`Specs: ${specs.join(', ')}`);
console.log(`Retries remaining: ${retries}`);
});Messages sent from worker processes to the main process.
interface WorkerMessageTypes {
'ready': { name: 'ready' };
'finishedCommand': {
name: 'finishedCommand';
content: { command: string; result: any };
};
'sessionStarted': {
name: 'sessionStarted';
content: {
sessionId?: string;
capabilities?: WebdriverIO.Capabilities;
isMultiremote?: boolean;
instances?: Record<string, { sessionId: string }>;
};
};
'error': {
name: 'error';
content: { name: string; message: string; stack: string };
};
}Track worker process state and lifecycle.
interface WorkerState {
isBusy: boolean; // Worker is executing a command
isKilled: boolean; // Worker process has been terminated
isReady: Promise<boolean>; // Promise resolving when worker is ready
isSetup: Promise<boolean>; // Promise resolving when session is setup
sessionId?: string; // Browser session ID
instances?: Record<string, { sessionId: string }>; // Multiremote instances
isMultiremote?: boolean; // Whether worker handles multiremote
}Usage Examples:
// Check worker state
if (worker.isBusy) {
console.log('Worker is currently busy');
} else {
console.log('Worker is available');
}
// Wait for worker to be ready
await worker.isReady;
console.log('Worker is ready to receive commands');
// Wait for session setup (for commands that require it)
await worker.isSetup;
console.log('Browser session is established');
// Check session info
if (worker.sessionId) {
console.log(`Worker session ID: ${worker.sessionId}`);
}
if (worker.isMultiremote && worker.instances) {
console.log('Multiremote sessions:', Object.keys(worker.instances));
}Workers support REPL debugging sessions for interactive test debugging.
// Workers automatically handle REPL debug messages
// When a test calls browser.debug(), the worker will:
// 1. Queue the REPL session
// 2. Start interactive session
// 3. Handle debug commands
// 4. Resume test execution when complete
// No additional code needed - REPL integration is automaticWorker output is transformed and aggregated with capability ID prefixing.
// Output streams are automatically managed
// All worker stdout/stderr is:
// 1. Prefixed with [cid] for identification
// 2. Filtered to remove debugger messages
// 3. Aggregated to main process streams
// 4. Optionally grouped by test spec
// Access aggregated output
const stdoutContent = worker.stdout.getContents();
const stderrContent = worker.stderr.getContents();Workers handle graceful shutdown with proper cleanup.
// Workers automatically handle:
// - SIGINT signals for graceful termination
// - Session cleanup before exit
// - Proper process termination
// - Xvfb cleanup when applicable
// Shutdown is handled automatically by LocalRunner.shutdown()Workers receive customized environment variables.
// Workers automatically receive:
// - WDIO_WORKER_ID: Worker capability ID
// - NODE_ENV: Set to 'test' by default
// - WDIO_LOG_PATH: Path to worker log file (if outputDir configured)
// - Custom runnerEnv variables from config
// - Propagated NODE_OPTIONS from parent process
// Environment is set automatically during worker startupworker.on('error', (error) => {
if (error.code === 'ENOENT') {
console.error('Node.js not found for worker process');
} else if (error.code === 'EACCES') {
console.error('Permission denied starting worker process');
}
});// Commands sent to busy workers are logged and ignored
await worker.postMessage('run', args);
// Check if worker can accept commands
if (!worker.isBusy) {
await worker.postMessage('newCommand', args);
} else {
console.log(`Worker ${worker.cid} is busy, command skipped`);
}worker.on('message', (payload) => {
if (payload.name === 'error') {
console.error('Worker session error:', payload.content);
// Handle session errors (network issues, browser crashes, etc.)
}
});Install with Tessl CLI
npx tessl i tessl/npm-wdio--local-runner