CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-jest-circus

The next-gen flux-based test runner for Jest that provides test framework globals and event-driven test execution

82

1.90x
Overview
Eval results
Files

event-system.mddocs/

Event System

Event-driven architecture for handling test lifecycle events and integrating with custom test environments. The event system provides comprehensive hooks into test execution phases for monitoring, reporting, and custom behavior.

Capabilities

Event Handler Types

Event handlers are functions that respond to test lifecycle events. Note that addEventHandler is not part of the public API - event handling is typically done through custom test environments.

interface EventHandler {
  /** Handle async events (most test lifecycle events) */
  (event: AsyncEvent, state: State): void | Promise<void>;
  /** Handle sync events (definition and error events) */
  (event: SyncEvent, state: State): void;
}

Event Integration with Test Environments

Custom test environments can handle events by implementing the handleTestEvent method.

/**
 * Custom test environment with event handling
 */
abstract class TestEnvironment {
  /**
   * Handle test events
   * @param event - The event that occurred
   * @param state - Current test state
   * @returns Promise that resolves when handling is complete
   */
  abstract handleTestEvent(event: Event, state: State): Promise<void>;
}

Usage Examples:

import { TestEnvironment as NodeEnvironment } from 'jest-environment-node';
import { Event, State } from 'jest-circus';

class CustomTestEnvironment extends NodeEnvironment {
  private testStartTime: number = 0;

  async handleTestEvent(event: Event, state: State): Promise<void> {
    switch (event.name) {
      case 'setup':
        console.log('Test suite setup');
        break;
        
      case 'test_start':
        this.testStartTime = Date.now();
        console.log(`Starting: ${event.test.name}`);
        break;
        
      case 'test_done':
        const duration = Date.now() - this.testStartTime;
        const status = event.test.errors.length > 0 ? 'FAILED' : 'PASSED';
        console.log(`${status}: ${event.test.name} (${duration}ms)`);
        
        // Custom logic for failed tests
        if (event.test.errors.length > 0) {
          await this.handleTestFailure(event.test);
        }
        break;
        
      case 'run_finish':
        await this.generateReport(state);
        break;
    }
  }

  private async handleTestFailure(test: TestEntry): Promise<void> {
    // Custom failure handling
    await this.captureScreenshot(test.name);
    await this.logSystemState();
  }

  private async generateReport(state: State): Promise<void> {
    // Generate custom test report
    const report = {
      totalTests: this.countTests(state.rootDescribeBlock),
      unhandledErrors: state.unhandledErrors.length,
      timestamp: new Date().toISOString()
    };
    await this.saveReport(report);
  }
}

module.exports = CustomTestEnvironment;

Event Types

Synchronous Events

Events that are dispatched synchronously and do not pause test execution.

type SyncEvent = 
  | StartDescribeDefinitionEvent
  | FinishDescribeDefinitionEvent  
  | AddHookEvent
  | AddTestEvent
  | ErrorEvent;

interface StartDescribeDefinitionEvent {
  name: 'start_describe_definition';
  blockName: BlockName;
  mode: BlockMode;
  asyncError: Error;
}

interface FinishDescribeDefinitionEvent {
  name: 'finish_describe_definition';
  blockName: BlockName;
  mode: BlockMode;
}

interface AddHookEvent {
  name: 'add_hook';
  hookType: HookType;
  fn: HookFn;
  timeout: number | undefined;
  asyncError: Error;
}

interface AddTestEvent {
  name: 'add_test';
  testName: TestName;
  fn: TestFn;
  mode?: TestMode;
  concurrent: boolean;
  timeout: number | undefined;
  failing: boolean;
  asyncError: Error;
}

interface ErrorEvent {
  name: 'error';
  error: Exception;
}

Asynchronous Events

Events that are dispatched asynchronously and pause test execution until all handlers complete.

type AsyncEvent = 
  | SetupEvent
  | IncludeTestLocationEvent
  | RunStartEvent
  | RunFinishEvent
  | RunDescribeStartEvent
  | RunDescribeFinishEvent
  | TestStartEvent
  | TestDoneEvent
  | TestRetryEvent
  | HookStartEvent
  | HookSuccessEvent
  | HookFailureEvent;

interface SetupEvent {
  name: 'setup';
  testNamePattern?: string;
  runtimeGlobals: JestGlobals;
  parentProcess: NodeJS.Process;
}

interface IncludeTestLocationEvent {
  name: 'include_test_location_in_result';
}

interface RunStartEvent {
  name: 'run_start';
}

interface RunFinishEvent {
  name: 'run_finish';
}

interface RunDescribeStartEvent {
  name: 'run_describe_start';
  describeBlock: DescribeBlock;
}

interface RunDescribeFinishEvent {
  name: 'run_describe_finish';
  describeBlock: DescribeBlock;
}

interface TestStartEvent {
  name: 'test_start';
  test: TestEntry;
}

interface TestDoneEvent {
  name: 'test_done';
  test: TestEntry;
}

interface TestRetryEvent {
  name: 'test_retry';
  test: TestEntry;
}

interface HookStartEvent {
  name: 'hook_start';
  hook: Hook;
}

interface HookSuccessEvent {
  name: 'hook_success';
  hook: Hook;
  describeBlock?: DescribeBlock;
  test?: TestEntry;
}

interface HookFailureEvent {
  name: 'hook_failure';
  error: Exception;
  hook: Hook;
  describeBlock?: DescribeBlock;
  test?: TestEntry;
}

Usage Examples:

import { addEventHandler } from "jest-circus";

// Handle specific event types
addEventHandler((event, state) => {
  // TypeScript type narrowing
  if (event.name === 'test_start') {
    // event is now typed as TestStartEvent
    console.log(`Test started: ${event.test.name}`);
    console.log(`Parent describe: ${event.test.parent.name}`);
  }
  
  if (event.name === 'hook_failure') {
    // event is now typed as HookFailureEvent  
    console.error(`${event.hook.type} hook failed:`, event.error);
    if (event.test) {
      console.error(`In test: ${event.test.name}`);
    }
  }
});

// Handle all test lifecycle events
addEventHandler(async (event, state) => {
  const testEvents = [
    'test_start', 
    'test_done', 
    'test_retry'
  ] as const;
  
  if (testEvents.includes(event.name as any)) {
    await logTestEvent(event, state);
  }
});

Event Timing and Execution

Important notes about event handling behavior:

  • Synchronous events (start_describe_definition, finish_describe_definition, add_hook, add_test, error) do not pause test execution
  • Asynchronous events pause execution until all handlers complete their promises
  • Event handlers should be registered before test execution begins
  • Multiple handlers can be registered and will execute in registration order
  • Throwing errors in event handlers will stop test execution
  • Event data should not be mutated as it may cause unexpected behavior

Usage Examples:

import { addEventHandler } from "jest-circus";

// Event handler that doesn't block
addEventHandler((event, state) => {
  // Synchronous logging - won't pause tests
  if (event.name === 'add_test') {
    console.log(`Registered test: ${event.testName}`);
  }
});

// Event handler that blocks until complete
addEventHandler(async (event, state) => {
  if (event.name === 'test_done') {
    // This will pause until the async operation completes
    await uploadTestResults(event.test);
  }
});

// Error handling in event handlers
addEventHandler(async (event, state) => {
  try {
    if (event.name === 'run_finish') {
      await generateReport(state);
    }
  } catch (error) {
    console.error('Report generation failed:', error);
    // Don't re-throw - would stop test execution
  }
});

Install with Tessl CLI

npx tessl i tessl/npm-jest-circus

docs

event-system.md

index.md

state-management.md

test-execution.md

test-globals.md

tile.json