ui/formatter.js

import { isEmpty, isString } from '../utils.js';

const
  off = '\x1b[0m',
  bold = '\x1b[1m',
  cyan = '\x1b[36m',
  red = '\x1b[31m',
  green = '\x1b[32m',
  yellow = '\x1b[33m';

const consoleWidth = 80;

/**
 * I present all the events from Testy as formatted text to a [console]{@link ConsoleUI}.
 */
export class Formatter {
  #console;
  #i18n;
  #timerName;
  #filter;

  constructor(console, i18n) {
    this.#console = console;
    this.#i18n = i18n;
    this.#timerName = this.#translated('total_time');
  }

  startTimer() {
    this.#console.time(this.#timerName);
  }

  endTimer() {
    this.#console.timeEnd(this.#timerName);
  }

  displayInitialInformation(configuration, paths) {
    this.#filter = configuration.filterRaw();
    this.#displaySeparator();
    this.#displayConfigurationSummary(paths, configuration);
    this.#displaySeparator();
  }

  displayRunnerEnd(runner) {
    this.#displayErrorsAndFailuresSummary(runner);
    this.#displayGeneralSummary(runner);
  }

  // displaying suites

  displaySuiteStart(suite) {
    this.#console.log(`\n${suite.name()}:`);
    this.#displaySeparator('-');
  }

  displaySuiteEnd(suite) {
    this.#displaySeparator('-');
    this.#console.log(`${this.#translated('summary_of')} ${suite.name()}:`);
    this.#displayCountFor(suite);
    this.#displaySeparator();
  }

  // displaying test results

  displayPendingResult(test) {
    this.#displayResult(this.#translated('wip'), test, yellow);
    if (test.isExplicitlyMarkedPending()) {
      this.#displayResultDetail(test.result().reason());
    }
  }

  displaySkippedResult(test) {
    this.#displayResult(this.#translated('skip'), test, cyan);
  }

  displaySuccessResult(test) {
    this.#displayResult(this.#translated('ok'), test, green);
  }

  displayFailureResult(test, failType) {
    this.#displayResult(this.#translated(failType), test, red);
    this.#displayResultDetail(test.result().failureMessage());
    this.#displayResultDetail(test.result().location());
  }

  // displaying other messages

  displayError(message) {
    this.#console.log(`${this.#withColor(this.#potentiallyInternationalized(message), red)}`);
  }

  #displayConfigurationSummary(paths, configuration) {
    const testPathsLabel = this.#translated('running_tests_in');
    const failFastLabel = this.#translated('fail_fast');
    const randomOrderLabel = this.#translated('random_order');
    this.#console.log(this.#inBold(this.#translated('starting_testy')));
    const padding = Math.max(testPathsLabel.length, failFastLabel.length, randomOrderLabel.length);
    this.#console.log(`${testPathsLabel.padEnd(padding)} : ${paths}`);
    this.#console.log(`${failFastLabel.padEnd(padding)} : ${this.#humanBoolean(configuration.failFastMode().enabled())}`);
    this.#console.log(`${randomOrderLabel.padEnd(padding)} : ${this.#humanBoolean(configuration.randomOrder())}`);
  }

  // private - displaying

  #displayResult(result, test, color) {
    this.#console.log(`[${color}${this.#inBold(result)}] ${this.#withColor(test.name(), color)}`);
  }

  #displayResultDetail(detail) {
    if (!isEmpty(detail)) {
      this.#console.log(`  => ${this.#potentiallyInternationalized(detail)}`);
    }
  }

  #displayErrorsAndFailuresSummary(runner) {
    if (runner.hasErrorsOrFailures()) {
      this.#console.log(`\n${this.#translated('failures_summary')}`);
      runner.allFailuresAndErrors().forEach(test => {
        const failType = test.isFailure() ? 'fail' : 'error';
        this.displayFailureResult(test, failType);
      });
      this.#displaySeparator();
    }
  }

  #displayGeneralSummary(runner) {
    this.#console.log(`\n${this.#translated('total')}`);
    this.#displayCountFor(runner);
    this.#displaySeparator();
  }

  #displayCountFor(runner) {
    const passedCount = this.#displayIfNonZero(runner.successCount(), this.#translated('passed'), green);
    const failureCount = this.#displayIfNonZero(runner.failuresCount(), this.#translated('failed'), red);
    const errorCount = this.#displayIfNonZero(runner.errorsCount(), this.#translated('errors'), red);
    const pendingCount = this.#displayIfNonZero(runner.pendingCount(), this.#translated('pending'), yellow);
    const skippedCount = this.#displayIfNonZero(runner.skippedCount(), this.#translated('skipped'), yellow);
    this.#console.log(`${runner.totalCount()} test(s)${passedCount}${failureCount}${errorCount}${pendingCount}${skippedCount}`);
    if (runner.totalCount() === 0) {
      this.#console.log(this.#withColor(`\nWarning: Make sure your files matches the ${this.#filter} naming filter.`, yellow));
    }
  }

  #displayIfNonZero(quantity, word, color = off) {
    const pluralizedWord = `${quantity} ${word}`;
    return quantity > 0 ? `, ${this.#withColor(pluralizedWord, color)}` : '';
  }

  #displaySeparator(character = '=') {
    this.#console.log(character.repeat(consoleWidth));
  }

  // private - formatting and localization

  #inBold(text) {
    return `${bold}${text}${off}`;
  }

  #withColor(text, color) {
    return `${color}${text}${off}`;
  }

  #humanBoolean(boolean) {
    return boolean === true ? this.#translated('yes') : this.#translated('no');
  }

  #translated(key) {
    return this.#i18n.translate(key);
  }

  #potentiallyInternationalized(text) {
    return isString(text) ? text : text.expressedIn(this.#i18n);
  }
}