Created
July 30, 2020 12:24
-
-
Save ksmithut/c4a1cf5f409f8297fead16ed976a0029 to your computer and use it in GitHub Desktop.
Test Framework
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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