UNPKG

fava

Version:

A wannabe tiny largely-drop-in replacement for ava that works in the browser too.

325 lines (324 loc) 13.6 kB
/* IMPORT */ import makeNakedPromise from 'promise-make-naked'; import color from 'tiny-colors'; import Assert from './assert.js'; import { NOOP } from './constants.js'; import Env from './env.js'; import { tapi } from './t.js'; import Utils from './utils.js'; /* MAIN */ class Tester { /* CONSTRUCTOR */ constructor(title, implementation, flags) { this.duration = 0; this.executed = false; this.logs = []; this.timeoutMessage = 'Test timeoud out'; this.timeoutMs = 0; this.passed = -1; this.prefixes = []; this.stats = { failed: 0, passed: 0, planned: 0, total: 0 }; this.teardowns = []; /* PRIVATE API */ this.makeAssertion = (assertion) => { const onSuccess = () => this.stats.passed += 1; const onError = () => this.stats.failed += 1; return (...args) => { this.stats.total += 1; const onCall = () => assertion.apply(undefined, args); return this.wrapCall(onCall, onSuccess, onError); }; }; this.wrapApi = (fn) => { return async () => { const api = this.api(); const tapiPrev = tapi.current; try { tapi.current = api; return await fn(api); } finally { tapi.current = tapiPrev; } }; }; this.wrapAssert = (fn) => { const { assert } = console; const wrap = () => { console.assert = Assert.true; }; const unwrap = () => { console.assert = assert; }; return () => { wrap(); return this.wrapCall(fn, unwrap, unwrap); }; }; this.wrapCall = (onCall, onSuccess, onError) => { try { const result = onCall(); if (result instanceof Promise) { result.then(onSuccess, onError); } else { onSuccess(); } return result; } catch (error) { onError(); throw error; } }; this.wrapEnv = (fn) => { if (!Env.is.node) return fn; const NODE_ENV = process.env['NODE_ENV']; const set = (value) => { try { // This throws an error in Deno const NODE_ENV_NAME = 'NODE' + '_' + 'ENV'; // Otherwise this gets messed up in Bun process.env[NODE_ENV_NAME] = value; } catch { } }; const define = () => { set('test'); }; const restore = () => { set(NODE_ENV); }; return () => { define(); return this.wrapCall(fn, restore, restore); }; }; this.wrapLogger = (fn) => { const { error, info, log, warn } = console; const wrap = () => { console.error = this.log; console.info = this.log; console.log = this.log; console.warn = this.log; }; const unwrap = () => { console.error = error; console.info = info; console.log = log; console.warn = warn; }; return () => { wrap(); return this.wrapCall(fn, unwrap, unwrap); }; }; this.wrapTimeout = (fn) => { return async () => { const { reject, promise: promiseTimeout } = makeNakedPromise(); const promiseTest = new Promise(resolve => resolve(fn())); const onTimeout = () => reject(new Error(this.timeoutMessage)); const onCleanup = () => clearTimeout(timeoutId); const onCall = () => Promise.race([promiseTest, promiseTimeout]); const timeoutId = setTimeout(onTimeout, this.timeoutMs || Env.options.timeout); return this.wrapCall(onCall, onCleanup, onCleanup); }; }; this.wrapTimer = (fn) => { let timestamp; const start = () => { timestamp = Date.now(); }; const stop = () => { this.duration = Date.now() - timestamp; }; return () => { start(); return this.wrapCall(fn, stop, stop); }; }; /* EXECUTION API */ this.api = () => { const self = this; return { /* VARIABLES */ get title() { return self.title; }, get passed() { return self.passed; }, get context() { return self.ctx; }, get ctx() { return self.ctx; }, set context(ctx) { self.ctx = ctx; }, set ctx(ctx) { self.ctx = ctx; }, /* CONTROL API */ get log() { return self.log; }, get plan() { return self.plan; }, get teardown() { return self.teardown; }, get timeout() { return self.timeout; }, /* ASSERTION API */ get pass() { return self.pass; }, get fail() { return self.fail; }, get assert() { return self.assert; }, get truthy() { return self.truthy; }, get falsy() { return self.falsy; }, get true() { return self.true; }, get false() { return self.false; }, get is() { return self.is; }, get not() { return self.not; }, get deepEqual() { return self.deepEqual; }, get notDeepEqual() { return self.notDeepEqual; }, get like() { return self.like; }, get notLike() { return self.notLike; }, get throws() { return self.throws; }, get throwsAsync() { return self.throwsAsync; }, get notThrows() { return self.notThrows; }, get notThrowsAsync() { return self.notThrowsAsync; }, get regex() { return self.regex; }, get notRegex() { return self.notRegex; }, get wait() { return self.wait; } }; }; this.call = async () => { await this.wrapAssert(this.wrapEnv(this.wrapLogger(this.wrapTimer(this.wrapTimeout(this.wrapApi(this.implementation))))))(); }; this.execute = async () => { this.executed = true; try { if (this.flags.todo) { if (this.implementation !== NOOP) { return this.result(0, '✖', 'red', 'A "todo" test must not provide an implementation'); } else { return this.result(-1, '!', 'yellow'); } } if (this.implementation === NOOP) { return this.result(0, '✖', 'red', 'A test must provide an implementation'); } if (this.flags.skip) { return this.result(-1, '!', 'yellow'); } await this.call(); if (this.flags.failing) { return this.result(0, '✖', 'red', 'A "failing" test is expected to fail, not pass'); } if (!this.stats.passed) { return this.result(0, '✖', 'red', 'No assertions executed'); } if (this.stats.planned && this.stats.passed !== this.stats.planned) { return this.result(0, '✖', 'red', `Expected ${color.green(String(this.stats.planned))} assertions, but got ${color.red(String(this.stats.passed))}`); } return this.result(1, '✔', 'green'); } catch (error) { if (this.flags.failing) { return this.result(1, '✔', 'yellow'); } else { if (Utils.lang.isError(error) || Utils.lang.isString(error)) { return this.result(0, '✖', 'red', error); } else { return this.result(0, '✖', 'red', 'Unknown error'); } } } }; this.result = async (passed, status, statusColor, error) => { //TODO: Prettier logging, error stacks in particular are super noisy this.passed = passed; const sectionWidth = Utils.getStdoutColumns(); const sectionHeader = (title) => `╭ ${title} ${'─'.repeat(sectionWidth - title.length - 4)}╮`; const sectionDivider = () => '─'.repeat(sectionWidth); const sectionFooter = () => `╰${'─'.repeat(sectionWidth - 2)}╯`; if (Env.options.verbose || !passed) { const prefix = this.prefixes.length ? `${this.prefixes.join(` ${color.dim('›')} `)} ${color.dim('›')} ` : ''; const suffix = this.duration >= 500 ? color.dim(` (${this.duration}ms)`) : ''; console.log(`${color[statusColor].bold(status)} ${prefix}${this.title}${suffix}`); if (error) { if (Utils.lang.isString(error)) { console.log(color.red(sectionHeader('Error'))); console.log(error); console.log(color.red(sectionFooter())); } else { const errorLines = await Utils.getErrorLines(error); if (errorLines.length) { console.log(color.red(sectionHeader('Error'))); console.log(error.message); console.log(color.red(sectionDivider())); for (const { url, content } of errorLines) { console.log(color.dim(url)); console.log(`${color.dim('╰─')} ${content}`); } console.log(color.red(sectionFooter())); } else { console.log(color.red(sectionHeader('Error Stack'))); console.log(error.stack || error.message); console.log(color.red(sectionFooter())); } } } if (this.logs.length) { if (Env.options.verbose || error) { const logsColor = error ? 'red' : 'green'; console.log(color[logsColor](sectionHeader('Logs'))); for (const log of this.logs) { console.log(log); } console.log(color[logsColor](sectionFooter())); } } } for (const teardown of this.teardowns) { try { await teardown(); } catch (error) { console.log(color.red(sectionHeader('Teardown - Error Stack'))); console.log(error); console.log(color.red(sectionFooter())); } } }; /* CONTROL API */ this.log = (...values) => { this.logs.push(...values); }; this.plan = (count) => { this.stats.planned = count; }; this.teardown = (fn) => { this.teardowns.push(fn); }; this.timeout = (ms, message) => { this.timeoutMessage = message || this.timeoutMessage; this.timeoutMs = ms; }; /* ASSERTION API */ this.pass = this.makeAssertion(Assert.pass); this.fail = this.makeAssertion(Assert.fail); this.assert = this.makeAssertion(Assert.assert); this.truthy = this.makeAssertion(Assert.truthy); this.falsy = this.makeAssertion(Assert.falsy); this.true = this.makeAssertion(Assert.true); this.false = this.makeAssertion(Assert.false); this.is = this.makeAssertion(Assert.is); this.not = this.makeAssertion(Assert.not); this.deepEqual = this.makeAssertion(Assert.deepEqual); this.notDeepEqual = this.makeAssertion(Assert.notDeepEqual); this.like = this.makeAssertion(Assert.like); this.notLike = this.makeAssertion(Assert.notLike); this.throws = this.makeAssertion(Assert.throws); this.throwsAsync = this.makeAssertion(Assert.throwsAsync); this.notThrows = this.makeAssertion(Assert.notThrows); this.notThrowsAsync = this.makeAssertion(Assert.notThrowsAsync); this.regex = this.makeAssertion(Assert.regex); this.notRegex = this.makeAssertion(Assert.notRegex); this.wait = Assert.wait; this.ctx = {}; this.flags = flags; this.implementation = implementation; this.title = title; } } /* EXPORT */ export default Tester;