Repository: elastic/synthetics
Pull request #492 (2022-04-15 18:26:19)

feat: introduce new reporter interface
+ part of #482 
+ changes the reporter interface to not be based on Event Emitter pattern instead use a dedicated interface which makes it easier to capture errors that arise out of `UnhandledPromiseRejections`.
+ Gives a clean interface which lets users to extend the Reporter if required to create new ones like `HTMLReporter` which could be used by our recorder. 
```ts
export interface Reporter {
  onStart?(params: StartEvent): void;
  onJourneyRegister?(journey: Journey): void;
  onJourneyStart?(journey: Journey, result: JourneyStartResult): void;
  onStepStart?(journey: Journey, step: Step): void;
  onStepEnd?(journey: Journey, step: Step, result: StepEndResult): void;
  onJourneyEnd?(
    journey: Journey,
    result: JourneyEndResult
  ): void | Promise<void>;
  onEnd?(): void | Promise<void>;
}
```

### Testing
+ Checkout the code and build the code `npm run build`.
+ Play with different reporters. 
```sh
# Default one
node dist/cli.js examples/todos --config ./examples/todos/synthetics.config.ts

# Mimick HB reporter JSON
node dist/cli.js examples/todos --config ./examples/todos/synthetics.config.ts --rich-events

# Junit
node dist/cli.js examples/todos --config ./examples/todos/synthetics.config.ts --reporter junit
```
+ Test with HB by installing the build globally `npm i -g .`
+ Run any HB inline monitors to check if the new reporter changes are working correctly. 


apmmachine: 
## :broken_heart: Build Failed


