UNPKG

@service-broker/test-utils

Version:
206 lines (187 loc) 6.5 kB
import assert from "assert/strict"; import util from "util"; import { green, red, yellowBright } from "yoctocolors"; import { assertRecord, lazy } from "./util.js"; interface Test { name: string run: Function } interface Suite { name: string beforeAll: Function[] afterAll: Function[] beforeEach: Function[] afterEach: Function[] tests: Test[] } const suites: Suite[] = [] const scheduleRun = lazy(() => setTimeout(run, 0)) const runAfterEverything: Function[] = [] export function describe( suiteName: string, setup: (opts: { beforeAll: (run: Function) => void afterAll: (run: Function) => void beforeEach: (run: Function) => void afterEach: (run: Function) => void test: (name: string, run: Function) => void }) => void ) { const suite: 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: Function) { runAfterEverything.push(run) } class FailedExpectation { expected: unknown actual: unknown stack?: string constructor(readonly reason: string, readonly path: string[] = []) { } } 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: string, expected: unknown, assert: (actual: unknown) => void) { Object.defineProperty(this, operator, { value: expected, enumerable: true }) Object.defineProperty(this, 'assert', { value: assert }) } } export function expect(actual: unknown, expected: unknown, path: string[] = []) { try { if (typeof expected == 'object' && expected != null) { if (expected instanceof Expectation) { try { (expected as any).assert(actual) } catch (err: any) { 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: Record<string, unknown>) { 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: string) { return new Expectation('ofType', expectedType, actual => { assert(typeof actual == expectedType, '!ofExpectedType') }) } export function oneOf(expectedValues: unknown[]) { return new Expectation('oneOf', expectedValues, actual => { assert(expectedValues.includes(actual), '!oneOfExpectedValues') }) }