Skip to content

Commit

Permalink
Merge 3f97d3e into b2e122f
Browse files Browse the repository at this point in the history
  • Loading branch information
papandreou committed Mar 12, 2017
2 parents b2e122f + 3f97d3e commit d3c896e
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 29 deletions.
62 changes: 44 additions & 18 deletions lib/Unexpected.js
Expand Up @@ -313,7 +313,7 @@ Unexpected.prototype.expandTypeAlternations = function (assertion) {
tails.forEach(function (tail) {
result.push([arg].concat(tail));
});
} else if (arg.type.is('assertion')) {
} else if (arg.type.is('assertion') && !arg.variants) {
result.push([
{ type: arg.type, minimum: 1, maximum: 1 },
{ type: that.typeByName.any, minimum: 0, maximum: Infinity }
Expand Down Expand Up @@ -405,7 +405,23 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
assertion = {
subject: tokens[0],
assertion: tokens[1],
args: tokens.slice(2)
args: tokens.slice(2).map(function (token) {
if (typeof token === 'string') {
return [
{
type: lookupType('assertion'),
minimum: 1,
maximum: 1,
variants: expandAssertion(token).reduce(function (variants, expandedAssertion) {
variants[expandedAssertion.text] = expandedAssertion;
return variants;
}, {})
}
];
} else {
return token;
}
})
};
}

Expand All @@ -418,16 +434,12 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
if (hasVarargs(assertion.subject)) {
throw new SyntaxError('The subject type cannot have varargs: ' + assertionString);
}
if (assertion.args.some(function (arg) { return typeof arg === 'string'; })) {
throw new SyntaxError('Only one assertion string is supported (see #225)');
}

if (assertion.args.slice(0, -1).some(hasVarargs)) {
throw new SyntaxError('Only the last argument type can have varargs: ' + assertionString);
}
if ([assertion.subject].concat(assertion.args.slice(0, -1)).some(function (argRequirements) {
return argRequirements.some(function (argRequirement) {
return argRequirement.type.is('assertion');
return argRequirement.type.is('assertion') && !argRequirement.variants;
});
})) {
throw new SyntaxError('Only the last argument type can be <assertion>: ' + assertionString);
Expand Down Expand Up @@ -969,25 +981,29 @@ Unexpected.prototype.lookupAssertionRule = function (subject, testDescriptionStr
return type;
}

function matches(value, assertionType, key, relaxed) {
if (assertionType.is('assertion') && typeof value === 'string') {
return true;
function matches(value, arg, key, relaxed) {
if (arg.type.is('assertion') && typeof value === 'string') {
if (arg.variants) {
return arg.variants[value];
} else {
return true;
}
}

if (relaxed) {
if (assertionType.identify === false) {
if (arg.type.identify === false) {
return that.types.some(function (type) {
return type.identify && type.is(assertionType) && type.identify(value);
return type.identify && type.is(arg.type) && type.identify(value);
});
}
return assertionType.identify(value);
return arg.type.identify(value);
} else {
return findTypeOf(value, key).is(assertionType);
return findTypeOf(value, key).is(arg.type);
}
}

function matchesHandler(handler, relaxed) {
if (!matches(subject, handler.subject.type, 'subject', relaxed)) {
if (!matches(subject, handler.subject, 'subject', relaxed)) {
return false;
}
if (requireAssertionSuffix && !handler.args.some(isAssertionArg)) {
Expand All @@ -1005,9 +1021,9 @@ Unexpected.prototype.lookupAssertionRule = function (subject, testDescriptionStr
var lastRequirement = handler.args[handler.args.length - 1];
return args.every(function (arg, i) {
if (i < handler.args.length - 1) {
return matches(arg, handler.args[i].type, i, relaxed);
return matches(arg, handler.args[i], i, relaxed);
} else {
return matches(arg, lastRequirement.type, i, relaxed);
return matches(arg, lastRequirement, i, relaxed);
}
});
}
Expand Down Expand Up @@ -1115,10 +1131,20 @@ Unexpected.prototype.expect = function expect(subject, testDescriptionString) {
wrappedExpect.args = args;
wrappedExpect.assertionRule = assertionRule;

if (assertionRule.args.some(function (arg) { return arg.variants; })) {
args = wrappedExpect.args.filter(function (arg, i) {
if (i < assertionRule.args.length && assertionRule.args[i].variants) {
extend(flags, assertionRule.args[i].variants[args[i]].flags);
wrappedExpect.alternations = wrappedExpect.alternations.concat(assertionRule.args[i].variants[args[i]].alternations);
} else {
return true;
}
});
}
wrappedExpect.subjectOutput = function (output) {
output.appendInspected(subject);
};
wrappedExpect.argsOutput = args.map(function (arg, i) {
wrappedExpect.argsOutput = wrappedExpect.args.map(function (arg, i) {
var argRule = wrappedExpect.assertionRule.args[i];
if (typeof arg === 'string' && (argRule && argRule.type.is('assertion') || wrappedExpect._getAssertionIndices().indexOf(i) >= 0)) {
return new AssertionString(arg);
Expand Down
16 changes: 12 additions & 4 deletions lib/createWrappedExpectProto.js
Expand Up @@ -104,8 +104,9 @@ module.exports = function createWrappedExpectProto(unexpected) {
}
assertionIndex = -1;
for (var i = 0 ; i < this.assertionRule.args.length ; i += 1) {
var type = this.assertionRule.args[i].type;
if (type.is('assertion') || type.is('expect.it')) {
var arg = this.assertionRule.args[i];
var type = arg.type;
if ((type.is('assertion') || type.is('expect.it')) && !arg.variants) {
assertionIndex = i;
break;
}
Expand Down Expand Up @@ -188,13 +189,20 @@ module.exports = function createWrappedExpectProto(unexpected) {
var args = this.args;
var currentAssertionRule = this.assertionRule;
var offset = 0;
var i;
OUTER: while (true) {
for (i = 0 ; i < currentAssertionRule.args.length - 1 ; i += 1) {
if (currentAssertionRule.args[i].variants) {
assertionIndices.push(offset + i);
}
}
if (currentAssertionRule.args.length > 1 && isAssertionArg(currentAssertionRule.args[currentAssertionRule.args.length - 2])) {
assertionIndices.push(offset + currentAssertionRule.args.length - 2);
var suffixAssertions = findSuffixAssertions(args[offset + currentAssertionRule.args.length - 2], unexpected.assertions);
if (suffixAssertions) {
for (var i = 0 ; i < suffixAssertions.length ; i += 1) {
if (suffixAssertions[i].args.some(isAssertionArg)) {
for (i = 0 ; i < suffixAssertions.length ; i += 1) {
var lastArg = suffixAssertions[i].args[suffixAssertions[i].args.length - 2];
if (lastArg && isAssertionArg(lastArg)) {
offset += currentAssertionRule.args.length - 1;
currentAssertionRule = suffixAssertions[i];
continue OUTER;
Expand Down
109 changes: 109 additions & 0 deletions test/api/addAssertion.spec.js
Expand Up @@ -622,4 +622,113 @@ describe('addAssertion', function () {
'-bar\n' +
'+foo');
});

describe('with multiple assertion strings', function () {
it('omits the assertion strings in the argument list, leaving only the <type>s', function () {
var clonedExpect = expect.clone();
clonedExpect.addAssertion('<number> [not] to be in the range from <number> up to [and including] <number>', function (expect, subject, lower, upper) {
expect.errorMode = 'nested';
expect(lower, 'to equal', 200);
expect(upper, 'to equal', 400);
});
clonedExpect(123, 'not to be in the range from', 200, 'up to and including', 400);
});

it('populates expect.flags with all flags set across the assertion strings', function () {
var clonedExpect = expect.clone();
clonedExpect.addAssertion('<number> [not] to be in the range from <number> up to [and including] <number>', function (expect) {
expect.errorMode = 'nested';
expect(expect.flags, 'to equal', {not: true, 'and including': true});
});
clonedExpect(123, 'not to be in the range from', 200, 'up to and including', 400);
});

it('populates expect.alternations with all alternations set across the assertion strings', function () {
var clonedExpect = expect.clone();
clonedExpect.addAssertion('<number> to be (within|in) the range from <number> up (to|foo) <number>', function (expect) {
expect.errorMode = 'nested';
expect(expect.alternations, 'to equal', ['within', 'foo']);
});
clonedExpect(123, 'to be within the range from', 200, 'up foo', 400);
});

it('should render the error message correctly when the assertion fails', function () {
var clonedExpect = expect.clone();
clonedExpect.addAssertion('<number> [not] to be in the range from <number> up to [and including] <number>', function (expect, subject, lower, upper) {
expect.errorMode = 'nested';
expect(subject, 'to be greater than or equal to', lower).and('to be less than or equal to', upper);
});
expect(function () {
clonedExpect(500, 'to be in the range from', 200, 'up to and including', 400);
}, 'to throw',
'expected 500 to be in the range from 200 up to and including 400\n' +
' expected 500 to be less than or equal to 400'
);
});

it('should use the full form of the assertion when suggesting it in case of a typo', function () {
var clonedExpect = expect.clone();
clonedExpect.addAssertion('<number> [not] to be in the range from <number> up to [and including] <number>', function (expect, subject, lower, upper) {
expect.errorMode = 'nested';
expect(subject, 'to be greater than or equal to', lower).and('to be less than or equal to', upper);
});
expect(function () {
clonedExpect(300, 'to be in the range from', 200, 'up to and including', 'four hundred');
}, 'to throw',
"expected 300 to be in the range from 200, 'up to and including', 'four hundred'\n" +
" The assertion does not have a matching signature for:\n" +
" <number> to be in the range from <number> <string> <string>\n" +
// Maybe: " <number> to be in the range from <number> up to and including <string>\n" +
" did you mean:\n" +
" <number> [not] to be in the range from <number> up to [and including] <number>"
);
});

it('works with expect.it', function () {
var clonedExpect = expect.clone();
clonedExpect.addAssertion('<number> [not] to be in the range from <number> up to [and including] <number>', function (expect, subject, lower, upper) {
expect.errorMode = 'nested';
expect(subject, 'to be greater than or equal to', lower).and('to be less than or equal to', upper);
});
expect({ foo: 300 }, 'to satisfy', { foo: clonedExpect.it('to be in the range from', 200, 'up to and including', 400) });
});

describe('with a multi-string assertion that ends with <assertion>', function () {
var clonedExpect = expect.clone();
clonedExpect.addAssertion('<number> [when] divided by <number> and multiplied [twice] by <number> <assertion?>', function (expect, subject, divisor, factor) {
expect.errorMode = 'nested';
return expect.shift(subject / divisor * (expect.flags.twice ? 2 : 1) * factor);
});

it('should succeed', function () {
clonedExpect(4, 'when divided by', 2, 'and multiplied twice by', 3, 'to equal', 12);
});

it('should fail with a the right error message', function () {
expect(function () {
clonedExpect(4, 'when divided by', 2, 'and multiplied twice by', 3, 'to equal', 14);
}, 'to throw',
'expected 4 when divided by 2 and multiplied twice by 3 to equal 14\n' +
' expected 12 to equal 14'
);
});

describe('when applied multiple times', function () {
it('should succeed', function () {
clonedExpect(4, 'when divided by', 2, 'and multiplied twice by', 3, 'when divided by', 6, 'and multiplied by', 4, 'to equal', 8);
});

it('should fail with a the right error message', function () {
expect(function () {
clonedExpect(4, 'when divided by', 2, 'and multiplied twice by', 3, 'when divided by', 6, 'and multiplied by', 4, 'to equal', 16);
}, 'to throw',
"expected 4\n" +
"when divided by 2 and multiplied twice by 3 when divided by 6 and multiplied by 4 to equal 16\n" +
" expected 12 when divided by 6 and multiplied by 4 to equal 16\n" +
" expected 8 to equal 16"
);
});
});
});
});
});
57 changes: 50 additions & 7 deletions test/assertionParser.spec.js
Expand Up @@ -113,13 +113,6 @@ describe('parseAssertion', function () {
}, 'to throw', 'Only the last argument type can have varargs: <any> [not] to be <any*> <string>');
});

// Under consideration here: https://github.com/unexpectedjs/unexpected/issues/225
it('throws if the argument list contains multiple assertion strings', function () {
expect(function () {
expect.parseAssertion('<number> to be in range from <number> up to [and including] <number> ');
}, 'to throw', 'Only one assertion string is supported (see #225)');
});

it('handles types with upper case characters', function () {
var assertion = expect.parseAssertion('<number|NaN> [not] to be NaN');
expect(assertion, 'to satisfy', [
Expand Down Expand Up @@ -190,4 +183,54 @@ describe('parseAssertion', function () {
}, 'to throw', 'Cannot parse token at index 19 in <Buffer> wh!!en foo<>');
});
});

describe('with multiple assertion strings', function () {
it('represents the additional assertion strings as a separate items with variants', function () {
expect(
expect.parseAssertion('<number> to be in range from <number> up to [and including] <number>'),
'to satisfy',
[
{
subject: {
minimum: 1,
maximum: 1,
type: { name: 'number' }
},
assertion: 'to be in range from',
args: [
{
minimum: 1,
maximum: 1,
type: { name: 'number' }
},
{
type: { name: 'assertion' },
minimum: 1,
maximum: 1,
variants: {
'up to and including': {
text: 'up to and including',
flags: { 'and including': true },
alternations: []
},
'up to': {
text: 'up to',
flags: { 'and including': false },
alternations: []
}
}
},
{
minimum: 1,
maximum: 1,
type: {
name: 'number'
}
}
]
}
]
);
});
});
});

0 comments on commit d3c896e

Please sign in to comment.