123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- var util = require("util");
- var assert = require("assert");
- var _ = require("underscore");
- var inspect = function(value) {
- return util.inspect(value, false, null);
- };
- exports.assertThat = function(value, matcher) {
- var result = matcher.matchesWithDescription(value);
- var message = "Expected " + matcher.describeSelf() +
- "\nbut " + result.description;
- assert.ok(result.matches, message);
- };
- exports.is = function(value) {
- if (value && value._isDuckMatcher) {
- return value;
- } else {
- return equalTo(value);
- }
- };
- var equalTo = exports.equalTo = function(matchValue) {
- return new Matcher({
- matches: function(value) {
- return _.isEqual(value, matchValue);
- },
- describeMismatch: function(value) {
- return "was " + inspect(value);
- },
- describeSelf: function() {
- return inspect(matchValue);
- }
- });
- };
- exports.isObject = function(object) {
- var matchers = valuesToMatchers(object);
-
- return new Matcher({
- matchesWithDescription: function(value) {
- var expectedKeys = ownKeys(object);
- var hasPropertiesResult = exports.hasProperties(matchers).matchesWithDescription(value);
-
- var unexpectedPropertyMismatches = ownKeys(value).filter(function(key) {
- return expectedKeys.indexOf(key) === -1
- }).map(function(key) {
- return "unexpected property: \"" + key + "\"";
- });
-
- var mismatchDescriptions =
- (hasPropertiesResult.matches ? [] : [hasPropertiesResult.description])
- .concat(unexpectedPropertyMismatches);
-
- if (mismatchDescriptions.length === 0) {
- return {matches: true};
- } else {
- return {matches: false, description: mismatchDescriptions.join("\n")};
- }
- },
- describeSelf: function() {
- return formatObjectOfMatchers(matchers);
- }
- });
- };
- exports.hasProperties = function(object) {
- var matchers = valuesToMatchers(object);
-
- return new Matcher({
- matchesWithDescription: function(value) {
- var expectedKeys = ownKeys(object);
- expectedKeys.sort(function(first, second) {
- if (first < second) {
- return -1;
- } else if (first > second) {
- return 1;
- } else {
- return 0;
- }
- });
- var propertyResults = expectedKeys.map(function(key) {
- var propertyMatcher = matchers[key];
- if (!objectHasOwnProperty(value, key)) {
- return {matches: false, description: util.format("missing property: \"%s\"", key)};
- } else if (!propertyMatcher.matches(value[key])) {
- var description = "value of property \"" + key + "\" didn't match:\n" +
- " " + indent(propertyMatcher.describeMismatch(value[key]), 1) + "\n" +
- " expected " + indent(propertyMatcher.describeSelf(), 1);
- return {matches: false, description: description};
- } else {
- return {matches: true};
- }
- });
-
- return combineMatchResults(propertyResults);
- },
- describeSelf: function() {
- return "object with properties " + formatObjectOfMatchers(matchers);
- }
- });
- };
- exports.isArray = function(expectedArray) {
- var elementMatchers = expectedArray.map(exports.is);
- return new Matcher({
- matchesWithDescription: function(value) {
- if (value.length !== elementMatchers.length) {
- return {matches: false, description: "was of length " + value.length};
- } else {
- var elementResults = _.zip(elementMatchers, value).map(function(values, index) {
- var expectedMatcher = values[0];
- var actual = values[1];
- if (expectedMatcher.matches(actual)) {
- return {matches: true};
- } else {
- var description = "element at index " + index + " didn't match:\n " + indent(expectedMatcher.describeMismatch(actual), 1)
- + "\n expected " + indent(expectedMatcher.describeSelf(), 1);
- return {matches: false, description: description};
- }
- });
-
- return combineMatchResults(elementResults);
- }
- },
- describeSelf: function() {
- return util.format("[%s]", _.invoke(elementMatchers, "describeSelf").join(", "));
- }
- });
- };
- var Matcher = function(matcher) {
- this._matcher = matcher;
- this._isDuckMatcher = true;
- };
- Matcher.prototype.matches = function(value) {
- if (this._matcher.matches) {
- return this._matcher.matches(value);
- } else {
- return this._matcher.matchesWithDescription(value).matches;
- }
- };
- Matcher.prototype.describeMismatch = function(value) {
- if (this._matcher.describeMismatch) {
- return this._matcher.describeMismatch(value);
- } else {
- return this._matcher.matchesWithDescription(value).description;
- }
- };
- Matcher.prototype.matchesWithDescription = function(value) {
- if (this._matcher.matchesWithDescription) {
- var result = this._matcher.matchesWithDescription(value);
- if (result.matches) {
- return {
- matches: true,
- description: ""
- };
- } else {
- return result;
- }
- } else {
- var isMatch = this.matches(value);
- return {
- matches: isMatch,
- description: isMatch ? "" : this.describeMismatch(value)
- };
- }
- };
- Matcher.prototype.describeSelf = function() {
- return this._matcher.describeSelf();
- };
- var combineMatchResults = function(results) {
- var mismatches = results.filter(function(result) {
- return !result.matches;
- });
- return combineMismatchs(mismatches);
- };
- var combineMismatchs = function(mismatches) {
- if (mismatches.length === 0) {
- return {matches: true};
- } else {
- var mismatchDescriptions = mismatches.map(function(mismatch) {
- return mismatch.description;
- });
- return {matches: false, description: mismatchDescriptions.join("\n")};
- }
- };
- var ownKeys = function(obj) {
- var keys = [];
- for (var key in obj) {
- if (objectHasOwnProperty(obj, key)) {
- keys.push(key);
- }
- }
- return keys;
- };
- var objectHasOwnProperty = function(obj, key) {
- return Object.prototype.hasOwnProperty.call(obj, key);
- };
- var objectMap = function(obj, func) {
- var matchers = {};
- _.forEach(obj, function(value, key) {
- if (_.has(obj, key)) {
- matchers[key] = func(value, key);
- }
- });
- return matchers;
-
- };
- var valuesToMatchers = function(obj) {
- return objectMap(obj, exports.is);
- };
- var formatObject = function(obj) {
- if (_.size(obj) === 0) {
- return "{}";
- } else {
- return util.format("{%s\n}", formatProperties(obj));
- }
- };
- var formatProperties = function(obj) {
- var properties = _.map(obj, function(value, key) {
- return {key: key, value: value};
- });
- var sortedProperties = _.sortBy(properties, function(property) {
- return property.key;
- });
- return "\n " + sortedProperties.map(function(property) {
- return indent(property.key + ": " + property.value, 1);
- }).join(",\n ");
- };
- var formatObjectOfMatchers = function(matchers) {
- return formatObject(objectMap(matchers, function(matcher) {
- return matcher.describeSelf();
- }));
- };
- var indent = function(str, indentationLevel) {
- var indentation = _.range(indentationLevel).map(function() {
- return " ";
- }).join("");
- return str.replace(/\n/g, "\n" + indentation);
- };
- exports.any = new Matcher({
- matchesWithDescription: function() {
- return {matches: true};
- },
- describeSelf: function() {
- return "<any>";
- }
- });
|