Skip to content

Instantly share code, notes, and snippets.

@ksmithut
Created July 30, 2020 12:24
Show Gist options
  • Save ksmithut/c4a1cf5f409f8297fead16ed976a0029 to your computer and use it in GitHub Desktop.
Save ksmithut/c4a1cf5f409f8297fead16ed976a0029 to your computer and use it in GitHub Desktop.
Test Framework
/**
* @param {number} open
* @param {number} close
* @returns {(str: string) => string}
*/
function color (open, close) {
const openStr = '\u001b[' + open + 'm'
const closeStr = '\u001b[' + close + 'm'
return str => `${openStr}${str}${closeStr}`
}
export const reset = color(0, 0)
export const bold = color(1, 22)
export const dim = color(2, 22)
export const italic = color(3, 23)
export const underline = color(4, 24)
export const inverse = color(7, 27)
export const hidden = color(8, 28)
export const strikethrough = color(9, 29)
export const black = color(30, 39)
export const red = color(31, 39)
export const green = color(32, 39)
export const yellow = color(33, 39)
export const blue = color(34, 39)
export const magenta = color(35, 39)
export const cyan = color(36, 39)
export const white = color(37, 39)
export const gray = color(90, 39)
export const grey = color(90, 39)
export const brightRed = color(91, 39)
export const brightGreen = color(92, 39)
export const brightYellow = color(93, 39)
export const brightBlue = color(94, 39)
export const brightMagenta = color(95, 39)
export const brightCyan = color(96, 39)
export const brightWhite = color(97, 39)
export const bgBlack = color(40, 49)
export const bgRed = color(41, 49)
export const bgGreen = color(42, 49)
export const bgYellow = color(43, 49)
export const bgBlue = color(44, 49)
export const bgMagenta = color(45, 49)
export const bgCyan = color(46, 49)
export const bgWhite = color(47, 49)
export const bgGray = color(100, 49)
export const bgGrey = color(100, 49)
export const bgBrightRed = color(101, 49)
export const bgBrightGreen = color(102, 49)
export const bgBrightYellow = color(103, 49)
export const bgBrightBlue = color(104, 49)
export const bgBrightMagenta = color(105, 49)
export const bgBrightCyan = color(106, 49)
export const bgBrightWhite = color(107, 49)
import { EventEmitter } from 'events'
import * as colors from './colors.js'
// @ts-ignore
const __filename = import.meta.url
/**
* @typedef {object} Test
* @property {string} description
* @property {() => any} handler
* @property {boolean} skip
* @property {Suite} suite
*/
/**
* @typedef {object} Suite
* @property {string} description
* @property {Suite?} parent
* @property {Suite[]} children
* @property {Test[]} tests
* @property {(() => any)[]} beforeHandlers
* @property {(() => any)[]} beforeEachHandlers
* @property {(() => any)[]} afterHandlers
* @property {(() => any)[]} afterEachHandlers
* @property {boolean} skip
*/
/**
* @type {Suite}
*/
const rootSuite = {
description: '',
children: [],
tests: [],
beforeHandlers: [],
beforeEachHandlers: [],
afterHandlers: [],
afterEachHandlers: [],
parent: null,
skip: false
}
/** @type {Suite[]} */
const stack = [rootSuite]
/**
* @param {string} description
* @param {() => void} handler
*/
export function describe (description, handler) {
const parent = stack[stack.length - 1]
/** @type {Suite} */
const suite = {
description,
children: [],
tests: [],
beforeHandlers: [],
beforeEachHandlers: [],
afterHandlers: [],
afterEachHandlers: [],
parent,
skip: false
}
parent.children.push(suite)
stack.push(suite)
handler()
stack.pop()
}
/**
* @param {string} description
* @param {() => void} handler
*/
export function test (description, handler) {
const parent = stack[stack.length - 1]
/** @type {Test} */
const test = {
description,
handler,
suite: parent,
skip: false
}
parent.tests.push(test)
}
/**
* @param {() => any} handler
*/
export function beforeAll (handler) {
const parent = stack[stack.length - 1]
parent.beforeHandlers.push(handler)
}
/**
* @param {() => any} handler
*/
export function beforeEach (handler) {
const parent = stack[stack.length - 1]
parent.beforeEachHandlers.push(handler)
}
/**
* @param {() => any} handler
*/
export function afterAll (handler) {
const parent = stack[stack.length - 1]
parent.afterHandlers.push(handler)
}
/**
* @param {() => any} handler
*/
export function afterEach (handler) {
const parent = stack[stack.length - 1]
parent.afterEachHandlers.push(handler)
}
/**
* @param {Suite?} suite
* @param {(suite: Suite) => void} handler
*/
function traverseAncestry (suite, handler) {
while (suite) {
handler(suite)
suite = suite.parent
}
}
/**
* @template TItem
* @param {TItem[]} arr
* @param {(item: TItem) => any} each
*/
async function promiseEach (arr, each) {
await arr.reduce(async (prev, item) => {
await prev
await each(item)
}, Promise.resolve())
}
/**
* @param {Suite} suite
* @param {EventEmitter} events
* @param {number} [depth = 0]
*/
async function runSuite (suite, events, depth = 0) {
events.emit('suite_start', { depth, suite })
// beforeAll
try {
await promiseEach(suite.beforeHandlers, async handler => {
await handler()
})
} catch (err) {
events.emit('before_all_error', { err, depth, suite })
return
}
/** @type {(() => any)[]} */
const beforeEachHandlers = []
/** @type {(() => any)[]} */
const afterEachHandlers = []
traverseAncestry(suite, parent => {
beforeEachHandlers.unshift(...parent.beforeEachHandlers)
afterEachHandlers.unshift(...parent.afterEachHandlers)
})
await promiseEach(suite.tests, async test => {
if (test.skip) {
events.emit('test_skip', { depth, suite, test })
return
}
// beforeEach
try {
await promiseEach(beforeEachHandlers, async handler => {
await handler()
})
} catch (err) {
events.emit('before_each_error', { err, depth, suite, test })
return
}
// actualTest
const start = process.hrtime.bigint()
try {
await test.handler()
const end = process.hrtime.bigint()
events.emit('test_success', { depth, suite, test, start, end })
} catch (err) {
const end = process.hrtime.bigint()
events.emit('test_failure', { err, depth, suite, test, start, end })
}
// afterEach
try {
await promiseEach(afterEachHandlers, async handler => {
await handler()
})
} catch (err) {
events.emit('after_each_error', { err, depth, suite, test })
}
})
await promiseEach(suite.children, async childSuite => {
await runSuite(childSuite, events, depth + 1)
})
try {
await promiseEach(suite.afterHandlers, async handler => {
await handler()
})
} catch (err) {
events.emit('after_all_error', { err, depth, suite })
}
}
process.nextTick(async () => {
stack.pop()
/** @type {{ err: any, suite: Suite, description: string }[]} */
const errors = []
const events = new EventEmitter()
/** @param {number} amount */
const pre = amount => ' '.repeat(amount)
events
.on('suite_start', ({ depth, suite }) => {
console.log(`${pre(depth)}${colors.underline(suite.description)}`)
})
.on('before_all_error', ({ err, depth, suite }) => {
const errorIndex = errors.push({ err, suite, description: 'beforeAll' })
const mark = colors.red(`(${errorIndex})`)
const description = colors.dim(colors.red('beforeAll'))
console.log(`${pre(depth + 1)}${mark} ${description}`)
})
.on('before_each_error', ({ err, depth, suite, test }) => {
const errorIndex = errors.push({
err,
suite,
description: `${test.description} (beforeEach)`
})
const mark = colors.red(`(${errorIndex})`)
const description = colors.dim(colors.red('beforeEach'))
console.log(`${pre(depth + 1)}${mark} ${description}`)
})
.on('test_skip', ({ depth, suite, test }) => {
const mark = colors.cyan('-')
const description = colors.dim(colors.cyan(test.description))
console.log(`${pre(depth + 1)}${mark} ${description}`)
})
.on('test_success', ({ depth, suite, test, start, end }) => {
const mark = colors.green('✔')
const ms = (Number(end - start) / 1e6).toFixed(2)
const duration = `(${ms}ms)`
const description = colors.dim(test.description)
console.log(`${pre(depth + 1)}${mark} ${description} ${duration}`)
})
.on('test_failure', ({ err, depth, suite, test, start, end }) => {
const errorIndex = errors.push({
err,
suite,
description: test.description
})
const mark = colors.red(`(${errorIndex})`)
const ms = (Number(end - start) / 1e6).toFixed(2)
const duration = `(${ms}ms)`
const description = colors.dim(colors.red(test.description))
console.log(`${pre(depth + 1)}${mark} ${description} ${duration}`)
})
.on('after_each_error', ({ err, depth, suite, test }) => {
const errorIndex = errors.push({
err,
suite,
description: `${test.description} (afterEach)`
})
const mark = colors.red(`(${errorIndex})`)
const description = colors.dim(colors.red('afterEach'))
console.log(`${pre(depth + 1)}${mark} ${description}`)
})
.on('after_all_error', ({ err, depth, suite }) => {
const errorIndex = errors.push({ err, suite, description: 'afterAll' })
const mark = colors.red(`(${errorIndex})`)
const description = colors.dim(colors.red('afterAll'))
console.log(`${pre(depth + 1)}${mark} ${description}`)
})
await runSuite(rootSuite, events)
console.log()
errors.forEach(({ err, suite, description }, i) => {
/** @type {string[]} */
const path = [description]
traverseAncestry(suite, parent => {
parent.parent && path.unshift(parent.description)
})
const pathStr = path
.map(path => colors.underline(path))
.join(colors.gray(' > '))
const index = colors.red(`(${i + 1})`)
console.log(` ${index} ${pathStr}`)
console.log()
if (err instanceof Error && err.stack) {
const stack = err.stack
.split('\n')
.filter(line => !line.includes(__filename))
.map(line => ` ${line}`)
.join('\n')
console.log(colors.dim(stack))
}
console.log('\n')
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment