zora
Version:
the lightest yet the fastest javascript testing library
783 lines (663 loc) • 18.4 kB
JavaScript
;
var node_util = require('node:util');
const Operator = {
EQUAL: 'equal',
NOT_EQUAL: 'notEqual',
IS: 'is',
OK: 'ok',
NOT_OK: 'notOk',
IS_NOT: 'isNot',
FAIL: 'fail',
THROWS: 'throws',
};
const getAssertionLocation = ({ rawStack }) => {
const stackline = (rawStack || '')
.split('\n')
.map((l) => l.trim())
.find((l) => l && !l.startsWith('Error'));
return (stackline || 'N/A').replace(/^at|^@/, '').trim();
};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var es6;
var hasRequiredEs6;
function requireEs6 () {
if (hasRequiredEs6) return es6;
hasRequiredEs6 = 1;
es6 = function equal(a, b) {
if (a === b) return true;
if (a && b && typeof a == 'object' && typeof b == 'object') {
if (a.constructor !== b.constructor) return false;
var length, i, keys;
if (Array.isArray(a)) {
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 ((a instanceof Map) && (b instanceof Map)) {
if (a.size !== b.size) return false;
for (i of a.entries())
if (!b.has(i[0])) return false;
for (i of a.entries())
if (!equal(i[1], b.get(i[0]))) return false;
return true;
}
if ((a instanceof Set) && (b instanceof Set)) {
if (a.size !== b.size) return false;
for (i of a.entries())
if (!b.has(i[0])) return false;
return true;
}
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0;)
if (a[i] !== b[i]) return false;
return true;
}
if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length) return false;
for (i = length; i-- !== 0;)
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
for (i = length; i-- !== 0;) {
var key = keys[i];
if (!equal(a[key], b[key])) return false;
}
return true;
}
// true if both NaN, false otherwise
return a!==a && b!==b;
};
return es6;
}
var es6Exports = /*@__PURE__*/ requireEs6();
var eq = /*@__PURE__*/getDefaultExportFromCjs(es6Exports);
const equal = (
actual,
expected,
description = 'should be equivalent'
) => ({
pass: eq(actual, expected),
actual,
expected,
description,
operator: Operator.EQUAL,
});
const notEqual = (
actual,
expected,
description = 'should not be equivalent'
) => ({
pass: !eq(actual, expected),
actual,
expected,
description,
operator: Operator.NOT_EQUAL,
});
const is = (actual, expected, description = 'should be the same') => ({
pass: Object.is(actual, expected),
actual,
expected,
description,
operator: Operator.IS,
});
const isNot = (
actual,
expected,
description = 'should not be the same'
) => ({
pass: !Object.is(actual, expected),
actual,
expected,
description,
operator: Operator.IS_NOT,
});
const ok = (actual, description = 'should be truthy') => ({
pass: Boolean(actual),
actual,
expected: 'truthy value',
description,
operator: Operator.OK,
});
const notOk = (actual, description = 'should be falsy') => ({
pass: !Boolean(actual),
actual,
expected: 'falsy value',
description,
operator: Operator.NOT_OK,
});
const fail = (description = 'fail called') => ({
pass: false,
actual: 'fail called',
expected: 'fail not called',
description,
operator: Operator.FAIL,
});
const throws = (func, expected, description = 'should throw') => {
let caught;
let pass;
let actual;
if (typeof expected === 'string') {
[expected, description] = [void 0, expected];
}
try {
func();
} catch (err) {
caught = { error: err };
}
pass = caught !== undefined;
actual = caught?.error;
if (expected instanceof RegExp) {
pass = expected.test(actual) || expected.test(actual && actual.message);
actual = actual?.message ?? actual;
expected = String(expected);
} else if (typeof expected === 'function' && caught) {
pass = actual instanceof expected;
actual = actual.constructor;
} else {
actual = pass ? 'error thrown' : 'no error thrown';
}
return {
pass,
actual,
expected: expected ?? 'any error thrown',
description: description,
operator: Operator.THROWS,
};
};
const Assert$1 = {
equal,
equals: equal,
eq: equal,
deepEqual: equal,
same: equal,
notEqual,
notEquals: notEqual,
notEq: notEqual,
notDeepEqual: notEqual,
notSame: notEqual,
is,
isNot,
ok,
truthy: ok,
notOk,
falsy: notOk,
fail,
throws,
};
const noop$1 = () => {};
var assertFactory = (
{ onResult = noop$1 } = {
onResult: noop$1,
},
) => {
return Object.fromEntries(
Object.entries(Assert$1).map(([methodName, fn]) => {
const decoratedFn = (...args) => {
const result = fn(...args);
if (result.pass === false) {
const stackTarget = {};
Error.captureStackTrace(stackTarget, decoratedFn);
const fullResult = {
...result,
at: getAssertionLocation({ rawStack: stackTarget.stack }),
};
onResult(fullResult);
return fullResult;
}
onResult(result);
return result;
};
return [methodName, decoratedFn];
}),
);
};
const Assert = Assert$1;
const MESSAGE_TYPE = {
TEST_START: 'TEST_START',
ASSERTION: 'ASSERTION',
TEST_END: 'TEST_END',
ERROR: 'ERROR'};
const newTestMessage = ({ description, skip }) => ({
type: MESSAGE_TYPE.TEST_START,
data: { description, skip },
});
const assertionMessage = (data) => ({
type: MESSAGE_TYPE.ASSERTION,
data,
});
const testEndMessage = ({ description, executionTime }) => ({
type: MESSAGE_TYPE.TEST_END,
data: {
description,
executionTime,
},
});
const errorMessage = ({ error }) => ({
type: MESSAGE_TYPE.ERROR,
data: {
error,
},
});
const isNode$1 = typeof process !== 'undefined';
const flatDiagnostic = ({ pass, description, ...rest }) => rest;
const createReplacer = () => {
const visited = new Set();
return (key, value) => {
if (isObject(value)) {
if (visited.has(value)) {
return '[__CIRCULAR_REF__]';
}
visited.add(value);
}
if (typeof value === 'symbol') {
return value.toString();
}
return value;
};
};
const isObject = (candidate) =>
typeof candidate === 'object' && candidate !== null;
const stringify = (value) => JSON.stringify(value, createReplacer());
const defaultSerializer = stringify;
const defaultLogger = (value) => console.log(value);
const isAssertionFailing = (message) =>
message.type === MESSAGE_TYPE.ASSERTION && !message.data.pass;
const isSkipped = (message) =>
message.type === MESSAGE_TYPE.TEST_START && message.data.skip;
const eventuallySetExitCode = (message) => {
if (isNode$1 && isAssertionFailing(message)) {
process.exitCode = 1;
}
};
const compose = (fns) => (arg) =>
fns.reduceRight((arg, fn) => fn(arg), arg);
const filter = (predicate) =>
async function* (stream) {
for await (const element of stream) {
if (predicate(element)) {
yield element;
}
}
};
const idSequence = () => {
let id = 0;
return () => ++id;
};
const createCounter = () => {
const nextId = idSequence();
let success = 0;
let failure = 0;
let skip = 0;
return Object.create(
{
increment(message) {
const { type } = message;
if (isSkipped(message)) {
skip++;
} else if (type === MESSAGE_TYPE.ASSERTION) {
success += message.data.pass === true ? 1 : 0;
failure += message.data.pass === false ? 1 : 0;
}
},
nextId,
},
{
success: {
enumerable: true,
get() {
return success;
},
},
failure: {
enumerable: true,
get() {
return failure;
},
},
skip: {
enumerable: true,
get() {
return skip;
},
},
total: {
enumerable: true,
get() {
return skip + failure + success;
},
},
}
);
};
const createWriter$1 = ({
log = defaultLogger,
serialize = defaultSerializer,
version = 13,
} = {}) => {
const print = (message, padding = 0) => {
log(message.padStart(message.length + padding * 4)); // 4 white space used as indent
};
const printYAML = (obj, padding = 0) => {
const YAMLPadding = padding + 0.5;
print('---', YAMLPadding);
for (const [prop, value] of Object.entries(obj)) {
print(`${prop}: ${serialize(value)}`, YAMLPadding + 0.5);
}
print('...', YAMLPadding);
};
const printComment = (comment, padding = 0) => {
print(`# ${comment}`, padding);
};
const printBailOut = () => {
print('Bail out! Unhandled error.');
};
const printTestStart = (newTestMessage) => {
const {
data: { description },
} = newTestMessage;
printComment(description);
};
const printAssertion = (assertionMessage, { id, comment = '' }) => {
const { data } = assertionMessage;
const { pass, description } = data;
const label = pass === true ? 'ok' : 'not ok';
const directiveComment = comment ? ` # ${comment}` : '';
print(`${label} ${id} - ${description}` + directiveComment);
if (pass === false) {
printYAML(flatDiagnostic(data));
}
};
const printSummary = ({ success, skip, failure, total }) => {
print('', 0);
print(`1..${total}`);
printComment(`tests ${total}`, 0);
printComment(`pass ${success}`, 0);
printComment(`fail ${failure}`, 0);
printComment(`skip ${skip}`, 0);
};
const printHeader = () => {
print(`TAP version ${version}`);
};
return {
print,
printYAML,
printComment,
printBailOut,
printTestStart,
printAssertion,
printSummary,
printHeader,
};
};
const isNotTestEnd = ({ type }) => type !== MESSAGE_TYPE.TEST_END;
const filterOutTestEnd = filter(isNotTestEnd);
const writeMessage$1 = ({ writer, nextId }) => {
const writerTable = {
[MESSAGE_TYPE.ASSERTION](message) {
return writer.printAssertion(message, { id: nextId() });
},
[MESSAGE_TYPE.TEST_START](message) {
if (message.data.skip) {
const skippedAssertionMessage = assertionMessage({
description: message.data.description,
pass: true,
});
return writer.printAssertion(skippedAssertionMessage, {
comment: 'SKIP',
id: nextId(),
});
}
return writer.printTestStart(message);
},
[MESSAGE_TYPE.ERROR](message) {
writer.printBailOut();
throw message.data.error;
},
};
return (message) => writerTable[message.type]?.(message);
};
var index$2 = ({ log = defaultLogger, serialize = defaultSerializer } = {}) =>
async (messageStream) => {
const writer = createWriter$1({
log,
serialize,
});
const counter = createCounter();
const write = writeMessage$1({ writer, nextId: counter.nextId });
const stream = filterOutTestEnd(messageStream);
writer.printHeader();
for await (const message of stream) {
counter.increment(message);
write(message);
eventuallySetExitCode(message);
}
writer.printSummary(counter);
};
var index$1 = ({
log = defaultLogger,
serialize = defaultSerializer,
} = {}) => {
const print = compose([log, serialize]);
return async (messageStream) => {
for await (const message of messageStream) {
eventuallySetExitCode(message);
print(message);
}
};
};
const curry2 = (fn) => (a, b) => {
if (b === void 0) {
return (b) => fn(a, b);
}
return fn(a, b);
};
const leftPad = curry2((offset, string) =>
string.padStart(string.length + offset)
);
const rightPad = curry2((offset, string) =>
string.padEnd(string.length + offset)
);
const withMargin = compose([leftPad(1), rightPad(1)]);
const bold = (s) => node_util.styleText('bold', s);
const underline = (s) => node_util.styleText('underline', s);
const bgRed = (s) => node_util.styleText('bgRed', s);
const bgGreen = (s) => node_util.styleText('bgGreen', s);
const bgYellow = (s) => node_util.styleText('bgYellow', s);
const gray = (s) => node_util.styleText('gray', s);
const whiteBright = (s) => node_util.styleText('whiteBright', s);
const cyan = (s) => node_util.styleText('cyan', s);
const badge = (fn) => compose([fn, bold, withMargin, String]);
const createTheme = ({
bgError = bgRed,
bgSuccess = bgGreen,
bgSkip = bgYellow,
disableFont = gray,
badgeFont = whiteBright,
adornerFont = cyan,
} = {}) => {
const success = compose([bgSuccess, badgeFont]);
const error = compose([bgError, badgeFont]);
return {
errorBadge: badge(error),
successBadge: badge(success),
skipBadge: badge(compose([bgSkip, badgeFont])),
disable: compose([disableFont, withMargin]),
header: bold,
adorner: adornerFont,
emphasis: compose([underline, bold]),
operator: adornerFont,
light: disableFont,
diffExpected: success,
diffActual: error,
};
};
createTheme();
createTheme();
const findConfigurationValue = (name) => {
if (isNode) {
return process.env[name];
} else if (isDeno) {
return Deno.env.get(name);
} else if (isBrowser) {
return window[name];
}
};
const isNode = typeof process !== 'undefined';
const isBrowser = typeof window !== 'undefined';
const isDeno = typeof Deno !== 'undefined';
const DEFAULT_TIMEOUT = 5_000;
const defaultOptions = Object.freeze({
skip: false,
timeout: findConfigurationValue('ZORA_TIMEOUT') || DEFAULT_TIMEOUT,
});
const noop = () => {};
const isTest = (assertionLike) =>
assertionLike[Symbol.asyncIterator] !== void 0;
Assert.test = (description, spec, opts = defaultOptions) =>
test$1(description, spec, opts);
Assert.skip = (description, spec, opts = defaultOptions) =>
test$1(description, spec, { ...opts, skip: true });
Assert.only = () => {
throw new Error(`Can not use "only" method when not in "run only" mode`);
};
const createTimeoutResult = ({ timeout }) => ({
operator: 'timeout',
pass: false,
actual: `test takes longer than ${timeout}ms to complete`,
expected: `test takes less than ${timeout}ms to complete`,
description:
'The test did no complete on time. refer to https://github.com/lorenzofox3/zora/tree/master/zora#test-timeout for more info',
});
const test$1 = (description, spec, opts = defaultOptions) => {
const { skip = false, timeout = DEFAULT_TIMEOUT } = opts;
const assertions = [];
let executionTime;
let done = false;
let error;
const onResult = (assertion) => {
if (done) {
throw new Error(`test "${description}"
tried to collect an assertion after it has run to its completion.
You might have forgotten to wait for an asynchronous task to complete
------
${spec.toString()}`);
}
assertions.push(assertion);
};
const specFn = skip
? noop
: function zora_spec_fn() {
return spec(assertFactory({ onResult }));
};
const testRoutine = (async function () {
const { promise, resolve } = Promise.withResolvers();
try {
const start = Date.now();
const timeoutId = setTimeout(() => {
const timeoutResult = createTimeoutResult({ timeout });
onResult(timeoutResult);
resolve(timeoutResult);
}, timeout);
const result = await Promise.race([specFn(), promise]);
clearTimeout(timeoutId);
executionTime = Date.now() - start;
return result;
} catch (e) {
error = e;
} finally {
done = true;
}
})();
return Object.assign(testRoutine, {
[Symbol.asyncIterator]: async function* () {
yield newTestMessage({ description, skip });
await testRoutine;
for (const assertion of assertions) {
if (isTest(assertion)) {
yield* assertion;
} else {
yield assertionMessage(assertion);
}
}
if (error) {
yield errorMessage({ error });
}
yield testEndMessage({ description, executionTime });
},
});
};
const createAssert = assertFactory;
const createHarness$1 = ({ onlyMode = false } = {}) => {
const tests = [];
// WARNING if the "onlyMode is passed to any harness, all the harnesses will be affected.
// However, we do not expect multiple harnesses to be used in the same process
if (onlyMode) {
const { skip, test } = Assert;
Assert.test = skip;
Assert.only = test;
}
const { test, skip, only } = createAssert({
onResult: (test) => tests.push(test),
});
// for convenience
test.only = only;
test.skip = skip;
return {
only,
test,
skip,
report({ reporter }) {
return reporter(createMessageStream(tests));
},
};
};
async function* createMessageStream(tests) {
for (const test of tests) {
yield* test;
}
}
let autoStart = true;
const harness = createHarness$1({
onlyMode: findConfigurationValue('ZORA_ONLY') !== void 0,
});
const only = harness.only;
const test = harness.test;
const skip = harness.skip;
const report = harness.report;
const hold = () => !(autoStart = false);
const createHarness = (opts) => {
hold();
return createHarness$1(opts);
};
const start = async () => {
if (autoStart) {
const reporter =
findConfigurationValue('ZORA_REPORTER') === 'json'
? index$1()
: index$2();
await report({ reporter });
}
};
// on next tick start reporting
if (!isBrowser) {
setTimeout(start, 0);
} else {
window.addEventListener('load', start);
}
exports.Assert = Assert;
exports.createHarness = createHarness;
exports.createJSONReporter = index$1;
exports.createTAPReporter = index$2;
exports.hold = hold;
exports.only = only;
exports.report = report;
exports.skip = skip;
exports.test = test;