UNPKG

tape

Version:

tap-producing test harness for node and browsers

1,074 lines (965 loc) 31.7 kB
'use strict'; var deepEqual = require('deep-equal'); var defined = require('defined'); var path = require('path'); var inherits = require('inherits'); var EventEmitter = require('events').EventEmitter; var hasOwn = require('hasown'); var isRegExp = require('is-regex'); var trim = require('string.prototype.trim'); var callBind = require('call-bind'); var callBound = require('call-bound'); var forEach = require('for-each'); var inspect = require('object-inspect'); var is = require('object-is/polyfill')(); var objectKeys = require('object-keys'); var every = require('array.prototype.every'); var mockProperty = require('mock-property'); var isEnumerable = callBound('Object.prototype.propertyIsEnumerable'); var toLowerCase = callBound('String.prototype.toLowerCase'); var isProto = callBound('Object.prototype.isPrototypeOf'); var $exec = callBound('RegExp.prototype.exec'); var objectToString = callBound('Object.prototype.toString'); var $split = callBound('String.prototype.split'); var $replace = callBound('String.prototype.replace'); var $strSlice = callBound('String.prototype.slice'); var $shift = callBound('Array.prototype.shift'); var $slice = callBound('Array.prototype.slice'); var nextTick = typeof setImmediate !== 'undefined' ? setImmediate : process.nextTick; var safeSetTimeout = setTimeout; var safeClearTimeout = clearTimeout; var safeNow = require('@ljharb/now'); /** @typedef {((c: unknown) => c is ErrorConstructor | TypeErrorConstructor | RangeErrorConstructor | EvalErrorConstructor | URIErrorConstructor | ReferenceErrorConstructor | SyntaxErrorConstructor)} IsErrorConstructor */ /** @import { TestOptions } from '../' */ /** @import { TestCase, Callback, WrappedCall } from './test' */ /** @import * as TestType from './test' */ /** @import { Result } from './results' */ var isErrorConstructor = isProto(Error, TypeError) // IE 8 is `false` here ? /** @type {IsErrorConstructor} */ function isErrorConstructor(C) { return isProto(Error, C); } : /** @type {IsErrorConstructor} */ function isErrorConstructor(C) { return isProto(Error, C) || isProto(TypeError, C) || isProto(RangeError, C) || isProto(SyntaxError, C) || isProto(ReferenceError, C) || isProto(EvalError, C) || isProto(URIError, C); }; /** @type {(...args: [name: string, opts?: TestOptions, cb?: TestCase] | [opts: TestOptions, cb?: TestCase] | [name: string, cb: TestCase] | [cb: TestCase]) => { name: string, opts: TestOptions, cb: TestCase }} */ function getTestArgs() { var name = '(anonymous)'; var opts = {}; var cb; for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (typeof arg === 'string') { name = arg; } else if (typeof arg === 'object') { opts = arg || opts; } else if (typeof arg === 'function') { cb = arg; } } return { name: name, opts: opts, cb: cb }; } /** * @constructor * @param {string} name * @param {TestOptions} opts * @param {TestCase} [cb] */ function Test(name, opts, cb) { if (!(this instanceof Test)) { return new Test(name, opts, cb); } var args = getTestArgs(name, opts, cb); this.readable = true; this.name = args.name || '(anonymous)'; this.assertCount = 0; this.pendingCount = 0; this._skip = args.opts.skip || false; this._todo = args.opts.todo || false; this._timeout = args.opts.timeout; this._ignoreSyncTimeout = 'ignoreSyncTimeout' in args.opts ? !!args.opts.ignoreSyncTimeout : !process.env.NODE_TAPE_STRICT_TIMEOUT; this._plan = undefined; this._cb = args.cb; this.ended = false; this._progeny = []; this._teardown = []; this._ok = true; this._objectPrintDepth = 5; var depthEnvVar = process.env.NODE_TAPE_OBJECT_PRINT_DEPTH; if (args.opts.objectPrintDepth) { this._objectPrintDepth = args.opts.objectPrintDepth; } else if (depthEnvVar) { if (toLowerCase(depthEnvVar) === 'infinity') { this._objectPrintDepth = Infinity; } else { this._objectPrintDepth = Number(depthEnvVar); } } for (var prop in this) { if (typeof this[prop] === 'function') { // @ts-expect-error TS can't write through a computed `keyof this` index this[prop] = callBind(this[prop], this); } } } inherits(Test, EventEmitter); /** @type {<T>(x: unknown) => x is PromiseLike<T>} */ function isPromiseLike(x) { return !!x && (typeof x === 'object' || typeof x === 'function') && 'then' in x && typeof x.then === 'function'; } /** @type {import('./test').prototype.run} @this {import('./test')} */ Test.prototype.run = function run() { this.emit('prerun'); if (!this._cb || this._skip) { this._end(); return; } if (this._timeout != null) { this.timeoutAfter(this._timeout); } var callbackReturn = this._cb(this); if (typeof Promise === 'function' && isPromiseLike(callbackReturn)) { var self = this; Promise.resolve(callbackReturn).then( function onResolve() { if (!self.calledEnd) { self.end(); } }, function onError(err) { if (err instanceof Error || objectToString(err) === '[object Error]') { self.ifError(err); } else { self.fail(err); } self.end(); } ); return; } this.emit('run'); }; /** @type {import('./test').prototype.test} @this {import('./test')} */ Test.prototype.test = function test(name, opts, cb) { var self = this; var t = /** @type {TestType} */ (/** @type {unknown} */ (new Test(name, opts, cb))); this._progeny[this._progeny.length] = t; this.pendingCount++; this.emit('test', t); t.on('prerun', function () { self.assertCount++; }); if (!self._pendingAsserts()) { nextTick(function () { self._end(); }); } nextTick(function () { if (!self._plan && self.pendingCount == self._progeny.length) { self._end(); } }); }; /** @type {import('./test').prototype.comment} @this {import('./test')} */ Test.prototype.comment = function comment(msg) { var that = this; forEach($split(trim(msg), '\n'), function (aMsg) { that.emit('result', $replace(trim(aMsg), /^#\s*/, '')); }); }; /** @type {import('./test').prototype.plan} @this {import('./test')} */ Test.prototype.plan = function plan(n) { this._plan = n; this.emit('plan', n); }; /** @type {import('./test').prototype.timeoutAfter} @this {import('./test')} */ Test.prototype.timeoutAfter = function timeoutAfter(ms) { if (!ms) { throw new Error('timeoutAfter requires a timespan'); } var self = this; var timedOut = false; var start = safeNow(); var timeout = safeSetTimeout(function () { timedOut = true; self.fail(self.name + ' timed out after ' + ms + 'ms'); self.end(); }, ms); this.once('end', function () { safeClearTimeout(timeout); if (!self._ignoreSyncTimeout && !timedOut && (safeNow() - start) >= ms) { self.fail(self.name + ' timed out after ' + ms + 'ms'); } }); }; /** @type {import('./test').prototype.end} @this {import('./test')} */ Test.prototype.end = function end(err) { if (arguments.length >= 1 && !!err) { this.ifError(err); } if (this.calledEnd) { this.fail('.end() already called'); } this.calledEnd = true; this._end(); }; /** @type {import('./test').prototype.teardown} @this {import('./test')} */ Test.prototype.teardown = function teardown(fn) { if (typeof fn !== 'function') { this.fail('teardown: ' + inspect(fn) + ' is not a function'); } else { this._teardown.push(fn); } }; /** @type {<T extends Callback>(original: undefined | T) => import('./test').WrapObject<T>} */ function wrapFunction(original) { if (typeof original !== 'undefined' && typeof original !== 'function') { throw new TypeError('`original` must be a function or `undefined`'); } var bound = original && callBind.apply(original); /** @type {WrappedCall[]} */ var calls = []; /** @type {import('./test').WrapObject<NonNullable<typeof original>>} */ var wrapObject = { __proto__: null, wrapped: /** @this {unknown} @returns {ReturnType<original>} */ function wrapped() { var args = $slice(arguments); var completed = false; try { var returned = bound ? bound(this, /** @type {readonly[]} */ (/** @type {unknown} */ (arguments))) : void undefined; calls[calls.length] = { args: args, receiver: this, returned: returned }; completed = true; return returned; } finally { if (!completed) { calls[calls.length] = { args: args, receiver: this, threw: true }; } } }, calls: calls, results: function results() { try { return calls; } finally { calls = []; wrapObject.calls = calls; } } }; return wrapObject; } /** @type {import('./test').prototype.capture} @this {import('./test')} */ Test.prototype.capture = function capture(obj, method) { if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { throw new TypeError('`obj` must be an object'); } if (typeof method !== 'string' && typeof method !== 'symbol') { throw new TypeError('`method` must be a string or a symbol'); } /** @type {Parameters<wrapFunction>[0]} */ var implementation = arguments.length > 2 ? arguments[2] : void undefined; if (typeof implementation !== 'undefined' && typeof implementation !== 'function') { throw new TypeError('`implementation`, if provided, must be a function'); } var wrapper = wrapFunction(implementation); var restore = mockProperty(obj, method, { value: wrapper.wrapped }); this.teardown(restore); wrapper.results.restore = restore; return wrapper.results; }; /** @type {import('./test').prototype.captureFn} @this {import('./test')} */ Test.prototype.captureFn = function captureFn(original) { if (typeof original !== 'function') { throw new TypeError('`original` must be a function'); } var wrapObject = wrapFunction(original); wrapObject.wrapped.calls = wrapObject.calls; return wrapObject.wrapped; }; /** @type {import('./test').prototype.intercept} @this {import('./test')} */ Test.prototype.intercept = function intercept(obj, property) { if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { throw new TypeError('`obj` must be an object'); } if (typeof property !== 'string' && typeof property !== 'symbol') { throw new TypeError('`property` must be a string or a symbol'); } /** @type {PropertyDescriptor} */ var desc = arguments.length > 2 ? arguments[2] : { __proto__: null }; if (typeof desc !== 'undefined' && (!desc || typeof desc !== 'object')) { throw new TypeError('`desc`, if provided, must be an object'); } if ('configurable' in desc && !desc.configurable) { throw new TypeError('`desc.configurable`, if provided, must be `true`, so that the interception can be restored later'); } var isData = 'writable' in desc || 'value' in desc; var isAccessor = 'get' in desc || 'set' in desc; if (isData && isAccessor) { throw new TypeError('`value` and `writable` can not be mixed with `get` and `set`'); } var strictMode = arguments.length > 3 ? arguments[3] : true; if (typeof strictMode !== 'boolean') { throw new TypeError('`strictMode`, if provided, must be a boolean'); } /** @type {import('./test').PropertyCall[]} */ var calls = []; var getter = desc.get && callBind.apply(desc.get); var setter = desc.set && callBind.apply(desc.set); var value = !isAccessor ? desc.value : void undefined; var writable = !!desc.writable; /** @type {<T extends {}>(this: T, ...args: unknown[]) => unknown} */ function getInterceptor() { /** @type {unknown[]} */ var args = $slice(arguments); if (isAccessor) { if (getter) { var completed = false; try { var returned = getter( this, /** @type {readonly []} */ (/** @type {unknown} */ (arguments)) ); completed = true; calls[calls.length] = { type: 'get', success: true, value: returned, args: args, receiver: this }; return returned; } finally { if (!completed) { calls[calls.length] = { type: 'get', success: false, threw: true, args: args, receiver: this }; } } } } calls[calls.length] = { type: 'get', success: true, value: value, args: args, receiver: this }; return value; } /** @type {<T extends {}>(this: T, v: unknown) => unknown} */ function setInterceptor(v) { var args = $slice(arguments); if (isAccessor && setter) { var completed = false; try { var returned = setter(this, /** @type {readonly [v: unknown]} */ (/** @type {unknown} */ (arguments))); completed = true; calls[calls.length] = { type: 'set', success: true, value: v, args: args, receiver: this }; return returned; } finally { if (!completed) { calls[calls.length] = { type: 'set', success: false, threw: true, args: args, receiver: this }; } } } var canSet = isAccessor || writable; if (canSet) { value = v; } calls[calls.length] = { type: 'set', success: !!canSet, value: value, args: args, receiver: this }; if (!canSet && strictMode) { throw new TypeError('Cannot assign to read only property `' + inspect(property) + '` of object `' + inspect(obj) + '`'); } return value; } var restore = mockProperty(/** @type {Parameters<typeof mockProperty>[0]} */ (obj), property, { nonEnumerable: !!desc.enumerable, get: getInterceptor, set: setInterceptor }); this.teardown(restore); function results() { try { return calls; } finally { calls = []; } } results.restore = restore; return results; }; /** @type {import('./test').prototype._end} @this {import('./test')} */ Test.prototype._end = function _end(err) { var self = this; if (!this._cb && !this._todo && !this._skip) { this.fail('# TODO ' + this.name); } if (this._progeny.length) { var t = $shift(this._progeny); t.on('end', function () { self._end(); }); t.run(); return; } function completeEnd() { if (!self.ended) { self.emit('end'); } var pendingAsserts = self._pendingAsserts(); if (!self._planError && self._plan !== undefined && pendingAsserts) { self._planError = true; self.fail('plan != count', { expected: self._plan, actual: self.assertCount }); } self.ended = true; } function next() { if (self._teardown.length === 0) { completeEnd(); return; } var fn = /** @type {import('./test').TeardownHandler} */ (self._teardown.shift()); var res; try { res = fn(); } catch (e) { // @ts-expect-error `e` will be stringified self.fail(e); } if (isPromiseLike(res)) { res.then(next, /** @type {(_err: unknown) => void} */function (_err) { // TODO: wth? err = err || _err; }); } else { next(); } } next(); }; /** @type {import('./test').prototype._exit} @this {import('./test')} */ Test.prototype._exit = function _exit() { if (this._plan !== undefined && !this._planError && this.assertCount !== this._plan) { this._planError = true; this.fail('plan != count', { expected: this._plan, actual: this.assertCount, exiting: true }); } else if (!this.ended) { this.fail('test exited without ending: ' + this.name, { exiting: true }); } }; /** @type {import('./test').prototype._pendingAsserts} */ Test.prototype._pendingAsserts = function _pendingAsserts() { if (this._plan === undefined) { return 1; } return this._plan - (this._progeny.length + this.assertCount); }; /** @type {import('./test').prototype._assert} */ Test.prototype._assert = function assert(ok, opts) { var self = this; var extra = opts.extra || {}; var actualOK = !!ok || !!extra.skip; var name = defined(extra.message, opts.message, '(unnamed assert)'); if (this.calledEnd && opts.operator !== 'fail') { this.fail('.end() already called: ' + name); return; } /** @type {Omit<Result, 'type' | 'test'>} */ var res = { id: self.assertCount++, ok: actualOK, skip: defined(extra.skip, opts.skip), todo: defined(extra.todo, opts.todo, self._todo), name: name, operator: defined(extra.operator, opts.operator), objectPrintDepth: self._objectPrintDepth }; if (hasOwn(opts, 'actual') || hasOwn(extra, 'actual')) { res.actual = defined(extra.actual, opts.actual); } if (hasOwn(opts, 'expected') || hasOwn(extra, 'expected')) { res.expected = defined(extra.expected, opts.expected); } this._ok = !!(this._ok && actualOK); if (!actualOK && !res.todo) { res.error = defined(extra.error, opts.error, new Error(res.name)); } if (!actualOK) { var e = new Error('exception'); var err = $split(e.stack || '', '\n'); var tapeDir = __dirname + path.sep; for (var i = 0; i < err.length; i++) { /* Stack trace lines may resemble one of the following. We need to correctly extract a function name (if any) and path / line number for each line. at myFunction (/path/to/file.js:123:45) at myFunction (/path/to/file.other-ext:123:45) at myFunction (/path to/file.js:123:45) at myFunction (C:\path\to\file.js:123:45) at myFunction (/path/to/file.js:123) at Test.<anonymous> (/path/to/file.js:123:45) at Test.bound [as run] (/path/to/file.js:123:45) at /path/to/file.js:123:45 Regex has three parts. First is non-capturing group for 'at ' (plus anything preceding it). /^(?:[^\s]*\s*\bat\s+)/ Second captures function call description (optional). This is not necessarily a valid JS function name, but just what the stack trace is using to represent a function call. It may look like `<anonymous>` or 'Test.bound [as run]'. For our purposes, we assume that, if there is a function name, it's everything leading up to the first open parentheses (trimmed) before our pathname. /(?:(.*)\s+\()?/ Last part captures file path plus line no (and optional column no). /((?:[/\\]|[a-zA-Z]:\\)[^:\)]+:(\d+)(?::(\d+))?)\)?/ In the future, if node supports more ESM URL protocols than `file`, the `file:` below will need to be expanded. */ var re = /^(?:[^\s]*\s*\bat\s+)(?:(.*)\s+\()?((?:[/\\]|[a-zA-Z]:\\|file:\/\/)[^:)]+:(\d+)(?::(\d+))?)\)?$/; // first tokenize the PWD, then tokenize tape var lineWithTokens = $replace( $replace( err[i], process.cwd(), path.sep + '$CWD' ), tapeDir, path.sep + '$TEST' + path.sep ); var m = re.exec(lineWithTokens); if (!m) { continue; } var callDescription = m[1] || '<anonymous>'; // first untokenize tape, and then untokenize the PWD, then strip the line/column var filePath = $replace( $replace( $replace(m[2], path.sep + '$TEST' + path.sep, tapeDir), path.sep + '$CWD', process.cwd() ), /:\d+:\d+$/, '' ); if ($strSlice(filePath, 0, tapeDir.length) === tapeDir) { continue; } // Function call description may not (just) be a function name. // Try to extract function name by looking at first "word" only. res.functionName = $split(callDescription, /\s+/)[0]; res.file = filePath; res.line = Number(m[3]); if (m[4]) { res.column = Number(m[4]); } res.at = callDescription + ' (' + filePath + ':' + res.line + (res.column ? ':' + res.column : '') + ')'; break; } } self.emit('result', res); var pendingAsserts = self._pendingAsserts(); if (!pendingAsserts) { if (extra.exiting) { self._end(); } else { nextTick(function () { self._end(); }); } } if (!self._planError && pendingAsserts < 0) { self._planError = true; self.fail('plan != count', { expected: self._plan, actual: /** @type {number} */ (self._plan) - pendingAsserts }); } }; /** @type {import('./test').prototype.fail} @this {import('./test')} */ Test.prototype.fail = function fail(msg, extra) { this._assert(false, { message: msg, operator: 'fail', extra: extra }); }; /** @type {import('./test').prototype.pass} @this {import('./test')} */ Test.prototype.pass = function pass(msg, extra) { this._assert(true, { message: msg, operator: 'pass', extra: extra }); }; /** @type {import('./test').prototype.skip} @this {import('./test')} */ Test.prototype.skip = function skip(msg, extra) { this._assert(true, { message: msg, operator: 'skip', skip: true, extra: extra }); }; /** @type {import('./test').prototype.ok} @this {import('./test')} */ var testAssert = function assert(value, msg, extra) { // eslint-disable-line func-style this._assert(value, { message: defined(msg, 'should be truthy'), operator: 'ok', expected: true, actual: value, extra: extra }); }; Test.prototype.ok = Test.prototype['true'] = Test.prototype.assert = testAssert; /** @type {import('./test').prototype.notOk} @this {import('./test')} */ function notOK(value, msg, extra) { this._assert(!value, { message: defined(msg, 'should be falsy'), operator: 'notOk', expected: false, actual: value, extra: extra }); } Test.prototype.notOk = Test.prototype['false'] = Test.prototype.notok = notOK; /** @type {import('./test').prototype.error} @this {import('./test')} */ function error(err, msg, extra) { this._assert(!err, { message: defined(msg, String(err)), operator: 'error', error: err, extra: extra }); } Test.prototype.error = Test.prototype.ifError = Test.prototype.ifErr = Test.prototype.iferror = error; /** @type {import('./test').prototype.equal} @this {import('./test')} */ function strictEqual(a, b, msg, extra) { if (arguments.length < 2) { throw new TypeError('two arguments must be provided to compare'); } this._assert(is(a, b), { message: defined(msg, 'should be strictly equal'), operator: 'equal', actual: a, expected: b, extra: extra }); } Test.prototype.equal = Test.prototype.equals = Test.prototype.isEqual = Test.prototype.strictEqual = Test.prototype.strictEquals = Test.prototype.is = strictEqual; /** @type {import('./test').prototype.notEqual} @this {import('./test')} */ function notStrictEqual(a, b, msg, extra) { if (arguments.length < 2) { throw new TypeError('two arguments must be provided to compare'); } this._assert(!is(a, b), { message: defined(msg, 'should not be strictly equal'), operator: 'notEqual', actual: a, expected: b, extra: extra }); } Test.prototype.notEqual = Test.prototype.notEquals = Test.prototype.isNotEqual = Test.prototype.doesNotEqual = Test.prototype.isInequal = Test.prototype.notStrictEqual = Test.prototype.notStrictEquals = Test.prototype.isNot = Test.prototype.not = notStrictEqual; /** @type {import('./test').prototype.looseEqual} @this {import('./test')} */ function looseEqual(a, b, msg, extra) { if (arguments.length < 2) { throw new TypeError('two arguments must be provided to compare'); } this._assert(a == b, { message: defined(msg, 'should be loosely equal'), operator: 'looseEqual', actual: a, expected: b, extra: extra }); } Test.prototype.looseEqual = Test.prototype.looseEquals = looseEqual; /** @type {import('./test').prototype.notLooseEqual} @this {import('./test')} */ function notLooseEqual(a, b, msg, extra) { if (arguments.length < 2) { throw new TypeError('two arguments must be provided to compare'); } this._assert(a != b, { message: defined(msg, 'should not be loosely equal'), operator: 'notLooseEqual', actual: a, expected: b, extra: extra }); } Test.prototype.notLooseEqual = Test.prototype.notLooseEquals = notLooseEqual; /** @type {import('./test').prototype.deepEqual} @this {import('./test')} */ function tapeDeepEqual(a, b, msg, extra) { if (arguments.length < 2) { throw new TypeError('two arguments must be provided to compare'); } this._assert(deepEqual(a, b, { strict: true }), { message: defined(msg, 'should be deeply equivalent'), operator: 'deepEqual', actual: a, expected: b, extra: extra }); } Test.prototype.deepEqual = Test.prototype.deepEquals = Test.prototype.isEquivalent = Test.prototype.same = tapeDeepEqual; /** @type {import('./test').prototype.notDeepEqual} @this {import('./test')} */ function notDeepEqual(a, b, msg, extra) { if (arguments.length < 2) { throw new TypeError('two arguments must be provided to compare'); } this._assert(!deepEqual(a, b, { strict: true }), { message: defined(msg, 'should not be deeply equivalent'), operator: 'notDeepEqual', actual: a, expected: b, extra: extra }); } Test.prototype.notDeepEqual = Test.prototype.notDeepEquals = Test.prototype.notEquivalent = Test.prototype.notDeeply = Test.prototype.notSame = Test.prototype.isNotDeepEqual = Test.prototype.isNotDeeply = Test.prototype.isNotEquivalent = Test.prototype.isInequivalent = notDeepEqual; /** @type {import('./test').prototype.deepLooseEqual} @this {import('./test')} */ function deepLooseEqual(a, b, msg, extra) { if (arguments.length < 2) { throw new TypeError('two arguments must be provided to compare'); } this._assert(deepEqual(a, b), { message: defined(msg, 'should be loosely deeply equivalent'), operator: 'deepLooseEqual', actual: a, expected: b, extra: extra }); } Test.prototype.deepLooseEqual = deepLooseEqual; /** @type {import('./test').prototype.notDeepLooseEqual} @this {import('./test')} */ function notDeepLooseEqual(a, b, msg, extra) { if (arguments.length < 2) { throw new TypeError('two arguments must be provided to compare'); } this._assert(!deepEqual(a, b), { message: defined(msg, 'should not be loosely deeply equivalent'), operator: 'notDeepLooseEqual', actual: a, expected: b, extra: extra }); } Test.prototype.notDeepLooseEqual = notDeepLooseEqual; var isObject = require('es-object-atoms/isObject'); /** @type {import('./test').prototype.throws} @this {import('./test')} */ Test.prototype['throws'] = function (fn, expected, msg, extra) { if (typeof expected === 'string') { msg = expected; expected = undefined; } /** @type {undefined | { error: unknown }} */ var caught; try { fn(); } catch (err) { caught = { error: err }; if (isObject(err) && 'message' in err && (!isEnumerable(err, 'message') || !hasOwn(err, 'message'))) { try { var message = err.message; delete err.message; err.message = message; } catch (e) { /**/ } } } /** @type {typeof caught | boolean} */ var passed = caught; if (caught) { if (typeof expected === 'string' && caught.error && /** @type {Error} */ (caught.error).message === expected) { throw new TypeError('The "error/message" argument is ambiguous. The error message ' + inspect(expected) + ' is identical to the message.'); } if (typeof expected === 'function') { if (typeof expected.prototype !== 'undefined' && caught.error instanceof expected) { passed = true; } else if (isErrorConstructor(expected)) { passed = false; } else { passed = expected.call({}, caught.error) === true; } } else if (isRegExp(expected)) { passed = $exec(expected, String(caught.error)) !== null; expected = inspect(expected); } else if (expected && typeof expected === 'object') { // Handle validation objects. if (caught.error && typeof caught.error === 'object') { var keys = objectKeys(expected); // Special handle errors to make sure the name and the message are compared as well. if (expected instanceof Error) { keys[keys.length] = 'name'; keys[keys.length] = 'message'; } else if (keys.length === 0) { throw new TypeError('`throws` validation object must not be empty'); } // TS TODO: `caught.error` and `expected` should both be `object` here passed = every(keys, /** @param {PropertyKey} key */ function (key) { // @ts-expect-error `caught-error` and `expected` are already narrowed to `object` if (typeof caught.error[key] === 'string' && isRegExp(expected[key]) && $exec(expected[key], caught.error[key]) !== null) { return true; } // @ts-expect-error `caught.error` and `expected` are already narrowed to `object` if (key in caught.error && deepEqual(caught.error[key], expected[key], { strict: true })) { return true; } return false; }); } else { passed = false; } } } this._assert(!!passed, { message: defined(msg, 'should throw'), operator: 'throws', actual: caught && caught.error, expected: expected, error: !passed && caught && caught.error, extra: extra }); }; /** @type {import('./test').prototype.doesNotThrow} @this {import('./test')} */ Test.prototype.doesNotThrow = function doesNotThrow(fn, expected, msg, extra) { if (typeof expected === 'string') { msg = expected; expected = undefined; } var caught; try { fn(); } catch (err) { caught = { error: err }; } this._assert(!caught, { message: defined(msg, 'should not throw'), operator: 'throws', actual: caught && caught.error, expected: expected, error: caught && caught.error, extra: extra }); }; /** @type {import('./test').prototype.match} @this {import('./test')} */ Test.prototype.match = function match(string, regexp, msg, extra) { if (!isRegExp(regexp)) { this._assert(false, { message: defined(msg, 'The "regexp" argument must be an instance of RegExp. Received type ' + typeof regexp + ' (' + inspect(regexp) + ')'), operator: 'match', actual: objectToString(regexp), expected: '[object RegExp]', extra: extra }); } else if (typeof string !== 'string') { this._assert(false, { message: defined(msg, 'The "string" argument must be of type string. Received type ' + typeof string + ' (' + inspect(string) + ')'), operator: 'match', actual: string === null ? null : typeof string, expected: 'string', extra: extra }); } else { var matches = $exec(regexp, string) !== null; var message = defined( msg, 'The input ' + (matches ? 'matched' : 'did not match') + ' the regular expression ' + inspect(regexp) + '. Input: ' + inspect(string) ); this._assert(matches, { message: message, operator: 'match', actual: string, expected: regexp, extra: extra }); } }; /** @type {import('./test').prototype.doesNotMatch} @this {import('./test')} */ Test.prototype.doesNotMatch = function doesNotMatch(string, regexp, msg, extra) { if (!isRegExp(regexp)) { this._assert(false, { message: defined(msg, 'The "regexp" argument must be an instance of RegExp. Received type ' + typeof regexp + ' (' + inspect(regexp) + ')'), operator: 'doesNotMatch', actual: objectToString(regexp), expected: '[object RegExp]', extra: extra }); } else if (typeof string !== 'string') { this._assert(false, { message: defined(msg, 'The "string" argument must be of type string. Received type ' + typeof string + ' (' + inspect(string) + ')'), operator: 'doesNotMatch', actual: string === null ? null : typeof string, expected: 'string', extra: extra }); } else { var matches = $exec(regexp, string) !== null; var message = defined( msg, 'The input ' + (matches ? 'was expected to not match' : 'did not match') + ' the regular expression ' + inspect(regexp) + '. Input: ' + inspect(string) ); this._assert(!matches, { message: message, operator: 'doesNotMatch', actual: string, expected: regexp, extra: extra }); } }; /** @type {import('./test').prototype.assertion} @this {import('./test')} */ Test.prototype.assertion = function assertion(fn) { // @ts-expect-error TODO FIXME return callBind.apply(fn)(this, $slice(arguments, 1)); }; module.exports = Test; // this must be assigned to module.exports or else attw complains /** @type {import('./test').skip} */ module.exports.skip = function skip() { var args = getTestArgs.apply(null, arguments); args.opts.skip = true; return /** @type {import('./test')} */ (/** @type {unknown} */ (new Test(args.name, args.opts, args.cb))); }; // vim: set softtabstop=4 shiftwidth=4: