UNPKG

18.5 kBJavaScriptView Raw
1/*
2 * Copyright (c) 2013, Groupon, Inc.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 * Redistributions of source code must retain the above copyright notice,
10 * this list of conditions and the following disclaimer.
11 *
12 * Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 *
16 * Neither the name of GROUPON nor the names of its contributors may be
17 * used to endorse or promote products derived from this software without
18 * specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
21 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
23 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
26 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32
33'use strict';
34
35var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
36
37var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
38
39function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
40
41function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
42
43var assert = void 0;
44
45var isEqual = require('lodash.isequal');
46
47var toString = Object.prototype.toString;
48
49var green = function green(x) {
50 return '\x1B[32m' + x + '\x1B[39m';
51};
52var red = function red(x) {
53 return '\x1B[31m' + x + '\x1B[39m';
54};
55var clear = '\x1b[39;49;00m';
56
57if (!((typeof process === 'undefined' ? 'undefined' : _typeof(process)) === 'object' && process.stdout && process.stdout.isTTY)) {
58 red = function red(x) {
59 return '' + x;
60 };
61 green = red;
62 clear = '';
63}
64
65// simplified isFoo versions to remove lodash/underscore dependency
66var is = ['Array', 'RegExp', 'String', 'Number', 'Date', 'Function'].reduce(function (o, t) {
67 return _extends({}, o, _defineProperty({}, t, function (obj) {
68 return toString.call(obj) === '[object ' + t + ']';
69 }));
70}, {
71 NaN: function NaN(obj) {
72 return is.Number(obj) && isNaN(obj);
73 },
74 Object: function Object(obj) {
75 var t = typeof obj === 'undefined' ? 'undefined' : _typeof(obj);
76 return t === 'function' || t === 'object' && !!obj;
77 },
78 Boolean: function Boolean(obj) {
79 return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
80 },
81 Undefined: function Undefined(obj) {
82 return obj === undefined;
83 },
84 Null: function Null(obj) {
85 return obj === null;
86 }
87});
88if (Array.isArray) is.Array = Array.isArray;
89
90function error(message, explanation, errProps) {
91 if (explanation != null) {
92 message = 'Assertion failed: ' + explanation + '\n' + clear + message;
93 }
94 var err = new Error(message);
95 if (errProps) {
96 Object.keys(errProps).forEach(function (prop) {
97 err[prop] = errProps[prop];
98 });
99 }
100 return err;
101}
102
103function nameNegative(name) {
104 if (name === 'truthy') {
105 return 'falsey';
106 }
107 if (name === 'resolves') {
108 return 'rejects';
109 }
110 return 'not' + name.charAt().toUpperCase() + name.slice(1);
111}
112
113function asRegExp(re) {
114 var flags = '';
115 if (re.global) flags += 'g';
116 if (re.multiline) flags += 'm';
117 if (re.ignoreCase) flags += 'i';
118 return '/' + re.source + '/' + flags;
119}
120
121function stringifyReplacer(key, val) {
122 if (typeof val === 'function') return toString.call(val);
123 if (is.RegExp(val)) return asRegExp(val);
124 if (is.Object(val) && !is.Array(val)) {
125 return Object.keys(val).sort().reduce(function (o, p) {
126 return _extends({}, o, _defineProperty({}, p, val[p]));
127 }, {});
128 }
129 return val;
130}
131
132function stringify(x) {
133 if (x == null) return '' + x;
134 if (is.NaN(x)) return 'NaN';
135 if (is.RegExp(x)) return asRegExp(x);
136 if ((typeof x === 'undefined' ? 'undefined' : _typeof(x)) === 'symbol') return x.toString();
137 var json = JSON.stringify(x, stringifyReplacer, 2);
138 var className = x && x.constructor && x.constructor.name;
139 if ((typeof x === 'undefined' ? 'undefined' : _typeof(x)) !== 'object' || className === 'Object' || className === 'Array') {
140 return json;
141 }
142
143 if (x instanceof Error || /Error/.test(className)) {
144 if (json === '{}') {
145 return x.stack;
146 }
147 return x.stack + '\nwith error metadata:\n' + json;
148 }
149 if (x.toString === toString) {
150 return className;
151 }
152 try {
153 return className + '[' + x + ']';
154 } catch (e) {
155 return className;
156 }
157}
158
159// assert that the function got `count` args (if an integer), one of the number
160// of args (if an array of legal counts), and if it was an array and the count
161// was equal to the last option (fully populated), that the first arg is a String
162// (that test's semantic explanation)
163function handleArgs(self, count, args, name, help) {
164 var negated = false;
165 if (is.String(self)) {
166 negated = true;
167 name = nameNegative(name);
168 }
169
170 var argc = args.length;
171 if (argc === count) return [name, negated];
172
173 var max = '';
174 if (is.Array(count) && count.indexOf(argc) !== -1) {
175 var n = count[count.length - 1];
176 if (argc !== n || is.String(args[0])) return [name, negated];
177 max = ',\nand when called with ' + n + ' args, the first arg must be a docstring';
178 }
179
180 var wantedArgCount = void 0;
181 if (is.Number(count)) {
182 wantedArgCount = count + ' argument';
183 } else {
184 wantedArgCount = count.slice(0, -1).join(', ');
185 count = count.pop();
186 wantedArgCount = wantedArgCount + ' or ' + count + ' argument';
187 }
188 if (count !== 1) wantedArgCount += 's';
189
190 var actualArgs = stringify([].slice.call(args)).slice(1, -1);
191
192 var functionSource = Function.prototype.toString.call(assert[name]);
193 var wantedArgNames = functionSource.match(/^function\s*[^(]*\s*\(([^)]*)/)[1];
194 if (max) {
195 wantedArgNames = 'explanation, ' + wantedArgNames;
196 }
197
198 var wanted = name + '(' + wantedArgNames + ')';
199 var actual = name + '(' + actualArgs + ')';
200 var message = green(wanted) + ' needs ' + (wantedArgCount + max) + '\nyour usage: ' + red(actual);
201
202 if (typeof help === 'function') {
203 help = help();
204 }
205 throw error(message, help);
206}
207
208function type(x) {
209 if (is.String(x)) return 'String';
210 if (is.Number(x)) return 'Number';
211 if (is.RegExp(x)) return 'RegExp';
212 if (is.Array(x)) return 'Array';
213 throw new TypeError('unsupported type: ' + x);
214}
215
216function abbreviate(name, value, threshold) {
217 var str = stringify(value);
218 if (str.length <= (threshold || 1024)) return str;
219 var desc = 'length: ' + value.length;
220 if (is.Array(value)) desc += '; ' + str.length + ' JSON encoded';
221 if (name) name += ' ';
222 return '' + name + type(value) + '[' + desc + ']';
223}
224
225// translates any argument we were meant to interpret as a type, into its name
226function getNameOfType(x) {
227 switch (false) {
228 case !(x == null):
229 return '' + x; // null / undefined
230 case !is.String(x):
231 return x;
232 case !is.Function(x):
233 return x.name;
234 case !is.NaN(x):
235 return 'NaN';
236 default:
237 return x;
238 }
239}
240
241// listing the most specific types first lets us iterate in order and verify that
242// the expected type was the first match
243var types = ['null', 'Date', 'Array', 'String', 'RegExp', 'Boolean', 'Function', 'Object', 'NaN', 'Number', 'undefined'];
244
245function implodeNicely(list, conjunction) {
246 var first = list.slice(0, -1).join(', ');
247 var last = list[list.length - 1];
248 return first + ' ' + (conjunction || 'and') + ' ' + last;
249}
250
251function isType(value, typeName) {
252 if (typeName === 'Date') return is.Date(value) && !is.NaN(+value);
253 return is['' + typeName[0].toUpperCase() + typeName.slice(1)](value);
254}
255
256// gets the name of the type that value is an incarnation of
257function getTypeName(value) {
258 return types.filter(isType.bind(null, value))[0];
259}
260
261var assertSync = {
262 truthy: function truthy(bool) {
263 var args = handleArgs(this, [1, 2], arguments, 'truthy');
264 var name = args[0];
265 var negated = args[1];
266 var explanation = void 0;
267 if (arguments.length === 2) {
268 explanation = arguments[0];
269 bool = arguments[1];
270 }
271 if (!bool && !negated || bool && negated) {
272 throw error('Expected ' + red(stringify(bool)) + ' to be ' + name, explanation);
273 }
274 },
275 expect: function expect(bool) {
276 var explanation = void 0;
277 if (arguments.length === 2) {
278 explanation = arguments[0];
279 bool = arguments[1];
280 }
281 if (explanation) return assertSync.equal(explanation, true, bool);
282 return assertSync.equal(true, bool);
283 },
284 equal: function equal(expected, actual) {
285 var explanation = void 0;
286 var negated = handleArgs(this, [2, 3], arguments, 'equal')[1];
287 if (arguments.length === 3) {
288 explanation = arguments[0];
289 expected = arguments[1];
290 actual = arguments[2];
291 }
292 if (negated) {
293 if (expected === actual) {
294 throw error('notEqual assertion expected ' + red(stringify(actual)) + ' to be exactly anything else', explanation);
295 }
296 } else if (expected !== actual) {
297 throw error('Expected: ' + green(stringify(expected)) + '\nActually: ' + ('' + red(stringify(actual))), explanation, { actual: actual, expected: expected });
298 }
299 },
300 deepEqual: function deepEqual(expected, actual) {
301 var explanation = void 0;
302 var negated = handleArgs(this, [2, 3], arguments, 'deepEqual')[1];
303 if (arguments.length === 3) {
304 explanation = arguments[0];
305 expected = arguments[1];
306 actual = arguments[2];
307 }
308 var isEq = isEqual(expected, actual);
309 if (isEq && !negated || !isEq && negated) return;
310
311 var wrongLooks = stringify(actual);
312 if (negated) {
313 throw error('notDeepEqual assertion expected exactly anything else but\n' + red(wrongLooks), explanation);
314 }
315
316 var rightLooks = stringify(expected);
317 var message = void 0;
318 if (wrongLooks === rightLooks) {
319 message = 'deepEqual ' + green(rightLooks) + ' failed on something that\n' + 'serializes to the same result (likely some function)';
320 } else {
321 message = 'Expected: ' + wrongLooks + ' to deepEqual ' + rightLooks;
322 }
323
324 throw error(message, explanation, { expected: expected, actual: actual });
325 },
326 include: function include(needle, haystack) {
327 var args = handleArgs(this, [2, 3], arguments, 'include');
328 var name = args[0];
329 var negated = args[1];
330 var explanation = void 0;
331 if (arguments.length === 3) {
332 explanation = arguments[0];
333 needle = arguments[1];
334 haystack = arguments[2];
335 }
336 if (is.String(haystack)) {
337 if (needle === '') {
338 var what = negated ? 'always-failing test' : 'no-op test';
339 throw error(what + ' detected: all strings contain the empty string!');
340 }
341 if (!is.String(needle) && !is.Number(needle) && !is.RegExp(needle)) {
342 var problem = 'needs a RegExp/String/Number needle for a String haystack';
343 throw new TypeError(name + ' ' + problem + '; you used:\n' + (name + ' ' + green(stringify(haystack)) + ', ' + red(stringify(needle))));
344 }
345 } else if (!is.Array(haystack)) {
346 needle = stringify(needle);
347 throw new TypeError(name + ' takes a String or Array haystack; you used:\n' + name + ' ' + red(stringify(haystack)) + ', ' + needle);
348 }
349
350 var contained = is.String(haystack) && is.RegExp(needle) ? haystack.match(needle) : haystack.indexOf(needle) > -1;
351
352 if (negated) {
353 if (contained) {
354 var message = '' + ('notInclude expected needle not to be found in ' + ('haystack\n- needle: ' + stringify(needle) + '\n haystack: ')) + abbreviate('', haystack);
355 if (is.String(haystack) && is.RegExp(needle)) {
356 message += ', but found:\n';
357 if (needle.global) {
358 message += contained.map(function (s) {
359 return '* ' + red(stringify(s));
360 }).join('\n');
361 } else {
362 message += '* ' + red(stringify(contained[0]));
363 }
364 }
365 throw error(message, explanation);
366 }
367 } else if (!contained) {
368 throw error(name + ' expected needle to be found in haystack\n' + ('- needle: ' + stringify(needle) + '\n') + ('haystack: ' + abbreviate('', haystack)), explanation);
369 }
370 },
371 match: function match(regexp, string) {
372 var args = handleArgs(this, [2, 3], arguments, 'match');
373 var name = args[0];
374 var negated = args[1];
375 var explanation = void 0;
376 if (arguments.length === 3) {
377 explanation = arguments[0];
378 regexp = arguments[1];
379 string = arguments[2];
380 }
381
382 var re = is.RegExp(regexp);
383 if (!re || !is.String(string)) {
384 string = abbreviate('string', string);
385 var oops = re ? 'string arg is not a String' : 'regexp arg is not a RegExp';
386 var called = name + ' ' + stringify(regexp) + ', ' + red(string);
387 throw new TypeError(name + ': ' + oops + '; you used:\n' + called);
388 }
389
390 var matched = regexp.test(string);
391 if (negated) {
392 if (!matched) return;
393 var message = 'Expected: ' + stringify(regexp) + '\nnot to match: ' + ('' + red(abbreviate('string', string)));
394 if (regexp.global) {
395 var count = string.match(regexp).length;
396 message += '\nMatches: ' + red(count);
397 }
398 throw error(message, explanation);
399 }
400 if (!matched) {
401 throw error('Expected: ' + stringify(regexp) + '\nto match: ' + ('' + red(abbreviate('string', string))), explanation);
402 }
403 },
404 throws: function throws(fn) {
405 var args = handleArgs(this, [1, 2], arguments, 'throws');
406 var name = args[0];
407 var negated = args[1];
408 var explanation = void 0;
409 if (arguments.length === 2) {
410 explanation = arguments[0];
411 fn = arguments[1];
412 }
413 if (typeof explanation === 'function') {
414 fn = explanation;
415 explanation = undefined;
416 }
417 if (typeof fn !== 'function') {
418 throw error(name + ' expects ' + green('a function') + ' but got ' + red(fn));
419 }
420
421 try {
422 fn();
423 } catch (err) {
424 if (negated) {
425 throw error('Threw an exception despite ' + name + ' assertion:\n' + ('' + err.message), explanation);
426 }
427 return err;
428 }
429
430 if (negated) return undefined;
431 throw error("Didn't throw an exception as expected to", explanation);
432 },
433 hasType: function hasType(expectedType, value) {
434 var args = handleArgs(this, [2, 3], arguments, 'hasType');
435 var name = args[0];
436 var negated = args[1];
437 var explanation = void 0;
438 if (arguments.length === 3) {
439 explanation = arguments[0];
440 expectedType = arguments[1];
441 value = arguments[2];
442 }
443
444 var stringType = getNameOfType(expectedType);
445 if (types.indexOf(stringType) === -1) {
446 var badArg = stringify(expectedType);
447 var suggestions = implodeNicely(types, 'or');
448 throw new TypeError(name + ': unknown expectedType ' + badArg + '; you used:\n' + name + ' ' + (red(badArg) + ', ' + stringify(value) + '\nDid you mean ' + suggestions + '?'));
449 }
450
451 var typeMatches = stringType === getTypeName(value);
452 if (!typeMatches && !negated || typeMatches && negated) {
453 value = red(stringify(value));
454 var toBeOrNotToBe = (negated ? 'not ' : '') + 'to be';
455 var message = 'Expected value ' + value + ' ' + toBeOrNotToBe + ' of type ' + stringType;
456 throw error(message, explanation);
457 }
458 }
459};
460
461// produce negatived versions of all the common assertion functions
462var positiveAssertions = ['truthy', 'equal', 'deepEqual', 'include', 'match', 'throws', 'hasType'];
463positiveAssertions.forEach(function (name) {
464 assertSync[nameNegative(name)] = function _oneTest() {
465 return assertSync[name].apply('!', arguments);
466 };
467});
468
469// borrowed from Q
470function isPromiseAlike(p) {
471 return p === Object(p) && typeof p.then === 'function';
472}
473
474// promise-specific tests
475assert = {
476 resolves: function resolves(testee) {
477 var name = handleArgs(this, [1, 2], arguments, 'resolves')[0];
478 var explanation = void 0;
479 if (arguments.length === 2) {
480 explanation = arguments[0];
481 testee = arguments[1];
482 }
483
484 if (!isPromiseAlike(testee)) {
485 throw error(name + ' expects ' + green('a promise') + ' but got ' + red(stringify(testee)));
486 }
487
488 if (name === 'rejects') {
489 return testee.then(function () {
490 throw error("Promise wasn't rejected as expected to", explanation);
491 }, function (x) {
492 return x;
493 });
494 }
495 return testee.catch(function (err) {
496 throw error('Promise was rejected despite resolves assertion:\n' + ('' + (err && err.message || err)), explanation);
497 });
498 },
499 rejects: function rejects() {
500 return assert.resolves.apply('!', arguments);
501 }
502};
503
504// union of promise-specific and promise-aware wrapped synchronous tests
505if (assertSync) {
506 Object.keys(assertSync).forEach(function (name) {
507 var fn = assertSync[name];
508 assert[name] = function _oneTest() {
509 if (arguments.length === 0) return fn();
510 var args = [].slice.call(arguments);
511 var testee = args.pop();
512 if (isPromiseAlike(testee)) {
513 return testee.then(function (val) {
514 return fn.apply(undefined, _toConsumableArray(args).concat([val]));
515 });
516 }
517 return fn.apply(undefined, _toConsumableArray(args).concat([testee]));
518 };
519 });
520}
521
522module.exports = assert;
523//# sourceMappingURL=assertive.js.map
\No newline at end of file