UNPKG

@service-broker/test-utils

Version:
193 lines (192 loc) 7.24 kB
import assert from "assert/strict"; import util from "util"; import { green, red, yellowBright } from "yoctocolors"; import { assertRecord, lazy } from "./util.js"; const suites = []; const scheduleRun = lazy(() => setTimeout(run, 0)); const runAfterEverything = []; export function describe(suiteName, setup) { const suite = { name: suiteName, beforeAll: [], afterAll: [], beforeEach: [], afterEach: [], tests: [] }; setup({ beforeAll: run => suite.beforeAll.push(run), afterAll: run => suite.afterAll.push(run), beforeEach: run => suite.beforeEach.push(run), afterEach: run => suite.afterEach.push(run), test: (name, run) => suite.tests.push({ name, run }) }); suites.push(suite); scheduleRun(); } export function afterEverything(run) { runAfterEverything.push(run); } class FailedExpectation { constructor(reason, path = []) { this.reason = reason; this.path = path; } } async function run() { const suiteName = process.argv[2]; const testName = process.argv[3]; const suitesToRun = suiteName ? suites.filter(x => x.name == suiteName) : suites; try { try { for (const suite of suitesToRun) { const testsToRun = testName ? suite.tests.filter(x => x.name == testName) : suite.tests; for (const run of suite.beforeAll) await run(); try { for (const test of testsToRun) { for (const run of suite.beforeEach) await run(); try { console.log("Running test '%s' '%s'", suite.name, test.name); await test.run(); } finally { for (const run of suite.afterEach) await run(); } } } finally { for (const run of suite.afterAll) await run(); } } } finally { for (const run of runAfterEverything) await run(); } console.log("Finished."); } catch (err) { if (err instanceof FailedExpectation) { if (err.expected) console.error(red('EXPECT'), util.inspect(err.expected, { depth: Infinity })); console.error(green('ACTUAL'), util.inspect(err.actual, { depth: Infinity })); console.error('Error:', yellowBright('.' + err.path.join('.') + ' ' + err.reason)); console.error(err.stack?.replace(/^Error\n/, '')); } else { console.error(err); } } } export class Expectation { constructor(operator, expected, assert) { Object.defineProperty(this, operator, { value: expected, enumerable: true }); Object.defineProperty(this, 'assert', { value: assert }); } } export function expect(actual, expected, path = []) { try { if (typeof expected == 'object' && expected != null) { if (expected instanceof Expectation) { try { expected.assert(actual); } catch (err) { if (err instanceof FailedExpectation) { err.path.splice(0, 0, ...path); throw err; } else { throw new FailedExpectation(err.message || err, path); } } } else if (expected instanceof Set) { try { assert.deepStrictEqual(actual, expected); } catch { throw new FailedExpectation('!equalExpected', path); } } else if (expected instanceof Map) { if (!(actual instanceof Map)) throw new FailedExpectation('!isMap', path); for (const [key] of actual) { if (!expected.has(key)) throw new FailedExpectation(`hasExtraKey '${key}'`, path); } for (const [key, expectedValue] of expected) { if (!actual.has(key)) throw new FailedExpectation(`missingKey '${key}'`, path); expect(actual.get(key), expectedValue, [...path, key]); } } else if (Array.isArray(expected)) { if (!Array.isArray(actual)) throw new FailedExpectation('!isArray', path); if (actual.length != expected.length) throw new FailedExpectation('!ofExpectedLength', path); for (let i = 0; i < actual.length; i++) expect(actual[i], expected[i], [...path, String(i)]); } else if (Buffer.isBuffer(expected)) { if (!Buffer.isBuffer(actual)) throw new FailedExpectation('!isBuffer', path); if (!actual.equals(expected)) throw new FailedExpectation('!equalExpected', path); } else { if (!(typeof actual == 'object' && actual != null)) throw new FailedExpectation('!isObject', path); assertRecord(expected); assertRecord(actual); for (const prop in actual) { if (!(prop in expected)) throw new FailedExpectation(`hasExtraProp '${prop}'`, path); } for (const prop in expected) { if (!(prop in actual)) throw new FailedExpectation(`missingProp '${prop}'`, path); expect(actual[prop], expected[prop], [...path, prop]); } } } else { if (actual !== expected) throw new FailedExpectation('!equalExpected', path); } } catch (err) { if (err instanceof FailedExpectation && path.length == 0) { err.actual = actual; err.expected = expected; Error.captureStackTrace(err, expect); } throw err; } } export function objectHaving(expectedProps) { return new Expectation('have', expectedProps, actual => { assert(typeof actual == 'object' && actual != null, '!isObject'); assertRecord(actual); for (const prop in expectedProps) { assert(prop in actual, `missingProp '${prop}'`); expect(actual[prop], expectedProps[prop], [prop]); } }); } export function valueOfType(expectedType) { return new Expectation('ofType', expectedType, actual => { assert(typeof actual == expectedType, '!ofExpectedType'); }); } export function oneOf(expectedValues) { return new Expectation('oneOf', expectedValues, actual => { assert(expectedValues.includes(actual), '!oneOfExpectedValues'); }); }