Skip to content

Commit

Permalink
WIP: Use array-changes and array-changes async to build better <array…
Browse files Browse the repository at this point in the history
…-like> to satisfy <array-like> diffs.
  • Loading branch information
papandreou committed Nov 15, 2015
1 parent a049f9a commit 8050e7e
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 23 deletions.
20 changes: 4 additions & 16 deletions lib/Unexpected.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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)) {
Expand Down
175 changes: 172 additions & 3 deletions lib/assertions.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -892,7 +896,172 @@ module.exports = function (expect) {
expect(subject, 'to equal', value);
});

expect.addAssertion('<object> to [exhaustively] satisfy <array-like|object>', 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('<array-like> to [exhaustively] satisfy <array-like>', 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('<object> to [exhaustively] satisfy <object>', function (expect, subject, value) {
var valueType = expect.argTypes[0];
var subjectType = expect.subjectType;
var commonType = expect.findCommonType(subject, value);
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions lib/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions test/unexpected.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" +
"]"
);
});
Expand Down Expand Up @@ -3652,7 +3652,7 @@ describe('unexpected', function () {
'expected [] to satisfy [ undefined ]\n' +
'\n' +
'[\n' +
' // missing: should satisfy undefined\n' +
' // missing undefined\n' +
']'
);
});
Expand Down Expand Up @@ -3680,7 +3680,7 @@ describe('unexpected', function () {
' 1,\n' +
' 2,\n' +
' 3\n' +
' // missing: should equal 4\n' +
' // missing 4\n' +
']');
});
});
Expand Down

0 comments on commit 8050e7e

Please sign in to comment.