CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-wdio--local-runner

A WebdriverIO runner to run tests locally within isolated worker processes

Pending
Overview
Eval results
Files

worker-management.mddocs/

Worker Process Management

Individual worker process management with lifecycle control, message passing, and browser session handling.

Capabilities

WorkerInstance Class

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

Constructor

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

Start Worker Process

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 Messages to Worker

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

Event System

Worker Events

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

Message Types

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

Worker State Management

State Properties

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

Advanced Features

REPL Integration

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 automatic

Output Stream Management

Worker 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();

Graceful Shutdown Handling

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()

Environment Variable Injection

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 startup

Error Handling

Process Spawn Errors

worker.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');
  }
});

Command Execution Errors

// 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`);
}

Session Management Errors

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

docs

index.md

runner-management.md

stream-debugging.md

worker-management.md

worker-process-execution.md

tile.json