379 lines
9.3 KiB
JavaScript
379 lines
9.3 KiB
JavaScript
|
'use strict';
|
||
|
const coreAssert = require('core-assert');
|
||
|
const deepEqual = require('lodash.isequal');
|
||
|
const observableToPromise = require('observable-to-promise');
|
||
|
const isObservable = require('is-observable');
|
||
|
const isPromise = require('is-promise');
|
||
|
const jestDiff = require('jest-diff');
|
||
|
const enhanceAssert = require('./enhance-assert');
|
||
|
const formatAssertError = require('./format-assert-error');
|
||
|
|
||
|
class AssertionError extends Error {
|
||
|
constructor(opts) {
|
||
|
super(opts.message || '');
|
||
|
this.name = 'AssertionError';
|
||
|
|
||
|
this.assertion = opts.assertion;
|
||
|
this.fixedSource = opts.fixedSource;
|
||
|
this.improperUsage = opts.improperUsage || false;
|
||
|
this.operator = opts.operator;
|
||
|
this.values = opts.values || [];
|
||
|
|
||
|
// Reserved for power-assert statements
|
||
|
this.statements = [];
|
||
|
|
||
|
if (opts.stack) {
|
||
|
this.stack = opts.stack;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
exports.AssertionError = AssertionError;
|
||
|
|
||
|
function getStack() {
|
||
|
const obj = {};
|
||
|
Error.captureStackTrace(obj, getStack);
|
||
|
return obj.stack;
|
||
|
}
|
||
|
|
||
|
function wrapAssertions(callbacks) {
|
||
|
const pass = callbacks.pass;
|
||
|
const pending = callbacks.pending;
|
||
|
const fail = callbacks.fail;
|
||
|
|
||
|
const noop = () => {};
|
||
|
const makeNoop = () => noop;
|
||
|
const makeRethrow = reason => () => {
|
||
|
throw reason;
|
||
|
};
|
||
|
|
||
|
const assertions = {
|
||
|
pass() {
|
||
|
pass(this);
|
||
|
},
|
||
|
|
||
|
fail(message) {
|
||
|
fail(this, new AssertionError({
|
||
|
assertion: 'fail',
|
||
|
message: message || 'Test failed via `t.fail()`'
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
is(actual, expected, message) {
|
||
|
if (actual === expected) {
|
||
|
pass(this);
|
||
|
} else {
|
||
|
const diff = formatAssertError.formatDiff(actual, expected);
|
||
|
const values = diff ? [diff] : [
|
||
|
formatAssertError.formatWithLabel('Actual:', actual),
|
||
|
formatAssertError.formatWithLabel('Must be strictly equal to:', expected)
|
||
|
];
|
||
|
|
||
|
fail(this, new AssertionError({
|
||
|
assertion: 'is',
|
||
|
message,
|
||
|
operator: '===',
|
||
|
values
|
||
|
}));
|
||
|
}
|
||
|
},
|
||
|
|
||
|
not(actual, expected, message) {
|
||
|
if (actual === expected) {
|
||
|
fail(this, new AssertionError({
|
||
|
assertion: 'not',
|
||
|
message,
|
||
|
operator: '!==',
|
||
|
values: [formatAssertError.formatWithLabel('Value is strictly equal:', actual)]
|
||
|
}));
|
||
|
} else {
|
||
|
pass(this);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
deepEqual(actual, expected, message) {
|
||
|
if (deepEqual(actual, expected)) {
|
||
|
pass(this);
|
||
|
} else {
|
||
|
const diff = formatAssertError.formatDiff(actual, expected);
|
||
|
const values = diff ? [diff] : [
|
||
|
formatAssertError.formatWithLabel('Actual:', actual),
|
||
|
formatAssertError.formatWithLabel('Must be deeply equal to:', expected)
|
||
|
];
|
||
|
|
||
|
fail(this, new AssertionError({
|
||
|
assertion: 'deepEqual',
|
||
|
message,
|
||
|
values
|
||
|
}));
|
||
|
}
|
||
|
},
|
||
|
|
||
|
notDeepEqual(actual, expected, message) {
|
||
|
if (deepEqual(actual, expected)) {
|
||
|
fail(this, new AssertionError({
|
||
|
assertion: 'notDeepEqual',
|
||
|
message,
|
||
|
values: [formatAssertError.formatWithLabel('Value is deeply equal:', actual)]
|
||
|
}));
|
||
|
} else {
|
||
|
pass(this);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
throws(fn, err, message) {
|
||
|
let promise;
|
||
|
if (isPromise(fn)) {
|
||
|
promise = fn;
|
||
|
} else if (isObservable(fn)) {
|
||
|
promise = observableToPromise(fn);
|
||
|
} else if (typeof fn !== 'function') {
|
||
|
fail(this, new AssertionError({
|
||
|
assertion: 'throws',
|
||
|
improperUsage: true,
|
||
|
message: '`t.throws()` must be called with a function, Promise, or Observable',
|
||
|
values: [formatAssertError.formatWithLabel('Called with:', fn)]
|
||
|
}));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let coreAssertThrowsErrorArg;
|
||
|
if (typeof err === 'string') {
|
||
|
const expectedMessage = err;
|
||
|
coreAssertThrowsErrorArg = error => error.message === expectedMessage;
|
||
|
} else {
|
||
|
// Assume it's a constructor function or regular expression
|
||
|
coreAssertThrowsErrorArg = err;
|
||
|
}
|
||
|
|
||
|
const test = (fn, stack) => {
|
||
|
let actual;
|
||
|
let threw = false;
|
||
|
try {
|
||
|
coreAssert.throws(() => {
|
||
|
try {
|
||
|
fn();
|
||
|
} catch (err) {
|
||
|
actual = err;
|
||
|
threw = true;
|
||
|
throw err;
|
||
|
}
|
||
|
}, coreAssertThrowsErrorArg);
|
||
|
return actual;
|
||
|
} catch (err) {
|
||
|
const values = threw ?
|
||
|
[formatAssertError.formatWithLabel('Threw unexpected exception:', actual)] :
|
||
|
null;
|
||
|
|
||
|
throw new AssertionError({
|
||
|
assertion: 'throws',
|
||
|
message,
|
||
|
stack,
|
||
|
values
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if (promise) {
|
||
|
// Record stack before it gets lost in the promise chain.
|
||
|
const stack = getStack();
|
||
|
const intermediate = promise.then(makeNoop, makeRethrow).then(fn => test(fn, stack));
|
||
|
pending(this, intermediate);
|
||
|
// Don't reject the returned promise, even if the assertion fails.
|
||
|
return intermediate.catch(noop);
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
const retval = test(fn);
|
||
|
pass(this);
|
||
|
return retval;
|
||
|
} catch (err) {
|
||
|
fail(this, err);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
notThrows(fn, message) {
|
||
|
let promise;
|
||
|
if (isPromise(fn)) {
|
||
|
promise = fn;
|
||
|
} else if (isObservable(fn)) {
|
||
|
promise = observableToPromise(fn);
|
||
|
} else if (typeof fn !== 'function') {
|
||
|
fail(this, new AssertionError({
|
||
|
assertion: 'notThrows',
|
||
|
improperUsage: true,
|
||
|
message: '`t.notThrows()` must be called with a function, Promise, or Observable',
|
||
|
values: [formatAssertError.formatWithLabel('Called with:', fn)]
|
||
|
}));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const test = (fn, stack) => {
|
||
|
try {
|
||
|
coreAssert.doesNotThrow(fn);
|
||
|
} catch (err) {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'notThrows',
|
||
|
message,
|
||
|
stack,
|
||
|
values: [formatAssertError.formatWithLabel('Threw:', err.actual)]
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if (promise) {
|
||
|
// Record stack before it gets lost in the promise chain.
|
||
|
const stack = getStack();
|
||
|
const intermediate = promise.then(noop, reason => test(makeRethrow(reason), stack));
|
||
|
pending(this, intermediate);
|
||
|
// Don't reject the returned promise, even if the assertion fails.
|
||
|
return intermediate.catch(noop);
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
test(fn);
|
||
|
pass(this);
|
||
|
} catch (err) {
|
||
|
fail(this, err);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
ifError(actual, message) {
|
||
|
if (actual) {
|
||
|
fail(this, new AssertionError({
|
||
|
assertion: 'ifError',
|
||
|
message,
|
||
|
values: [formatAssertError.formatWithLabel('Error:', actual)]
|
||
|
}));
|
||
|
} else {
|
||
|
pass(this);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
snapshot(actual, message) {
|
||
|
const state = this._test.getSnapshotState();
|
||
|
const result = state.match(this.title, actual);
|
||
|
if (result.pass) {
|
||
|
pass(this);
|
||
|
} else {
|
||
|
const diff = jestDiff(result.expected.trim(), result.actual.trim(), {expand: true})
|
||
|
// Remove annotation
|
||
|
.split('\n')
|
||
|
.slice(3)
|
||
|
.join('\n');
|
||
|
fail(this, new AssertionError({
|
||
|
assertion: 'snapshot',
|
||
|
message: message || 'Did not match snapshot',
|
||
|
values: [{label: 'Difference:', formatted: diff}]
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const enhancedAssertions = enhanceAssert(pass, fail, {
|
||
|
truthy(actual, message) {
|
||
|
if (!actual) {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'truthy',
|
||
|
message,
|
||
|
operator: '!!',
|
||
|
values: [formatAssertError.formatWithLabel('Value is not truthy:', actual)]
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
falsy(actual, message) {
|
||
|
if (actual) {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'falsy',
|
||
|
message,
|
||
|
operator: '!',
|
||
|
values: [formatAssertError.formatWithLabel('Value is not falsy:', actual)]
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
true(actual, message) {
|
||
|
if (actual !== true) {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'true',
|
||
|
message,
|
||
|
values: [formatAssertError.formatWithLabel('Value is not `true`:', actual)]
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
false(actual, message) {
|
||
|
if (actual !== false) {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'false',
|
||
|
message,
|
||
|
values: [formatAssertError.formatWithLabel('Value is not `false`:', actual)]
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
regex(string, regex, message) {
|
||
|
if (typeof string !== 'string') {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'regex',
|
||
|
improperUsage: true,
|
||
|
message: '`t.regex()` must be called with a string',
|
||
|
values: [formatAssertError.formatWithLabel('Called with:', string)]
|
||
|
});
|
||
|
}
|
||
|
if (!(regex instanceof RegExp)) {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'regex',
|
||
|
improperUsage: true,
|
||
|
message: '`t.regex()` must be called with a regular expression',
|
||
|
values: [formatAssertError.formatWithLabel('Called with:', regex)]
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (!regex.test(string)) {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'regex',
|
||
|
message,
|
||
|
values: [
|
||
|
formatAssertError.formatWithLabel('Value must match expression:', string),
|
||
|
formatAssertError.formatWithLabel('Regular expression:', regex)
|
||
|
]
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
notRegex(string, regex, message) {
|
||
|
if (typeof string !== 'string') {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'notRegex',
|
||
|
improperUsage: true,
|
||
|
message: '`t.notRegex()` must be called with a string',
|
||
|
values: [formatAssertError.formatWithLabel('Called with:', string)]
|
||
|
});
|
||
|
}
|
||
|
if (!(regex instanceof RegExp)) {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'notRegex',
|
||
|
improperUsage: true,
|
||
|
message: '`t.notRegex()` must be called with a regular expression',
|
||
|
values: [formatAssertError.formatWithLabel('Called with:', regex)]
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (regex.test(string)) {
|
||
|
throw new AssertionError({
|
||
|
assertion: 'notRegex',
|
||
|
message,
|
||
|
values: [
|
||
|
formatAssertError.formatWithLabel('Value must not match expression:', string),
|
||
|
formatAssertError.formatWithLabel('Regular expression:', regex)
|
||
|
]
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return Object.assign(assertions, enhancedAssertions);
|
||
|
}
|
||
|
exports.wrapAssertions = wrapAssertions;
|