Skip to content

Commit

Permalink
Merge d7b7c4b into 96f346b
Browse files Browse the repository at this point in the history
  • Loading branch information
papandreou committed Nov 22, 2015
2 parents 96f346b + d7b7c4b commit 766565f
Show file tree
Hide file tree
Showing 5 changed files with 422 additions and 44 deletions.
2 changes: 1 addition & 1 deletion lib/Unexpected.js
Expand Up @@ -502,7 +502,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
180 changes: 177 additions & 3 deletions lib/assertions.js
@@ -1,5 +1,8 @@
/*global setTimeout*/
var utils = require('./utils');
var arrayChanges = require('array-changes');
var arrayChangesAsync = require('array-changes-async');
var throwIfNonUnexpectedError = require('./throwIfNonUnexpectedError');
var objectIs = utils.objectIs;
var isRegExp = utils.isRegExp;
var isArray = utils.isArray;
Expand Down Expand Up @@ -632,7 +635,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 +895,178 @@ module.exports = function (expect) {
expect(subject, 'to equal', value);
});

expect.addAssertion('<object> to [exhaustively] satisfy <array-like|object>', function (expect, subject, value) {
expect.addAssertion('<array-like> to [exhaustively] satisfy <array-like>', function (expect, subject, value) {
expect.errorMode = 'bubble';
var keyPromises = new Array(value.length);
var i;
var valueKeys = new Array(value.length);
for (i = 0 ; i < value.length ; i += 1) {
valueKeys[i] = i;
keyPromises[i] = expect.promise(function () {
var valueKeyType = expect.findTypeOf(value[i]);
if (valueKeyType.is('function')) {
return value[i](subject[i]);
} else {
return expect(subject[i], 'to [exhaustively] satisfy', value[i]);
}
});
}
return expect.promise.all([
expect.promise(function () {
expect(subject, 'to only have keys', valueKeys);
}),
expect.promise.all(keyPromises)
]).caught(function () {
var subjectType = expect.subjectType;
return expect.promise.settle(keyPromises).then(function () {
var 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) {
var indexByIndexChanges = [];
for (i = 0 ; i < subject.length ; i += 1) {
var 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);
}

var isAsync = false;
var changes = arrayChanges(subject, value, function equal(a, b, aIndex, bIndex) {
var existingResult = aIndex < subject.length && toSatisfyMatrix[aIndex][bIndex];
if (typeof existingResult !== 'undefined') {
return existingResult === true;
}
var result;
try {
result = expect(a, 'to [exhaustively] satisfy', b);
} catch (err) {
throwIfNonUnexpectedError(err);
toSatisfyMatrix[aIndex][bIndex] = err;
return false;
}
result.then(function () {}, function () {});
if (result.isPending()) {
isAsync = true;
return false;
}
toSatisfyMatrix[aIndex][bIndex] = true;
return true;
}, function (a, b) {
return subjectType.similar(a, b);
});
if (isAsync) {
return expect.promise(function (resolve, reject) {
arrayChangesAsync(subject, value, function equal(a, b, aIndex, bIndex, cb) {
var existingResult = aIndex < subject.length && toSatisfyMatrix[aIndex][bIndex];
if (typeof existingResult !== 'undefined') {
return cb(existingResult === true);
}
expect.promise(function () {
return expect(a, 'to [exhaustively] satisfy', b);
}).then(function () {
toSatisfyMatrix[aIndex][bIndex] = true;
cb(true);
}, function (err) {
toSatisfyMatrix[aIndex][bIndex] = err;
cb(false);
});
}, function (a, b, aIndex, bIndex, cb) {
cb(subjectType.similar(a, b));
}, resolve);
}).then(failWithChanges);
} else {
return failWithChanges(changes);
}

function failWithChanges(changes) {
expect.errorMode = 'default';
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 = toSatisfyMatrix[diffItem.actualIndex][diffItem.expectedIndex];
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;
if (toSatisfyResult.getLabel()) {
this.error(toSatisfyResult.getLabel() || 'should satisfy').sp()
.block(inspect(diffItem.expected));

if (valueDiff) {
this.nl().append(valueDiff.diff);
}
} else {
this.appendErrorMessage(toSatisfyResult);
}
});
}
}
}).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;
if (subject === value) {
Expand Down Expand Up @@ -1229,7 +1403,7 @@ module.exports = function (expect) {
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
70 changes: 35 additions & 35 deletions lib/types.js
Expand Up @@ -300,6 +300,38 @@ module.exports = function (expect) {
this.suffix(output, actual);

return result;
},

similar: function (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;
});
}
});

Expand All @@ -314,39 +346,6 @@ module.exports = function (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.addType({
name: 'magicpen',
identify: function (obj) {
Expand Down Expand Up @@ -472,11 +471,12 @@ module.exports = function (expect) {
return this.baseType.diff(actual, expected, output);
}

var changes = arrayChanges(actual, expected, equal, structurallySimilar);

output.append(this.prefix(output.clone(), actual)).nl().indentLines();

var type = this;
var changes = arrayChanges(actual, expected, equal, function (a, b) {
return type.similar(a, b);
});
var indexOfLastNonInsert = changes.reduce(function (previousValue, diffItem, index) {
return (diffItem.type === 'insert') ? previousValue : index;
}, -1);
Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -20,7 +20,8 @@
},
"main": "./lib/index.js",
"dependencies": {
"array-changes": "1.0.3",
"array-changes": "1.2.0",
"array-changes-async": "2.0.2",
"bluebird": "2.9.34",
"detect-indent": "3.0.1",
"diff": "1.1.0",
Expand Down

0 comments on commit 766565f

Please sign in to comment.