property.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. /*
  2. * Should
  3. * Copyright(c) 2010-2014 TJ Holowaychuk <tj@vision-media.ca>
  4. * MIT Licensed
  5. */
  6. var util = require('../util');
  7. var eql = require('should-equal');
  8. var aSlice = Array.prototype.slice;
  9. module.exports = function(should, Assertion) {
  10. var i = should.format;
  11. /**
  12. * Asserts given object has some descriptor. **On success it change given object to be value of property**.
  13. *
  14. * @name propertyWithDescriptor
  15. * @memberOf Assertion
  16. * @category assertion property
  17. * @param {string} name Name of property
  18. * @param {Object} desc Descriptor like used in Object.defineProperty (not required to add all properties)
  19. * @example
  20. *
  21. * ({ a: 10 }).should.have.propertyWithDescriptor('a', { enumerable: true });
  22. */
  23. Assertion.add('propertyWithDescriptor', function(name, desc) {
  24. this.params = {actual: this.obj, operator: 'to have own property with descriptor ' + i(desc)};
  25. var obj = this.obj;
  26. this.have.ownProperty(name);
  27. should(Object.getOwnPropertyDescriptor(Object(obj), name)).have.properties(desc);
  28. });
  29. function processPropsArgs() {
  30. var args = {};
  31. if(arguments.length > 1) {
  32. args.names = aSlice.call(arguments);
  33. } else {
  34. var arg = arguments[0];
  35. if(typeof arg === 'string') {
  36. args.names = [arg];
  37. } else if(util.isIndexable(arg)) {
  38. args.names = arg;
  39. } else {
  40. args.names = Object.keys(arg);
  41. args.values = arg;
  42. }
  43. }
  44. return args;
  45. }
  46. /**
  47. * Asserts given object has enumerable property with optionally value. **On success it change given object to be value of property**.
  48. *
  49. * @name enumerable
  50. * @memberOf Assertion
  51. * @category assertion property
  52. * @param {string} name Name of property
  53. * @param {*} [val] Optional property value to check
  54. * @example
  55. *
  56. * ({ a: 10 }).should.have.enumerable('a');
  57. */
  58. Assertion.add('enumerable', function(name, val) {
  59. name = util.convertPropertyName(name);
  60. this.params = {
  61. operator: "to have enumerable property " + util.formatProp(name) + (arguments.length > 1 ? " equal to " + i(val): "")
  62. };
  63. var desc = { enumerable: true };
  64. if(arguments.length > 1) desc.value = val;
  65. this.have.propertyWithDescriptor(name, desc);
  66. });
  67. /**
  68. * Asserts given object has enumerable properties
  69. *
  70. * @name enumerables
  71. * @memberOf Assertion
  72. * @category assertion property
  73. * @param {Array|...string|Object} names Names of property
  74. * @example
  75. *
  76. * ({ a: 10, b: 10 }).should.have.enumerables('a');
  77. */
  78. Assertion.add('enumerables', function(names) {
  79. var args = processPropsArgs.apply(null, arguments);
  80. this.params = {
  81. operator: "to have enumerables " + args.names.map(util.formatProp)
  82. };
  83. var obj = this.obj;
  84. args.names.forEach(function(name) {
  85. should(obj).have.enumerable(name);
  86. });
  87. });
  88. /**
  89. * Asserts given object has property with optionally value. **On success it change given object to be value of property**.
  90. *
  91. * @name property
  92. * @memberOf Assertion
  93. * @category assertion property
  94. * @param {string} name Name of property
  95. * @param {*} [val] Optional property value to check
  96. * @example
  97. *
  98. * ({ a: 10 }).should.have.property('a');
  99. */
  100. Assertion.add('property', function(name, val) {
  101. name = util.convertPropertyName(name);
  102. if(arguments.length > 1) {
  103. var p = {};
  104. p[name] = val;
  105. this.have.properties(p);
  106. } else {
  107. this.have.properties(name);
  108. }
  109. this.obj = this.obj[name];
  110. });
  111. /**
  112. * Asserts given object has properties. On this method affect .any modifier, which allow to check not all properties.
  113. *
  114. * @name properties
  115. * @memberOf Assertion
  116. * @category assertion property
  117. * @param {Array|...string|Object} names Names of property
  118. * @example
  119. *
  120. * ({ a: 10 }).should.have.properties('a');
  121. * ({ a: 10, b: 20 }).should.have.properties([ 'a' ]);
  122. * ({ a: 10, b: 20 }).should.have.properties({ b: 20 });
  123. */
  124. Assertion.add('properties', function(names) {
  125. var values = {};
  126. if(arguments.length > 1) {
  127. names = aSlice.call(arguments);
  128. } else if(!Array.isArray(names)) {
  129. if(typeof names == 'string' || typeof names == 'symbol') {
  130. names = [names];
  131. } else {
  132. values = names;
  133. names = Object.keys(names);
  134. }
  135. }
  136. var obj = Object(this.obj), missingProperties = [];
  137. //just enumerate properties and check if they all present
  138. names.forEach(function(name) {
  139. if(!(name in obj)) missingProperties.push(util.formatProp(name));
  140. });
  141. var props = missingProperties;
  142. if(props.length === 0) {
  143. props = names.map(util.formatProp);
  144. } else if(this.anyOne) {
  145. props = names.filter(function(name) {
  146. return missingProperties.indexOf(util.formatProp(name)) < 0;
  147. }).map(util.formatProp);
  148. }
  149. var operator = (props.length === 1 ?
  150. 'to have property ' : 'to have ' + (this.anyOne ? 'any of ' : '') + 'properties ') + props.join(', ');
  151. this.params = {obj: this.obj, operator: operator};
  152. //check that all properties presented
  153. //or if we request one of them that at least one them presented
  154. this.assert(missingProperties.length === 0 || (this.anyOne && missingProperties.length != names.length));
  155. // check if values in object matched expected
  156. var valueCheckNames = Object.keys(values);
  157. if(valueCheckNames.length) {
  158. var wrongValues = [];
  159. props = [];
  160. // now check values, as there we have all properties
  161. valueCheckNames.forEach(function(name) {
  162. var value = values[name];
  163. if(!eql(obj[name], value).result) {
  164. wrongValues.push(util.formatProp(name) + ' of ' + i(value) + ' (got ' + i(obj[name]) + ')');
  165. } else {
  166. props.push(util.formatProp(name) + ' of ' + i(value));
  167. }
  168. });
  169. if((wrongValues.length !== 0 && !this.anyOne) || (this.anyOne && props.length === 0)) {
  170. props = wrongValues;
  171. }
  172. operator = (props.length === 1 ?
  173. 'to have property ' : 'to have ' + (this.anyOne ? 'any of ' : '') + 'properties ') + props.join(', ');
  174. this.params = {obj: this.obj, operator: operator};
  175. //if there is no not matched values
  176. //or there is at least one matched
  177. this.assert(wrongValues.length === 0 || (this.anyOne && wrongValues.length != valueCheckNames.length));
  178. }
  179. });
  180. /**
  181. * Asserts given object has property `length` with given value `n`
  182. *
  183. * @name length
  184. * @alias Assertion#lengthOf
  185. * @memberOf Assertion
  186. * @category assertion property
  187. * @param {number} n Expected length
  188. * @param {string} [description] Optional message
  189. * @example
  190. *
  191. * [1, 2].should.have.length(2);
  192. */
  193. Assertion.add('length', function(n, description) {
  194. this.have.property('length', n, description);
  195. });
  196. Assertion.alias('length', 'lengthOf');
  197. var hasOwnProperty = Object.prototype.hasOwnProperty;
  198. /**
  199. * Asserts given object has own property. **On success it change given object to be value of property**.
  200. *
  201. * @name ownProperty
  202. * @alias Assertion#hasOwnProperty
  203. * @memberOf Assertion
  204. * @category assertion property
  205. * @param {string} name Name of property
  206. * @param {string} [description] Optional message
  207. * @example
  208. *
  209. * ({ a: 10 }).should.have.ownProperty('a');
  210. */
  211. Assertion.add('ownProperty', function(name, description) {
  212. name = util.convertPropertyName(name);
  213. this.params = {
  214. actual: this.obj,
  215. operator: 'to have own property ' + util.formatProp(name),
  216. message: description
  217. };
  218. this.assert(hasOwnProperty.call(this.obj, name));
  219. this.obj = this.obj[name];
  220. });
  221. Assertion.alias('ownProperty', 'hasOwnProperty');
  222. /**
  223. * Asserts given object is empty. For strings, arrays and arguments it checks .length property, for objects it checks keys.
  224. *
  225. * @name empty
  226. * @memberOf Assertion
  227. * @category assertion property
  228. * @example
  229. *
  230. * ''.should.be.empty();
  231. * [].should.be.empty();
  232. * ({}).should.be.empty();
  233. */
  234. Assertion.add('empty', function() {
  235. this.params = {operator: 'to be empty'};
  236. if(util.length(this.obj) !== void 0) {
  237. should(this.obj).have.property('length', 0);
  238. } else {
  239. var obj = Object(this.obj); // wrap to reference for booleans and numbers
  240. for(var prop in obj) {
  241. should(this.obj).not.have.ownProperty(prop);
  242. }
  243. }
  244. }, true);
  245. /**
  246. * Asserts given object has exact keys. Compared to `properties`, `keys` does not accept Object as a argument.
  247. *
  248. * @name keys
  249. * @alias Assertion#key
  250. * @memberOf Assertion
  251. * @category assertion property
  252. * @param {Array|...string} [keys] Keys to check
  253. * @example
  254. *
  255. * ({ a: 10 }).should.have.keys('a');
  256. * ({ a: 10, b: 20 }).should.have.keys('a', 'b');
  257. * ({ a: 10, b: 20 }).should.have.keys([ 'a', 'b' ]);
  258. * ({}).should.have.keys();
  259. */
  260. Assertion.add('keys', function(keys) {
  261. if(arguments.length > 1) keys = aSlice.call(arguments);
  262. else if(arguments.length === 1 && typeof keys === 'string') keys = [keys];
  263. else if(arguments.length === 0) keys = [];
  264. keys = keys.map(String);
  265. var obj = Object(this.obj);
  266. // first check if some keys are missing
  267. var missingKeys = [];
  268. keys.forEach(function(key) {
  269. if(!hasOwnProperty.call(this.obj, key))
  270. missingKeys.push(util.formatProp(key));
  271. }, this);
  272. // second check for extra keys
  273. var extraKeys = [];
  274. Object.keys(obj).forEach(function(key) {
  275. if(keys.indexOf(key) < 0) {
  276. extraKeys.push(util.formatProp(key));
  277. }
  278. });
  279. var verb = keys.length === 0 ? 'to be empty' :
  280. 'to have ' + (keys.length === 1 ? 'key ' : 'keys ');
  281. this.params = {operator: verb + keys.map(util.formatProp).join(', ')};
  282. if(missingKeys.length > 0)
  283. this.params.operator += '\n\tmissing keys: ' + missingKeys.join(', ');
  284. if(extraKeys.length > 0)
  285. this.params.operator += '\n\textra keys: ' + extraKeys.join(', ');
  286. this.assert(missingKeys.length === 0 && extraKeys.length === 0);
  287. });
  288. Assertion.alias("keys", "key");
  289. /**
  290. * Asserts given object has nested property in depth by path. **On success it change given object to be value of final property**.
  291. *
  292. * @name propertyByPath
  293. * @memberOf Assertion
  294. * @category assertion property
  295. * @param {Array|...string} properties Properties path to search
  296. * @example
  297. *
  298. * ({ a: {b: 10}}).should.have.propertyByPath('a', 'b').eql(10);
  299. */
  300. Assertion.add('propertyByPath', function(properties) {
  301. if(arguments.length > 1) properties = aSlice.call(arguments);
  302. else if(arguments.length === 1 && typeof properties == 'string') properties = [properties];
  303. else if(arguments.length === 0) properties = [];
  304. var allProps = properties.map(util.formatProp);
  305. properties = properties.map(String);
  306. var obj = should(Object(this.obj));
  307. var foundProperties = [];
  308. var currentProperty;
  309. while(currentProperty = properties.shift()) {
  310. this.params = {operator: 'to have property by path ' + allProps.join(', ') + ' - failed on ' + util.formatProp(currentProperty)};
  311. obj = obj.have.property(currentProperty);
  312. foundProperties.push(currentProperty);
  313. }
  314. this.params = {obj: this.obj, operator: 'to have property by path ' + allProps.join(', ')};
  315. this.obj = obj.obj;
  316. });
  317. };