fava
Version:
A wannabe tiny largely-drop-in replacement for ava that works in the browser too.
325 lines (324 loc) • 13.6 kB
JavaScript
/* 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;