UNPKG

unexpected

Version:
1,676 lines (1,555 loc) 79.8 kB
/*global setTimeout*/ const utils = require('./utils'); const arrayChanges = require('array-changes'); const arrayChangesAsync = require('array-changes-async'); const throwIfNonUnexpectedError = require('./throwIfNonUnexpectedError'); const objectIs = utils.objectIs; const isRegExp = utils.isRegExp; const extend = utils.extend; module.exports = expect => { expect.addAssertion('<any> [not] to be (ok|truthy)', (expect, subject) => { const not = !!expect.flags.not; const condition = !!subject; if (condition === not) { expect.fail(); } }); expect.addAssertion( '<any> [not] to be (ok|truthy) <string>', (expect, subject, message) => { const not = !!expect.flags.not; const condition = !!subject; if (condition === not) { expect.fail({ errorMode: 'bubble', message }); } } ); expect.addAssertion('<any> [not] to be <any>', (expect, subject, value) => { expect(objectIs(subject, value), '[not] to be truthy'); }); expect.addAssertion( '<string> [not] to be <string>', (expect, subject, value) => { expect(subject, '[not] to equal', value); } ); expect.addAssertion('<boolean> [not] to be true', (expect, subject) => { expect(subject, '[not] to be', true); }); expect.addAssertion('<boolean> [not] to be false', (expect, subject) => { expect(subject, '[not] to be', false); }); expect.addAssertion('<any> [not] to be falsy', (expect, subject) => { expect(subject, '[!not] to be truthy'); }); expect.addAssertion( '<any> [not] to be falsy <string>', (expect, subject, message) => { const not = !!expect.flags.not; const condition = !!subject; if (condition !== not) { expect.fail({ errorMode: 'bubble', message }); } } ); expect.addAssertion('<any> [not] to be null', (expect, subject) => { expect(subject, '[not] to be', null); }); expect.addAssertion('<any> [not] to be undefined', (expect, subject) => { expect(typeof subject === 'undefined', '[not] to be truthy'); }); expect.addAssertion('<any> to be defined', (expect, subject) => { expect(subject, 'not to be undefined'); }); expect.addAssertion('<number|NaN> [not] to be NaN', (expect, subject) => { expect(isNaN(subject), '[not] to be truthy'); }); expect.addAssertion( '<number> [not] to be close to <number> <number?>', (expect, subject, value, epsilon) => { expect.errorMode = 'bubble'; if (typeof epsilon !== 'number') { epsilon = 1e-9; } expect.withError( () => { expect( Math.abs(subject - value), '[not] to be less than or equal to', epsilon ); }, e => { expect.fail(output => { output .error('expected ') .appendInspected(subject) .sp() .error(expect.testDescription) .sp() .appendInspected(value) .sp() .text('(epsilon: ') .jsNumber(epsilon.toExponential()) .text(')'); }); } ); } ); expect.addAssertion( '<any> [not] to be (a|an) <type>', (expect, subject, type) => { expect.argsOutput[0] = output => { output.text(type.name); }; expect(type.identify(subject), '[not] to be true'); } ); expect.addAssertion( '<any> [not] to be (a|an) <string>', (expect, subject, typeName) => { typeName = /^reg(?:exp?|ular expression)$/.test(typeName) ? 'regexp' : typeName; expect.argsOutput[0] = output => { output.jsString(typeName); }; if (!expect.getType(typeName)) { expect.errorMode = 'nested'; expect.fail(output => { output .error('Unknown type:') .sp() .jsString(typeName); }); } expect(expect.subjectType.is(typeName), '[not] to be truthy'); } ); expect.addAssertion( '<any> [not] to be (a|an) <function>', (expect, subject, Constructor) => { const className = utils.getFunctionName(Constructor); if (className) { expect.argsOutput[0] = output => { output.text(className); }; } expect(subject instanceof Constructor, '[not] to be truthy'); } ); expect.addAssertion( '<any> [not] to be one of <array>', (expect, subject, superset) => { let found = false; for (let i = 0; i < superset.length; i += 1) { found = found || objectIs(subject, superset[i]); } if (found === expect.flags.not) { expect.fail(); } } ); // Alias for common '[not] to be (a|an)' assertions expect.addAssertion( '<any> [not] to be an (object|array)', (expect, subject) => { expect(subject, '[not] to be an', expect.alternations[0]); } ); expect.addAssertion( '<any> [not] to be a (boolean|number|string|function|regexp|regex|regular expression|date)', (expect, subject) => { expect(subject, '[not] to be a', expect.alternations[0]); } ); expect.addAssertion( '<string> to be (the empty|an empty|a non-empty) string', (expect, subject) => { expect( subject, expect.alternations[0] === 'a non-empty' ? 'not to be empty' : 'to be empty' ); } ); expect.addAssertion( '<array-like> to be (the empty|an empty|a non-empty) array', (expect, subject) => { expect( subject, expect.alternations[0] === 'a non-empty' ? 'not to be empty' : 'to be empty' ); } ); expect.addAssertion('<string> to match <regexp>', (expect, subject, regexp) => expect.withError( () => { const captures = subject.match(regexp); expect(captures, 'to be truthy'); return captures; }, e => { e.label = 'should match'; expect.fail(e); } ) ); expect.addAssertion( '<string> not to match <regexp>', (expect, subject, regexp) => expect.withError( () => { expect(regexp.test(subject), 'to be false'); }, e => { expect.fail({ label: 'should not match', diff(output) { output.inline = false; let lastIndex = 0; function flushUntilIndex(i) { if (i > lastIndex) { output.text(subject.substring(lastIndex, i)); lastIndex = i; } } subject.replace(new RegExp(regexp.source, 'g'), ($0, index) => { flushUntilIndex(index); lastIndex += $0.length; output.removedHighlight($0); }); flushUntilIndex(subject.length); return output; } }); } ) ); expect.addAssertion( '<object|function> [not] to have own property <string>', (expect, subject, key) => { expect(subject.hasOwnProperty(key), '[not] to be truthy'); return subject[key]; } ); expect.addAssertion( '<object|function> [not] to have (enumerable|configurable|writable) property <string>', (expect, subject, key) => { const descriptor = expect.alternations[0]; expect( Object.getOwnPropertyDescriptor(subject, key)[descriptor], '[not] to be truthy' ); return subject[key]; } ); expect.addAssertion( '<object|function> [not] to have property <string>', (expect, subject, key) => { const subjectType = expect.findTypeOf(subject); const subjectKey = subjectType.is('function') ? subject[key] : subjectType.valueForKey(subject, key); expect(subjectKey, '[!not] to be undefined'); return subjectKey; } ); expect.addAssertion( '<object|function> to have [own] property <string> <any>', (expect, subject, key, expectedPropertyValue) => expect(subject, 'to have [own] property', key).then( actualPropertyValue => { expect.argsOutput = function() { this.appendInspected(key) .sp() .error('with a value of') .sp() .appendInspected(expectedPropertyValue); }; expect(actualPropertyValue, 'to equal', expectedPropertyValue); return actualPropertyValue; } ) ); expect.addAssertion( '<object|function> [not] to have [own] properties <array>', (expect, subject, propertyNames) => { const unsupportedPropertyNames = []; propertyNames.forEach(propertyName => { if ( typeof propertyName !== 'string' && typeof propertyName !== 'number' ) { unsupportedPropertyNames.push(propertyName); } }); if (unsupportedPropertyNames.length > 0) { expect.errorMode = 'nested'; expect.fail(function() { this.error( 'All expected properties must be passed as strings or numbers, but these are not:' ).indentLines(); unsupportedPropertyNames.forEach(function(propertyName) { this.nl() .i() .appendInspected(propertyName); }, this); this.outdentLines(); }); } propertyNames.forEach(propertyName => { expect(subject, '[not] to have [own] property', String(propertyName)); }); } ); expect.addAssertion( '<object|function> to have [own] properties <object>', (expect, subject, properties) => { expect.withError( () => { Object.keys(properties).forEach(property => { const value = properties[property]; if (typeof value === 'undefined') { expect(subject, 'not to have [own] property', property); } else { expect(subject, 'to have [own] property', property, value); } }); }, e => { expect.fail({ diff(output, diff) { output.inline = false; const expected = extend({}, properties); const actual = {}; const propertyNames = expect.findTypeOf(subject).getKeys(subject); // Might put duplicates into propertyNames, but that does not matter: for (const propertyName in subject) { if (!subject.hasOwnProperty(propertyName)) { propertyNames.push(propertyName); } } propertyNames.forEach(propertyName => { if ( (!expect.flags.own || subject.hasOwnProperty(propertyName)) && !(propertyName in properties) ) { expected[propertyName] = subject[propertyName]; } if ( (!expect.flags.own || subject.hasOwnProperty(propertyName)) && !(propertyName in actual) ) { actual[propertyName] = subject[propertyName]; } }); return utils.wrapConstructorNameAroundOutput( diff(actual, expected), subject ); } }); } ); } ); expect.addAssertion( '<string|array-like> [not] to have length <number>', (expect, subject, length) => { if (!expect.flags.not) { expect.errorMode = 'nested'; } expect(subject.length, '[not] to be', length); } ); expect.addAssertion( '<string|array-like> [not] to be empty', (expect, subject) => { expect(subject, '[not] to have length', 0); } ); expect.addAssertion( '<string|array-like|object> to be non-empty', (expect, subject) => { expect(subject, 'not to be empty'); } ); expect.addAssertion( '<object> to [not] [only] have keys <array>', (expect, subject, keys) => { const keysInSubject = {}; const subjectType = expect.findTypeOf(subject); const subjectKeys = subjectType.getKeys(subject); subjectKeys.forEach(key => { keysInSubject[key] = true; }); if (expect.flags.not && keys.length === 0) { return; } const hasKeys = keys.every(key => keysInSubject[key]); if (expect.flags.only) { expect(hasKeys, 'to be truthy'); expect.withError( () => { expect(subjectKeys.length === keys.length, '[not] to be truthy'); }, err => { expect.fail({ diff: !expect.flags.not && ((output, diff, inspect, equal) => { output.inline = true; const keyInValue = {}; keys.forEach(key => { keyInValue[key] = true; }); const subjectIsArrayLike = subjectType.is('array-like'); subjectType.prefix(output, subject); output.nl().indentLines(); subjectKeys.forEach((key, index) => { const propertyOutput = subjectType.property( output.clone(), key, inspect(subjectType.valueForKey(subject, key)), subjectIsArrayLike ); const delimiterOutput = subjectType.delimiter( output.clone(), index, subjectKeys.length ); output .i() .block(function() { this.append(propertyOutput).amend(delimiterOutput); if (!keyInValue[key]) { this.sp().annotationBlock(function() { this.error('should be removed'); }); } }) .nl(); }); output.outdentLines(); subjectType.suffix(output, subject); return output; }) }); } ); } else { expect(hasKeys, '[not] to be truthy'); } } ); expect.addAssertion('<object> [not] to be empty', (expect, subject) => { if ( expect.flags.not && !expect.findTypeOf(subject).getKeys(subject).length ) { return expect.fail(); } expect(subject, 'to [not] only have keys', []); }); expect.addAssertion( '<object> not to have keys <array>', (expect, subject, keys) => { expect(subject, 'to not have keys', keys); } ); expect.addAssertion( '<object> not to have key <string>', (expect, subject, value) => { expect(subject, 'to not have keys', [value]); } ); expect.addAssertion('<object> not to have keys <string+>', function( expect, subject, value ) { expect( subject, 'to not have keys', Array.prototype.slice.call(arguments, 2) ); }); expect.addAssertion( '<object> to [not] [only] have key <string>', (expect, subject, value) => { expect(subject, 'to [not] [only] have keys', [value]); } ); expect.addAssertion('<object> to [not] [only] have keys <string+>', function( expect, subject ) { expect( subject, 'to [not] [only] have keys', Array.prototype.slice.call(arguments, 2) ); }); expect.addAssertion('<string> [not] to contain <string+>', function( expect, subject ) { const args = Array.prototype.slice.call(arguments, 2); args.forEach(arg => { if (arg === '') { throw new Error( `The '${ expect.testDescription }' assertion does not support the empty string` ); } }); expect.withError( () => { args.forEach(arg => { expect(subject.indexOf(arg) !== -1, '[not] to be truthy'); }); }, e => { expect.fail({ diff(output) { output.inline = false; let lastIndex = 0; function flushUntilIndex(i) { if (i > lastIndex) { output.text(subject.substring(lastIndex, i)); lastIndex = i; } } if (expect.flags.not) { subject.replace( new RegExp( args.map(arg => utils.escapeRegExpMetaChars(arg)).join('|'), 'g' ), ($0, index) => { flushUntilIndex(index); lastIndex += $0.length; output.removedHighlight($0); } ); flushUntilIndex(subject.length); } else { const ranges = []; args.forEach(arg => { let needle = arg; let partial = false; while (needle.length > 1) { let found = false; lastIndex = -1; let index; do { index = subject.indexOf(needle, lastIndex + 1); if (index !== -1) { found = true; ranges.push({ startIndex: index, endIndex: index + needle.length, partial }); } lastIndex = index; } while (lastIndex !== -1); if (found) { break; } needle = arg.substr(0, needle.length - 1); partial = true; } }); lastIndex = 0; ranges .sort((a, b) => a.startIndex - b.startIndex) .forEach(({ startIndex, endIndex, partial }) => { flushUntilIndex(startIndex); const firstUncoveredIndex = Math.max(startIndex, lastIndex); if (endIndex > firstUncoveredIndex) { if (partial) { output.partialMatch( subject.substring(firstUncoveredIndex, endIndex) ); } else { output.match( subject.substring(firstUncoveredIndex, endIndex) ); } lastIndex = endIndex; } }); flushUntilIndex(subject.length); } return output; } }); } ); }); expect.addAssertion('<array-like> [not] to contain <any+>', function( expect, subject ) { const args = Array.prototype.slice.call(arguments, 2); expect.withError( () => { args.forEach(arg => { expect( subject && Array.prototype.some.call(subject, item => expect.equal(item, arg) ), '[not] to be truthy' ); }); }, e => { expect.fail({ diff: expect.flags.not && ((output, diff, inspect, equal) => diff( subject, Array.prototype.filter.call( subject, item => !args.some(arg => equal(item, arg)) ) )) }); } ); }); expect.addAssertion( [ '<string> [not] to begin with <string>', '<string> [not] to start with <string>' ], (expect, subject, value) => { if (value === '') { throw new Error( `The '${ expect.testDescription }' assertion does not support a prefix of the empty string` ); } var isTruncated = false; var outputSubject = utils.truncateSubjectStringForBegin(subject, value); if (outputSubject === null) { outputSubject = subject; } else { isTruncated = true; } expect.subjectOutput = output => { output = output.jsString( "'" + outputSubject.replace(/\n/g, '\\n') + "'" ); if (isTruncated) { output.jsComment('...'); } }; expect.withError( () => { expect(subject.substr(0, value.length), '[not] to equal', value); }, err => { expect.fail({ diff(output) { output.inline = false; if (expect.flags.not) { output .removedHighlight(value) .text(subject.substr(value.length)); } else { let i = 0; while (subject[i] === value[i]) { i += 1; } if (i === 0) { // No common prefix, omit diff return null; } else { output .partialMatch(subject.substr(0, i)) .text(outputSubject.substr(i)) .jsComment(isTruncated ? '...' : ''); } } return output; } }); } ); } ); expect.addAssertion( '<string> [not] to end with <string>', (expect, subject, value) => { if (value === '') { throw new Error( `The '${ expect.testDescription }' assertion does not support a suffix of the empty string` ); } var isTruncated = false; var outputSubject = utils.truncateSubjectStringForEnd(subject, value); if (outputSubject === null) { outputSubject = subject; } else { isTruncated = true; } expect.subjectOutput = output => { if (isTruncated) { output = output.jsComment('...'); } output.jsString("'" + outputSubject.replace(/\n/g, '\\n') + "'"); }; expect.withError( () => { expect(subject.substr(-value.length), '[not] to equal', value); }, err => { expect.fail({ diff(output) { output.inline = false; if (expect.flags.not) { output .text(subject.substr(0, subject.length - value.length)) .removedHighlight(value); } else { let i = 0; while ( outputSubject[outputSubject.length - 1 - i] === value[value.length - 1 - i] ) { i += 1; } if (i === 0) { // No common suffix, omit diff return null; } output .jsComment(isTruncated ? '...' : '') .text(outputSubject.substr(0, outputSubject.length - i)) .partialMatch( outputSubject.substr( outputSubject.length - i, outputSubject.length ) ); } return output; } }); } ); } ); expect.addAssertion('<number> [not] to be finite', (expect, subject) => { expect(isFinite(subject), '[not] to be truthy'); }); expect.addAssertion('<number> [not] to be infinite', (expect, subject) => { expect(!isNaN(subject) && !isFinite(subject), '[not] to be truthy'); }); expect.addAssertion( '<number> [not] to be within <number> <number>', (expect, subject, start, finish) => { expect.argsOutput = output => { output .appendInspected(start) .text('..') .appendInspected(finish); }; expect(subject >= start && subject <= finish, '[not] to be truthy'); } ); expect.addAssertion( '<string> [not] to be within <string> <string>', (expect, subject, start, finish) => { expect.argsOutput = output => { output .appendInspected(start) .text('..') .appendInspected(finish); }; expect(subject >= start && subject <= finish, '[not] to be truthy'); } ); expect.addAssertion( '<number> [not] to be (less than|below) <number>', (expect, subject, value) => { expect(subject < value, '[not] to be truthy'); } ); expect.addAssertion( '<string> [not] to be (less than|below) <string>', (expect, subject, value) => { expect(subject < value, '[not] to be truthy'); } ); expect.addAssertion( '<number> [not] to be less than or equal to <number>', (expect, subject, value) => { expect(subject <= value, '[not] to be truthy'); } ); expect.addAssertion( '<string> [not] to be less than or equal to <string>', (expect, subject, value) => { expect(subject <= value, '[not] to be truthy'); } ); expect.addAssertion( '<number> [not] to be (greater than|above) <number>', (expect, subject, value) => { expect(subject > value, '[not] to be truthy'); } ); expect.addAssertion( '<string> [not] to be (greater than|above) <string>', (expect, subject, value) => { expect(subject > value, '[not] to be truthy'); } ); expect.addAssertion( '<number> [not] to be greater than or equal to <number>', (expect, subject, value) => { expect(subject >= value, '[not] to be truthy'); } ); expect.addAssertion( '<string> [not] to be greater than or equal to <string>', (expect, subject, value) => { expect(subject >= value, '[not] to be truthy'); } ); expect.addAssertion('<number> [not] to be positive', (expect, subject) => { expect(subject, '[not] to be greater than', 0); }); expect.addAssertion('<number> [not] to be negative', (expect, subject) => { expect(subject, '[not] to be less than', 0); }); expect.addAssertion('<any> to equal <any>', (expect, subject, value) => { expect.withError( () => { expect(expect.equal(value, subject), 'to be truthy'); }, e => { expect.fail({ label: 'should equal', diff(output, diff) { return diff(subject, value); } }); } ); }); expect.addAssertion('<any> not to equal <any>', (expect, subject, value) => { expect(expect.equal(value, subject), 'to be falsy'); }); expect.addAssertion('<function> to error', (expect, subject) => expect .promise(() => subject()) .then( () => { expect.fail(); }, error => error ) ); expect.addAssertion( '<function> to error [with] <any>', (expect, subject, arg) => expect(subject, 'to error').then(error => { expect.errorMode = 'nested'; return expect.withError( () => { if ( error.isUnexpected && (typeof arg === 'string' || isRegExp(arg)) ) { return expect(error, 'to have message', arg); } else { return expect(error, 'to satisfy', arg); } }, e => { e.originalError = error; throw e; } ); }) ); expect.addAssertion('<function> not to error', (expect, subject) => { let threw = false; return expect .promise(() => { try { return subject(); } catch (e) { threw = true; throw e; } }) .caught(error => { expect.errorMode = 'nested'; expect.fail({ output(output) { output .error(threw ? 'threw' : 'returned promise rejected with') .error(': ') .appendErrorMessage(error); }, originalError: error }); }); }); expect.addAssertion('<function> not to throw', (expect, subject) => { let threw = false; let error; try { subject(); } catch (e) { error = e; threw = true; } if (threw) { expect.errorMode = 'nested'; expect.fail({ output(output) { output.error('threw: ').appendErrorMessage(error); }, originalError: error }); } }); expect.addAssertion( '<function> to (throw|throw error|throw exception)', (expect, subject) => { try { subject(); } catch (e) { return e; } expect.errorMode = 'nested'; expect.fail('did not throw'); } ); expect.addAssertion( '<function> to throw (a|an) <function>', (expect, subject, value) => { const constructorName = utils.getFunctionName(value); if (constructorName) { expect.argsOutput[0] = output => { output.jsFunctionName(constructorName); }; } expect.errorMode = 'nested'; return expect(subject, 'to throw').then(error => { expect(error, 'to be a', value); }); } ); expect.addAssertion( '<function> to (throw|throw error|throw exception) <any>', (expect, subject, arg) => { expect.errorMode = 'nested'; return expect(subject, 'to throw').then(error => { const isUnexpected = error && error._isUnexpected; // in the presence of a matcher an error must have been thrown. expect.errorMode = 'nested'; return expect.withError( () => { if (isUnexpected && (typeof arg === 'string' || isRegExp(arg))) { return expect( error.getErrorMessage('text').toString(), 'to satisfy', arg ); } else { return expect(error, 'to satisfy', arg); } }, err => { err.originalError = error; throw err; } ); }); } ); expect.addAssertion( '<function> to have arity <number>', (expect, { length }, value) => { expect(length, 'to equal', value); } ); expect.addAssertion( [ '<object> to have values [exhaustively] satisfying <any>', '<object> to have values [exhaustively] satisfying <assertion>', '<object> to be (a map|a hash|an object) whose values [exhaustively] satisfy <any>', '<object> to be (a map|a hash|an object) whose values [exhaustively] satisfy <assertion>' ], (expect, subject, nextArg) => { expect.errorMode = 'nested'; expect(subject, 'not to be empty'); expect.errorMode = 'bubble'; const keys = expect.subjectType.getKeys(subject); const expected = {}; keys.forEach((key, index) => { if (typeof nextArg === 'string') { expected[key] = s => expect.shift(s); } else if (typeof nextArg === 'function') { expected[key] = s => nextArg._expectIt ? nextArg(s, expect.context) : nextArg(s, index); } else { expected[key] = nextArg; } }); return expect.withError( () => expect(subject, 'to [exhaustively] satisfy', expected), err => { expect.fail({ message(output) { output.append( expect.standardErrorMessage(output.clone(), { compact: err && err._isUnexpected && err.hasDiff() }) ); }, diff(output) { const diff = err.getDiff({ output }); diff.inline = true; return diff; } }); } ); } ); expect.addAssertion( [ '<array-like> to have items [exhaustively] satisfying <any>', '<array-like> to have items [exhaustively] satisfying <assertion>', '<array-like> to be an array whose items [exhaustively] satisfy <any>', '<array-like> to be an array whose items [exhaustively] satisfy <assertion>' ], (expect, subject, ...rest) => { // ... expect.errorMode = 'nested'; expect(subject, 'not to be empty'); expect.errorMode = 'bubble'; return expect.withError( () => expect(subject, 'to have values [exhaustively] satisfying', ...rest), err => { expect.fail({ message(output) { output.append( expect.standardErrorMessage(output.clone(), { compact: err && err._isUnexpected && err.hasDiff() }) ); }, diff(output) { const diff = err.getDiff({ output }); diff.inline = true; return diff; } }); } ); } ); expect.addAssertion( [ '<object> to have keys satisfying <any>', '<object> to have keys satisfying <assertion>', '<object> to be (a map|a hash|an object) whose (keys|properties) satisfy <any>', '<object> to be (a map|a hash|an object) whose (keys|properties) satisfy <assertion>' ], (expect, subject, ...rest) => { expect.errorMode = 'nested'; expect(subject, 'not to be empty'); expect.errorMode = 'default'; const keys = expect.subjectType.getKeys(subject); return expect(keys, 'to have items satisfying', ...rest); } ); expect.addAssertion( [ '<object> [not] to have a value [exhaustively] satisfying <any>', '<object> [not] to have a value [exhaustively] satisfying <assertion>' ], (expect, subject, nextArg) => { expect.errorMode = 'nested'; expect(subject, 'not to be empty'); expect.errorMode = 'bubble'; const subjectType = expect.findTypeOf(subject); const keys = subjectType.getKeys(subject); const not = !!expect.flags.not; const keyResults = new Array(keys.length); expect.withError( () => expect.promise[not ? 'all' : 'any']( keys.map((key, index) => { let expected; if (typeof nextArg === 'string') { expected = s => expect.shift(s); } else if (typeof nextArg === 'function') { expected = s => nextArg(s, index); } else { expected = nextArg; } keyResults[key] = expect.promise(() => expect( subjectType.valueForKey(subject, key), '[not] to [exhaustively] satisfy', expected ) ); return keyResults[key]; }) ), err => { expect.fail({ message(output) { output.append( expect.standardErrorMessage(output.clone(), { compact: err && err._isUnexpected && err.hasDiff() }) ); }, diff: expect.flags.not && ((output, diff, inspect, equal) => { const expectedObject = subjectType.is('array-like') ? [] : {}; keys.forEach(key => { if (keyResults[key].isFulfilled()) { expectedObject[key] = subjectType.valueForKey(subject, key); } }); return diff(subject, expectedObject); }) }); } ); } ); expect.addAssertion( [ '<array-like> [not] to have an item [exhaustively] satisfying <any>', '<array-like> [not] to have an item [exhaustively] satisfying <assertion>' ], (expect, subject, ...rest) => { expect.errorMode = 'nested'; expect(subject, 'not to be empty'); expect.errorMode = 'default'; return expect( subject, '[not] to have a value [exhaustively] satisfying', ...rest ); } ); expect.addAssertion('<object> to be canonical', (expect, subject) => { const stack = []; (function traverse(obj) { let i; for (i = 0; i < stack.length; i += 1) { if (stack[i] === obj) { return; } } if (obj && typeof obj === 'object') { const keys = Object.keys(obj); for (i = 0; i < keys.length - 1; i += 1) { expect(keys[i], 'to be less than', keys[i + 1]); } stack.push(obj); keys.forEach(key => { traverse(obj[key]); }); stack.pop(); } })(subject); }); expect.addAssertion( '<Error> to have message <any>', (expect, subject, value) => { expect.errorMode = 'nested'; return expect( subject.isUnexpected ? subject.getErrorMessage('text').toString() : subject.message, 'to satisfy', value ); } ); expect.addAssertion( '<Error> to [exhaustively] satisfy <Error>', (expect, subject, value) => { expect(subject.constructor, 'to be', value.constructor); const unwrappedValue = expect.argTypes[0].unwrap(value); return expect.withError( () => expect(subject, 'to [exhaustively] satisfy', unwrappedValue), e => { expect.fail({ diff(output, diff) { output.inline = false; const unwrappedSubject = expect.subjectType.unwrap(subject); return utils.wrapConstructorNameAroundOutput( diff(unwrappedSubject, unwrappedValue), subject ); } }); } ); } ); expect.addAssertion( '<Error> to [exhaustively] satisfy <object>', (expect, subject, value) => { const valueType = expect.argTypes[0]; const subjectKeys = expect.subjectType.getKeys(subject); const valueKeys = valueType.getKeys(value); const convertedSubject = {}; subjectKeys.concat(valueKeys).forEach(key => { convertedSubject[key] = subject[key]; }); return expect(convertedSubject, 'to [exhaustively] satisfy', value); } ); expect.addAssertion( '<Error> to [exhaustively] satisfy <regexp|string>', (expect, { message }, value) => expect(message, 'to [exhaustively] satisfy', value) ); expect.addAssertion( '<Error> to [exhaustively] satisfy <any>', (expect, { message }, value) => expect(message, 'to [exhaustively] satisfy', value) ); expect.addAssertion( '<binaryArray> to [exhaustively] satisfy <expect.it>', (expect, subject, value) => expect.withError( () => value(subject, expect.context), e => { expect.fail({ diff(output, diff, inspect, equal) { output.inline = false; return output.appendErrorMessage(e); } }); } ) ); expect.addAssertion( '<UnexpectedError> to [exhaustively] satisfy <function>', (expect, subject, value) => expect.promise(() => { subject.serializeMessage(expect.outputFormat()); return value(subject); }) ); expect.addAssertion( '<any|Error> to [exhaustively] satisfy <function>', (expect, subject, value) => expect.promise(() => value(subject)) ); if (typeof Buffer !== 'undefined') { expect.addAssertion( '<Buffer> [when] decoded as <string> <assertion?>', (expect, subject, value) => expect.shift(subject.toString(value)) ); } expect.addAssertion( '<any> not to [exhaustively] satisfy [assertion] <any>', (expect, subject, value) => expect.promise((resolve, reject) => expect .promise(() => expect(subject, 'to [exhaustively] satisfy [assertion]', value) ) .then(() => { try { expect.fail(); } catch (e) { reject(e); } }) .caught(e => { if (!e || !e._isUnexpected) { reject(e); } else { resolve(); } }) ) ); expect.addAssertion( '<any> to [exhaustively] satisfy assertion <any>', (expect, subject, value) => { expect.errorMode = 'bubble'; // to satisfy assertion 'to be a number' => to be a number return expect(subject, 'to [exhaustively] satisfy', value); } ); expect.addAssertion( '<any> to [exhaustively] satisfy assertion <assertion>', (expect, subject) => { expect.errorMode = 'bubble'; // to satisfy assertion 'to be a number' => to be a number return expect.shift(); } ); expect.addAssertion( '<any> to [exhaustively] satisfy [assertion] <expect.it>', (expect, subject, value) => expect.withError( () => value(subject, expect.context), e => { expect.fail({ diff(output) { output.inline = false; return output.appendErrorMessage(e); } }); } ) ); expect.addAssertion( '<regexp> to [exhaustively] satisfy <regexp>', (expect, subject, value) => { expect(subject, 'to equal', value); } ); expect.addAssertion( '<string> to [exhaustively] satisfy <regexp>', (expect, subject, value) => { expect.errorMode = 'bubble'; return expect(subject, 'to match', value); } ); expect.addAssertion( '<function> to [exhaustively] satisfy <function>', (expect, subject, value) => { expect.errorMode = 'bubble'; expect(subject, 'to equal', value); } ); expect.addAssertion( '<binaryArray> to [exhaustively] satisfy <binaryArray>', (expect, subject, value) => { expect.errorMode = 'bubble'; expect(subject, 'to equal', value); } ); expect.addAssertion( '<any> to [exhaustively] satisfy <any>', (expect, subject, value) => { expect.errorMode = 'bubble'; expect(subject, 'to equal', value); } ); expect.addAssertion( '<array-like> to [exhaustively] satisfy <array-like>', (expect, subject, value) => { expect.errorMode = 'bubble'; const subjectType = expect.subjectType; const subjectKeys = subjectType.getKeys(subject); const valueType = expect.argTypes[0]; const valueKeys = valueType.getKeys(value).filter( key => utils.numericalRegExp.test(key) || typeof key === 'symbol' || // include keys whose value is not undefined on either LHS or RHS typeof valueType.valueForKey(value, key) !== 'undefined' || typeof subjectType.valueForKey(subject, key) !== 'undefined' ); const keyPromises = {}; valueKeys.forEach(function(keyInValue) { keyPromises[keyInValue] = expect.promise(function() { const subjectKey = subjectType.valueForKey(subject, keyInValue); const valueKey = valueType.valueForKey(value, keyInValue); const valueKeyType = expect.findTypeOf(valueKey); if (valueKeyType.is('function')) { return valueKey(subjectKey); } else { return expect(subjectKey, 'to [exhaustively] satisfy', valueKey); } }); }); return expect.promise .all([ expect.promise(() => { // create subject key presence object const remainingKeysInSubject = {}; subjectType.getKeys(subject).forEach(key => { remainingKeysInSubject[key] = 1; // present in subject }); // discard or mark missing each previously seen value key valueKeys.forEach(key => { if (!remainingKeysInSubject[key]) { remainingKeysInSubject[key] = 2; // present in value } else { delete remainingKeysInSubject[key]; } }); // filter outstanding keys which should not lead to an error const outstandingKeys = Object.keys(remainingKeysInSubject).filter( key => utils.numericalRegExp.test(key) || typeof key === 'symbol' || typeof subjectType.valueForKey(subject, key) !== 'undefined' || remainingKeysInSubject[key] === 2 ); // key checking succeeds with no outstanding keys expect(outstandingKeys.length === 0, 'to be truthy'); }), expect.promise.all(keyPromises) ]) .caught(() => { let i = 0; return expect.promise.settle(keyPromises).then(() => { const toSatisfyMatrix = new Array(subject.length); for (i = 0; i < subject.length; i += 1) { toSatisfyMatrix[i] = new Array(value.length); if (i < value.length) { toSatisfyMatrix[i][i] = keyPromises[i].isFulfilled() || keyPromises[i].reason(); } } if (subject.length > 10 || value.length > 10) { const indexByIndexChanges = []; for (i = 0; i < subject.length; i += 1) { const promise = keyPromises[i]; if (i < value.length) { indexByIndexChanges.push({ type: promise.isFulfilled() ? 'equal' : 'similar', value: subject[i], expected: value[i], actualIndex: i, expectedIndex: i, last: i === Math.max(subject.length, value.length) - 1 }); } else { indexByIndexChanges.push({ type: 'remove', value: subject[i], actualIndex: i, last: i === subject.length - 1 }); } } for (i = subject.length; i < value.length; i += 1) { indexByIndexChanges.push({ type: 'insert', value: value[i], expectedIndex: i }); } return failWithChanges(indexByIndexChanges); } let isAsync = false; const subjectElements = utils.duplicateArrayLikeUsingType( subject, subjectType ); const valueElements = utils.duplicateArrayLikeUsingType( value, valueType ); const nonNumericalKeysAndSymbols = !subjectType.numericalPropertiesOnly && utils.uniqueNonNumericalStringsAndSymbols(subjectKeys, valueKeys); const changes = arrayChanges( subjectElements, valueElements, function equal(a, b, aIndex, bIndex) { toSatisfyMatrix[aIndex] = toSatisfyMatrix[aIndex] || []; const existingResult = toSatisfyMatrix[aIndex][bIndex]; if (typeof existingResult !== 'undefined') { return existingResult === true; } let result; try { result = expect(a, 'to [exhaustively] satisfy', b); } catch (err) { throwIfNonUnexpectedError(err); toSatisfyMatrix[aIndex][bIndex] = err; return false; } result.then(() => {}, () => {}); if (result.isPending()) { isAsync = true; return false; } toSatisfyMatrix[aIndex][bIndex] = true; return true; }, (a, b) => subjectType.similar(a, b), { includeNonNumericalProperties: nonNumericalKeysAndSymbols } ); if (isAsync) { return expect .promise((resolve, reject) => { arrayChangesAsync( subject, value, function equal(a, b, aIndex, bIndex, cb) { toSatisfyMatrix[aIndex] = toSatisfyMatrix[aIndex] || []; const existingResult = toSatisfyMatrix[aIndex][bIndex]; if (typeof existingResult !== 'undefined') { return cb(existingResult === true); } expect .promise(() => expect(a, 'to [exhaustively] satisfy', b) ) .then( () => { toSatisfyMatrix[aIndex][bIndex] = true; cb(true); }, err => { toSatisfyMatrix[aIndex][bIndex] = err; cb(false); } ); }, (a, b, aIndex, bIndex, cb) => { cb(subjectType.similar(a, b)); }, nonNumericalKeysAndSymbols, resolve ); }) .then(failWithChanges); } else { return failWithChanges(changes); } function failWithChanges(changes) { expect.errorMode = 'default'; expect.fail({ diff(output, diff, inspect, equal) { output.inline = true; const indexOfLastNonInsert = changes.reduce( (previousValue, { type }, index) =>