core/test_runner.js

import { Test } from './test.js';
import { TestSuite } from './test_suite.js';
import { FailFast } from '../config/fail_fast.js';
import { shuffle } from '../utils.js';

/**
 * I am responsible for executing [test suites]{@link TestSuite} using a given [configuration]{@link Configuration},
 * collecting the results of each test of each suite and reporting the aggregated [results]({@link TestResult})
 * using a callback-based mechanism.
 */
export class TestRunner {
  #suites;
  #callbacks;
  #currentSuite;
  #failFastMode;
  #randomOrder;
  #testExecutionTimeoutMs;

  constructor(callbacks) {
    this.#suites = [];
    this.#callbacks = callbacks;
    this.#failFastMode = FailFast.default();
  }

  // Configuration

  registerSuite(name, suiteBody, callbacks) {
    const suiteToAdd = new TestSuite(name, suiteBody, callbacks);
    return this.addSuite(suiteToAdd);
  }

  registerTest(name, testBody, callbacks) {
    const testToAdd = new Test(name, testBody, callbacks);
    this.#currentSuite.addTest(testToAdd);
    return testToAdd;
  }

  registerBefore(beforeBlock) {
    this.#currentSuite.before(beforeBlock);
  }

  registerAfter(afterBlock) {
    this.#currentSuite.after(afterBlock);
  }

  addSuite(suiteToAdd) {
    this.#suites.push(suiteToAdd);
    this.#setCurrentSuite(suiteToAdd);
    return suiteToAdd;
  }

  configureWith(configuration) {
    this.#failFastMode = configuration.failFastMode();
    this.#randomOrder = configuration.randomOrder();
    this.#testExecutionTimeoutMs = configuration.timeoutMs();
  }

  // Executing

  async run() {
    this.#randomizeSuites();
    // eslint-disable-next-line no-restricted-syntax
    for (const suite of this.#suites) {
      this.#setCurrentSuite(suite);
      // eslint-disable-next-line no-await-in-loop
      await suite.run(this.#executionContext());
    }
    this.#finish();
  }

  setResultForCurrentTest(result) {
    this.#currentSuite.currentTest().setResult(result);
  }

  // Counting

  successCount() {
    return this.#countEach('successCount');
  }

  pendingCount() {
    return this.#countEach('pendingCount');
  }

  errorsCount() {
    return this.#countEach('errorsCount');
  }

  failuresCount() {
    return this.#countEach('failuresCount');
  }

  skippedCount() {
    return this.#countEach('skippedCount');
  }

  totalCount() {
    return this.#countEach('totalCount');
  }

  allFailuresAndErrors() {
    return this.#suites.flatMap(suite => suite.allFailuresAndErrors());
  }

  hasErrorsOrFailures() {
    return this.allFailuresAndErrors().length > 0;
  }

  // Private

  #setCurrentSuite(suite) {
    this.#currentSuite = suite;
  }

  #countEach(property) {
    return this.#suites.reduce((count, suite) =>
      count + suite[property](), 0,
    );
  }
  #randomizeSuites() {
    if (this.#randomOrder) {
      shuffle(this.#suites);
    }
  }

  #executionContext() {
    return {
      failFastMode: this.#failFastMode,
      randomOrderMode: this.#randomOrder,
      testExecutionTimeoutMs: this.#testExecutionTimeoutMs,
    };
  }

  #finish() {
    this.#callbacks.onFinish(this);
    if (this.hasErrorsOrFailures()) {
      return this.#callbacks.onFailure(this);
    } else {
      return this.#callbacks.onSuccess(this);
    }
  }
}