Skip to content

Commit

Permalink
Implement expect.child(), exportStyle, and exportAssertion
Browse files Browse the repository at this point in the history
  • Loading branch information
papandreou committed Apr 6, 2017
1 parent 33ac468 commit 0bdf0b7
Show file tree
Hide file tree
Showing 6 changed files with 831 additions and 57 deletions.
163 changes: 123 additions & 40 deletions lib/Unexpected.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,17 @@ function Unexpected(options) {
// Make bound versions of these two helpers up front to save a bit when creating wrapped expects:
var that = this;
this.getType = function (typeName) {
return utils.findFirst(that.types, function (type) {
return type.name === typeName;
});
return that.typeByName[typeName] || (that.parent && that.parent.getType(typeName));
};
this.findTypeOf = function (obj) {
return utils.findFirst(that.types || [], function (type) {
return type.identify(obj);
});
return type.identify && type.identify(obj);
}) || (that.parent && that.parent.findTypeOf(obj));
};
this.findTypeOfWithParentType = function (obj, requiredParentType) {
return utils.findFirst(that.types || [], function (type) {
return type.identify && type.identify(obj) && (!requiredParentType || type.is(requiredParentType));
}) || (that.parent && that.parent.findTypeOfWithParentType(obj, requiredParentType));
};
this.findCommonType = function (a, b) {
var aAncestorIndex = {};
Expand Down Expand Up @@ -316,10 +319,10 @@ Unexpected.prototype.expandTypeAlternations = function (assertion) {
} else if (arg.type.is('assertion')) {
result.push([
{ type: arg.type, minimum: 1, maximum: 1 },
{ type: that.typeByName.any, minimum: 0, maximum: Infinity }
{ type: that.getType('any'), minimum: 0, maximum: Infinity }
]);
result.push([
{ type: that.typeByName['expect.it'], minimum: 1, maximum: 1 }
{ type: that.getType('expect.it'), minimum: 1, maximum: 1 }
]);
if (arg.minimum === 0) { // <assertion?>
result.push([]);
Expand Down Expand Up @@ -354,25 +357,21 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
var tokens = [];
var nextIndex = 0;

function lookupType(typeName) {
var result = that.typeByName[typeName];
if (!result) {
throw new Error('Unknown type: ' + typeName + ' in ' + assertionString);
}
return result;
}

function parseType(assertionString) {
return assertionString.split('|').map(function (type) {
var matchNameAndOperator = type.match(/^([a-z_](?:|[a-z0-9_.-]*[_a-z0-9]))([+*?]|)$/i);
function parseTypeToken(typeToken) {
return typeToken.split('|').map(function (typeDeclaration) {
var matchNameAndOperator = typeDeclaration.match(/^([a-z_](?:|[a-z0-9_.-]*[_a-z0-9]))([+*?]|)$/i);
if (!matchNameAndOperator) {
throw new SyntaxError('Cannot parse type declaration:' + type);
throw new SyntaxError('Cannot parse type declaration:' + typeDeclaration);
}
var type = that.getType(matchNameAndOperator[1]);
if (!type) {
throw new Error('Unknown type: ' + matchNameAndOperator[1] + ' in ' + assertionString);
}
var operator = matchNameAndOperator[2];
return {
minimum: !operator || operator === '+' ? 1 : 0,
maximum: operator === '*' || operator === '+' ? Infinity : 1,
type: lookupType(matchNameAndOperator[1])
type: type
};
});
}
Expand All @@ -387,7 +386,7 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
throw new SyntaxError('Cannot parse token at index ' + nextIndex + ' in ' + assertionString);
}
if ($1) {
tokens.push(parseType($1));
tokens.push(parseTypeToken($1));
} else {
tokens.push($2.trim());
}
Expand All @@ -397,9 +396,9 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
var assertion;
if (tokens.length === 1 && typeof tokens[0] === 'string') {
assertion = {
subject: parseType('any'),
subject: parseTypeToken('any'),
assertion: tokens[0],
args: [parseType('any*')]
args: [parseTypeToken('any*')]
};
} else {
assertion = {
Expand Down Expand Up @@ -447,7 +446,6 @@ Unexpected.prototype.parseAssertion = function (assertionString) {
})) {
throw new SyntaxError('<assertion+> and <assertion*> are not allowed: ' + assertionString);
}

return this.expandTypeAlternations(assertion);
};

Expand Down Expand Up @@ -484,14 +482,30 @@ Unexpected.prototype.fail = function (arg) {
output.error('Explicit failure');
}
};
var expect = this.expect;
Object.keys(arg).forEach(function (key) {
var value = arg[key];
if (key === 'diff') {
error.createDiff = value;
if (typeof value === 'function' && this.parent) {
error.createDiff = function (output, diff, inspect, equal) {
var childOutput = expect.createOutput(output.format);
childOutput.inline = output.inline;
childOutput.output = output.output;
return value(childOutput, function diff(actual, expected) {
return expect.diff(actual, expected, childOutput.clone());
}, function inspect(v, depth) {
return childOutput.clone().appendInspected(v, (depth || defaultDepth) - 1);
}, function (actual, expected) {
return expect.equal(actual, expected);
});
};
} else {
error.createDiff = value;
}
} else if (key !== 'message') {
error[key] = value;
}
});
}, this);
} else {
var placeholderArgs;
if (arguments.length > 0) {
Expand Down Expand Up @@ -547,10 +561,14 @@ function calculateAssertionSpecificity(assertion) {
}));
}

