arvo-event-handler
Version:
A complete set of orthogonal event handler and orchestration primitives for Arvo based applications, featuring declarative state machines (XState), imperative resumables for agentic workflows, contract-based routing, OpenTelemetry observability, and in-me
177 lines (176 loc) • 6.22 kB
TypeScript
import type { ArvoEvent } from 'arvo-core';
import IArvoEventHandler from '../IArvoEventHandler';
/**
* Defines a single test step in an event handler test sequence.
*
* Each step receives the output events from the previous step, generates a new input event,
* and validates either the output events or expected errors. Steps are executed sequentially,
* allowing you to test complex event-driven workflows.
*/
export type ArvoTestStep = {
/**
* Generates the input event for this step based on previous step's output.
* Receives null on the first step or an array of ArvoEvents from the previous step.
*/
input: ((prev: ArvoEvent[] | null) => ArvoEvent) | ((prev: ArvoEvent[] | null) => Promise<ArvoEvent>);
} & ({
/**
* Optional validator for output events when the handler executes successfully.
*
* @param event - Array of events emitted by the handler
* @returns true if the events match expectations, false otherwise
*/
expectedEvents?: ((event: ArvoEvent[]) => boolean) | ((event: ArvoEvent[]) => Promise<boolean>);
expectedError?: never;
} | {
/**
* Optional validator for errors when the handler is expected to throw.
*
* @param error - The error thrown by the handler
* @returns true if the error matches expectations, false otherwise
*/
expectedError?: ((error: Error) => boolean) | ((error: Error) => Promise<boolean>);
expectedEvents?: never;
});
/**
* Defines a complete test case containing one or more sequential steps.
*
* Test cases execute steps in order, passing output events from each step to the next.
* This allows testing of complex event chains and workflows. Optional repeat configuration
* helps handle flaky tests by running them multiple times and checking success rate.
*
* @example
* ```typescript
* const testCase: ArvoTestCase = {
* name: 'User registration flow',
* steps: [
* { input: () => createUserEvent(), expectedEvents: (e) => e.length === 2 },
* { input: (prev) => prev[0], expectedEvents: (e) => e[0].type === 'email.sent' }
* ],
* repeat: { times: 10, successThreshold: 95 }
* };
* ```
*/
export type ArvoTestCase = {
/** Descriptive name for the test case, displayed in test output. */
name: string;
/** Sequential steps to execute in order. Must contain at least one step. */
steps: [ArvoTestStep, ...ArvoTestStep[]];
/**
* Optional configuration for running the test multiple times.
* Useful for testing non-deterministic handlers or catching intermittent failures.
* The test passes only if the success rate meets or exceeds the threshold.
*/
repeat?: {
/** Number of times to execute the entire test case */
times: number;
/**
* Minimum percentage of successful runs required for the test to pass (0-100).
* For example, 95 means at least 95% of runs must succeed.
*/
successThreshold: number;
};
};
/**
* Configuration for the event handler or function under test.
*
* Supports testing either an IArvoEventHandler instance or a raw async function.
* Multiple configs can be provided to test the same cases against different implementations.
*/
export type ArvoTestConfig = {
/**
* Optional display name for the test suite.
* If not provided, defaults to the handler source or function name.
*/
name?: string;
} & ({
/** IArvoEventHandler instance to test. */
handler?: IArvoEventHandler;
fn?: never;
} | {
/** Raw async function to test that accepts an ArvoEvent and returns events. */
fn?: (event: ArvoEvent) => Promise<{
events: ArvoEvent[];
}>;
handler?: never;
});
/**
* Complete test suite definition combining configuration and test cases.
*
* A test suite can test one or more handlers/functions against the same set of test cases.
* When multiple configs are provided, each test case runs against all configs, enabling
* cross-implementation testing and comparison.
*
* @example
* ```typescript
* const suite: ArvoTestSuite = {
* config: [
* { name: 'V1 Handler', handler: handlerV1 },
* { name: 'V2 Handler', handler: handlerV2 }
* ],
* cases: [
* {
* name: 'Should process user event',
* steps: [{ input: () => userEvent, expectedEvents: (e) => e.length > 0 }]
* },
* {
* name: 'Step 2',
* steps: [{ input: () => someEvent, expectedEvents: (e) => expect(e.type).toBe('com.some.event') }]
* }
* ]
* };
* ```
*/
export type ArvoTestSuite = {
/** Handler or function configuration(s) to test. */
config: ArvoTestConfig | ArvoTestConfig[];
/** Array of test cases to execute against the configured handler(s) */
cases: ArvoTestCase[];
};
/**
* Result of executing a single test run.
*/
export type ArvoTestResult = {
/** Whether the test execution passed all steps successfully */
success: boolean;
/** Detailed error message if the test failed. */
error?: string;
/** Iteration number when using repeat configuration. */
iteration?: number;
/** Index of the last completed step (0-based). */
step?: number;
};
/**
* Adapter interface for integrating with different test frameworks.
*
* Provides a unified interface for test framework primitives (describe, test, beforeEach),
* enabling the same test suites to run on Vitest, Jest, Mocha, or any other framework.
* Implementations should wrap their framework's native functions.
*
* @example
* ```typescript
* // Vitest adapter
* import { describe, test, beforeEach } from 'vitest';
*
* const vitestAdapter: IArvoTestFramework = {
* describe,
* test,
* beforeEach
* };
*
* // Mocha adapter
* const mochaAdapter: IArvoTestFramework = {
* describe,
* test: it,
* beforeEach
* };
* ```
*/
export interface IArvoTestFramework {
/** Groups related tests into a test suite. */
describe(name: string, fn: () => void): void;
/** Defines a single test case. */
test(name: string, fn: () => Promise<void>): void;
/** Runs setup logic before each test in the current describe block. */
beforeEach(fn: () => void): void;
}