For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /api/javascript-api/reporter.md.
close
  • English
  • Reporter API

    The Reporter API allows you to create custom test result processors and output formatters. This interface is experimental and may change in future versions.

    Note: For usage examples and configuration guidance, see the Reporters guide.

    Using custom reporter

    Create a custom reporter by implementing the Reporter interface:

    import type { Reporter, TestFileResult } from '@rstest/core';
    
    const customReporter: Reporter = {
      onTestFileStart(test) {
        console.log(`Starting: ${test.testPath}`);
      },
    
      onTestFileResult(result) {
        console.log(`Finished: ${result.testPath}`);
      },
    
      onTestRunEnd({ results, duration }) {
        console.log(`All tests completed in ${duration.totalTime}ms`);
      },
    };

    Use the custom reporter in your configuration:

    import { defineConfig } from '@rstest/core';
    import { customReporter } from './path/to/custom-reporter';
    
    export default defineConfig({
      reporters: [customReporter],
    });

    Interface overview

    The Reporter interface provides lifecycle hooks for test execution. Each hook is called at specific points during the test run.

    Important: For the most up-to-date interface definition and complete type signatures, please refer to the source code.

    Hook categories

    • File-level hooks: onTestFileStart, onTestFileReady, onTestFileResult
    • Suite-level hooks: onTestSuiteStart, onTestSuiteResult
    • Case-level hooks: onTestCaseStart, onTestCaseResult
    • Run-level hooks: onTestRunStart, onTestRunEnd, onUserConsoleLog, onExit

    Basic interface structure

    interface Reporter {
      onTestFileStart?(test: TestFileInfo): void;
      onTestFileReady?(test: TestFileInfo): void;
      onTestFileResult?(test: TestFileResult): void;
      onTestSuiteStart?(test: TestSuiteInfo): void;
      onTestSuiteResult?(result: TestResult): void;
      onTestCaseStart?(test: TestCaseInfo): void;
      onTestCaseResult?(result: TestResult): void;
      onTestRunEnd?(payload: {
        results: TestFileResult[]; // Test file results; in watch mode, accumulated over the whole session.
        testResults: TestResult[]; // Results of every test, not grouped by file.
        summary: {
          tests: {
            total: number; // Total number of tests.
            passed: number; // Number of passed tests.
            failed: number; // Number of failed tests.
            skipped: number; // Number of skipped tests.
            todo: number; // Number of todo tests.
          };
          files: {
            total: number; // Total number of test files.
            failed: number; // Number of failed test files.
          };
        };
        duration: {
          totalTime: number; // Total duration in milliseconds.
          buildTime: number; // Build duration in milliseconds.
          testTime: number; // Test execution duration in milliseconds.
        };
        snapshotSummary: SnapshotSummary;
        unhandledErrors: {
          name?: string; // Error class name.
          message: string; // Error message.
          stack?: string; // Serialized stack trace.
          diff?: string; // Formatted assertion diff.
          expected?: string; // Serialized expected assertion value.
          actual?: string; // Serialized actual assertion value.
          retryCount?: number; // Retry attempt that produced this error.
          fullStack?: boolean; // Whether to print the full stack trace.
        }[];
        coverage?: CoverageMapData; // Present when coverage is enabled.
        rerunTestPaths?: string[]; // Watch only: files executed in the current cycle.
        getSourcemap: (sourcePath: string) => Promise<SourceMapInput | null>; // Resolves the source map of a built file.
      }): void | Promise<void>;
      onUserConsoleLog?(log: UserConsoleLog): void;
      onExit?(): MaybePromise<void>;
    }

    Starting in v0.12.0, Rstest also calls onExit when the reporter's owning context is released after normal run, list, or merge completion, watch close, or startup failure. It continues to run on abnormal CLI exits. List contexts currently do not attach reporters, so they have no reporter hooks to call. Use onExit to release resources such as streams or renderers. Errors from the hook do not replace the operation's result or error.

    Metadata result fields are available on reporter results. JSON-serializable runtime metadata written through context.task.meta is available on the TestResult passed to onTestCaseResult. Metadata written by hooks through ctx.meta is available on the corresponding suite result in onTestSuiteResult, and file-level hook metadata is available on TestFileResult.meta in onTestFileResult.

    onTestRunEnd

    onTestRunEnd is called when a run cycle finishes, with the results and aggregates of that cycle. Starting in v0.12.0, the payload carries summary and rerunTestPaths, and unhandledErrors is always an array of serialized errors.

    In watch mode, results, testResults, and summary accumulate over the whole session: each file keeps its latest result, and deleted files are removed. rerunTestPaths lists the files executed in the current cycle and is absent for one-shot runs. Every entry in unhandledErrors is a plain object that can be serialized to JSON.

    TestResult

    TestResult and TestFileResult are exported from @rstest/core. Suite and case hooks receive a TestResult; file hooks and onTestRunEnd receive TestFileResult values, which extend TestResult with the results of the file's tests.

    • Type:
    type TestResultStatus = 'skip' | 'pass' | 'fail' | 'todo';
    
    interface TestResult {
      testId: string; // Task identifier.
      status: TestResultStatus; // Final result status.
      name: string; // Display name of the test, suite, or test file.
      testPath: string; // Path of the owning test file.
      parentNames?: string[]; // Names of the enclosing suites.
      duration?: number; // Execution duration in milliseconds.
      errors?: SerializedError[]; // Errors from the final attempt.
      retryErrors?: SerializedError[]; // Errors from earlier retry attempts.
      retryCount?: number; // Number of retry attempts performed.
      project: string; // Project name.
      meta?: TaskMeta; // Serializable metadata attached to the task.
      heap?: number; // Heap usage in bytes, sampled when `logHeapUsage` is enabled.
    }

    TestFileResult

    • Type:
    interface TestFileResult extends TestResult {
      results: TestResult[]; // Results of the tests declared in this file.
      snapshotResult?: SnapshotResult; // Snapshot statistics of this file.
      coverage?: Record<string, FileCoverageData>; // Per-file coverage data when coverage is enabled.
    }

    SerializedError has the same shape as the entries of unhandledErrors in the onTestRunEnd payload. SnapshotResult comes from @vitest/snapshot and FileCoverageData from istanbul-lib-coverage.

    Examples

    Simple custom reporter

    import type { Reporter } from '@rstest/core';
    
    const simpleReporter: Reporter = {
      onTestFileStart(test) {
        console.log(`📁 ${test.testPath}`);
      },
    
      onTestCaseResult(result) {
        const status = result.status === 'pass' ? '✅' : '❌';
        console.log(`${status} ${result.name}`);
      },
    
      onTestRunEnd({ results }) {
        const passed = results.filter((r) => r.status === 'pass').length;
        const failed = results.filter((r) => r.status === 'fail').length;
        console.log(`\n📊 ${passed} passed, ${failed} failed`);
      },
    };

    File output reporter

    import { writeFileSync } from 'node:fs';
    import type { Reporter } from '@rstest/core';
    
    const jsonReporter: Reporter = {
      onTestRunEnd({ results }) {
        const report = {
          timestamp: new Date().toISOString(),
          results: results.map((r) => ({
            path: r.testPath,
            status: r.status,
            duration: r.duration,
            errors: r.errors,
          })),
        };
    
        writeFileSync('test-report.json', JSON.stringify(report, null, 2));
      },
    };

    Reading runtime metadata

    import type { Reporter } from '@rstest/core';
    
    const metadataReporter: Reporter = {
      onTestCaseResult(result) {
        console.log(result.name, result.meta);
      },
    
      onTestSuiteResult(result) {
        console.log(result.name, result.meta);
      },
    
      onTestFileResult(result) {
        console.log(result.testPath, result.meta);
      },
    };