// expect.addAssertion(pattern, handler)
// expect.addAssertion([pattern, ...], handler)
Unexpected.prototype.addAssertion = function (patternOrPatterns, handler) {
if (arguments.length > 2 || typeof handler !== 'function' || (typeof patternOrPatterns !== 'string' && !Array.isArray(patternOrPatterns))) {
Unexpected.prototype.addAssertion = function (patternOrPatterns, handler, childUnexpected) {
var maxArguments;
if (typeof childUnexpected === 'object') {
maxArguments = 3;
} else {
maxArguments = 2;
}
if (arguments.length > maxArguments || typeof handler !== 'function' || (typeof patternOrPatterns !== 'string' && !Array.isArray(patternOrPatterns))) {
var errorMessage = "Syntax: expect.addAssertion(<string|array[string]>, function (expect, subject, ...) { ... });";
if ((typeof handler === 'string' || Array.isArray(handler)) && typeof arguments[2] === 'function') {
errorMessage +=
Expand Down Expand Up @@ -595,7 +613,8 @@ Unexpected.prototype.addAssertion = function (patternOrPatterns, handler) {
subject: assertionDeclaration.subject,
args: assertionDeclaration.args,
testDescriptionString: expandedAssertion.text,
declaration: pattern
declaration: pattern,
unexpected: childUnexpected
});
});
});
Expand Down Expand Up @@ -624,7 +643,7 @@ Unexpected.prototype.addAssertion = function (patternOrPatterns, handler) {
return this.expect; // for chaining
};

