Skip to content

Commit

Permalink
Implement compound assertions (#223)
Browse files Browse the repository at this point in the history
Make lookupAssertionRule return the assertion rule or undefined.
Previously it would throw if a suitable assertion rule wasn't found.

Also consolidated the two pieces of code that fail with an "assertion not found"
type of error into one function.
  • Loading branch information
papandreou committed Dec 23, 2015
1 parent bda26cf commit e55a295
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 56 deletions.
8 changes: 0 additions & 8 deletions lib/AssertionStringType.js

This file was deleted.

105 changes: 58 additions & 47 deletions lib/Unexpected.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ var testFrameworkPatch = require('./testFrameworkPatch');
var defaultDepth = require('./defaultDepth');
var createWrappedExpectProto = require('./createWrappedExpectProto');
var AssertionString = require('./AssertionString');
var AssertionStringType = require('./AssertionStringType');
var throwIfNonUnexpectedError = require('./throwIfNonUnexpectedError');

var anyType = {
Expand Down Expand Up @@ -298,9 +297,9 @@ Unexpected.prototype.expandTypeAlternations = function (assertion) {
tails.forEach(function (tail) {
result.push([arg].concat(tail));
});
} else if (arg.type === AssertionStringType) {
} else if (arg.type.is('assertion')) {
result.push([
{ type: AssertionStringType, minimum: 1, maximum: 1, isAssertion: true },
{ type: arg.type, minimum: 1, maximum: 1, isAssertion: true },
{ type: that.typeByName['any'], minimum: 0, maximum: Infinity }
]);
result.push([
Expand Down Expand Up @@ -340,9 +339,6 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
var nextIndex = 0;

function lookupType(typeName) {
if (typeName === 'assertion') {
return AssertionStringType;
}
var result = that.typeByName[typeName];
if (!result) {
throw new Error('Unknown type: ' + typeName + ' in ' + assertionString);
Expand Down Expand Up @@ -371,7 +367,6 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
});
}
assertionString.replace(/\s*<((?:[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])[?*+]?)(?:\|(?:[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])[?*+]?))*)>|\s*([^<]+)/ig, function ($0, $1, $2, index) {

if (index !== nextIndex) {
throw new SyntaxError('Cannot parse token at index ' + nextIndex + ' in ' + assertionString);
}
Expand Down Expand Up @@ -412,15 +407,15 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
}
if ([assertion.subject].concat(assertion.args.slice(0, -1)).some(function (argRequirements) {
return argRequirements.some(function (argRequirement) {
return argRequirement.type === AssertionStringType;
return argRequirement.type.is('assertion');
});
})) {
throw new SyntaxError('Only the last argument type can be <assertion>: ' + assertionString);
}

var lastArgRequirements = assertion.args[assertion.args.length - 1] || [];
var assertionRequirements = lastArgRequirements.filter(function (argRequirement) {
return argRequirement.type === AssertionStringType;
return argRequirement.type.is('assertion');
});

if (assertionRequirements.length > 0 && lastArgRequirements.length > 1) {
Expand Down Expand Up @@ -623,7 +618,7 @@ Unexpected.prototype.addType = function (type) {
throw new Error('Type ' + type.name + ' must specify an identify function or be declared abstract by setting identify to false');
}

if (this.getType(type.name) || type.name === 'assertion') {
if (this.getType(type.name)) {
throw new Error('The type with the name ' + type.name + ' already exists');
}

Expand Down Expand Up @@ -829,21 +824,38 @@ function calculateLimits(items) {
}

Unexpected.prototype.throwAssertionNotFoundError = function (subject, testDescriptionString, args) {
var candidateHandlers = this.assertions[testDescriptionString];
if (candidateHandlers) {
this.fail({
errorMode: 'bubbleThrough',
message: function (output) {
var subjectOutput = function (output) {
output.appendInspected(subject);
};
var argsOutput = function (output) {
output.appendItems(args, ', ');
};
output.append(createStandardErrorMessage(output.clone(), subjectOutput, testDescriptionString, argsOutput)).nl()
.indentLines();
output.i().error('No matching assertion, did you mean:').nl();
var assertionDeclarations = Object.keys(candidateHandlers.reduce(function (result, handler) {
result[handler.declaration] = true;
return result;
}, {})).sort();
assertionDeclarations.forEach(function (declaration, i) {
output.nl(i > 0 ? 1 : 0).i().text(declaration);
});
}
});
}

var assertionsWithScore = [];
var assertionStrings = Object.keys(this.assertions);
var that = this;

function lookup(assertionString) {
try {
return that.lookupAssertionRule(subject, assertionString, args);
} catch (e) {
return null;
}
}

function compareAssertions(a, b) {
var aAssertion = lookup(a);
var bAssertion = lookup(b);
var aAssertion = that.lookupAssertionRule(subject, a, args);
var bAssertion = that.lookupAssertionRule(subject, b, args);
if (!aAssertion && !bAssertion) {
return 0;
}
Expand Down Expand Up @@ -898,15 +910,14 @@ Unexpected.prototype.throwAssertionNotFoundError = function (subject, testDescri
});
};

Unexpected.prototype.lookupAssertionRule = function (subject, testDescriptionString, args) {
Unexpected.prototype.lookupAssertionRule = function (subject, testDescriptionString, args, requireAssertionSuffix) {
var that = this;
if (typeof testDescriptionString !== 'string') {
throw new Error('The expect function requires the second parameter to be a string.');
}

var handlers = this.assertions[testDescriptionString];
if (!handlers) {
this.throwAssertionNotFoundError(subject, testDescriptionString, args);
return;
}
var cachedTypes = {};

Expand All @@ -920,7 +931,7 @@ Unexpected.prototype.lookupAssertionRule = function (subject, testDescriptionStr
}

function matches(value, assertionType, key, relaxed) {
if (assertionType === AssertionStringType && typeof value === 'string') {
if (assertionType.is('assertion') && typeof value === 'string') {
return true;
}

Expand All @@ -940,6 +951,9 @@ Unexpected.prototype.lookupAssertionRule = function (subject, testDescriptionStr
if (!matches(subject, handler.subject.type, 'subject', relaxed)) {
return false;
}
if (requireAssertionSuffix && !handler.args.some(function (arg) { return arg.type.is('assertion'); })) {
return false;
}

var requireArgumentsLength = calculateLimits(handler.args);

Expand Down Expand Up @@ -972,28 +986,6 @@ Unexpected.prototype.lookupAssertionRule = function (subject, testDescriptionStr
return handler;
}
}

that.fail({
errorMode: 'bubbleThrough',
message: function (output) {
var subjectOutput = function (output) {
output.appendInspected(subject);
};
var argsOutput = function (output) {
output.appendItems(args, ', ');
};
output.append(createStandardErrorMessage(output.clone(), subjectOutput, testDescriptionString, argsOutput)).nl()
.indentLines();
output.i().error('No matching assertion, did you mean:').nl();
var assertionDeclarations = Object.keys(handlers.reduce(function (result, handler) {
result[handler.declaration] = true;
return result;
}, {})).sort();
assertionDeclarations.forEach(function (declaration, i) {
output.nl(i > 0 ? 1 : 0).i().text(declaration);
});
}
});
};

function makeExpectFunction(unexpected) {
Expand All @@ -1019,6 +1011,25 @@ Unexpected.prototype.expect = function expect(subject, testDescriptionString) {

function executeExpect(subject, testDescriptionString, args) {
var assertionRule = that.lookupAssertionRule(subject, testDescriptionString, args);

if (!assertionRule) {
var tokens = testDescriptionString.split(' ');
for (var n = tokens.length - 1; n > 0 ; n -= 1) {
var prefix = tokens.slice(0, n).join(' ');
var argsWithAssertionPrepended = [ tokens.slice(n).join(' ') ].concat(args);
assertionRule = that.assertions[prefix] && that.lookupAssertionRule(subject, prefix, argsWithAssertionPrepended, true);
if (assertionRule) {
// Great, found the longest prefix of the string that yielded a suitable assertion for the given subject and args
testDescriptionString = prefix;
args = argsWithAssertionPrepended;
break;
}
}
if (!assertionRule) {
that.throwAssertionNotFoundError(subject, testDescriptionString, args);
}
}

var flags = extend({}, assertionRule.flags);
var wrappedExpect = function () {
var subject = arguments[0];
Expand Down Expand Up @@ -1051,7 +1062,7 @@ Unexpected.prototype.expect = function expect(subject, testDescriptionString) {
};
wrappedExpect.argsOutput = args.map(function (arg, i) {
var argRule = wrappedExpect.assertionRule.args[i];
if (typeof arg === 'string' && (argRule && (argRule.type === AssertionStringType) || wrappedExpect._getAssertionIndices().indexOf(i) >= 0)) {
if (typeof arg === 'string' && (argRule && argRule.type.is('assertion') || wrappedExpect._getAssertionIndices().indexOf(i) >= 0)) {
return new AssertionString(arg);
}

Expand Down
11 changes: 11 additions & 0 deletions lib/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var arrayChanges = require('array-changes');
var leven = require('leven');
var detectIndent = require('detect-indent');
var defaultDepth = require('./defaultDepth');
var AssertionString = require('./AssertionString');

module.exports = function (expect) {
expect.addType({
Expand Down Expand Up @@ -971,4 +972,14 @@ module.exports = function (expect) {
output.jsPrimitive(value);
}
});

expect.addType({
name: 'assertion',
identify: function (value) {
return value instanceof AssertionString;
},
inspect: function (value, depth, output) {
output.error(value.text);
}
});
};
2 changes: 1 addition & 1 deletion test/assertionParser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('parseAssertion', function () {
assertion: 'when decoded as',
args: [
{ type: { name: 'string' }, minimum: 1, maximum: 1 },
{ type: { name: 'assertion-string' }, minimum: 1, maximum: 1 },
{ type: { name: 'assertion' }, minimum: 1, maximum: 1 },
{ type: { name: 'any' }, minimum: 0, maximum: Infinity }
]
},
Expand Down
73 changes: 73 additions & 0 deletions test/unexpected.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8476,4 +8476,77 @@ describe('unexpected', function () {
});
});
});

describe('with the next assertion as a continuation', function () {
describe('with "to have items satisfying" followed by another assertion', function () {
it('should succeed', function () {
return expect([ 123 ], 'to have items satisfying to be a number');
});

it('should fail', function () {
return expect(function () {
return expect([ 123 ], 'to have items satisfying to be a boolean');
}, 'to error',
"expected [ 123 ] to have items satisfying to be a boolean\n" +
"\n" +
"[\n" +
" 123 // should be a boolean\n" +
"]"
);
});
});

describe('with "to have items satisfying" twice followed by another assertion', function () {
it('should succeed', function () {
return expect([ [ 123 ] ], 'to have items satisfying to have items satisfying to be a number');
});

it('should fail', function () {
return expect(function () {
return expect([ [ 123 ] ], 'to have items satisfying to have items satisfying to be a boolean');
}, 'to error',
"expected [ [ 123 ] ]\n" +
"to have items satisfying to have items satisfying to be a boolean\n" +
"\n" +
"[\n" +
" [\n" +
" 123 // should be a boolean\n" +
" ]\n" +
"]"
);
});
});

describe('with "when rejected" followed by another assertion', function () {
it('should succeed', function () {
return expect(expect.promise.reject(123), 'when rejected to satisfy', 123);
});

it('should fail', function () {
return expect(function () {
return expect(expect.promise.reject(true), 'when rejected to be a number');
}, 'to error',
"expected Promise (rejected) => true when rejected to be a number\n" +
" expected true to be a number"
);
});
});

describe('with "when rejected" twice followed by another assertion', function () {
it('should succeed', function () {
return expect(expect.promise.reject(expect.promise.reject(123)), 'when rejected when rejected to satisfy', 123);
});

it('should fail', function () {
return expect(function () {
return expect(expect.promise.reject(expect.promise.reject(true)), 'when rejected when rejected to be a number');
}, 'to error',
"expected Promise (rejected) => Promise (rejected) => true\n" +
"when rejected when rejected to be a number\n" +
" expected Promise (rejected) => true when rejected to be a number\n" +
" expected true to be a number"
);
});
});
});
});

0 comments on commit e55a295

Please sign in to comment.