Source: test/Runner.js

import Test from './Test.js'
import EventHandler from '../EventHandler.js'

/**
Runner is responsible for running {@link Test}s and returning the results
It also emits a series of events that test harnesses can use to render status as tests are run
*/
const Runner = class extends EventHandler {
	/**
	Run tests and return a data structure of results
	@param {Test[]} tests
	@return {Object[]} - Object attributes: {bool} passed, {Test} test, {string} message
	@property {number} passedCount
	@property {number} failedCount
	@property {number} assertionCount
	*/
	async run(tests) {
		this.trigger(this.events.RunStarted, this, tests)
		const results = []
		results.passedCount = 0
		results.failedCount = 0
		results.assertionCount = 0

		for (let i = 0; i < tests.length; i++) {
			const test = tests[i]
			this._listenToTest(i, test)
			const { passed, context } = await test.run()
			results.push({
				test: test,
				passed: passed,
				context: context,
			})
			if (passed) {
				results.passedCount += 1
			} else {
				results.failedCount += 1
			}
			results.assertionCount += test.assertionCount
		}

		this.trigger(this.events.RunEnded, this, results)
		return results
	}

	/**
	Run tests and print a summary to the console
	*/
	async runAndLog(tests) {
		const results = await Runner.run(tests)
		console.log(
			`total: ${results.length}  passed: ${results.passedCount}  failed: ${results.failedCount} assertions: ${results.assertionCount}`
		)
	}

	/**
	Run tests and return TAP formatted results
	@param {Test[]} tests
	@return {string} - TAP formatted results
	*/
	async runAndTAP(tests) {
		return TAPFormatter.formatTestResults(await Runner.run(tests))
	}

	_listenToTest(index, test) {
		// Proxies test events into runner events with a test index
		test.addListener(Test.Started, (eventName, test) => {
			this.trigger(this.events.Started, index, test)
		})

		test.addListener(Test.SetupSucceeded, (eventName, test, context) => {
			this.trigger(this.events.SetupSucceeded, index, test, context)
		})

		test.addListener(Test.SetupFailed, (eventName, test, context) => {
			this.trigger(this.events.SetupFailed, index, test, context)
		})

		test.addListener(Test.Passed, (eventName, test, context) => {
			this.trigger(this.events.Passed, index, test, context)
		})

		test.addListener(Test.Failed, (eventName, test, context) => {
			this.trigger(this.events.Failed, index, test, context)
		})

		test.addListener(Test.TeardownSucceeded, (eventName, test, context) => {
			this.trigger(this.events.TeardownSucceeded, index, test, context)
		})

		test.addListener(Test.TeardownFailed, (eventName, test, context) => {
			this.trigger(this.events.TeardownFailed, index, test, context)
		})
	}
}

// Events
const events = {
	RunStarted: Symbol('run-started'),
	RunEnded: Symbol('run-ended'),
	Started: Symbol('test-started'),
	SetupSucceeded: Symbol('test-setup-succeeded'),
	SetupFailed: Symbol('test-setup-failed'),
	Passed: Symbol('test-passed'),
	Failed: Symbol('test-failed'),
	TeardownSucceeded: Symbol('test-teardown-succeeded'),
	TeardownFailed: Symbol('test-teardown-failed'),
}

Runner.prototype.events = events

/**
Implements formatting of test results into TAP v13
http://testanything.org/tap-version-13-specification.html
*/
const TAPFormatter = class {
	/**
	@param {Object[]} testResults - data returned from Runner.run(...)
	@return {string} - TAP formatted results
	*/
	static formatTestResults(testResults) {
		const tapLines = [] // An array of lines of text
		tapLines.push('TAP version 13')
		tapLines.push(`1..${testResults.length}`)
		for (let i = 0; i < testResults.length; i++) {
			tapLines.push(...TAPFormatter.formatTestResult(i + 1, testResults[i]))
		}
		return tapLines.join('\n')
	}

	static formatTestResult(testNumber, testResult) {
		const tapLines = []
		const status = testResult.passed ? 'ok' : 'not ok'
		tapLines.push(`${status} ${testNumber} ${testResult.test.name}`)
		if (testResult.context !== null && Object.keys(testResult.context).length > 0) {
			tapLines.push('\t---')
			for (const key of Object.keys(testResult.context)) {
				tapLines.push(`\t${key}: ${testResult.context[key]}`)
			}
			tapLines.push('\t...')
		}
		return tapLines
	}
}

export default Runner