Unexpected.prototype.addType = function (type) {
Unexpected.prototype.addType = function (type, childUnexpected) {
var that = this;
var baseType;
if (typeof type.name !== 'string' || !/^[a-z_](?:|[a-z0-9_.-]*[_a-z0-9])$/i.test(type.name)) {
Expand All @@ -635,14 +654,12 @@ 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)) {
if (this.typeByName[type.name]) {
throw new Error('The type with the name ' + type.name + ' already exists');
}

if (type.base) {
baseType = utils.findFirst(this.types, function (t) {
return t.name === type.base;
});
baseType = this.getType(type.base);

if (!baseType) {
throw new Error('Unknown base type: ' + type.base);
Expand All @@ -657,7 +674,7 @@ Unexpected.prototype.addType = function (type) {
throw new Error('You need to pass the output to baseType.inspect() as the third parameter');
}

return baseType.inspect(value, depth, output.clone(), function (value, depth) {
return baseType.inspect(value, depth, output, function (value, depth) {
return output.clone().appendInspected(value, depth);
});
};
Expand Down Expand Up @@ -688,10 +705,25 @@ Unexpected.prototype.addType = function (type) {
extendedType.inspect = function (obj, depth, output, inspect) {
if (arguments.length < 2 || (!output || !output.isMagicPen)) {
return 'type: ' + type.name;
} else if (childUnexpected) {
var childOutput = childUnexpected.createOutput(output.format);
return originalInspect.call(this, obj, depth, childOutput, inspect) || childOutput;
} else {
return originalInspect.call(this, obj, depth, output, inspect);
return originalInspect.call(this, obj, depth, output, inspect) || output;
}
};

if (childUnexpected) {
extendedType.childUnexpected = childUnexpected;
var originalDiff = extendedType.diff;
extendedType.diff = function (actual, expected, output, inspect, diff, equal) {
var childOutput = childUnexpected.createOutput(output.format);
// Make sure that already buffered up output is preserved:
childOutput.output = output.output;
return originalDiff.call(this, actual, expected, childOutput, inspect, diff, equal) || output;
};
}

if (extendedType.identify === false) {
this.types.push(extendedType);
} else {
Expand Down Expand Up @@ -769,6 +801,11 @@ Unexpected.prototype.use = function (plugin) {

if (plugin.dependencies) {
var installedPlugins = this.installedPlugins;
var instance = this.parent;
while (instance) {
Array.prototype.push.apply(installedPlugins, instance.installedPlugins);
instance = instance.parent;
}
var unfulfilledDependencies = plugin.dependencies.filter(function (dependency) {
return !installedPlugins.some(function (plugin) {
return getPluginName(plugin) === dependency;
Expand Down Expand Up @@ -831,6 +868,7 @@ function installExpectMethods(unexpected, expectFunction) {
expect.addType = unexpected.addType.bind(unexpected);
expect.getType = unexpected.getType;
expect.clone = unexpected.clone.bind(unexpected);
expect.child = unexpected.child.bind(unexpected);
expect.toString = unexpected.toString.bind(unexpected);
expect.assertions = unexpected.assertions;
expect.use = expect.installPlugin = unexpected.use.bind(unexpected);
Expand Down Expand Up @@ -891,7 +929,12 @@ Unexpected.prototype.throwAssertionNotFoundError = function (subject, testDescri
}

var assertionsWithScore = [];
var assertionStrings = Object.keys(this.assertions);
var assertionStrings = [];
var instance = this;
while (instance) {
Array.prototype.push.apply(assertionStrings, Object.keys(instance.assertions));
instance = instance.parent;
}

function compareAssertions(a, b) {
var aAssertion = that.lookupAssertionRule(subject, a, args);
Expand Down Expand Up @@ -954,7 +997,15 @@ Unexpected.prototype.lookupAssertionRule = function (subject, testDescriptionStr
if (typeof testDescriptionString !== 'string') {
throw new Error('The expect function requires the second parameter to be a string or an expect.it.');
}
var handlers = this.assertions[testDescriptionString];
var handlers;
var instance = this;
while (instance) {
var instanceHandlers = instance.assertions[testDescriptionString];
if (instanceHandlers) {
handlers = handlers ? handlers.concat(instanceHandlers) : instanceHandlers;
}
instance = instance.parent;
}
if (!handlers) {
return null;
}
Expand Down Expand Up @@ -1076,6 +1127,9 @@ Unexpected.prototype.expect = function expect(subject, testDescriptionString) {
that.throwAssertionNotFoundError(subject, testDescriptionString, args);
}
}
if (assertionRule && assertionRule.unexpected && assertionRule.unexpected !== that) {
return assertionRule.unexpected.expect.apply(assertionRule.unexpected.expect, [subject, testDescriptionString].concat(args));
}

var flags = extend({}, assertionRule.flags);
var wrappedExpect = function (subject, testDescriptionString) {
Expand Down Expand Up @@ -1284,6 +1338,35 @@ Unexpected.prototype.clone = function () {
return makeExpectFunction(unexpected);
};

Unexpected.prototype.child = function () {
var childUnexpected = new Unexpected({
assertions: {},
types: [],
typeByName: {},
output: this.output.clone(),
format: this.outputFormat(),
installedPlugins: []
});
var parent = childUnexpected.parent = this;
var childExpect = makeExpectFunction(childUnexpected);
childExpect.exportAssertion = function (testDescription, handler) {
parent.addAssertion(testDescription, handler, childUnexpected);
return this;
};
childExpect.exportType = function (type) {
parent.addType(type, childUnexpected);
return this;
};
childExpect.exportStyle = function (name, handler) {
parent.addStyle(name, function () { // ...
var childOutput = childExpect.createOutput(this.format);
this.append(handler.apply(childOutput, arguments) || childOutput);
});
return this;
};
return childExpect;
};

Unexpected.prototype.outputFormat = function (format) {
if (typeof format === 'undefined') {
return this._outputFormat;
Expand Down

0 comments on commit 0bdf0b7

Please sign in to comment.