<!-- BUILD BADGES-->
> _the below badges are clickable and redirect to their specific view in the CI or DOCS_
[![Pipeline View](https://img.shields.io/badge/pipeline-pipeline%20-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-492/detail/PR-492/1//pipeline) [![Test View](https://img.shields.io/badge/test-test-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-492/detail/PR-492/1//tests) [![Changes](https://img.shields.io/badge/changes-changes-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-492/detail/PR-492/1//changes) [![Artifacts](https://img.shields.io/badge/artifacts-artifacts-yellow)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-492/detail/PR-492/1//artifacts) [![preview](https://img.shields.io/badge/docs-preview-yellowgreen)](http://synthetics_492.docs-preview.app.elstc.co/diff)  [![preview](https://img.shields.io/badge/elastic-observability-blue)](https://ci-stats.elastic.co/app/apm/services/apm-ci/transactions/view?rangeFrom=2022-04-15T18:16:33.756Z&rangeTo=2022-04-15T18:36:33.756Z&transactionName=apm-agent-rum/elastic-synthetics/PR-{number}&transactionType=job&latencyAggregationType=avg&traceId=8c8f478c070a5d50c8757d56396c08fb&transactionId=55ab7d5ed5a97981)

<!-- BUILD SUMMARY-->
<details><summary>Expand to view the summary</summary>
<p>

#### Build stats




* Start Time: 2022-04-15T18:26:33.756+0000


* Duration: 5 min 45 sec





</p>
</details>

<!-- TEST RESULTS IF ANY-->


<!-- STEPS ERRORS IF ANY -->












### Steps errors [![1](https://img.shields.io/badge/1%20-red)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-rum%2Felastic-synthetics%2FPR-492/detail/PR-492/1//pipeline)
  <details><summary>Expand to view the steps failures</summary>
  <p>


  
  
  
  
##### `Checks linting`
<ul>
<li>Took 0 min 4 sec . View more details <a href="https://apm-ci.elastic.co//blue/rest/organizations/jenkins/pipelines/apm-agent-rum/pipelines/elastic-synthetics/pipelines/PR-492/runs/1/steps/184/log/?start=0">here</a></li>
<li>Description: <code>npm run-script lint</code></l1>
</ul>
  
  </p>
  </details>



## :robot: GitHub comments

To re-run your PR in the CI, just comment with:



- `/test` : Re-trigger the build.




<!--COMMENT_GENERATED_WITH_ID_comment.id-->

Patch:
diff --git a/src/common_types.ts b/src/common_types.ts
index 589ea49..4d49b3b 100644
--- a/src/common_types.ts
+++ b/src/common_types.ts
@@ -32,7 +32,7 @@ import {
   Page,
 } from 'playwright-chromium';
 import { Step } from './dsl';
-import { Reporter, reporters } from './reporters';
+import { BuiltInReporterName, ReporterInstance } from './reporters';
 
 export type VoidCallback = () => void;
 export type Location = {
@@ -48,7 +48,6 @@ export type HooksArgs = {
 };
 export type HooksCallback = (args: HooksArgs) => void;
 export type StatusValue = 'succeeded' | 'failed' | 'skipped';
-export type Reporters = keyof typeof reporters;
 
 export type NetworkConditions = {
   offline: boolean;
@@ -171,6 +170,8 @@ export type NetworkInfo = {
   };
 } & DefaultPluginOutput;
 
+export type PageMetrics = Record<string, number>;
+
 export type BrowserMessage = {
   text: string;
   type: string;
@@ -203,7 +204,7 @@ type BaseArgs = {
 
 export type CliArgs = BaseArgs & {
   config?: string;
-  reporter?: Reporters;
+  reporter?: BuiltInReporterName;
   pattern?: string;
   inline?: boolean;
   require?: Array<string>;
@@ -224,7 +225,7 @@ export type RunOptions = BaseArgs & {
   environment?: string;
   playwrightOptions?: PlaywrightOptions;
   networkConditions?: NetworkConditions;
-  reporter?: CliArgs['reporter'] | Reporter;
+  reporter?: BuiltInReporterName | ReporterInstance;
 };
 
 export type PlaywrightOptions = LaunchOptions & BrowserContextOptions;
@@ -232,3 +233,45 @@ export type SyntheticsConfig = {
   params?: Params;
   playwrightOptions?: PlaywrightOptions;
 };
+
+/** Runner Payload types */
+export type JourneyResult = {
+  status: StatusValue;
+  error?: Error;
+  networkinfo?: PluginOutput['networkinfo'];
+  browserconsole?: PluginOutput['browserconsole'];
+};
+
+export type StepResult = {
+  status: StatusValue;
+  url?: string;
+  error?: Error;
+  pagemetrics?: PageMetrics;
+  filmstrips?: PluginOutput['filmstrips'];
+  metrics?: PluginOutput['metrics'];
+  traces?: PluginOutput['traces'];
+};
+
+/** Reporter and Runner contract */
+export type StartEvent = {
+  numJourneys: number;
+  networkConditions?: NetworkConditions;
+};
+
+export type JourneyStartResult = {
+  timestamp: number;
+  params?: Params;
+};
+
+export type JourneyEndResult = JourneyStartResult &
+  JourneyResult & {
+    start: number;
+    end: number;
+    options: RunOptions;
+    timestamp: number;
+  };
+
+export type StepEndResult = StepResult & {
+  start: number;
+  end: number;
+};
diff --git a/src/core/runner.ts b/src/core/runner.ts
index 89502da..89f4a99 100644
--- a/src/core/runner.ts
+++ b/src/core/runner.ts
@@ -23,12 +23,11 @@
  *
  */
 
-import { once, EventEmitter } from 'events';
 import { join } from 'path';
 import { rm } from 'fs/promises';
 import { Journey } from '../dsl/journey';
 import { Step } from '../dsl/step';
-import { reporters } from '../reporters';
+import { reporters, Reporter } from '../reporters';
 import {
   CACHE_PATH,
   monotonicTimeInSeconds,
@@ -39,17 +38,16 @@ import {
   writeFileAsync,
 } from '../helpers';
 import {
-  StatusValue,
   HooksCallback,
   Params,
-  NetworkConditions,
-  PluginOutput,
   HooksArgs,
   Driver,
   Screenshot,
   RunOptions,
+  JourneyResult,
+  StepResult,
 } from '../common_types';
-import { PageMetrics, PluginManager } from '../plugins';
+import { PluginManager } from '../plugins';
 import { PerformanceManager } from '../plugins';
 import { Gatherer } from './gatherer';
 import { log } from './logger';
@@ -64,70 +62,12 @@ type JourneyContext = {
   pluginManager: PluginManager;
 };
 
-type StepResult = {
-  status: StatusValue;
-  url?: string;
-  error?: Error;
-  pagemetrics?: PageMetrics;
-  filmstrips?: PluginOutput['filmstrips'];
-  metrics?: PluginOutput['metrics'];
-  traces?: PluginOutput['traces'];
-};
-
-type JourneyResult = {
-  status: StatusValue;
-  error?: Error;
-  networkinfo?: PluginOutput['networkinfo'];
-  browserconsole?: PluginOutput['browserconsole'];
-};
-
 type RunResult = Record<string, JourneyResult>;
 
-interface StepEvent {
-  step: Step;
-  journey: Journey;
-}
-
-interface JourneyEvent {
-  journey: Journey;
-  timestamp: number;
-  params?: Params;
-}
-
-interface Events {
-  start: {
-    numJourneys: number;
-    networkConditions?: NetworkConditions;
-  };
-  'journey:register': {
-    journey: Journey;
-  };
-  'journey:start': JourneyEvent;
-  'journey:end': JourneyEvent &
-    JourneyResult & {
-      start: number;
-      end: number;
-      options: RunOptions;
-      timestamp: number;
-    };
-  'journey:end:reported': unknown;
-  'step:start': StepEvent;
-  'step:end': StepEvent &
-    StepResult & {
-      start: number;
-      end: number;
-    };
-  end: unknown;
-}
-
-export default interface Runner {
-  on<K extends keyof Events>(name: K, listener: (v: Events[K]) => void): this;
-  emit<K extends keyof Events>(event: K, args: Events[K]): boolean;
-}
-
-export default class Runner extends EventEmitter {
-  active = false;
-  currentJourney?: Journey = null;
+export default class Runner {
+  #active = false;
+  #reporter: Reporter;
+  #currentJourney?: Journey = null;
   journeys: Journey[] = [];
   hooks: SuiteHooks = { beforeAll: [], afterAll: [] };
   hookError: Error | undefined;
@@ -185,9 +125,26 @@ export default class Runner extends EventEmitter {
     this.hooks[type].push(callback);
   }
 
+  get currentJourney() {
+    return this.#currentJourney;
+  }
+
   addJourney(journey: Journey) {
     this.journeys.push(journey);
-    this.currentJourney = journey;
+    this.#currentJourney = journey;
+  }
+
+  setReporter(options: RunOptions) {
+    /**
+     * Set up the corresponding reporter and fallback
+     * to default reporter if not provided
+     */
+    const { reporter, outfd } = options;
+    const Reporter =
+      typeof reporter === 'function'
+        ? reporter
+        : reporters[reporter] || reporters['default'];
+    this.#reporter = new Reporter({ fd: outfd });
   }
 
   async runBeforeAllHook(args: HooksArgs) {
@@ -288,7 +245,7 @@ export default class Runner extends EventEmitter {
     let skipStep = false;
     for (const step of journey.steps) {
       const start = monotonicTimeInSeconds();
-      this.emit('step:start', { journey, step });
+      this.#reporter?.onStepStart?.(journey, step);
       let data: StepResult = { status: 'succeeded' };
       if (skipStep) {
         data.status = 'skipped';
@@ -299,9 +256,7 @@ export default class Runner extends EventEmitter {
          */
         if (data.error) skipStep = true;
       }
-      this.emit('step:end', {
-        journey,
-        step,
+      this.#reporter?.onStepEnd?.(journey, step, {
         start,
         end: monotonicTimeInSeconds(),
         ...data,
@@ -315,12 +270,16 @@ export default class Runner extends EventEmitter {
   }
 
   registerJourney(journey: Journey, context: JourneyContext) {
-    this.currentJourney = journey;
+    this.#currentJourney = journey;
     const timestamp = getTimestamp();
     const { params } = context;
-    this.emit('journey:start', { journey, timestamp, params });
+    this.#reporter?.onJourneyStart?.(journey, {
+      timestamp,
+      params,
+    });
     /**
-     * Load and register the steps for the current journey
+     * Exeucute the journey callback which would
+     * register the steps for the current journey
      */
     journey.callback({ ...context.driver, params });
   }
@@ -333,8 +292,7 @@ export default class Runner extends EventEmitter {
     const end = monotonicTimeInSeconds();
     const { pluginManager, start, status, error } = result;
     const pluginOutput = await pluginManager.output();
-    this.emit('journey:end', {
-      journey,
+    await this.#reporter?.onJourneyEnd?.(journey, {
       status,
       error,
       start,
@@ -344,13 +302,6 @@ export default class Runner extends EventEmitter {
       ...pluginOutput,
       browserconsole: status == 'failed' ? pluginOutput.browserconsole : [],
     });
-    /**
-     * Wait for the all the reported events to be consumed aschronously
-     * by reporter.
-     */
-    if (options.reporter === 'json') {
-      await once(this, 'journey:end:reported');
-    }
     // clear screenshots cache after each journey
     await rm(Runner.screenshotPath, { recursive: true, force: true });
   }
@@ -360,8 +311,7 @@ export default class Runner extends EventEmitter {
    */
   async runFakeJourney(journey: Journey, options: RunOptions) {
     const start = monotonicTimeInSeconds();
-    this.emit('journey:start', {
-      journey,
+    this.#reporter.onJourneyStart?.(journey, {
       timestamp: getTimestamp(),
       params: options.params,
     });
@@ -369,24 +319,18 @@ export default class Runner extends EventEmitter {
       status: 'failed',
       error: this.hookError,
     };
-    this.emit('journey:end', {
-      journey,
+    await this.#reporter.onJourneyEnd?.(journey, {
       timestamp: getTimestamp(),
       start,
       options,
       end: monotonicTimeInSeconds(),
       ...result,
     });
-    if (options.reporter === 'json') {
-      await once(this, 'journey:end:reported');
-    }
     return result;
   }
 
   async runJourney(journey: Journey, options: RunOptions) {
-    const result: JourneyResult = {
-      status: 'succeeded',
-    };
+    const result: JourneyResult = { status: 'succeeded' };
     const context = await Runner.createContext(options);
     log(`Runner: start journey (${journey.name})`);
     try {
@@ -419,18 +363,10 @@ export default class Runner extends EventEmitter {
   }
 
   async init(options: RunOptions) {
-    const { reporter, outfd, networkConditions } = options;
-    /**
-     * Set up the corresponding reporter and fallback
-     */
-    const Reporter =
-      typeof reporter === 'function'
-        ? reporter
-        : reporters[reporter] || reporters['default'];
-    new Reporter(this, { fd: outfd });
-    this.emit('start', {
+    this.setReporter(options);
+    this.#reporter.onStart?.({
       numJourneys: this.journeys.length,
-      networkConditions,
+      networkConditions: options.networkConditions,
     });
     /**
      * Set up the directory for caching screenshots
@@ -440,10 +376,10 @@ export default class Runner extends EventEmitter {
 
   async run(options: RunOptions) {
     const result: RunResult = {};
-    if (this.active) {
+    if (this.#active) {
       return result;
     }
-    this.active = true;
+    this.#active = true;
     log(`Runner: run ${this.journeys.length} journeys`);
     this.init(options);
     await this.runBeforeAllHook({
@@ -457,7 +393,7 @@ export default class Runner extends EventEmitter {
        * Used by heartbeat to gather all registered journeys
        */
       if (dryRun) {
-        this.emit('journey:register', { journey });
+        this.#reporter.onJourneyRegister?.(journey);
         continue;
       }
       if (!journey.isMatch(match, tags)) {
@@ -478,14 +414,14 @@ export default class Runner extends EventEmitter {
   }
 
   async reset() {
+    this.#currentJourney = null;
+    this.journeys = [];
+    this.#active = false;
     /**
      * Clear all cache data stored for post processing by
      * the current synthetic agent run
      */
     await rm(CACHE_PATH, { recursive: true, force: true });
-    this.currentJourney = null;
-    this.journeys = [];
-    this.active = false;
-    this.emit('end', {});
+    await this.#reporter?.onEnd?.();
   }
 }
diff --git a/src/plugins/performance.ts b/src/plugins/performance.ts
index 7a609ae..0c3b921 100644
--- a/src/plugins/performance.ts
+++ b/src/plugins/performance.ts
@@ -23,7 +23,7 @@
  *
  */
 
-import { Driver } from '../common_types';
+import { Driver, PageMetrics } from '../common_types';
 
 const supportedMetrics = new Set<string>([
   'Timestamp',
@@ -41,22 +41,6 @@ const supportedMetrics = new Set<string>([
   'JSHeapTotalSize',
 ]);
 
-export interface PageMetrics {
-  Timestamp?: number;
-  Documents?: number;
-  Frames?: number;
-  JSEventListeners?: number;
-  Nodes?: number;
-  LayoutCount?: number;
-  RecalcStyleCount?: number;
-  LayoutDuration?: number;
-  RecalcStyleDuration?: number;
-  ScriptDuration?: number;
-  TaskDuration?: number;
-  JSHeapUsedSize?: number;
-  JSHeapTotalSize?: number;
-}
-
 export class PerformanceManager {
   constructor(private driver: Driver) {}
 
diff --git a/src/reporters/base.ts b/src/reporters/base.ts
index f264342..fdd9493 100644
--- a/src/reporters/base.ts
+++ b/src/reporters/base.ts
@@ -25,7 +25,6 @@
 
 import SonicBoom from 'sonic-boom';
 import { green, red, cyan } from 'kleur/colors';
-import Runner from '../core/runner';
 import {
   symbols,
   indent,
@@ -33,7 +32,13 @@ import {
   findPWLogsIndexes,
   rewriteErrorStack,
 } from '../helpers';
-import { ReporterOptions } from './reporter';
+import { Reporter, ReporterOptions } from '.';
+import {
+  JourneyEndResult,
+  JourneyStartResult,
+  StepEndResult,
+} from '../common_types';
+import { Journey, Step } from '../dsl';
 
 function renderError(error) {
   let output = '';
@@ -58,11 +63,16 @@ function renderDuration(durationMs) {
   return parseInt(durationMs);
 }
 
-export default class BaseReporter {
+export default class BaseReporter implements Reporter {
   stream: SonicBoom;
   fd: number;
+  metrics = {
+    succeeded: 0,
+    failed: 0,
+    skipped: 0,
+  };
 
-  constructor(public runner: Runner, options: ReporterOptions = {}) {
+  constructor(options: ReporterOptions = {}) {
     this.fd = options.fd || process.stdout.fd;
     /**
      * minLength is set to 1 byte to make sure we flush the
@@ -70,66 +80,57 @@ export default class BaseReporter {
      * before destroying the pipe with underlying file descriptor
      */
     this.stream = new SonicBoom({ fd: this.fd, sync: true, minLength: 1 });
-
-    // flushSync is used here to make sure all the data from the underlying
-    // SonicBoom stream buffer is completely written to the fd before closing
-    // the process
-    this.runner.on('end', () => this.stream.flushSync());
-
-    this._registerListeners();
   }
 
-  _registerListeners() {
-    const result = {
-      succeeded: 0,
-      failed: 0,
-      skipped: 0,
-    };
-
-    this.runner.on('journey:start', ({ journey }) => {
-      this.write(`\nJourney: ${journey.name}`);
-    });
-
-    this.runner.on('step:end', ({ step, start, end, error, status }) => {
-      const message = `${symbols[status]}  Step: '${
-        step.name
-      }' ${status} (${renderDuration((end - start) * 1000)} ms)`;
-      this.write(indent(message));
-      error && this.write(renderError(error));
-      result[status]++;
-    });
-
-    this.runner.on('journey:end', ({ error }) => {
-      const { failed, succeeded, skipped } = result;
-      const total = failed + succeeded + skipped;
+  onJourneyStart(journey: Journey, {}: JourneyStartResult) {
+    this.write(`\nJourney: ${journey.name}`);
+  }
 
-      /**
-       * Render the error on the terminal only when no steps could
-       * be executed which means the error happened in one of the hooks
-       */
-      if (total === 0 && error) {
-        this.write(renderError(error));
-      }
-    });
+  onStepEnd(journey: Journey, step: Step, result: StepEndResult) {
+    const { status, end, start, error } = result;
+    const message = `${symbols[status]}  Step: '${
+      step.name
+    }' ${status} (${renderDuration((end - start) * 1000)} ms)`;
+    this.write(indent(message));
+    error && this.write(renderError(error));
+    this.metrics[status]++;
+  }
 
-    this.runner.on('end', () => {
-      const { failed, succeeded, skipped } = result;
-      const total = failed + succeeded + skipped;
+  /* eslint-disable @typescript-eslint/no-unused-vars */
+  onJourneyEnd(journey: Journey, { error }: JourneyEndResult) {
+    const { failed, succeeded, skipped } = this.metrics;
+    const total = failed + succeeded + skipped;
+    /**
+     * Render the error on the terminal only when no steps could
+     * be executed which means the error happened in one of the hooks
+     */
+    if (total === 0 && error) {
+      this.write(renderError(error));
+    }
+  }
 
-      let message = '\n';
-      if (total === 0) {
-        message = 'No tests found!';
-        message += ` (${renderDuration(now())} ms) \n`;
-        this.write(message);
-        return;
-      }
+  onEnd() {
+    const { failed, succeeded, skipped } = this.metrics;
+    const total = failed + succeeded + skipped;
 
-      message += succeeded > 0 ? green(` ${succeeded} passed`) : '';
-      message += failed > 0 ? red(` ${failed} failed`) : '';
-      message += skipped > 0 ? cyan(` ${skipped} skipped`) : '';
+    let message = '\n';
+    if (total === 0) {
+      message = 'No tests found!';
       message += ` (${renderDuration(now())} ms) \n`;
       this.write(message);
-    });
+      return;
+    }
+
+    message += succeeded > 0 ? green(` ${succeeded} passed`) : '';
+    message += failed > 0 ? red(` ${failed} failed`) : '';
+    message += skipped > 0 ? cyan(` ${skipped} skipped`) : '';
+    message += ` (${renderDuration(now())} ms) \n`;
+    this.write(message);
+
+    // flushSync is used here to make sure all the data from the underlying
+    // SonicBoom stream buffer is completely written to the fd before closing
+    // the process
+    this.stream.flushSync();
   }
 
   write(message) {
diff --git a/src/reporters/index.ts b/src/reporters/index.ts
index 7f5cc0b..7b9f9b3 100644
--- a/src/reporters/index.ts
+++ b/src/reporters/index.ts
@@ -26,11 +26,34 @@
 import BaseReporter from './base';
 import JSONReporter from './json';
 import JUnitReporter from './junit';
-import Reporter, { ReporterOptions } from './reporter';
+import { Journey, Step } from '../dsl';
+import {
+  StartEvent,
+  JourneyEndResult,
+  JourneyStartResult,
+  StepEndResult,
+} from '../common_types';
 
-export { Reporter, ReporterOptions };
-export const reporters = {
+export type ReporterOptions = { fd?: number; colors?: boolean };
+export type BuiltInReporterName = 'default' | 'json' | 'junit';
+export type ReporterInstance = new (opts: ReporterOptions) => Reporter;
+export const reporters: {
+  [key in BuiltInReporterName]: ReporterInstance;
+} = {
   default: BaseReporter,
   json: JSONReporter,
   junit: JUnitReporter,
 };
+
+export interface Reporter {
+  onStart?(params: StartEvent): void;
+  onJourneyRegister?(journey: Journey): void;
+  onJourneyStart?(journey: Journey, result: JourneyStartResult): void;
+  onStepStart?(journey: Journey, step: Step): void;
+  onStepEnd?(journey: Journey, step: Step, result: StepEndResult): void;
+  onJourneyEnd?(
+    journey: Journey,
+    result: JourneyEndResult
+  ): void | Promise<void>;
+  onEnd?(): void | Promise<void>;
+}
diff --git a/src/reporters/json.ts b/src/reporters/json.ts
index a89407c..3b6dad2 100644
--- a/src/reporters/json.ts
+++ b/src/reporters/json.ts
@@ -47,8 +47,12 @@ import {
   PerfMetrics,
   Params,
   Screenshot,
+  StartEvent,
+  JourneyStartResult,
+  StepEndResult,
+  JourneyEndResult,
+  PageMetrics,
 } from '../common_types';
-import { PageMetrics } from '../plugins';
 
 /* eslint-disable @typescript-eslint/no-var-requires */
 const { version, name } = require('../../package.json');
@@ -296,179 +300,183 @@ export async function gatherScreenshots(
 }
 
 export default class JSONReporter extends BaseReporter {
-  override _registerListeners() {
+  onStart(params: StartEvent) {
     /**
      * report the number of journeys that exists on a suite which
      * could be used for better sharding
      */
-    this.runner.on('start', ({ numJourneys, networkConditions }) => {
-      this.writeJSON({
-        type: 'synthetics/metadata',
-        root_fields: {
-          num_journeys: numJourneys,
-        },
-        payload: networkConditions
-          ? {
-              network_conditions: networkConditions,
-            }
-          : undefined,
-      });
+    this.writeJSON({
+      type: 'synthetics/metadata',
+      root_fields: {
+        num_journeys: params.numJourneys,
+      },
+      payload: params.networkConditions
+        ? {
+            network_conditions: params.networkConditions,
+          }
+        : undefined,
     });
+  }
 
-    this.runner.on('journey:register', ({ journey }) => {
-      this.writeJSON({
-        type: 'journey/register',
-        journey,
-      });
+  onJourneyRegister(journey: Journey): void {
+    this.writeJSON({
+      type: 'journey/register',
+      journey,
     });
+  }
 
-    this.runner.on('journey:start', ({ journey, timestamp, params }) => {
-      this.writeJSON({
-        type: 'journey/start',
-        journey,
-        timestamp,
-        payload: { params, source: journey.callback.toString() },
-      });
+  override onJourneyStart(
+    journey: Journey,
+    { timestamp, params }: JourneyStartResult
+  ) {
+    this.writeJSON({
+      type: 'journey/start',
+      journey,
+      timestamp,
+      payload: { params, source: journey.callback.toString() },
     });
+  }
 
-    this.runner.on(
-      'step:end',
-      ({
-        journey,
-        step,
-        start,
-        end,
-        error,
-        url,
-        status,
-        pagemetrics,
-        traces,
-        metrics,
-        filmstrips,
-      }) => {
-        this.writeMetrics(journey, step, 'relative_trace', traces);
-        this.writeMetrics(journey, step, 'experience', metrics);
-        if (filmstrips) {
-          // Write each filmstrip separately so that we don't get documents that are too large
-          filmstrips.forEach((strip, index) => {
-            this.writeJSON({
-              type: 'step/filmstrips',
-              journey,
-              step,
-              payload: { index },
-              root_fields: {
-                browser: { relative_trace: { start: strip.start } },
-              },
-              blob: strip.blob,
-              blob_mime: strip.mime,
-            });
-          });
-        }
+  override onStepEnd(
+    journey: Journey,
+    step: Step,
+    {
+      start,
+      end,
+      error,
+      url,
+      status,
+      pagemetrics,
+      traces,
+      metrics,
+      filmstrips,
+    }: StepEndResult
+  ) {
+    this.writeMetrics(journey, step, 'relative_trace', traces);
+    this.writeMetrics(journey, step, 'experience', metrics);
+    if (filmstrips) {
+      // Write each filmstrip separately so that we don't get documents that are too large
+      filmstrips.forEach((strip, index) => {
         this.writeJSON({
-          type: 'step/end',
+          type: 'step/filmstrips',
           journey,
-          step: {
-            ...step,
-            duration: {
-              us: getDurationInUs(end - start),
-            },
-          },
-          url,
-          error,
-          payload: {
-            source: step.callback.toString(),
-            url,
-            status,
-            pagemetrics,
+          step,
+          payload: { index },
+          root_fields: {
+            browser: { relative_trace: { start: strip.start } },
           },
+          blob: strip.blob,
+          blob_mime: strip.mime,
         });
-      }
-    );
+      });
+    }
 
-    this.runner.on(
-      'journey:end',
-      async ({
-        journey,
-        timestamp,
-        start,
-        end,
-        networkinfo,
-        browserconsole,
+    this.writeJSON({
+      type: 'step/end',
+      journey,
+      step: {
+        ...step,
+        duration: {
+          us: getDurationInUs(end - start),
+        },
+      },
+      url,
+      error,
+      payload: {
+        source: step.callback.toString(),
+        url,
         status,
-        error,
-        options,
-      }) => {
-        const { ssblocks, screenshots } = options;
-        const writeScreenshots =
-          screenshots === 'on' ||
-          (screenshots === 'only-on-failure' && status === 'failed');
-        if (writeScreenshots) {
-          await gatherScreenshots(
-            join(CACHE_PATH, 'screenshots'),
-            async screenshot => {
-              const { data, timestamp, step } = screenshot;
-              if (!data) {
-                return;
-              }
-              if (ssblocks) {
-                await this.writeScreenshotBlocks(journey, screenshot);
-              } else {
-                this.writeJSON({
-                  type: 'step/screenshot',
-                  timestamp,
-                  journey,
-                  step,
-                  blob: data,
-                  blob_mime: 'image/jpeg',
-                });
-              }
-            }
-          );
-        }
+        pagemetrics,
+      },
+    });
+  }
 
-        if (networkinfo) {
-          networkinfo.forEach(ni => {
-            const { ecs, payload } = formatNetworkFields(ni);
-            this.writeJSON({
-              type: 'journey/network_info',
-              journey,
-              timestamp: ni.timestamp,
-              root_fields: snakeCaseKeys(ecs),
-              step: ni.step,
-              payload: snakeCaseKeys(payload),
-            });
-          });
-        }
-        if (browserconsole) {
-          browserconsole.forEach(({ timestamp, text, type, step, error }) => {
+  override async onJourneyEnd(
+    journey: Journey,
+    {
+      timestamp,
+      start,
+      end,
+      networkinfo,
+      browserconsole,
+      status,
+      error,
+      options,
+    }: JourneyEndResult
+  ) {
+    const { ssblocks, screenshots } = options;
+    const writeScreenshots =
+      screenshots === 'on' ||
+      (screenshots === 'only-on-failure' && status === 'failed');
+    if (writeScreenshots) {
+      await gatherScreenshots(
+        join(CACHE_PATH, 'screenshots'),
+        async screenshot => {
+          const { data, timestamp, step } = screenshot;
+          if (!data) {
+            return;
+          }
+          if (ssblocks) {
+            await this.writeScreenshotBlocks(journey, screenshot);
+          } else {
             this.writeJSON({
-              type: 'journey/browserconsole',
-              journey,
+              type: 'step/screenshot',
               timestamp,
+              journey,
               step,
-              error,
-              payload: {
-                text,
-                type,
-              } as Payload,
+              blob: data,
+              blob_mime: 'image/jpeg',
             });
-          });
+          }
         }
+      );
+    }
 
+    if (networkinfo) {
+      networkinfo.forEach(ni => {
+        const { ecs, payload } = formatNetworkFields(ni);
         this.writeJSON({
-          type: 'journey/end',
+          type: 'journey/network_info',
+          journey,
+          timestamp: ni.timestamp,
+          root_fields: snakeCaseKeys(ecs),
+          step: ni.step,
+          payload: snakeCaseKeys(payload),
+        });
+      });
+    }
+
+    if (browserconsole) {
+      browserconsole.forEach(({ timestamp, text, type, step, error }) => {
+        this.writeJSON({
+          type: 'journey/browserconsole',
           journey,
           timestamp,
+          step,
           error,
           payload: {
-            start,
-            end,
-            status,
-          },
+            text,
+            type,
+          } as Payload,
         });
-        process.nextTick(() => this.runner.emit('journey:end:reported', {}));
-      }
-    );
+      });
+    }
+
+    this.writeJSON({
+      type: 'journey/end',
+      journey,
+      timestamp,
+      error,
+      payload: {
+        start,
+        end,
+        status,
+      },
+    });
+  }
+
+  override onEnd() {
+    this.stream.flushSync();
   }
 
   async writeScreenshotBlocks(journey: Journey, screenshot: Screenshot) {
diff --git a/src/reporters/junit.ts b/src/reporters/junit.ts
index e9eda58..1fad13a 100644
--- a/src/reporters/junit.ts
+++ b/src/reporters/junit.ts
@@ -23,8 +23,14 @@
  *
  */
 
-import { mkdirSync, writeFileSync } from 'fs';
+import { mkdir, writeFile } from 'fs/promises';
 import { dirname } from 'path';
+import {
+  JourneyEndResult,
+  JourneyStartResult,
+  StepEndResult,
+} from '../common_types';
+import { Journey, Step } from '../dsl';
 import { formatError, indent, now } from '../helpers';
 import BaseReporter from './base';
 
@@ -51,104 +57,103 @@ export default class JUnitReporter extends BaseReporter {
   private totalTests = 0;
   private totalFailures = 0;
   private totalSkipped = 0;
+  #journeyMap = new Map<string, XMLEntry>();
 
-  override _registerListeners() {
-    const journeyMap = new Map<string, XMLEntry>();
+  override onJourneyStart(journey: Journey, {}: JourneyStartResult) {
+    if (!this.#journeyMap.has(journey.name)) {
+      const entry = {
+        name: 'testsuite',
+        attributes: {
+          name: journey.name,
+          tests: 0,
+          failures: 0,
+          skipped: 0,
+          errors: 0,
+        },
+        children: [],
+      };
+      this.#journeyMap.set(journey.name, entry);
+    }
+  }
 
-    this.runner.on('journey:start', ({ journey }) => {
-      if (!journeyMap.has(journey.name)) {
-        const entry = {
-          name: 'testsuite',
-          attributes: {
-            name: journey.name,
-            tests: 0,
-            failures: 0,
-            skipped: 0,
-            errors: 0,
-          },
-          children: [],
-        };
-        journeyMap.set(journey.name, entry);
-      }
-    });
+  override onStepEnd(
+    journey: Journey,
+    step: Step,
+    { status, error, start, end }: StepEndResult
+  ) {
+    if (!this.#journeyMap.has(journey.name)) {
+      return;
+    }
+    const entry = this.#journeyMap.get(journey.name);
+    const caseEntry = {
+      name: 'testcase',
+      attributes: {
+        name: step.name,
+        classname: journey.name + ' ' + step.name,
+        time: end - start,
+      },
+      children: [],
+    };
 
-    this.runner.on(
-      'step:end',
-      ({ journey, step, status, error, start, end }) => {
-        if (!journeyMap.has(journey.name)) {
-          return;
-        }
-        const entry = journeyMap.get(journey.name);
-        const caseEntry = {
-          name: 'testcase',
-          attributes: {
-            name: step.name,
-            classname: journey.name + ' ' + step.name,
-            time: end - start,
-          },
-          children: [],
-        };
+    entry.attributes.tests++;
+    if (status === 'failed') {
+      const { name, message, stack } = formatError(error);
+      caseEntry.children.push({
+        name: 'failure',
+        attributes: {
+          message,
+          type: name,
+        },
+        text: stack,
+      });
+      entry.attributes.failures++;
+    } else if (status === 'skipped') {
+      caseEntry.children.push({
+        name: 'skipped',
+        attributes: {
+          message: 'previous step failed',
+        },
+      });
+      entry.attributes.skipped++;
+    }
+    entry.children.push(caseEntry);
+  }
 
-        entry.attributes.tests++;
-        if (status === 'failed') {
-          const { name, message, stack } = formatError(error);
-          caseEntry.children.push({
-            name: 'failure',
-            attributes: {
-              message,
-              type: name,
-            },
-            text: stack,
-          });
-          entry.attributes.failures++;
-        } else if (status === 'skipped') {
-          caseEntry.children.push({
-            name: 'skipped',
-            attributes: {
-              message: 'previous step failed',
-            },
-          });
-          entry.attributes.skipped++;
-        }
-        entry.children.push(caseEntry);
-      }
-    );
+  override onJourneyEnd(journey: Journey, {}: JourneyEndResult) {
+    if (!this.#journeyMap.has(journey.name)) {
+      return;
+    }
+    const { attributes } = this.#journeyMap.get(journey.name);
+    this.totalTests += attributes.tests;
+    this.totalFailures += attributes.failures;
+    this.totalSkipped += attributes.skipped;
+  }
 
-    this.runner.on('journey:end', ({ journey }) => {
-      if (!journeyMap.has(journey.name)) {
-        return;
-      }
-      const { attributes } = journeyMap.get(journey.name);
-      this.totalTests += attributes.tests;
-      this.totalFailures += attributes.failures;
-      this.totalSkipped += attributes.skipped;
-    });
+  override async onEnd() {
+    const root: XMLEntry = {
+      name: 'testsuites',
+      attributes: {
+        name: '',
+        tests: this.totalTests,
+        failures: this.totalFailures,
+        skipped: this.totalSkipped,
+        errors: 0,
+        time: parseInt(String(now())) / 1000,
+      },
+      children: [...this.#journeyMap.values()],
+    };
 
-    this.runner.on('end', () => {
-      const root: XMLEntry = {
-        name: 'testsuites',
-        attributes: {
-          name: '',
-          tests: this.totalTests,
-          failures: this.totalFailures,
-          skipped: this.totalSkipped,
-          errors: 0,
-          time: parseInt(String(now())) / 1000,
-        },
-        children: [...journeyMap.values()],
-      };
-      const output = serializeEntries(root).join('\n');
-      /**
-       * write the xml output to a file if specified via env flag
-       */
-      const fileName = process.env['SYNTHETICS_JUNIT_FILE'];
-      if (fileName) {
-        mkdirSync(dirname(fileName), { recursive: true });
-        writeFileSync(fileName, output);
-      } else {
-        this.write(output);
-      }
-    });
+    const output = serializeEntries(root).join('\n');
+    /**
+     * write the xml output to a file if specified via env flag
+     */
+    const fileName = process.env['SYNTHETICS_JUNIT_FILE'];
+    if (fileName) {
+      await mkdir(dirname(fileName), { recursive: true });
+      await writeFile(fileName, output);
+    } else {
+      this.write(output);
+    }
   }
 }
 
diff --git a/src/reporters/reporter.ts b/src/reporters/reporter.ts
deleted file mode 100644
index dbe5c7b..0000000
--- a/src/reporters/reporter.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) 2020-present, Elastic NV
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- */
-
-import Runner from '../core/runner';
-
-export type ReporterOptions = {
-  fd?: number;
-  colors?: boolean;
-};
-
-export default interface Reporter {
-  new (runner: Runner, options: ReporterOptions): any;
-}


Test patch:
diff --git a/__tests__/core/runner.test.ts b/__tests__/core/runner.test.ts
index 2980ed7..76c1de0 100644
--- a/__tests__/core/runner.test.ts
+++ b/__tests__/core/runner.test.ts
@@ -27,41 +27,47 @@ import fs from 'fs';
 import { Gatherer } from '../../src/core/gatherer';
 import Runner from '../../src/core/runner';
 import { step, journey } from '../../src/core';
-import { Journey } from '../../src/dsl';
+import { Journey, Step } from '../../src/dsl';
 import { Server } from '../utils/server';
-import { generateTempPath } from '../../src/helpers';
+import { generateTempPath, noop } from '../../src/helpers';
 import { wsEndpoint } from '../utils/test-config';
+import { Reporter } from '../../src/reporters';
+import {
+  JourneyEndResult,
+  JourneyStartResult,
+  RunOptions,
+  StartEvent,
+  StepEndResult,
+} from '../../src/common_types';
 
 describe('runner', () => {
-  let runner: Runner, server: Server;
-  const noop = () => {};
-  const dest = generateTempPath();
+  let runner: Runner,
+    server: Server,
+    dest: string,
+    defaultRunOptions: RunOptions = {};
+
+  beforeAll(async () => (server = await Server.create()));
   beforeEach(async () => {
     runner = new Runner();
+    dest = generateTempPath();
+    defaultRunOptions = {
+      wsEndpoint,
+      outfd: fs.openSync(dest, 'w'),
+    };
   });
-  beforeAll(async () => {
-    server = await Server.create();
-  });
-  afterAll(async () => {
-    await server.close();
-    fs.unlinkSync(dest);
-  });
-
-  it('support emitting/subscribing to events', () => {
-    runner.on('start', ({ numJourneys }) => {
-      expect(numJourneys).toBe(1);
-    });
-    runner.emit('start', { numJourneys: 1 });
+  afterEach(() => {
+    try {
+      fs.accessSync(dest);
+      fs.unlinkSync(dest);
+    } catch (_) {}
   });
+  afterAll(async () => await server.close());
 
   it('add journeys', () => {
     const j1 = new Journey({ name: 'j1' }, noop);
     const j2 = new Journey({ name: 'j2' }, noop);
-    expect(runner.currentJourney).toBeNull();
     runner.addJourney(j1);
-    expect(runner.currentJourney).toEqual(j1);
     runner.addJourney(j2);
-    expect(runner.currentJourney).toEqual(j2);
     expect(runner.journeys.length).toBe(2);
   });
 
@@ -72,51 +78,54 @@ describe('runner', () => {
     expect(runner.hooks.afterAll).toEqual([noop]);
   });
 
-  it('run journey - with events payload', async () => {
-    const journey = new Journey({ name: 'j1' }, noop);
-    const step = journey.addStep('step1', noop);
-    runner.on('journey:start', event => {
-      expect(event).toMatchObject({
-        journey,
-        timestamp: expect.any(Number),
-      });
-    });
-    runner.on('step:start', event => {
-      expect(event).toMatchObject({
-        journey,
-        step,
-      });
-    });
-    runner.on('step:end', event => {
-      expect(event).toMatchObject({
-        journey,
-        step,
-        status: 'succeeded',
-        url: 'about:blank',
-        start: expect.any(Number),
-        end: expect.any(Number),
-      });
-    });
-    runner.on('journey:end', event => {
-      expect(event).toMatchObject({
-        journey,
-        status: 'succeeded',
-        start: expect.any(Number),
-        end: expect.any(Number),
-      });
-    });
-    const result = await runner.runJourney(journey, { wsEndpoint });
-    expect(result.status).toBe('succeeded');
+  it.only('run journey - report results payload', async () => {
+    const j1 = new Journey({ name: 'j1' }, noop);
+    const s1 = j1.addStep('step1', noop);
+    runner.addJourney(j1);
+    class ResultReporter implements Reporter {
+      onJourneyStart(journey: Journey, result: JourneyStartResult) {
+        expect(j1).toEqual(journey);
+        expect(result).toMatchObject({
+          timestamp: expect.any(Number),
+        });
+      }
+      onStepStart(journey: Journey, step: Step) {
+        expect(j1).toEqual(journey);
+        expect(s1).toEqual(step);
+      }
+      onStepEnd(journey: Journey, step: Step, result: StepEndResult) {
+        expect(j1).toEqual(journey);
+        expect(s1).toEqual(step);
+        expect(result).toMatchObject({
+          status: 'succeeded',
+          url: 'about:blank',
+          start: expect.any(Number),
+          end: expect.any(Number),
+        });
+      }
+      onJourneyEnd(journey: Journey, result: JourneyEndResult) {
+        expect(j1).toEqual(journey);
+        expect(result).toMatchObject({
+          status: 'succeeded',
+          start: expect.any(Number),
+          end: expect.any(Number),
+        });
+      }
+    }
+    const opts = { ...defaultRunOptions, reporter: ResultReporter };
+
+    const result = await runner.run(opts);
+    expect(result[j1.name].status).toBe('succeeded');
   });
 
   it('run journey - failed when any step fails', async () => {
     const journey = new Journey({ name: 'failed-journey' }, noop);
     journey.addStep('step1', noop);
     const error = new Error('Broken step 2');
-    journey.addStep('step2', async () => {
+    journey.addStep('step2', () => {
       throw error;
     });
-    const result = await runner.runJourney(journey, { wsEndpoint });
+    const result = await runner.runJourney(journey, defaultRunOptions);
     expect(result).toEqual({
       status: 'failed',
       error,
@@ -127,7 +136,7 @@ describe('runner', () => {
     const journey = new Journey({ name: 'with hooks' }, noop);
     journey.addHook('before', noop);
     journey.addHook('after', noop);
-    const result = await runner.runJourney(journey, { wsEndpoint });
+    const result = await runner.runJourney(journey, defaultRunOptions);
     expect(result).toEqual({
       status: 'succeeded',
     });
@@ -140,7 +149,7 @@ describe('runner', () => {
     journey.addHook('after', () => {
       throw error;
     });
-    const result = await runner.runJourney(journey, { wsEndpoint });
+    const result = await runner.runJourney(journey, defaultRunOptions);
     expect(result).toEqual({
       status: 'failed',
       error,
@@ -154,10 +163,7 @@ describe('runner', () => {
     });
     runner.addJourney(new Journey({ name: 'j1' }, () => step('step1', noop)));
     runner.addJourney(new Journey({ name: 'j2' }, () => step('step1', noop)));
-    const result = await runner.run({
-      wsEndpoint,
-      outfd: fs.openSync(dest, 'w'),
-    });
+    const result = await runner.run(defaultRunOptions);
     expect(result).toEqual({
       j1: { status: 'failed', error },
       j2: { status: 'failed', error },
@@ -170,7 +176,7 @@ describe('runner', () => {
         await page.goto(server.TEST_PAGE);
       });
     });
-    const runOptions = { metrics: true };
+    const runOptions = { ...defaultRunOptions, metrics: true };
     const context = await Runner.createContext(runOptions);
     await runner.registerJourney(j1, context);
     const result = await runner.runSteps(j1, context, runOptions);
@@ -190,10 +196,9 @@ describe('runner', () => {
         await (page as any).clickkkkkk();
       });
     });
-    const runOptions = {};
-    const context = await Runner.createContext(runOptions);
+    const context = await Runner.createContext(defaultRunOptions);
     await runner.registerJourney(j1, context);
-    const result = await runner.runSteps(j1, context, runOptions);
+    const result = await runner.runSteps(j1, context, defaultRunOptions);
     await Gatherer.stop();
     expect(result).toEqual([
       {
@@ -211,9 +216,9 @@ describe('runner', () => {
         await page.goto(url);
       });
     });
-    const context = await Runner.createContext({});
+    const context = await Runner.createContext(defaultRunOptions);
     await runner.registerJourney(j1, context);
-    const result = await runner.runSteps(j1, context, {});
+    const result = await runner.runSteps(j1, context, defaultRunOptions);
     await Gatherer.stop();
     expect(result).toEqual([
       {
@@ -230,9 +235,9 @@ describe('runner', () => {
         await page.goto('blah');
       });
     });
-    const context = await Runner.createContext({});
+    const context = await Runner.createContext(defaultRunOptions);
     await runner.registerJourney(j1, context);
-    const result = await runner.runSteps(j1, context, {});
+    const result = await runner.runSteps(j1, context, defaultRunOptions);
     await Gatherer.stop();
     expect(result).toEqual([
       {
@@ -259,9 +264,9 @@ describe('runner', () => {
         await page1.waitForLoadState();
       });
     });
-    const context = await Runner.createContext({});
+    const context = await Runner.createContext(defaultRunOptions);
     await runner.registerJourney(j1, context);
-    const result = await runner.runSteps(j1, context, {});
+    const result = await runner.runSteps(j1, context, defaultRunOptions);
     await Gatherer.stop();
     expect(result).toEqual([
       {
@@ -285,10 +290,13 @@ describe('runner', () => {
         throw error;
       });
     });
-    const runOptions = {};
-    const context = await Runner.createContext(runOptions);
+    const context = await Runner.createContext(defaultRunOptions);
     await runner.registerJourney(j1, context);
-    const [step1, step2] = await runner.runSteps(j1, context, runOptions);
+    const [step1, step2] = await runner.runSteps(
+      j1,
+      context,
+      defaultRunOptions
+    );
     await Gatherer.stop();
     expect(step1).toEqual({
       status: 'succeeded',
@@ -305,10 +313,7 @@ describe('runner', () => {
     const name = 'test-journey';
     const journey = new Journey({ name }, noop);
     runner.addJourney(journey);
-    const result = await runner.run({
-      wsEndpoint,
-      outfd: fs.openSync(dest, 'w'),
-    });
+    const result = await runner.run(defaultRunOptions);
     expect(result).toEqual({
       [name]: { status: 'succeeded' },
     });
@@ -319,8 +324,7 @@ describe('runner', () => {
     runner.addJourney(new Journey({ name: 'j2' }, noop));
     expect(
       await runner.run({
-        wsEndpoint,
-        outfd: fs.openSync(dest, 'w'),
+        ...defaultRunOptions,
         match: 'j2',
       })
     ).toEqual({
@@ -333,8 +337,7 @@ describe('runner', () => {
     runner.addJourney(new Journey({ name: 'tagj2', tags: ['j2'] }, noop));
     expect(
       await runner.run({
-        wsEndpoint,
-        outfd: fs.openSync(dest, 'w'),
+        ...defaultRunOptions,
         match: 'j*',
       })
     ).toEqual({
@@ -351,8 +354,7 @@ describe('runner', () => {
     runner.addJourney(new Journey({ name: 'j5', tags: ['foo'] }, noop));
     expect(
       await runner.run({
-        wsEndpoint,
-        outfd: fs.openSync(dest, 'w'),
+        ...defaultRunOptions,
         tags: ['foo*'],
         match: 'j*',
       })
@@ -369,8 +371,7 @@ describe('runner', () => {
     runner.addJourney(new Journey({ name: 'j3', tags: ['hello:baz'] }, noop));
     expect(
       await runner.run({
-        wsEndpoint,
-        outfd: fs.openSync(dest, 'w'),
+        ...defaultRunOptions,
         tags: ['hello:b*'],
       })
     ).toEqual({
@@ -385,8 +386,7 @@ describe('runner', () => {
     runner.addJourney(new Journey({ name: 'j3', tags: ['hello:baz'] }, noop));
     expect(
       await runner.run({
-        wsEndpoint,
-        outfd: fs.openSync(dest, 'w'),
+        ...defaultRunOptions,
         tags: ['!hello:b*'],
       })
     ).toEqual({
@@ -402,10 +402,7 @@ describe('runner', () => {
       throw error;
     });
     runner.addJourney(j2);
-    const result = await runner.run({
-      wsEndpoint,
-      outfd: fs.openSync(dest, 'w'),
-    });
+    const result = await runner.run(defaultRunOptions);
     expect(result).toEqual({
       j1: { status: 'succeeded' },
       j2: { status: 'failed', error },
@@ -416,10 +413,14 @@ describe('runner', () => {
     runner.addJourney(new Journey({ name: 'j1' }, noop));
     runner.addJourney(new Journey({ name: 'j2' }, noop));
     let count = 0;
-    runner.on('journey:register', () => count++);
+    class DryRunReporter implements Reporter {
+      onJourneyRegister() {
+        count++;
+      }
+    }
     const result = await runner.run({
-      wsEndpoint,
-      outfd: fs.openSync(dest, 'w'),
+      ...defaultRunOptions,
+      reporter: DryRunReporter,
       dryRun: true,
     });
     expect(result).toEqual({});
@@ -443,10 +444,7 @@ describe('runner', () => {
     runner.addJourney(j1);
     runner.addJourney(j2);
 
-    await runner.run({
-      wsEndpoint,
-      outfd: fs.openSync(dest, 'w'),
-    });
+    await runner.run(defaultRunOptions);
     expect(result).toEqual([
       'beforeAll1',
       'beforeAll2',
@@ -483,10 +481,9 @@ describe('runner', () => {
       url: 'http://local.dev',
     };
     await runner.run({
-      wsEndpoint,
+      ...defaultRunOptions,
       params,
       environment: 'testing',
-      outfd: fs.openSync(dest, 'w'),
     });
 
     expect(result).toEqual([
@@ -500,37 +497,30 @@ describe('runner', () => {
 
   it('run - supports custom reporters', async () => {
     let reporter;
-    class Reporter {
+    class CustomReporter implements Reporter {
       messages: string[] = [];
-
-      constructor(
-        public readonly runner: Runner,
-        public readonly options: any
-      ) {
+      constructor() {
         reporter = this;
-
-        this.runner.on('start', ({ numJourneys }) => {
-          this.messages.push(`numJourneys ${numJourneys}`);
-        });
-        this.runner.on('journey:start', ({ journey }) => {
-          this.messages.push(`journey:start ${journey.name}`);
-        });
-        this.runner.on('journey:end', ({ journey }) => {
-          this.messages.push(`journey:end ${journey.name}`);
-        });
-        this.runner.on('end', () => {
-          this.messages.push(`end`);
-        });
+      }
+      onStart({ numJourneys }: StartEvent) {
+        this.messages.push(`numJourneys ${numJourneys}`);
+      }
+      onJourneyStart(journey: Journey) {
+        this.messages.push(`journey:start ${journey.name}`);
+      }
+      onJourneyEnd(journey: Journey) {
+        this.messages.push(`journey:end ${journey.name}`);
+      }
+      onEnd() {
+        this.messages.push(`end`);
       }
     }
 
     runner.addJourney(new Journey({ name: 'foo' }, noop));
     const result = await runner.run({
-      reporter: Reporter,
-      wsEndpoint,
-      outfd: fs.openSync(dest, 'w'),
+      ...defaultRunOptions,
+      reporter: CustomReporter,
     });
-
     expect(result).toEqual({
       foo: {
         status: 'succeeded',
@@ -542,10 +532,6 @@ describe('runner', () => {
       'journey:end foo',
       'end',
     ]);
-    expect(reporter.runner).toBeInstanceOf(Runner);
-    expect(reporter.options).toEqual({
-      fd: expect.any(Number),
-    });
   });
 
   const readAndCloseStreamJson = () => {
@@ -571,10 +557,9 @@ describe('runner', () => {
     runner.addJourney(j2);
 
     await runner.run({
-      wsEndpoint,
+      ...defaultRunOptions,
       reporter: 'json',
       screenshots: 'on',
-      outfd: fs.openSync(dest, 'w'),
     });
 
     const screenshotJson = readAndCloseStreamJson().filter(
@@ -612,10 +597,9 @@ describe('runner', () => {
     runner.addJourney(j1);
 
     await runner.run({
-      wsEndpoint,
+      ...defaultRunOptions,
       reporter: 'json',
       screenshots: 'on',
-      outfd: fs.openSync(dest, 'w'),
     });
 
     const screenshotDocs = readAndCloseStreamJson().filter(
@@ -640,7 +624,7 @@ describe('runner', () => {
         await page.waitForLoadState('networkidle');
       });
     });
-    const runOptions = { trace: true };
+    const runOptions = { ...defaultRunOptions, trace: true };
     const context = await Runner.createContext(runOptions);
     await runner.registerJourney(j1, context);
     const [step1, step2] = await runner.runSteps(j1, context, runOptions);
@@ -664,11 +648,10 @@ describe('runner', () => {
     });
     runner.addJourney(j1);
     await runner.run({
+      ...defaultRunOptions,
       reporter: 'json',
       screenshots: 'on',
       network: true,
-      wsEndpoint,
-      outfd: fs.openSync(dest, 'w'),
     });
 
     const events = readAndCloseStreamJson().map(event => ({
@@ -701,10 +684,10 @@ describe('runner', () => {
     });
     runner.addJourney(j1);
     await runner.run({
+      ...defaultRunOptions,
       reporter: 'json',
       network: true,
       trace: true,
-      outfd: fs.openSync(dest, 'w'),
     });
     const events = readAndCloseStreamJson().map(event => event.type);
     expect(events[events.length - 1]).toBe('journey/end');
diff --git a/__tests__/reporters/__snapshots__/json.test.ts.snap b/__tests__/reporters/__snapshots__/json.test.ts.snap
index 524ccc3..fa8ec4e 100644
--- a/__tests__/reporters/__snapshots__/json.test.ts.snap
+++ b/__tests__/reporters/__snapshots__/json.test.ts.snap
@@ -207,7 +207,7 @@ exports[`json reporter writes each step as NDJSON to the FD 1`] = `
 {\\"type\\":\\"step/metrics\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"s1\\",\\"index\\":1},\\"root_fields\\":{\\"browser\\":{\\"relative_trace\\":{\\"name\\":\\"layoutShift\\",\\"type\\":\\"mark\\",\\"start\\":{\\"us\\":463045197179},\\"score\\":0.19932291666666668}},\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"package_version\\":\\"0.0.1\\"}
 {\\"type\\":\\"step/metrics\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"s1\\",\\"index\\":1},\\"root_fields\\":{\\"browser\\":{\\"experience\\":{\\"lcp\\":{\\"us\\":200},\\"fcp\\":{\\"us\\":100},\\"dcl\\":{\\"us\\":300},\\"load\\":{\\"us\\":400},\\"cls\\":0.123}},\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"package_version\\":\\"0.0.1\\"}
 {\\"type\\":\\"step/filmstrips\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"s1\\",\\"index\\":1},\\"root_fields\\":{\\"browser\\":{\\"relative_trace\\":{\\"start\\":{\\"us\\":392583998697}}},\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"index\\":0},\\"blob\\":\\"dummy\\",\\"blob_mime\\":\\"image/jpeg\\",\\"package_version\\":\\"0.0.1\\"}
-{\\"type\\":\\"step/end\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"s1\\",\\"index\\":1,\\"status\\":\\"succeeded\\",\\"duration\\":{\\"us\\":10000000}},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"source\\":\\"() => { }\\",\\"url\\":\\"dummy\\",\\"status\\":\\"succeeded\\"},\\"url\\":\\"dummy\\",\\"package_version\\":\\"0.0.1\\"}
+{\\"type\\":\\"step/end\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"s1\\",\\"index\\":1,\\"status\\":\\"succeeded\\",\\"duration\\":{\\"us\\":10000000}},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"source\\":\\"function noop() { }\\",\\"url\\":\\"dummy\\",\\"status\\":\\"succeeded\\"},\\"url\\":\\"dummy\\",\\"package_version\\":\\"0.0.1\\"}
 {\\"type\\":\\"journey/network_info\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"root_fields\\":{\\"user_agent\\":{},\\"http\\":{\\"request\\":{}},\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"browser\\":{},\\"is_navigation_request\\":true},\\"package_version\\":\\"0.0.1\\"}
 {\\"type\\":\\"journey/browserconsole\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"step-name\\",\\"index\\":0},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"text\\":\\"Boom\\",\\"type\\":\\"error\\"},\\"error\\":{\\"name\\":\\"Error\\",\\"message\\":\\"boom\\",\\"stack\\":\\"\\"},\\"package_version\\":\\"0.0.1\\"}
 {\\"type\\":\\"journey/end\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\",\\"status\\":\\"succeeded\\"},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"start\\":0,\\"end\\":11,\\"status\\":\\"succeeded\\"},\\"package_version\\":\\"0.0.1\\"}
diff --git a/__tests__/reporters/base.test.ts b/__tests__/reporters/base.test.ts
index a7fdd78..70e91bc 100644
--- a/__tests__/reporters/base.test.ts
+++ b/__tests__/reporters/base.test.ts
@@ -29,22 +29,31 @@
 process.env.NO_COLOR = '1';
 
 import fs from 'fs';
+import SonicBoom from 'sonic-boom';
 import { step, journey } from '../../src/core';
-import Runner from '../../src/core/runner';
 import BaseReporter from '../../src/reporters/base';
 import * as helpers from '../../src/helpers';
 
 describe('base reporter', () => {
   let dest: string;
-  let stream;
-  let runner: Runner;
+  let stream: SonicBoom;
+  let reporter: BaseReporter;
   const timestamp = 1600300800000000;
   const j1 = journey('j1', () => {});
 
+  const readAndCloseStream = async () => {
+    /**
+     * Close the underlying stream writing to FD to read all the contents
+     */
+    stream.end();
+    await new Promise(resolve => stream.once('finish', resolve));
+    return fs.readFileSync(fs.openSync(dest, 'r'), 'utf-8');
+  };
+
   beforeEach(() => {
-    runner = new Runner();
     dest = helpers.generateTempPath();
-    stream = new BaseReporter(runner, { fd: fs.openSync(dest, 'w') }).stream;
+    reporter = new BaseReporter({ fd: fs.openSync(dest, 'w') });
+    stream = reporter.stream;
     jest.spyOn(helpers, 'now').mockImplementation(() => 0);
   });
 
@@ -57,40 +66,27 @@ describe('base reporter', () => {
   });
 
   it('writes each step to the FD', async () => {
-    runner.emit('start', { numJourneys: 1 });
-    runner.emit('journey:start', {
-      journey: j1,
+    reporter.onJourneyStart(j1, {
       params: { environment: 'testing' },
       timestamp,
     });
-    const error = {
-      name: 'Error',
-      message: 'step failed',
-      stack: 'Error: step failed',
-    };
-    runner.emit('step:end', {
-      journey: j1,
+    reporter.onStepEnd(j1, step('s1', helpers.noop), {
       status: 'failed',
-      error,
-      step: step('s1', () => {}),
+      error: {
+        name: 'Error',
+        message: 'step failed',
+        stack: 'Error: step failed',
+      },
       url: 'dummy',
       start: 0,
       end: 1,
     });
-    runner.emit('end', 'done');
-    /**
-     * Close the underyling stream writing to FD to read all its contents
-     */
-    stream.end();
-    await new Promise(resolve => stream.once('finish', resolve));
-    const buffer = fs.readFileSync(fs.openSync(dest, 'r'));
-    expect(buffer.toString()).toMatchSnapshot();
+    reporter.onEnd();
+    expect((await readAndCloseStream()).toString()).toMatchSnapshot();
   });
 
   it('render hook errors without steps', async () => {
-    runner.emit('start', { numJourneys: 1 });
-    runner.emit('journey:start', {
-      journey: j1,
+    reporter.onJourneyStart(j1, {
       params: { environment: 'testing' },
       timestamp,
     });
@@ -99,8 +95,7 @@ describe('base reporter', () => {
       message: 'before hook failed',
       stack: 'Error: before hook failed',
     };
-    runner.emit('journey:end', {
-      journey: j1,
+    reporter.onJourneyEnd(j1, {
       timestamp,
       status: 'failed',
       error,
@@ -108,13 +103,7 @@ describe('base reporter', () => {
       end: 1,
       options: {},
     });
-    runner.emit('end', 'done');
-    /**
-     * Close the underyling stream writing to FD to read all its contents
-     */
-    stream.end();
-    await new Promise(resolve => stream.once('finish', resolve));
-    const buffer = fs.readFileSync(fs.openSync(dest, 'r'));
-    expect(buffer.toString()).toMatchSnapshot();
+    reporter.onEnd();
+    expect((await readAndCloseStream()).toString()).toMatchSnapshot();
   });
 });
diff --git a/__tests__/reporters/json.test.ts b/__tests__/reporters/json.test.ts
index b9d93c9..be688d0 100644
--- a/__tests__/reporters/json.test.ts
+++ b/__tests__/reporters/json.test.ts
@@ -26,6 +26,7 @@
 import fs, { mkdirSync } from 'fs';
 import { join } from 'path';
 import snakeCaseKeys from 'snakecase-keys';
+import SonicBoom from 'sonic-boom';
 import { step, journey } from '../../src/core';
 import JSONReporter, {
   formatNetworkFields,
@@ -33,7 +34,6 @@ import JSONReporter, {
   getScreenshotBlocks,
 } from '../../src/reporters/json';
 import * as helpers from '../../src/helpers';
-import Runner from '../../src/core/runner';
 import { NETWORK_INFO } from '../fixtures/networkinfo';
 import { StatusValue } from '../../src/common_types';
 
@@ -49,8 +49,8 @@ jest.mock(
 describe('json reporter', () => {
   let dest: string;
   const j1 = journey('j1', () => {});
-  let stream;
-  let runner: Runner;
+  let stream: SonicBoom;
+  let reporter: JSONReporter;
   const timestamp = 1600300800000000;
   const originalProcess = global.process;
   const FIXTURES_DIR = join(__dirname, '..', 'fixtures');
@@ -68,9 +68,9 @@ describe('json reporter', () => {
   });
 
   beforeEach(() => {
-    runner = new Runner();
     dest = helpers.generateTempPath();
-    stream = new JSONReporter(runner, { fd: fs.openSync(dest, 'w') }).stream;
+    reporter = new JSONReporter({ fd: fs.openSync(dest, 'w') });
+    stream = reporter.stream;
     jest.spyOn(helpers, 'getTimestamp').mockImplementation(() => timestamp);
   });
 
@@ -82,11 +82,9 @@ describe('json reporter', () => {
     /**
      * Close the underlying stream writing to FD to read all the contents
      */
-    stream.once('drain', () => stream.end());
+    stream.once('drain', stream.end);
     await new Promise(resolve => stream.once('finish', resolve));
-    const fd = fs.openSync(dest, 'r');
-    const buffer = fs.readFileSync(fd, 'utf-8');
-    return buffer;
+    return fs.readFileSync(fs.openSync(dest, 'r'), 'utf-8');
   };
 
   const readAndCloseStreamJson = async () => {
@@ -105,18 +103,13 @@ describe('json reporter', () => {
   it('writes each step as NDJSON to the FD', async () => {
     const error = new Error('boom');
     error.stack = '';
-    runner.emit('journey:register', {
-      journey: j1,
-    });
-    runner.emit('journey:start', {
-      journey: j1,
+    reporter.onJourneyRegister(j1);
+    reporter.onJourneyStart(j1, {
       params: { environment: 'testing' },
       timestamp,
     });
-    runner.emit('step:end', {
-      journey: j1,
+    reporter.onStepEnd(j1, step('s1', helpers.noop), {
       status: 'succeeded',
-      step: step('s1', () => {}),
       url: 'dummy',
       start: 0,
       end: 10,
@@ -161,8 +154,7 @@ describe('json reporter', () => {
         cls: 0.123,
       },
     });
-    runner.emit('journey:end', {
-      journey: j1,
+    reporter.onJourneyEnd(j1, {
       timestamp,
       status: 'succeeded',
       start: 0,
@@ -186,7 +178,7 @@ describe('json reporter', () => {
         },
       ],
     });
-    runner.emit('end', 'done');
+    reporter.onEnd();
     expect((await readAndCloseStream()).toString()).toMatchSnapshot();
   });
 
@@ -205,10 +197,8 @@ describe('json reporter', () => {
   it('writes step errors to the top level', async () => {
     const myErr = new Error('myError');
 
-    runner.emit('step:end', {
-      journey: j1,
+    reporter.onStepEnd(j1, step('s2', helpers.noop), {
       status: 'failed',
-      step: step('s2', () => {}),
       url: 'dummy2',
       start: 11,
       end: 20,
@@ -224,8 +214,7 @@ describe('json reporter', () => {
   it('writes journey errors to the top level', async () => {
     const myErr = new Error('myError');
 
-    runner.emit('journey:end', {
-      journey: j1,
+    reporter.onJourneyEnd(j1, {
       timestamp,
       start: 0,
       end: 1,
@@ -242,14 +231,17 @@ describe('json reporter', () => {
 
   it('writes full journey info if present', async () => {
     const journeyOpts = { name: 'name', id: 'id', tags: ['tag1', 'tag2'] };
-    runner.emit('journey:end', {
-      journey: journey(journeyOpts, () => {}),
-      timestamp,
-      start: 0,
-      end: 1,
-      status: 'skipped',
-      options: {},
-    });
+
+    reporter.onJourneyEnd(
+      journey(journeyOpts, () => {}),
+      {
+        timestamp,
+        start: 0,
+        end: 1,
+        status: 'skipped',
+        options: {},
+      }
+    );
 
     const journeyEnd = (await readAndCloseStreamJson()).find(
       json => json.type == 'journey/end'
@@ -258,9 +250,7 @@ describe('json reporter', () => {
   });
 
   it('captures number of journeys as metadata event', async () => {
-    runner.emit('start', {
-      numJourneys: 10,
-    });
+    reporter.onStart({ numJourneys: 10 });
 
     expect((await readAndCloseStream()).toString()).toMatchSnapshot();
   });
@@ -302,8 +292,7 @@ describe('json reporter', () => {
     });
 
     const emitEnd = (options, status = 'failed' as StatusValue) =>
-      runner.emit('journey:end', {
-        journey: j1,
+      reporter.onJourneyEnd(j1, {
         timestamp,
         start: 0,
         end: 2,
diff --git a/__tests__/reporters/junit.test.ts b/__tests__/reporters/junit.test.ts
index c41ecf6..910ad4e 100644
--- a/__tests__/reporters/junit.test.ts
+++ b/__tests__/reporters/junit.test.ts
@@ -25,36 +25,31 @@
 
 import fs from 'fs';
 import { join } from 'path';
+import SonicBoom from 'sonic-boom';
 import { step, journey } from '../../src/core';
-import Runner from '../../src/core/runner';
 import JUnitReporter from '../../src/reporters/junit';
 import * as helpers from '../../src/helpers';
 
 describe('junit reporter', () => {
   beforeEach(() => {});
   let dest: string;
-  let runner: Runner;
-  let stream;
+  let stream: SonicBoom;
+  let reporter: JUnitReporter;
   const timestamp = 1600300800000000;
   const j1 = journey('j1', () => {});
   const s1 = step('s1', () => {});
 
-  beforeAll(() => {
-    runner = new Runner();
-  });
-
   beforeEach(() => {
-    runner = new Runner();
     dest = helpers.generateTempPath();
-    stream = new JUnitReporter(runner, { fd: fs.openSync(dest, 'w') }).stream;
+    reporter = new JUnitReporter({ fd: fs.openSync(dest, 'w') });
+    stream = reporter.stream;
     jest.spyOn(helpers, 'now').mockImplementation(() => 0);
   });
 
   afterAll(() => fs.unlinkSync(dest));
 
   it('writes the output to fd', async () => {
-    runner.emit('journey:start', {
-      journey: j1,
+    reporter.onJourneyStart(j1, {
       params: { environment: 'testing' },
       timestamp,
     });
@@ -64,66 +59,54 @@ describe('junit reporter', () => {
      * so keep it simple
      */
     error.stack = 'at /__tests/reporters/junit.test.ts';
-    runner.emit('step:end', {
-      journey: j1,
+    reporter.onStepEnd(j1, s1, {
       status: 'failed',
       error,
-      step: s1,
       start: 0,
       end: 1,
     });
-    runner.emit('step:end', {
-      journey: j1,
+    reporter.onStepEnd(j1, step('s2', helpers.noop), {
       status: 'skipped',
-      step: step('s2', () => {}),
       start: 0,
       end: 1,
     });
-    runner.emit('journey:end', {
-      journey: j1,
+    reporter.onJourneyEnd(j1, {
       timestamp,
       start: 0,
       end: 2,
       status: 'failed',
       options: {},
     });
-    runner.emit('end', 'done');
+    reporter.onEnd();
     /**
      * Close the underyling stream writing to FD to read all its contents
      */
     stream.end();
     await new Promise(resolve => stream.once('finish', resolve));
-    const fd = fs.openSync(dest, 'r');
-    const buffer = fs.readFileSync(fd);
+    const buffer = fs.readFileSync(fs.openSync(dest, 'r'));
     expect(buffer.toString()).toMatchSnapshot();
   });
 
   it('writes the output to a file', async () => {
     const filepath = join(__dirname, '../../tmp', 'junit.xml');
     process.env.SYNTHETICS_JUNIT_FILE = filepath;
-    runner.emit('journey:start', {
-      journey: j1,
-      params: {
-        environment: 'testing',
-      },
+    reporter.onJourneyStart(j1, {
+      params: { environment: 'testing' },
       timestamp,
     });
-    runner.emit('step:end', {
-      journey: j1,
+    reporter.onStepEnd(j1, s1, {
       status: 'skipped',
-      step: s1,
       start: 0,
       end: 1,
     });
-    runner.emit('journey:end', {
-      journey: j1,
+    reporter.onJourneyEnd(j1, {
       timestamp,
       start: 0,
       end: 2,
       status: 'failed',
       options: {},
     });
-    runner.emit('end', 'done');
+    await reporter.onEnd();
     stream.end();
     expect(fs.readFileSync(filepath, 'utf-8')).toMatchSnapshot();
     fs.unlinkSync(filepath);