matcher.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. "use strict";
  2. var arrayProto = require("@sinonjs/commons").prototypes.array;
  3. var deepEqual = require("./deep-equal").use(match); // eslint-disable-line no-use-before-define
  4. var every = require("@sinonjs/commons").every;
  5. var functionName = require("@sinonjs/commons").functionName;
  6. var get = require("lodash").get;
  7. var iterableToString = require("./iterable-to-string");
  8. var objectProto = require("@sinonjs/commons").prototypes.object;
  9. var stringProto = require("@sinonjs/commons").prototypes.string;
  10. var typeOf = require("@sinonjs/commons").typeOf;
  11. var valueToString = require("@sinonjs/commons").valueToString;
  12. var arrayIndexOf = arrayProto.indexOf;
  13. var arrayEvery = arrayProto.every;
  14. var join = arrayProto.join;
  15. var map = arrayProto.map;
  16. var some = arrayProto.some;
  17. var hasOwnProperty = objectProto.hasOwnProperty;
  18. var isPrototypeOf = objectProto.isPrototypeOf;
  19. var stringIndexOf = stringProto.indexOf;
  20. function assertType(value, type, name) {
  21. var actual = typeOf(value);
  22. if (actual !== type) {
  23. throw new TypeError(
  24. "Expected type of " +
  25. name +
  26. " to be " +
  27. type +
  28. ", but was " +
  29. actual
  30. );
  31. }
  32. }
  33. function assertMethodExists(value, method, name, methodPath) {
  34. if (value[method] == null) {
  35. throw new TypeError(
  36. "Expected " + name + " to have method " + methodPath
  37. );
  38. }
  39. }
  40. var matcher = {
  41. toString: function() {
  42. return this.message;
  43. }
  44. };
  45. function isMatcher(object) {
  46. return isPrototypeOf(matcher, object);
  47. }
  48. function matchObject(actual, expectation) {
  49. if (actual === null || actual === undefined) {
  50. return false;
  51. }
  52. return arrayEvery(Object.keys(expectation), function(key) {
  53. var exp = expectation[key];
  54. var act = actual[key];
  55. if (isMatcher(exp)) {
  56. if (!exp.test(act)) {
  57. return false;
  58. }
  59. } else if (typeOf(exp) === "object") {
  60. if (!matchObject(act, exp)) {
  61. return false;
  62. }
  63. } else if (!deepEqual(act, exp)) {
  64. return false;
  65. }
  66. return true;
  67. });
  68. }
  69. var TYPE_MAP = {
  70. function: function(m, expectation, message) {
  71. m.test = expectation;
  72. m.message = message || "match(" + functionName(expectation) + ")";
  73. },
  74. number: function(m, expectation) {
  75. m.test = function(actual) {
  76. // we need type coercion here
  77. return expectation == actual; // eslint-disable-line eqeqeq
  78. };
  79. },
  80. object: function(m, expectation) {
  81. var array = [];
  82. if (typeof expectation.test === "function") {
  83. m.test = function(actual) {
  84. return expectation.test(actual) === true;
  85. };
  86. m.message = "match(" + functionName(expectation.test) + ")";
  87. return m;
  88. }
  89. array = map(Object.keys(expectation), function(key) {
  90. return key + ": " + valueToString(expectation[key]);
  91. });
  92. m.test = function(actual) {
  93. return matchObject(actual, expectation);
  94. };
  95. m.message = "match(" + join(array, ", ") + ")";
  96. return m;
  97. },
  98. regexp: function(m, expectation) {
  99. m.test = function(actual) {
  100. return typeof actual === "string" && expectation.test(actual);
  101. };
  102. },
  103. string: function(m, expectation) {
  104. m.test = function(actual) {
  105. return (
  106. typeof actual === "string" &&
  107. stringIndexOf(actual, expectation) !== -1
  108. );
  109. };
  110. m.message = 'match("' + expectation + '")';
  111. }
  112. };
  113. function match(expectation, message) {
  114. var m = Object.create(matcher);
  115. var type = typeOf(expectation);
  116. if (type in TYPE_MAP) {
  117. TYPE_MAP[type](m, expectation, message);
  118. } else {
  119. m.test = function(actual) {
  120. return deepEqual(actual, expectation);
  121. };
  122. }
  123. if (!m.message) {
  124. m.message = "match(" + valueToString(expectation) + ")";
  125. }
  126. return m;
  127. }
  128. matcher.or = function(m2) {
  129. if (!arguments.length) {
  130. throw new TypeError("Matcher expected");
  131. } else if (!isMatcher(m2)) {
  132. m2 = match(m2);
  133. }
  134. var m1 = this;
  135. var or = Object.create(matcher);
  136. or.test = function(actual) {
  137. return m1.test(actual) || m2.test(actual);
  138. };
  139. or.message = m1.message + ".or(" + m2.message + ")";
  140. return or;
  141. };
  142. matcher.and = function(m2) {
  143. if (!arguments.length) {
  144. throw new TypeError("Matcher expected");
  145. } else if (!isMatcher(m2)) {
  146. m2 = match(m2);
  147. }
  148. var m1 = this;
  149. var and = Object.create(matcher);
  150. and.test = function(actual) {
  151. return m1.test(actual) && m2.test(actual);
  152. };
  153. and.message = m1.message + ".and(" + m2.message + ")";
  154. return and;
  155. };
  156. match.isMatcher = isMatcher;
  157. match.any = match(function() {
  158. return true;
  159. }, "any");
  160. match.defined = match(function(actual) {
  161. return actual !== null && actual !== undefined;
  162. }, "defined");
  163. match.truthy = match(function(actual) {
  164. return !!actual;
  165. }, "truthy");
  166. match.falsy = match(function(actual) {
  167. return !actual;
  168. }, "falsy");
  169. match.same = function(expectation) {
  170. return match(function(actual) {
  171. return expectation === actual;
  172. }, "same(" + valueToString(expectation) + ")");
  173. };
  174. match.in = function(arrayOfExpectations) {
  175. if (typeOf(arrayOfExpectations) !== "array") {
  176. throw new TypeError("array expected");
  177. }
  178. return match(function(actual) {
  179. return some(arrayOfExpectations, function(expectation) {
  180. return expectation === actual;
  181. });
  182. }, "in(" + valueToString(arrayOfExpectations) + ")");
  183. };
  184. match.typeOf = function(type) {
  185. assertType(type, "string", "type");
  186. return match(function(actual) {
  187. return typeOf(actual) === type;
  188. }, 'typeOf("' + type + '")');
  189. };
  190. match.instanceOf = function(type) {
  191. if (
  192. typeof Symbol === "undefined" ||
  193. typeof Symbol.hasInstance === "undefined"
  194. ) {
  195. assertType(type, "function", "type");
  196. } else {
  197. assertMethodExists(
  198. type,
  199. Symbol.hasInstance,
  200. "type",
  201. "[Symbol.hasInstance]"
  202. );
  203. }
  204. return match(function(actual) {
  205. return actual instanceof type;
  206. }, "instanceOf(" +
  207. (functionName(type) || Object.prototype.toString.call(type)) +
  208. ")");
  209. };
  210. function createPropertyMatcher(propertyTest, messagePrefix) {
  211. return function(property, value) {
  212. assertType(property, "string", "property");
  213. var onlyProperty = arguments.length === 1;
  214. var message = messagePrefix + '("' + property + '"';
  215. if (!onlyProperty) {
  216. message += ", " + valueToString(value);
  217. }
  218. message += ")";
  219. return match(function(actual) {
  220. if (
  221. actual === undefined ||
  222. actual === null ||
  223. !propertyTest(actual, property)
  224. ) {
  225. return false;
  226. }
  227. return onlyProperty || deepEqual(actual[property], value);
  228. }, message);
  229. };
  230. }
  231. match.has = createPropertyMatcher(function(actual, property) {
  232. if (typeof actual === "object") {
  233. return property in actual;
  234. }
  235. return actual[property] !== undefined;
  236. }, "has");
  237. match.hasOwn = createPropertyMatcher(function(actual, property) {
  238. return hasOwnProperty(actual, property);
  239. }, "hasOwn");
  240. match.hasNested = function(property, value) {
  241. assertType(property, "string", "property");
  242. var onlyProperty = arguments.length === 1;
  243. var message = 'hasNested("' + property + '"';
  244. if (!onlyProperty) {
  245. message += ", " + valueToString(value);
  246. }
  247. message += ")";
  248. return match(function(actual) {
  249. if (
  250. actual === undefined ||
  251. actual === null ||
  252. get(actual, property) === undefined
  253. ) {
  254. return false;
  255. }
  256. return onlyProperty || deepEqual(get(actual, property), value);
  257. }, message);
  258. };
  259. match.every = function(predicate) {
  260. if (!isMatcher(predicate)) {
  261. throw new TypeError("Matcher expected");
  262. }
  263. return match(function(actual) {
  264. if (typeOf(actual) === "object") {
  265. return every(Object.keys(actual), function(key) {
  266. return predicate.test(actual[key]);
  267. });
  268. }
  269. return (
  270. !!actual &&
  271. typeOf(actual.forEach) === "function" &&
  272. every(actual, function(element) {
  273. return predicate.test(element);
  274. })
  275. );
  276. }, "every(" + predicate.message + ")");
  277. };
  278. match.some = function(predicate) {
  279. if (!isMatcher(predicate)) {
  280. throw new TypeError("Matcher expected");
  281. }
  282. return match(function(actual) {
  283. if (typeOf(actual) === "object") {
  284. return !every(Object.keys(actual), function(key) {
  285. return !predicate.test(actual[key]);
  286. });
  287. }
  288. return (
  289. !!actual &&
  290. typeOf(actual.forEach) === "function" &&
  291. !every(actual, function(element) {
  292. return !predicate.test(element);
  293. })
  294. );
  295. }, "some(" + predicate.message + ")");
  296. };
  297. match.array = match.typeOf("array");
  298. match.array.deepEquals = function(expectation) {
  299. return match(function(actual) {
  300. // Comparing lengths is the fastest way to spot a difference before iterating through every item
  301. var sameLength = actual.length === expectation.length;
  302. return (
  303. typeOf(actual) === "array" &&
  304. sameLength &&
  305. every(actual, function(element, index) {
  306. var expected = expectation[index];
  307. return typeOf(expected) === "array" &&
  308. typeOf(element) === "array"
  309. ? match.array.deepEquals(expected).test(element)
  310. : deepEqual(expected, element);
  311. })
  312. );
  313. }, "deepEquals([" + iterableToString(expectation) + "])");
  314. };
  315. match.array.startsWith = function(expectation) {
  316. return match(function(actual) {
  317. return (
  318. typeOf(actual) === "array" &&
  319. every(expectation, function(expectedElement, index) {
  320. return actual[index] === expectedElement;
  321. })
  322. );
  323. }, "startsWith([" + iterableToString(expectation) + "])");
  324. };
  325. match.array.endsWith = function(expectation) {
  326. return match(function(actual) {
  327. // This indicates the index in which we should start matching
  328. var offset = actual.length - expectation.length;
  329. return (
  330. typeOf(actual) === "array" &&
  331. every(expectation, function(expectedElement, index) {
  332. return actual[offset + index] === expectedElement;
  333. })
  334. );
  335. }, "endsWith([" + iterableToString(expectation) + "])");
  336. };
  337. match.array.contains = function(expectation) {
  338. return match(function(actual) {
  339. return (
  340. typeOf(actual) === "array" &&
  341. every(expectation, function(expectedElement) {
  342. return arrayIndexOf(actual, expectedElement) !== -1;
  343. })
  344. );
  345. }, "contains([" + iterableToString(expectation) + "])");
  346. };
  347. match.map = match.typeOf("map");
  348. match.map.deepEquals = function mapDeepEquals(expectation) {
  349. return match(function(actual) {
  350. // Comparing lengths is the fastest way to spot a difference before iterating through every item
  351. var sameLength = actual.size === expectation.size;
  352. return (
  353. typeOf(actual) === "map" &&
  354. sameLength &&
  355. every(actual, function(element, key) {
  356. return expectation.has(key) && expectation.get(key) === element;
  357. })
  358. );
  359. }, "deepEquals(Map[" + iterableToString(expectation) + "])");
  360. };
  361. match.map.contains = function mapContains(expectation) {
  362. return match(function(actual) {
  363. return (
  364. typeOf(actual) === "map" &&
  365. every(expectation, function(element, key) {
  366. return actual.has(key) && actual.get(key) === element;
  367. })
  368. );
  369. }, "contains(Map[" + iterableToString(expectation) + "])");
  370. };
  371. match.set = match.typeOf("set");
  372. match.set.deepEquals = function setDeepEquals(expectation) {
  373. return match(function(actual) {
  374. // Comparing lengths is the fastest way to spot a difference before iterating through every item
  375. var sameLength = actual.size === expectation.size;
  376. return (
  377. typeOf(actual) === "set" &&
  378. sameLength &&
  379. every(actual, function(element) {
  380. return expectation.has(element);
  381. })
  382. );
  383. }, "deepEquals(Set[" + iterableToString(expectation) + "])");
  384. };
  385. match.set.contains = function setContains(expectation) {
  386. return match(function(actual) {
  387. return (
  388. typeOf(actual) === "set" &&
  389. every(expectation, function(element) {
  390. return actual.has(element);
  391. })
  392. );
  393. }, "contains(Set[" + iterableToString(expectation) + "])");
  394. };
  395. match.bool = match.typeOf("boolean");
  396. match.number = match.typeOf("number");
  397. match.string = match.typeOf("string");
  398. match.object = match.typeOf("object");
  399. match.func = match.typeOf("function");
  400. match.regexp = match.typeOf("regexp");
  401. match.date = match.typeOf("date");
  402. match.symbol = match.typeOf("symbol");
  403. module.exports = match;