UNPKG

supertape

Version:

📼 Supertape simplest high speed test runner with superpowers

330 lines (263 loc) 7.42 kB
import {isDeepStrictEqual} from 'node:util'; import diff from './diff.mjs'; import {formatOutput, parseAt} from './format.js'; const {entries} = Object; const isAsync = (a) => a[Symbol.toStringTag] === 'AsyncFunction'; const encode = (a) => a .replace('^', '\\^') .replace(')', '\\)') .replace('(', '\\('); const maybeRegExp = (a) => { if (!isStr(a)) return a; return RegExp(encode(a)); }; const isFn = (a) => typeof a === 'function'; const isStr = (a) => typeof a === 'string'; const isObj = (a) => typeof a === 'object'; const end = () => {}; const ok = (result, message = 'should be truthy') => ({ is: Boolean(result), expected: true, result, message, }); const notOk = (result, message = 'should be falsy') => ({ is: !result, expected: false, result: result && stringify(result), message, }); const validateRegExp = (regexp) => { if (!isObj(regexp) && !isStr(regexp)) return Error('regexp should be RegExp or String'); if (!regexp) return Error('regexp cannot be empty'); return null; }; const {stringify} = JSON; function match(result, regexp, message = 'should match') { const error = validateRegExp(regexp); if (error) return fail(error); const is = maybeRegExp(regexp).test(result); return { is, result, expected: regexp, message, }; } function notMatch(result, regexp, message = 'should not match') { const {is} = match(result, regexp, message); return { is: !is, result, expected: regexp, message, }; } function equal(result, expected, message = 'should equal') { let output = ''; const is = Object.is(result, expected); if (!is) output = diff(expected, result) || ' result: values not equal, but deepEqual'; return { is, result, expected, message, output, }; } function notEqual(result, expected, message = 'should not equal') { const is = !Object.is(result, expected); const output = is ? '' : diff(expected, result); return { is, result, expected, message, output, }; } const pass = (message = '(unnamed assert)') => ({ is: true, output: '', message, }); const fail = (error, at) => ({ is: false, stack: error.stack, output: '', message: error, at, }); const deepEqual = (result, expected, message = 'should deep equal') => { const is = isDeepStrictEqual(result, expected); const output = is ? '' : diff(expected, result); return { is: is || !output, result, expected, message, output, }; }; const notDeepEqual = (result, expected, message = 'should not deep equal') => { const is = !isDeepStrictEqual(result, expected); const output = is ? '' : diff(expected, result); return { is, result, expected, message, output, }; }; const comment = ({formatter}) => (message) => { const messages = message .trim() .split('\n'); for (const current of messages) { const line = current .trim() .replace(/^#\s*/, ''); formatter.emit('comment', line); } }; export const operators = { equal, notEqual, deepEqual, notDeepEqual, ok, notOk, pass, fail, end, match, notMatch, }; const initOperator = (runnerState) => (name) => { const fn = operators[name]; if (isAsync(fn)) return async (...a) => { const [valid, end] = validateEnd({ name, operators, runnerState, }); if (!valid) return end; const testState = await fn(...a); run(name, runnerState, testState); return testState; }; return (...a) => { const [valid, end] = validateEnd({ name, operators, runnerState, }); if (!valid) return end; const testState = fn(...a); if (testState instanceof Promise) throw Error(`☝️ Looks like test function returned Promise, but it was determined as not async function. Maybe the reason is 'curry', try to create to separate functions instead`); run(name, runnerState, testState); return testState; }; }; const VALID = true; const INVALID = false; function validateEnd({name, operators, runnerState}) { const { isEnded, incAssertionsCount, } = runnerState; if (name === 'end' && isEnded()) return [INVALID, run('fail', runnerState, operators.fail(`Cannot use a couple 't.end()' operators in one test`))]; if (name === 'end') { isEnded(true); return [INVALID, end]; } incAssertionsCount(); if (isEnded()) return [INVALID, run('fail', runnerState, operators.fail(`Cannot run assertions after 't.end()' called`))]; return [VALID]; } const validate = (a) => { if (isFn(a)) return fail(`☝️ Looks like operator returns function, it will always fail: '${a}'`); return a; }; const returnMissing = () => fail('☝️ Looks like operator returns nothing, it will always fail'); function run(name, {formatter, count, incCount, incPassed, incFailed}, testState = returnMissing()) { const { is, message, expected, result, output, stack, at, } = validate(testState); incCount(); if (is) { incPassed(); formatter.emit('test:success', { count: count(), message, }); return; } incFailed(); const errorStack = stack || Error(message).stack; const reason = stack ? 'exception' : 'user'; formatter.emit('test:fail', { count: count(), message, operator: name, result, expected, output, errorStack: formatOutput(errorStack), at: at || parseAt(errorStack, { reason, }), }); } export const initOperators = ({formatter, count, incCount, incPassed, incFailed, incAssertionsCount, isEnded, extensions}) => { const operator = initOperator({ formatter, count, incCount, incPassed, incFailed, isEnded, incAssertionsCount, }); const extendedOperators = {}; for (const [name, fn] of entries(extensions)) { operators[name] = fn(operators); extendedOperators[name] = operator(name); } return { equal: operator('equal'), notEqual: operator('notEqual'), deepEqual: operator('deepEqual'), notDeepEqual: operator('notDeepEqual'), ok: operator('ok'), notOk: operator('notOk'), pass: operator('pass'), fail: operator('fail'), comment: comment({ formatter, }), match: operator('match'), notMatch: operator('notMatch'), end: operator('end'), ...extendedOperators, }; };