zora
Version:
tap test harness for nodejs and browsers
714 lines (689 loc) • 23.4 kB
JavaScript
var zora = (function (exports) {
'use strict';
const startTestMessage = (test, offset) => ({
type: "TEST_START" /* TEST_START */,
data: test,
offset
});
const assertionMessage = (assertion, offset) => ({
type: "ASSERTION" /* ASSERTION */,
data: assertion,
offset
});
const endTestMessage = (test, offset) => ({
type: "TEST_END" /* TEST_END */,
data: test,
offset
});
const bailout = (error, offset) => ({
type: "BAIL_OUT" /* BAIL_OUT */,
data: error,
offset
});
const delegateToCounter = (counter) => (target) => Object.defineProperties(target, {
skipCount: {
get() {
return counter.skipCount;
}
},
failureCount: {
get() {
return counter.failureCount;
}
},
successCount: {
get() {
return counter.successCount;
}
},
count: {
get() {
return counter.count;
}
}
});
const counter = () => {
let success = 0;
let failure = 0;
let skip = 0;
return Object.defineProperties({
update(assertion) {
const { pass, skip: isSkipped } = assertion;
if (isSkipped) {
skip++;
}
else if (!isAssertionResult(assertion)) {
skip += assertion.skipCount;
success += assertion.successCount;
failure += assertion.failureCount;
}
else if (pass) {
success++;
}
else {
failure++;
}
}
}, {
successCount: {
get() {
return success;
}
},
failureCount: {
get() {
return failure;
}
},
skipCount: {
get() {
return skip;
}
},
count: {
get() {
return skip + success + failure;
}
}
});
};
const defaultTestOptions = Object.freeze({
offset: 0,
skip: false,
runOnly: false
});
const noop = () => {
};
const TesterPrototype = {
[Symbol.asyncIterator]: async function* () {
await this.routine;
for (const assertion of this.assertions) {
if (assertion[Symbol.asyncIterator]) {
// Sub test
yield startTestMessage({ description: assertion.description }, this.offset);
yield* assertion;
if (assertion.error !== null) {
// Bubble up the error and return
this.error = assertion.error;
this.pass = false;
return;
}
}
yield assertionMessage(assertion, this.offset);
this.pass = this.pass && assertion.pass;
this.counter.update(assertion);
}
return this.error !== null ?
yield bailout(this.error, this.offset) :
yield endTestMessage(this, this.offset);
}
};
const testerLikeProvider = (BaseProto = TesterPrototype) => (assertions, routine, offset) => {
const testCounter = counter();
const withTestCounter = delegateToCounter(testCounter);
let pass = true;
return withTestCounter(Object.create(BaseProto, {
routine: {
value: routine
},
assertions: {
value: assertions
},
offset: {
value: offset
},
counter: {
value: testCounter
},
length: {
get() {
return assertions.length;
}
},
pass: {
enumerable: true,
get() {
return pass;
},
set(val) {
pass = val;
}
}
}));
};
const testerFactory = testerLikeProvider();
const map = fn => async function* (stream) {
for await (const m of stream) {
yield fn(m);
}
};
const tester = (description, spec, { offset = 0, skip = false, runOnly = false } = defaultTestOptions) => {
let executionTime = 0;
let error = null;
const assertions = [];
const collect = item => assertions.push(item);
const specFunction = skip === true ? noop : function zora_spec_fn() {
return spec(assert(collect, offset, runOnly));
};
const testRoutine = (async function () {
try {
const start = Date.now();
const result = await specFunction();
executionTime = Date.now() - start;
return result;
}
catch (e) {
error = e;
}
})();
return Object.defineProperties(testerFactory(assertions, testRoutine, offset), {
error: {
get() {
return error;
},
set(val) {
error = val;
}
},
executionTime: {
enumerable: true,
get() {
return executionTime;
}
},
skip: {
value: skip
},
description: {
enumerable: true,
value: description
}
});
};
var isArray = Array.isArray;
var keyList = Object.keys;
var hasProp = Object.prototype.hasOwnProperty;
var fastDeepEqual = function equal(a, b) {
if (a === b) return true;
if (a && b && typeof a == 'object' && typeof b == 'object') {
var arrA = isArray(a)
, arrB = isArray(b)
, i
, length
, key;
if (arrA && arrB) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0;)
if (!equal(a[i], b[i])) return false;
return true;
}
if (arrA != arrB) return false;
var dateA = a instanceof Date
, dateB = b instanceof Date;
if (dateA != dateB) return false;
if (dateA && dateB) return a.getTime() == b.getTime();
var regexpA = a instanceof RegExp
, regexpB = b instanceof RegExp;
if (regexpA != regexpB) return false;
if (regexpA && regexpB) return a.toString() == b.toString();
var keys = keyList(a);
length = keys.length;
if (length !== keyList(b).length)
return false;
for (i = length; i-- !== 0;)
if (!hasProp.call(b, keys[i])) return false;
for (i = length; i-- !== 0;) {
key = keys[i];
if (!equal(a[key], b[key])) return false;
}
return true;
}
return a!==a && b!==b;
};
const isAssertionResult = (result) => {
return 'operator' in result;
};
const specFnRegexp = /zora_spec_fn/;
const zoraInternal = /zora\/dist\/bundle/;
const filterStackLine = l => (l && !zoraInternal.test(l) && !l.startsWith('Error') || specFnRegexp.test(l));
const getAssertionLocation = () => {
const err = new Error();
const stack = (err.stack || '')
.split('\n')
.map(l => l.trim())
.filter(filterStackLine);
const userLandIndex = stack.findIndex(l => specFnRegexp.test(l));
const stackline = userLandIndex >= 1 ? stack[userLandIndex - 1] : (stack[0] || 'N/A');
return stackline
.replace(/^at|^@/, '');
};
const assertMethodHook = (fn) => function (...args) {
// @ts-ignore
return this.collect(fn(...args));
};
const aliasMethodHook = (methodName) => function (...args) {
return this[methodName](...args);
};
const AssertPrototype = {
equal: assertMethodHook((actual, expected, description = 'should be equivalent') => ({
pass: fastDeepEqual(actual, expected),
actual,
expected,
description,
operator: "equal" /* EQUAL */
})),
equals: aliasMethodHook('equal'),
eq: aliasMethodHook('equal'),
deepEqual: aliasMethodHook('equal'),
notEqual: assertMethodHook((actual, expected, description = 'should not be equivalent') => ({
pass: !fastDeepEqual(actual, expected),
actual,
expected,
description,
operator: "notEqual" /* NOT_EQUAL */
})),
notEquals: aliasMethodHook('notEqual'),
notEq: aliasMethodHook('notEqual'),
notDeepEqual: aliasMethodHook('notEqual'),
is: assertMethodHook((actual, expected, description = 'should be the same') => ({
pass: Object.is(actual, expected),
actual,
expected,
description,
operator: "is" /* IS */
})),
same: aliasMethodHook('is'),
isNot: assertMethodHook((actual, expected, description = 'should not be the same') => ({
pass: !Object.is(actual, expected),
actual,
expected,
description,
operator: "isNot" /* IS_NOT */
})),
notSame: aliasMethodHook('isNot'),
ok: assertMethodHook((actual, description = 'should be truthy') => ({
pass: Boolean(actual),
actual,
expected: 'truthy value',
description,
operator: "ok" /* OK */
})),
truthy: aliasMethodHook('ok'),
notOk: assertMethodHook((actual, description = 'should be falsy') => ({
pass: !Boolean(actual),
actual,
expected: 'falsy value',
description,
operator: "notOk" /* NOT_OK */
})),
falsy: aliasMethodHook('notOk'),
fail: assertMethodHook((description = 'fail called') => ({
pass: false,
actual: 'fail called',
expected: 'fail not called',
description,
operator: "fail" /* FAIL */
})),
throws: assertMethodHook((func, expected, description) => {
let caught;
let pass;
let actual;
if (typeof expected === 'string') {
[expected, description] = [description, expected];
}
try {
func();
}
catch (err) {
caught = { error: err };
}
pass = caught !== undefined;
actual = caught && caught.error;
if (expected instanceof RegExp) {
pass = expected.test(actual) || expected.test(actual && actual.message);
actual = actual && actual.message || actual;
expected = String(expected);
}
else if (typeof expected === 'function' && caught) {
pass = actual instanceof expected;
actual = actual.constructor;
}
return {
pass,
actual,
expected,
description: description || 'should throw',
operator: "throws" /* THROWS */
};
}),
doesNotThrow: assertMethodHook((func, expected, description) => {
let caught;
if (typeof expected === 'string') {
[expected, description] = [description, expected];
}
try {
func();
}
catch (err) {
caught = { error: err };
}
return {
pass: caught === undefined,
expected: 'no thrown error',
actual: caught && caught.error,
operator: "doesNotThrow" /* DOES_NOT_THROW */,
description: description || 'should not throw'
};
})
};
const assert = (collect, offset, runOnly = false) => {
const actualCollect = item => {
if (!item.pass) {
item.at = getAssertionLocation();
}
collect(item);
return item;
};
const test = (description, spec, opts) => {
const options = Object.assign({}, defaultTestOptions, opts, { offset: offset + 1, runOnly });
const subTest = tester(description, spec, options);
collect(subTest);
return subTest.routine;
};
const skip = (description, spec, opts) => {
return test(description, spec, Object.assign({}, opts, { skip: true }));
};
return Object.assign(Object.create(AssertPrototype, { collect: { value: actualCollect } }), {
test(description, spec, opts = {}) {
if (runOnly) {
return skip(description, spec, opts);
}
return test(description, spec, opts);
},
skip(description, spec = noop, opts = {}) {
return skip(description, spec, opts);
},
only(description, spec, opts = {}) {
const specFn = runOnly === false ? _ => {
throw new Error(`Can not use "only" method when not in run only mode`);
} : spec;
return test(description, specFn, opts);
}
});
};
const flatten = map(m => {
m.offset = 0;
return m;
});
const print = (message, offset = 0) => {
console.log(message.padStart(message.length + (offset * 4))); // 4 white space used as indent (see tap-parser)
};
const stringifySymbol = (key, value) => {
if (typeof value === 'symbol') {
return value.toString();
}
return value;
};
const printYAML = (obj, offset = 0) => {
const YAMLOffset = offset + 0.5;
print('---', YAMLOffset);
for (const [prop, value] of Object.entries(obj)) {
print(`${prop}: ${JSON.stringify(value, stringifySymbol)}`, YAMLOffset + 0.5);
}
print('...', YAMLOffset);
};
const comment = (value, offset) => {
print(`# ${value}`, offset);
};
const subTestPrinter = (prefix = '') => (message) => {
const { data } = message;
const value = `${prefix}${data.description}`;
comment(value, message.offset);
};
const indentedSubTest = subTestPrinter('Subtest: ');
const flatSubTest = subTestPrinter();
const testEnd = (message) => {
const length = message.data.length;
const { offset } = message;
print(`1..${length}`, offset);
};
const bailOut = (message) => {
print('Bail out! Unhandled error.');
};
const indentedDiagnostic = ({ expected, pass, description, actual, operator, at = 'N/A', ...rest }) => ({
wanted: expected,
found: actual,
at,
operator,
...rest
});
const flatDiagnostic = ({ pass, description, ...rest }) => rest;
const indentedAssertion = (message, idIter) => {
const { data, offset } = message;
const { pass, description } = data;
const label = pass === true ? 'ok' : 'not ok';
const { value: id } = idIter.next();
if (isAssertionResult(data)) {
print(`${label} ${id} - ${description}`, offset);
if (pass === false) {
printYAML(indentedDiagnostic(data), offset);
}
}
else {
const comment = data.skip === true ? 'SKIP' : `${data.executionTime}ms`;
print(`${pass ? 'ok' : 'not ok'} ${id} - ${description} # ${comment}`, message.offset);
}
};
const flatAssertion = (message, idIter) => {
const { data, offset } = message;
const { pass, description } = data;
const label = pass === true ? 'ok' : 'not ok';
if (isAssertionResult(data)) {
const { value: id } = idIter.next();
print(`${label} ${id} - ${description}`, offset);
if (pass === false) {
printYAML(flatDiagnostic(data), offset);
}
}
else if (data.skip) {
const { value: id } = idIter.next();
print(`${pass ? 'ok' : 'not ok'} ${id} - ${description} # SKIP`, offset);
}
};
const indentedReport = (message, id) => {
switch (message.type) {
case "TEST_START" /* TEST_START */:
id.fork();
indentedSubTest(message);
break;
case "ASSERTION" /* ASSERTION */:
indentedAssertion(message, id);
break;
case "TEST_END" /* TEST_END */:
id.merge();
testEnd(message);
break;
case "BAIL_OUT" /* BAIL_OUT */:
bailOut();
throw message.data;
}
};
const flatReport = (message, id) => {
switch (message.type) {
case "TEST_START" /* TEST_START */:
flatSubTest(message);
break;
case "ASSERTION" /* ASSERTION */:
flatAssertion(message, id);
break;
case "BAIL_OUT" /* BAIL_OUT */:
bailOut();
throw message.data;
}
};
const summary = (harness) => {
print('', 0);
comment(harness.pass ? 'ok' : 'not ok', 0);
comment(`success: ${harness.successCount}`, 0);
comment(`skipped: ${harness.skipCount}`, 0);
comment(`failure: ${harness.failureCount}`, 0);
};
const id = function* () {
let i = 0;
while (true) {
yield ++i;
}
};
const idGen = () => {
let stack = [id()];
return {
[Symbol.iterator]() {
return this;
},
next() {
return stack[0].next();
},
fork() {
stack.unshift(id());
},
merge() {
stack.shift();
}
};
};
const tapeTapLike = async (stream) => {
const src = flatten(stream);
const id = idGen();
print('TAP version 13');
for await (const message of src) {
flatReport(message, id);
}
print(`1..${stream.count}`, 0);
summary(stream);
};
const mochaTapLike = async (stream) => {
print('TAP version 13');
const id = idGen();
for await (const message of stream) {
indentedReport(message, id);
}
summary(stream);
};
const harnessFactory = ({ runOnly = false, indent = false } = {
runOnly: false,
indent: false
}) => {
const tests = [];
const rootOffset = 0;
const collect = item => tests.push(item);
const api = assert(collect, rootOffset, runOnly);
let error = null;
const factory = testerLikeProvider(Object.assign(api, TesterPrototype, {
report: async function (reporter) {
const rep = reporter || (indent ? mochaTapLike : tapeTapLike);
return rep(this);
}
}));
return Object.defineProperties(factory(tests, Promise.resolve(), rootOffset), {
error: {
get() {
return error;
},
set(val) {
error = val;
}
}
});
};
const findConfigurationFlag = (name) => {
if (typeof process !== 'undefined') {
return process.env[name] === 'true';
// @ts-ignore
}
else if (typeof window !== 'undefined') {
// @ts-ignore
return Boolean(window[name]);
}
return false;
};
const defaultTestHarness = harnessFactory({
runOnly: findConfigurationFlag('RUN_ONLY')
});
let autoStart = true;
let indent = findConfigurationFlag('INDENT');
const rootTest = defaultTestHarness.test.bind(defaultTestHarness);
rootTest.indent = () => {
console.warn('indent function is deprecated, use "INDENT" configuration flag instead');
indent = true;
};
const test = rootTest;
const skip = defaultTestHarness.skip.bind(defaultTestHarness);
const only = defaultTestHarness.only.bind(defaultTestHarness);
rootTest.skip = skip;
const equal = defaultTestHarness.equal.bind(defaultTestHarness);
const equals = equal;
const eq = equal;
const deepEqual = equal;
const notEqual = defaultTestHarness.notEqual.bind(defaultTestHarness);
const notEquals = notEqual;
const notEq = notEqual;
const notDeepEqual = notEqual;
const is = defaultTestHarness.is.bind(defaultTestHarness);
const same = is;
const isNot = defaultTestHarness.isNot.bind(defaultTestHarness);
const notSame = isNot;
const ok = defaultTestHarness.ok.bind(defaultTestHarness);
const truthy = ok;
const notOk = defaultTestHarness.notOk.bind(defaultTestHarness);
const falsy = notOk;
const fail = defaultTestHarness.fail.bind(defaultTestHarness);
const throws = defaultTestHarness.throws.bind(defaultTestHarness);
const doesNotThrow = defaultTestHarness.doesNotThrow.bind(defaultTestHarness);
const createHarness = (opts = {}) => {
autoStart = false;
return harnessFactory(opts);
};
const start = () => {
if (autoStart) {
defaultTestHarness.report(indent ? mochaTapLike : tapeTapLike);
}
};
// on next tick start reporting
// @ts-ignore
if (typeof window === 'undefined') {
setTimeout(start, 0);
}
else {
// @ts-ignore
window.addEventListener('load', start);
}
exports.AssertPrototype = AssertPrototype;
exports.createHarness = createHarness;
exports.deepEqual = deepEqual;
exports.doesNotThrow = doesNotThrow;
exports.eq = eq;
exports.equal = equal;
exports.equals = equals;
exports.fail = fail;
exports.falsy = falsy;
exports.is = is;
exports.isNot = isNot;
exports.mochaTapLike = mochaTapLike;
exports.notDeepEqual = notDeepEqual;
exports.notEq = notEq;
exports.notEqual = notEqual;
exports.notEquals = notEquals;
exports.notOk = notOk;
exports.notSame = notSame;
exports.ok = ok;
exports.only = only;
exports.same = same;
exports.skip = skip;
exports.tapeTapLike = tapeTapLike;
exports.test = test;
exports.throws = throws;
exports.truthy = truthy;
return exports;
}({}));
//# sourceMappingURL=zora.js.map