diff --git a/lib/Unexpected.js b/lib/Unexpected.js index 02e72e29b..05a60ded7 100644 --- a/lib/Unexpected.js +++ b/lib/Unexpected.js @@ -487,13 +487,7 @@ Unexpected.prototype.fail = function (arg) { error[key] = additionalProperties[key]; }); } else { - var placeholderArgs; - if (arguments.length > 0) { - placeholderArgs = new Array(arguments.length - 1); - for (var i = 1 ; i < arguments.length ; i += 1) { - placeholderArgs[i - 1] = arguments[i]; - } - } + var placeholderArgs = Array.prototype.slice.call(arguments, 1); error.errorMode = 'bubble'; error.output = function (output) { var message = arg ? String(arg) : 'Explicit failure'; @@ -502,7 +496,7 @@ Unexpected.prototype.fail = function (arg) { var match = placeholderRegexp.exec(token); if (match) { var index = match[1]; - if (placeholderArgs && index in placeholderArgs) { + if (index in placeholderArgs) { var placeholderArg = placeholderArgs[index]; if (placeholderArg && placeholderArg.isMagicPen) { output.append(placeholderArg); @@ -1026,10 +1020,7 @@ Unexpected.prototype.expect = function expect(subject, testDescriptionString) { return Boolean(flags[flag]) !== Boolean(negate) ? flag + ' ' : ''; }).trim(); - var args = new Array(arguments.length - 2); - for (var i = 2 ; i < arguments.length ; i += 1) { - args[i - 2] = arguments[i]; - } + var args = Array.prototype.slice.call(arguments, 2); return wrappedExpect.callInNestedContext(function () { return executeExpect(subject, testDescriptionString, args); }); @@ -1070,10 +1061,7 @@ Unexpected.prototype.expect = function expect(subject, testDescriptionString) { return oathbreaker(assertionRule.handler.apply(wrappedExpect, [wrappedExpect, subject].concat(args))); } - var args = new Array(arguments.length - 2); - for (var i = 2 ; i < arguments.length ; i += 1) { - args[i - 2] = arguments[i]; - } + var args = Array.prototype.slice.call(arguments, 2); try { var result = executeExpect(subject, testDescriptionString, args); if (isPendingPromise(result)) { diff --git a/lib/assertions.js b/lib/assertions.js index 2dadb1564..c73185c78 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1,5 +1,9 @@ /*global setTimeout*/ var utils = require('./utils'); +var arrayChanges = require('array-changes'); +var arrayChangesAsync = require('array-changes-async'); +var throwIfNonUnexpectedError = require('./throwIfNonUnexpectedError'); +var leven = require('leven'); var objectIs = utils.objectIs; var isRegExp = utils.isRegExp; var isArray = utils.isArray; @@ -632,7 +636,7 @@ module.exports = function (expect) { expect.errorMode = 'bubble'; var keys = expect.subjectType.getKeys(subject); - var expected = Array.isArray(subject) ? [] : {}; + var expected = {}; keys.forEach(function (key, index) { if (typeof nextArg === 'string') { expected[key] = function (s) { @@ -892,7 +896,172 @@ module.exports = function (expect) { expect(subject, 'to equal', value); }); - expect.addAssertion(' to [exhaustively] satisfy ', function (expect, subject, value) { + // FIXME: Don't duplicate (also found in types.js, but closes over expect): + function structurallySimilar(a, b) { + var typeA = typeof a; + var typeB = typeof b; + + if (typeA !== typeB) { + return false; + } + + if (typeA === 'string') { + return leven(a, b) < a.length / 2; + } + + if (typeA !== 'object' || !a) { + return false; + } + + if (utils.isArray(a) && utils.isArray(b)) { + return true; + } + + var aKeys = expect.findTypeOf(a).getKeys(a); + var bKeys = expect.findTypeOf(b).getKeys(b); + var numberOfSimilarKeys = 0; + var requiredSimilarKeys = Math.round(Math.max(aKeys.length, bKeys.length) / 2); + return aKeys.concat(bKeys).some(function (key) { + if (key in a && key in b) { + numberOfSimilarKeys += 1; + } + + return numberOfSimilarKeys >= requiredSimilarKeys; + }); + } + + expect.addAssertion(' to [exhaustively] satisfy ', function (expect, subject, value) { + var keys = expect.argTypes[0].getKeys(value); + var subjectType = expect.subjectType; + + return expect.promise.all([ + expect.promise(function () { + expect(subject, 'to only have keys', keys); + }), + expect.promise.all(keys.map(function (key) { + return expect.promise(function () { + var valueKeyType = expect.findTypeOf(value[key]); + if (valueKeyType.is('function')) { + return value[key](subject[key]); + } else { + return expect(subject[key], 'to [exhaustively] satisfy', value[key]); + } + }); + })) + ]).caught(function () { + var toSatisfyResults = []; + function registerToSatisfyResult(a, b, err) { + toSatisfyResults.push([a, b, err]); + } + function lookupToSatisfyResult(a, b) { + for (var i = 0 ; i < toSatisfyResults.length ; i += 1) { + if (toSatisfyResults[i][0] === a && toSatisfyResults[i][1] === b) { + return toSatisfyResults[i][2]; + } + } + } + var isAsync = false; + var changes = arrayChanges(subject, value, function equal(a, b) { + var existingResult = lookupToSatisfyResult(a, b); + if (typeof existingResult !== 'undefined') { + return !existingResult; + } + var result; + try { + result = expect(a, 'to satisfy', b); + } catch (err) { + throwIfNonUnexpectedError(err); + registerToSatisfyResult(a, b, err); + return false; + } + if (result && typeof result.then === 'function') { + if (result.isPending()) { + isAsync = true; + } + result.then(function () {}, function () {}); + return false; + } + registerToSatisfyResult(a, b, true); + return true; + }, structurallySimilar); + if (isAsync) { + return expect.promise(function (resolve, reject) { + arrayChangesAsync(subject, value, function equal(a, b, aIndex, bIndex, cb) { + var existingResult = lookupToSatisfyResult(a, b); + if (typeof existingResult !== 'undefined') { + return cb(!existingResult); + } + expect.promise(function () { + return expect(a, 'to satisfy', b); + }).then(function () { + registerToSatisfyResult(a, b, true); + cb(true); + }, function (err) { + registerToSatisfyResult(a, b, err); + cb(false); + }); + }, function (a, b, aIndex, bIndex, cb) { + cb(structurallySimilar(a, b)); + }, resolve); + }).then(failWithChanges); + } else { + return failWithChanges(changes); + } + + function failWithChanges(changes) { + expect.fail({ + diff: function (output, diff, inspect, equal) { + var result = { + diff: output, + inline: true + }; + var indexOfLastNonInsert = changes.reduce(function (previousValue, diffItem, index) { + return (diffItem.type === 'insert') ? previousValue : index; + }, -1); + output.append(subjectType.prefix(output.clone(), subject)).nl().indentLines(); + changes.forEach(function (diffItem, index) { + var delimiterOutput = subjectType.delimiter(output.clone(), index, indexOfLastNonInsert + 1); + output.i().block(function () { + var type = diffItem.type; + if (type === 'insert') { + this.annotationBlock(function () { + if (expect.findTypeOf(diffItem.value).is('function')) { + this.error('missing: should satisfy ').block(inspect(diffItem.value)); + } else { + this.error('missing ').block(inspect(diffItem.value)); + } + }); + } else if (type === 'remove') { + this.block(inspect(diffItem.value).amend(delimiterOutput.sp()).error('// should be removed')); + } else if (type === 'equal') { + this.block(inspect(diffItem.value).amend(delimiterOutput)); + } else { + var toSatisfyResult = lookupToSatisfyResult(diffItem.value, diffItem.expected); + if (typeof toSatisfyResult !== 'undefined' && toSatisfyResult !== true) { + var valueDiff = toSatisfyResult && toSatisfyResult !== true && toSatisfyResult.getDiff({ output: output.clone() }); + if (valueDiff && valueDiff.inline) { + this.block(valueDiff.diff.amend(delimiterOutput)); + } else { + this.block(inspect(diffItem.value).amend(delimiterOutput)).sp().annotationBlock(function () { + this.omitSubject = diffItem.value; + this.appendErrorMessage(toSatisfyResult); + }); + } + } else { + this.block(inspect(diffItem.value).amend(delimiterOutput)); + } + } + }).nl(); + }); + output.outdentLines().append(subjectType.suffix(output.clone(), subject)); + return result; + } + }); + } + }); + }); + + expect.addAssertion(' to [exhaustively] satisfy ', function (expect, subject, value) { var valueType = expect.argTypes[0]; var subjectType = expect.subjectType; var commonType = expect.findCommonType(subject, value); @@ -1218,7 +1387,7 @@ module.exports = function (expect) { expect.errorMode = 'nested'; return subject.then(function (fulfillmentValue) { if (expect.findTypeOf(nextAssertion).is('expect.it')) { - // Force a failing expect.it error message to be property nested instead of replacing the default error message: + // Force a failing expect.it error message to be properly nested instead of replacing the default error message: return expect.promise(function () { return expect.shift(fulfillmentValue, 0); }).caught(function (err) { diff --git a/lib/styles.js b/lib/styles.js index a419dc71e..680be1433 100644 --- a/lib/styles.js +++ b/lib/styles.js @@ -195,6 +195,12 @@ module.exports = function (expect) { }); }); + expect.addStyle('shouldSatisfy', function (expected, err) { + this.error((err && err.label) || 'should satisfy').sp().block(function () { + this.appendInspected(expected); + }); + }); + expect.addStyle('errorName', function (error) { if (typeof error.name === 'string' && error.name !== 'Error') { this.text(error.name); diff --git a/package.json b/package.json index 2d57669c4..f5a315e5c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "main": "./lib/index.js", "dependencies": { "array-changes": "1.0.3", + "array-changes-async": "git://github.com/bruderstein/array-changes-async.git#0037dabad0d34dc9c4ed40da121713fa54df9047", + "arraydiff-async": "git://github.com/bruderstein/arraydiff-async.git#a63028cb977e14c0f3a62ad756f0cad952cddacb", "bluebird": "2.9.34", "detect-indent": "3.0.1", "diff": "1.1.0", diff --git a/test/unexpected.spec.js b/test/unexpected.spec.js index 18838ceb7..978e857d7 100644 --- a/test/unexpected.spec.js +++ b/test/unexpected.spec.js @@ -3102,8 +3102,8 @@ describe('unexpected', function () { "expected [] to satisfy [ 1, 2 ]\n" + "\n" + "[\n" + - " // missing: should equal 1\n" + - " // missing: should equal 2\n" + + " // missing 1\n" + + " // missing 2\n" + "]" ); }); @@ -3652,7 +3652,7 @@ describe('unexpected', function () { 'expected [] to satisfy [ undefined ]\n' + '\n' + '[\n' + - ' // missing: should satisfy undefined\n' + + ' // missing undefined\n' + ']' ); }); @@ -3680,7 +3680,7 @@ describe('unexpected', function () { ' 1,\n' + ' 2,\n' + ' 3\n' + - ' // missing: should equal 4\n' + + ' // missing 4\n' + ']'); }); });