index.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. var util = require("util");
  2. var assert = require("assert");
  3. var _ = require("underscore");
  4. var inspect = function(value) {
  5. return util.inspect(value, false, null);
  6. };
  7. exports.assertThat = function(value, matcher) {
  8. var result = matcher.matchesWithDescription(value);
  9. var message = "Expected " + matcher.describeSelf() +
  10. "\nbut " + result.description;
  11. assert.ok(result.matches, message);
  12. };
  13. exports.is = function(value) {
  14. if (value && value._isDuckMatcher) {
  15. return value;
  16. } else {
  17. return equalTo(value);
  18. }
  19. };
  20. var equalTo = exports.equalTo = function(matchValue) {
  21. return new Matcher({
  22. matches: function(value) {
  23. return _.isEqual(value, matchValue);
  24. },
  25. describeMismatch: function(value) {
  26. return "was " + inspect(value);
  27. },
  28. describeSelf: function() {
  29. return inspect(matchValue);
  30. }
  31. });
  32. };
  33. exports.isObject = function(object) {
  34. var matchers = valuesToMatchers(object);
  35. return new Matcher({
  36. matchesWithDescription: function(value) {
  37. var expectedKeys = ownKeys(object);
  38. var hasPropertiesResult = exports.hasProperties(matchers).matchesWithDescription(value);
  39. var unexpectedPropertyMismatches = ownKeys(value).filter(function(key) {
  40. return expectedKeys.indexOf(key) === -1
  41. }).map(function(key) {
  42. return "unexpected property: \"" + key + "\"";
  43. });
  44. var mismatchDescriptions =
  45. (hasPropertiesResult.matches ? [] : [hasPropertiesResult.description])
  46. .concat(unexpectedPropertyMismatches);
  47. if (mismatchDescriptions.length === 0) {
  48. return {matches: true};
  49. } else {
  50. return {matches: false, description: mismatchDescriptions.join("\n")};
  51. }
  52. },
  53. describeSelf: function() {
  54. return formatObjectOfMatchers(matchers);
  55. }
  56. });
  57. };
  58. exports.hasProperties = function(object) {
  59. var matchers = valuesToMatchers(object);
  60. return new Matcher({
  61. matchesWithDescription: function(value) {
  62. var expectedKeys = ownKeys(object);
  63. expectedKeys.sort(function(first, second) {
  64. if (first < second) {
  65. return -1;
  66. } else if (first > second) {
  67. return 1;
  68. } else {
  69. return 0;
  70. }
  71. });
  72. var propertyResults = expectedKeys.map(function(key) {
  73. var propertyMatcher = matchers[key];
  74. if (!objectHasOwnProperty(value, key)) {
  75. return {matches: false, description: util.format("missing property: \"%s\"", key)};
  76. } else if (!propertyMatcher.matches(value[key])) {
  77. var description = "value of property \"" + key + "\" didn't match:\n" +
  78. " " + indent(propertyMatcher.describeMismatch(value[key]), 1) + "\n" +
  79. " expected " + indent(propertyMatcher.describeSelf(), 1);
  80. return {matches: false, description: description};
  81. } else {
  82. return {matches: true};
  83. }
  84. });
  85. return combineMatchResults(propertyResults);
  86. },
  87. describeSelf: function() {
  88. return "object with properties " + formatObjectOfMatchers(matchers);
  89. }
  90. });
  91. };
  92. exports.isArray = function(expectedArray) {
  93. var elementMatchers = expectedArray.map(exports.is);
  94. return new Matcher({
  95. matchesWithDescription: function(value) {
  96. if (value.length !== elementMatchers.length) {
  97. return {matches: false, description: "was of length " + value.length};
  98. } else {
  99. var elementResults = _.zip(elementMatchers, value).map(function(values, index) {
  100. var expectedMatcher = values[0];
  101. var actual = values[1];
  102. if (expectedMatcher.matches(actual)) {
  103. return {matches: true};
  104. } else {
  105. var description = "element at index " + index + " didn't match:\n " + indent(expectedMatcher.describeMismatch(actual), 1)
  106. + "\n expected " + indent(expectedMatcher.describeSelf(), 1);
  107. return {matches: false, description: description};
  108. }
  109. });
  110. return combineMatchResults(elementResults);
  111. }
  112. },
  113. describeSelf: function() {
  114. return util.format("[%s]", _.invoke(elementMatchers, "describeSelf").join(", "));
  115. }
  116. });
  117. };
  118. var Matcher = function(matcher) {
  119. this._matcher = matcher;
  120. this._isDuckMatcher = true;
  121. };
  122. Matcher.prototype.matches = function(value) {
  123. if (this._matcher.matches) {
  124. return this._matcher.matches(value);
  125. } else {
  126. return this._matcher.matchesWithDescription(value).matches;
  127. }
  128. };
  129. Matcher.prototype.describeMismatch = function(value) {
  130. if (this._matcher.describeMismatch) {
  131. return this._matcher.describeMismatch(value);
  132. } else {
  133. return this._matcher.matchesWithDescription(value).description;
  134. }
  135. };
  136. Matcher.prototype.matchesWithDescription = function(value) {
  137. if (this._matcher.matchesWithDescription) {
  138. var result = this._matcher.matchesWithDescription(value);
  139. if (result.matches) {
  140. return {
  141. matches: true,
  142. description: ""
  143. };
  144. } else {
  145. return result;
  146. }
  147. } else {
  148. var isMatch = this.matches(value);
  149. return {
  150. matches: isMatch,
  151. description: isMatch ? "" : this.describeMismatch(value)
  152. };
  153. }
  154. };
  155. Matcher.prototype.describeSelf = function() {
  156. return this._matcher.describeSelf();
  157. };
  158. var combineMatchResults = function(results) {
  159. var mismatches = results.filter(function(result) {
  160. return !result.matches;
  161. });
  162. return combineMismatchs(mismatches);
  163. };
  164. var combineMismatchs = function(mismatches) {
  165. if (mismatches.length === 0) {
  166. return {matches: true};
  167. } else {
  168. var mismatchDescriptions = mismatches.map(function(mismatch) {
  169. return mismatch.description;
  170. });
  171. return {matches: false, description: mismatchDescriptions.join("\n")};
  172. }
  173. };
  174. var ownKeys = function(obj) {
  175. var keys = [];
  176. for (var key in obj) {
  177. if (objectHasOwnProperty(obj, key)) {
  178. keys.push(key);
  179. }
  180. }
  181. return keys;
  182. };
  183. var objectHasOwnProperty = function(obj, key) {
  184. return Object.prototype.hasOwnProperty.call(obj, key);
  185. };
  186. var objectMap = function(obj, func) {
  187. var matchers = {};
  188. _.forEach(obj, function(value, key) {
  189. if (_.has(obj, key)) {
  190. matchers[key] = func(value, key);
  191. }
  192. });
  193. return matchers;
  194. };
  195. var valuesToMatchers = function(obj) {
  196. return objectMap(obj, exports.is);
  197. };
  198. var formatObject = function(obj) {
  199. if (_.size(obj) === 0) {
  200. return "{}";
  201. } else {
  202. return util.format("{%s\n}", formatProperties(obj));
  203. }
  204. };
  205. var formatProperties = function(obj) {
  206. var properties = _.map(obj, function(value, key) {
  207. return {key: key, value: value};
  208. });
  209. var sortedProperties = _.sortBy(properties, function(property) {
  210. return property.key;
  211. });
  212. return "\n " + sortedProperties.map(function(property) {
  213. return indent(property.key + ": " + property.value, 1);
  214. }).join(",\n ");
  215. };
  216. var formatObjectOfMatchers = function(matchers) {
  217. return formatObject(objectMap(matchers, function(matcher) {
  218. return matcher.describeSelf();
  219. }));
  220. };
  221. var indent = function(str, indentationLevel) {
  222. var indentation = _.range(indentationLevel).map(function() {
  223. return " ";
  224. }).join("");
  225. return str.replace(/\n/g, "\n" + indentation);
  226. };
  227. exports.any = new Matcher({
  228. matchesWithDescription: function() {
  229. return {matches: true};
  230. },
  231. describeSelf: function() {
  232. return "<any>";
  233. }
  234. });