1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | module.exports = function (chai, _) {
|
12 |
|
13 | var Assertion = chai.Assertion
|
14 | , flag = _.flag
|
15 | , i = _.inspect
|
16 | , STATE_KEY = typeof Symbol === 'undefined' ? '__state' : Symbol('state')
|
17 | , spyAmount = 0
|
18 | , DEFAULT_SANDBOX = new Sandbox()
|
19 | , noop = function () {}
|
20 |
|
21 | |
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | function Sandbox() {
|
31 | this[STATE_KEY] = {};
|
32 | }
|
33 |
|
34 | |
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | Sandbox.prototype.on = function (object, methodName, fn) {
|
57 | if (Array.isArray(methodName)) {
|
58 | return methodName.map(function (name) {
|
59 | return this.on(object, name, fn);
|
60 | }, this);
|
61 | }
|
62 |
|
63 | var isMethod = typeof object[methodName] === 'function'
|
64 |
|
65 | if (methodName in object && !isMethod) {
|
66 | throw new Error([
|
67 | 'Unable to spy property "', methodName,
|
68 | '". Only methods and non-existing properties can be spied.'
|
69 | ].join(''))
|
70 | }
|
71 |
|
72 | if (isMethod && object[methodName].__spy) {
|
73 | throw new Error('"' + methodName + '" is already a spy')
|
74 | }
|
75 |
|
76 | var method = chai.spy('object.' + methodName, fn || object[methodName]);
|
77 | var trackingId = ++spyAmount
|
78 |
|
79 | this[STATE_KEY][trackingId] = method;
|
80 | method.__spy.tracked = {
|
81 | object: object
|
82 | , methodName: methodName
|
83 | , originalMethod: object[methodName]
|
84 | , isOwnMethod: Object.hasOwnProperty.call(object, methodName)
|
85 | };
|
86 | object[methodName] = method;
|
87 |
|
88 | return method;
|
89 | };
|
90 |
|
91 | |
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 | Sandbox.prototype.restore = function (object, methods) {
|
108 | var exitAfter = 0, restored = 0;
|
109 | var sandbox = this;
|
110 |
|
111 | if (methods) {
|
112 | if (!Array.isArray(methods)) {
|
113 | methods = [methods]
|
114 | }
|
115 | exitAfter = methods.length;
|
116 | }
|
117 |
|
118 | Object.keys(this[STATE_KEY]).some(function (spyId) {
|
119 | var spy = sandbox[STATE_KEY][spyId];
|
120 | var tracked = spy.__spy.tracked;
|
121 | var shouldRestoreThisObject = !object || object === tracked.object;
|
122 | var shouldRestoreThisMethod = !methods || methods.indexOf(tracked.methodName) !== -1;
|
123 |
|
124 |
|
125 | if (!shouldRestoreThisObject || !shouldRestoreThisMethod) {
|
126 | return false;
|
127 | }
|
128 |
|
129 | delete sandbox[STATE_KEY][spyId];
|
130 | sandbox.restoreTrackedObject(spy);
|
131 |
|
132 | if (++restored === exitAfter) {
|
133 | return true;
|
134 | }
|
135 | });
|
136 |
|
137 | return this;
|
138 | };
|
139 |
|
140 | |
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | Sandbox.prototype.restoreTrackedObject = function (spy) {
|
154 | var tracked = spy.__spy.tracked;
|
155 |
|
156 | if (!tracked) {
|
157 | throw new Error('It is not possible to restore a non-tracked spy.')
|
158 | }
|
159 |
|
160 | if (tracked.isOwnMethod) {
|
161 | tracked.object[tracked.methodName] = tracked.originalMethod;
|
162 | } else {
|
163 | delete tracked.object[tracked.methodName];
|
164 | }
|
165 |
|
166 | spy.__spy.tracked = null;
|
167 | };
|
168 |
|
169 | |
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 | chai.spy = function (name, fn) {
|
185 | if (typeof name === 'function') {
|
186 | fn = name;
|
187 | name = undefined;
|
188 | }
|
189 |
|
190 | fn = fn || noop;
|
191 |
|
192 | function makeProxy (length, fn) {
|
193 | switch (length) {
|
194 | case 0 : return function () { return fn.apply(this, arguments); };
|
195 | case 1 : return function (a) { return fn.apply(this, arguments); };
|
196 | case 2 : return function (a,b) { return fn.apply(this, arguments); };
|
197 | case 3 : return function (a,b,c) { return fn.apply(this, arguments); };
|
198 | case 4 : return function (a,b,c,d) { return fn.apply(this, arguments); };
|
199 | case 5 : return function (a,b,c,d,e) { return fn.apply(this, arguments); };
|
200 | case 6 : return function (a,b,c,d,e,f) { return fn.apply(this, arguments); };
|
201 | case 7 : return function (a,b,c,d,e,f,g) { return fn.apply(this, arguments); };
|
202 | case 8 : return function (a,b,c,d,e,f,g,h) { return fn.apply(this, arguments); };
|
203 | case 9 : return function (a,b,c,d,e,f,g,h,i) { return fn.apply(this, arguments); };
|
204 | default : return function (a,b,c,d,e,f,g,h,i,j) { return fn.apply(this, arguments); };
|
205 | }
|
206 | };
|
207 |
|
208 | var proxy = makeProxy(fn.length, function () {
|
209 | var args = Array.prototype.slice.call(arguments);
|
210 | proxy.__spy.calls.push(args);
|
211 | proxy.__spy.called = true;
|
212 | return fn.apply(this, args);
|
213 | });
|
214 |
|
215 | proxy.prototype = fn.prototype;
|
216 | proxy.toString = function toString() {
|
217 | var state = this.__spy;
|
218 | var l = state.calls.length;
|
219 | var s = "{ Spy";
|
220 | if (state.name)
|
221 | s += " '" + state.name + "'";
|
222 | if (l > 0)
|
223 | s += ", " + l + " call" + (l > 1 ? 's' : '');
|
224 | s += " }";
|
225 | return s + (fn !== noop ? "\n" + fn.toString() : '');
|
226 | };
|
227 |
|
228 | proxy.__spy = {
|
229 | calls: []
|
230 | , called: false
|
231 | , name: name
|
232 | };
|
233 |
|
234 | return proxy;
|
235 | }
|
236 |
|
237 | |
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 | chai.spy.sandbox = function () {
|
253 | return new Sandbox()
|
254 | };
|
255 |
|
256 | |
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | chai.spy.on = function () {
|
269 | return DEFAULT_SANDBOX.on.apply(DEFAULT_SANDBOX, arguments)
|
270 | };
|
271 |
|
272 | |
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 | chai.spy.interface = function (name, methods) {
|
293 | var defs = {};
|
294 |
|
295 | if (name && typeof name === 'object') {
|
296 | methods = Object.keys(name);
|
297 | defs = name;
|
298 | name = 'mock';
|
299 | }
|
300 |
|
301 | return methods.reduce(function (object, methodName) {
|
302 | object[methodName] = chai.spy(name + '.' + methodName, defs[methodName]);
|
303 | return object;
|
304 | }, {});
|
305 | };
|
306 |
|
307 | |
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 | chai.spy.restore = function () {
|
325 | return DEFAULT_SANDBOX.restore.apply(DEFAULT_SANDBOX, arguments)
|
326 | };
|
327 |
|
328 | |
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 | chai.spy.returns = function (value) {
|
341 | return chai.spy(function () {
|
342 | return value;
|
343 | });
|
344 | };
|
345 |
|
346 | |
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 | Assertion.addProperty('spy', function () {
|
359 | this.assert(
|
360 | 'undefined' !== typeof this._obj.__spy
|
361 | , 'expected ' + this._obj + ' to be a spy'
|
362 | , 'expected ' + this._obj + ' to not be a spy');
|
363 | return this;
|
364 | });
|
365 |
|
366 | |
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 |
|
374 |
|
375 | function assertCalled (n) {
|
376 | new Assertion(this._obj).to.be.spy;
|
377 | var spy = this._obj.__spy;
|
378 |
|
379 | if (n) {
|
380 | this.assert(
|
381 | spy.calls.length === n
|
382 | , 'expected ' + this._obj + ' to have been called #{exp} but got #{act}'
|
383 | , 'expected ' + this._obj + ' to have not been called #{exp}'
|
384 | , n
|
385 | , spy.calls.length
|
386 | );
|
387 | } else {
|
388 | this.assert(
|
389 | spy.called === true
|
390 | , 'expected ' + this._obj + ' to have been called'
|
391 | , 'expected ' + this._obj + ' to not have been called'
|
392 | );
|
393 | }
|
394 | }
|
395 |
|
396 | function assertCalledChain () {
|
397 | new Assertion(this._obj).to.be.spy;
|
398 | }
|
399 |
|
400 | Assertion.addChainableMethod('called', assertCalled, assertCalledChain);
|
401 |
|
402 | |
403 |
|
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 | Assertion.addProperty('once', function () {
|
411 | new Assertion(this._obj).to.be.spy;
|
412 | this.assert(
|
413 | this._obj.__spy.calls.length === 1
|
414 | , 'expected ' + this._obj + ' to have been called once but got #{act}'
|
415 | , 'expected ' + this._obj + ' to not have been called once'
|
416 | , 1
|
417 | , this._obj.__spy.calls.length );
|
418 | });
|
419 |
|
420 | |
421 |
|
422 |
|
423 |
|
424 |
|
425 |
|
426 |
|
427 |
|
428 | Assertion.addProperty('twice', function () {
|
429 | new Assertion(this._obj).to.be.spy;
|
430 | this.assert(
|
431 | this._obj.__spy.calls.length === 2
|
432 | , 'expected ' + this._obj + ' to have been called twice but got #{act}'
|
433 | , 'expected ' + this._obj + ' to not have been called twice'
|
434 | , 2
|
435 | , this._obj.__spy.calls.length
|
436 | );
|
437 | });
|
438 |
|
439 | |
440 |
|
441 |
|
442 |
|
443 |
|
444 |
|
445 |
|
446 | function nthCallWith(spy, n, expArgs) {
|
447 | if (spy.calls.length < n) return false;
|
448 |
|
449 | var actArgs = spy.calls[n].slice()
|
450 | , passed = 0;
|
451 |
|
452 | expArgs.forEach(function (expArg) {
|
453 | for (var i = 0; i < actArgs.length; i++) {
|
454 | if (_.eql(actArgs[i], expArg)) {
|
455 | passed++;
|
456 | actArgs.splice(i, 1);
|
457 | break;
|
458 | }
|
459 | }
|
460 | });
|
461 |
|
462 | return passed === expArgs.length
|
463 | }
|
464 |
|
465 | function numberOfCallsWith(spy, expArgs) {
|
466 | var found = 0
|
467 | , calls = spy.calls;
|
468 |
|
469 | for (var i = 0; i < calls.length; i++) {
|
470 | if (nthCallWith(spy, i, expArgs)) {
|
471 | found++;
|
472 | }
|
473 | }
|
474 |
|
475 | return found;
|
476 | }
|
477 |
|
478 | Assertion.addProperty('first', function () {
|
479 | if ('undefined' !== this._obj.__spy) {
|
480 | _.flag(this, 'spy nth call with', 1);
|
481 | }
|
482 | });
|
483 |
|
484 | Assertion.addProperty('second', function () {
|
485 | if ('undefined' !== this._obj.__spy) {
|
486 | _.flag(this, 'spy nth call with', 2);
|
487 | }
|
488 | });
|
489 |
|
490 | Assertion.addProperty('third', function () {
|
491 | if ('undefined' !== this._obj.__spy) {
|
492 | _.flag(this, 'spy nth call with', 3);
|
493 | }
|
494 | });
|
495 |
|
496 | Assertion.addProperty('on');
|
497 |
|
498 | Assertion.addChainableMethod('nth', function (n) {
|
499 | if ('undefined' !== this._obj.__spy) {
|
500 | _.flag(this, 'spy nth call with', n);
|
501 | }
|
502 | });
|
503 |
|
504 | function generateOrdinalNumber(n) {
|
505 | if (n === 1) return 'first';
|
506 | if (n === 2) return 'second';
|
507 | if (n === 3) return 'third';
|
508 | return n + 'th';
|
509 | }
|
510 |
|
511 | |
512 |
|
513 |
|
514 |
|
515 |
|
516 | function assertWith() {
|
517 | new Assertion(this._obj).to.be.spy;
|
518 | var expArgs = [].slice.call(arguments, 0)
|
519 | , spy = this._obj.__spy
|
520 | , calls = spy.calls
|
521 | , always = _.flag(this, 'spy always')
|
522 | , nthCall = _.flag(this, 'spy nth call with');
|
523 |
|
524 | if (always) {
|
525 | var passed = numberOfCallsWith(spy, expArgs);
|
526 | this.assert(
|
527 | passed === calls.length
|
528 | , 'expected ' + this._obj + ' to have been always called with #{exp} but got ' + passed + ' out of ' + calls.length
|
529 | , 'expected ' + this._obj + ' to have not always been called with #{exp}'
|
530 | , expArgs
|
531 | );
|
532 | } else if (nthCall) {
|
533 | var ordinalNumber = generateOrdinalNumber(nthCall),
|
534 | actArgs = calls[nthCall - 1];
|
535 | new Assertion(this._obj).to.be.have.been.called.min(nthCall);
|
536 | this.assert(
|
537 | nthCallWith(spy, nthCall - 1, expArgs)
|
538 | , 'expected ' + this._obj + ' to have been called at the ' + ordinalNumber + ' time with #{exp} but got #{act}'
|
539 | , 'expected ' + this._obj + ' to have not been called at the ' + ordinalNumber + ' time with #{exp}'
|
540 | , expArgs
|
541 | , actArgs
|
542 | );
|
543 | } else {
|
544 | var passed = numberOfCallsWith(spy, expArgs);
|
545 | this.assert(
|
546 | passed > 0
|
547 | , 'expected ' + this._obj + ' to have been called with #{exp}'
|
548 | , 'expected ' + this._obj + ' to have not been called with #{exp} but got ' + passed + ' times'
|
549 | , expArgs
|
550 | );
|
551 | }
|
552 | }
|
553 |
|
554 | function assertWithChain () {
|
555 | if ('undefined' !== this._obj.__spy) {
|
556 | _.flag(this, 'spy with', true);
|
557 | }
|
558 | }
|
559 |
|
560 | Assertion.addChainableMethod('with', assertWith, assertWithChain);
|
561 |
|
562 | Assertion.addProperty('always', function () {
|
563 | if ('undefined' !== this._obj.__spy) {
|
564 | _.flag(this, 'spy always', true);
|
565 | }
|
566 | });
|
567 |
|
568 | |
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 |
|
576 |
|
577 | Assertion.addMethod('exactly', function () {
|
578 | new Assertion(this._obj).to.be.spy;
|
579 | var always = _.flag(this, 'spy always')
|
580 | , _with = _.flag(this, 'spy with')
|
581 | , args = [].slice.call(arguments, 0)
|
582 | , calls = this._obj.__spy.calls
|
583 | , nthCall = _.flag(this, 'spy nth call with')
|
584 | , passed;
|
585 |
|
586 | if (always && _with) {
|
587 | passed = 0
|
588 | calls.forEach(function (call) {
|
589 | if (call.length !== args.length) return;
|
590 | if (_.eql(call, args)) passed++;
|
591 | });
|
592 |
|
593 | this.assert(
|
594 | passed === calls.length
|
595 | , 'expected ' + this._obj + ' to have been always called with exactly #{exp} but got ' + passed + ' out of ' + calls.length
|
596 | , 'expected ' + this._obj + ' to have not always been called with exactly #{exp}'
|
597 | , args
|
598 | );
|
599 | } else if(_with && nthCall) {
|
600 | var ordinalNumber = generateOrdinalNumber(nthCall),
|
601 | actArgs = calls[nthCall - 1];
|
602 | new Assertion(this._obj).to.be.have.been.called.min(nthCall);
|
603 | this.assert(
|
604 | _.eql(actArgs, args)
|
605 | , 'expected ' + this._obj + ' to have been called at the ' + ordinalNumber + ' time with exactly #{exp} but got #{act}'
|
606 | , 'expected ' + this._obj + ' to have not been called at the ' + ordinalNumber + ' time with exactly #{exp}'
|
607 | , args
|
608 | , actArgs
|
609 | );
|
610 | } else if (_with) {
|
611 | passed = 0;
|
612 | calls.forEach(function (call) {
|
613 | if (call.length !== args.length) return;
|
614 | if (_.eql(call, args)) passed++;
|
615 | });
|
616 |
|
617 | this.assert(
|
618 | passed > 0
|
619 | , 'expected ' + this._obj + ' to have been called with exactly #{exp}'
|
620 | , 'expected ' + this._obj + ' to not have been called with exactly #{exp} but got ' + passed + ' times'
|
621 | , args
|
622 | );
|
623 | } else {
|
624 | this.assert(
|
625 | this._obj.__spy.calls.length === args[0]
|
626 | , 'expected ' + this._obj + ' to have been called #{exp} times but got #{act}'
|
627 | , 'expected ' + this._obj + ' to not have been called #{exp} times'
|
628 | , args[0]
|
629 | , this._obj.__spy.calls.length
|
630 | );
|
631 | }
|
632 | });
|
633 |
|
634 | |
635 |
|
636 |
|
637 |
|
638 |
|
639 |
|
640 |
|
641 |
|
642 |
|
643 | function above (_super) {
|
644 | return function (n) {
|
645 | if ('undefined' !== typeof this._obj.__spy) {
|
646 | new Assertion(this._obj).to.be.spy;
|
647 |
|
648 | this.assert(
|
649 | this._obj.__spy.calls.length > n
|
650 | , 'expected ' + this._obj + ' to have been called more than #{exp} times but got #{act}'
|
651 | , 'expected ' + this._obj + ' to have been called at most #{exp} times but got #{act}'
|
652 | , n
|
653 | , this._obj.__spy.calls.length
|
654 | );
|
655 | } else {
|
656 | _super.apply(this, arguments);
|
657 | }
|
658 | }
|
659 | }
|
660 |
|
661 | Assertion.overwriteMethod('above', above);
|
662 | Assertion.overwriteMethod('gt', above);
|
663 |
|
664 | |
665 |
|
666 |
|
667 |
|
668 |
|
669 |
|
670 |
|
671 |
|
672 |
|
673 | function below (_super) {
|
674 | return function (n) {
|
675 | if ('undefined' !== typeof this._obj.__spy) {
|
676 | new Assertion(this._obj).to.be.spy;
|
677 |
|
678 | this.assert(
|
679 | this._obj.__spy.calls.length < n
|
680 | , 'expected ' + this._obj + ' to have been called fewer than #{exp} times but got #{act}'
|
681 | , 'expected ' + this._obj + ' to have been called at least #{exp} times but got #{act}'
|
682 | , n
|
683 | , this._obj.__spy.calls.length
|
684 | );
|
685 | } else {
|
686 | _super.apply(this, arguments);
|
687 | }
|
688 | }
|
689 | }
|
690 |
|
691 | Assertion.overwriteMethod('below', below);
|
692 | Assertion.overwriteMethod('lt', below);
|
693 |
|
694 | |
695 |
|
696 |
|
697 |
|
698 |
|
699 |
|
700 |
|
701 |
|
702 |
|
703 | function min (_super) {
|
704 | return function (n) {
|
705 | if ('undefined' !== typeof this._obj.__spy) {
|
706 | new Assertion(this._obj).to.be.spy;
|
707 |
|
708 | this.assert(
|
709 | this._obj.__spy.calls.length >= n
|
710 | , 'expected ' + this._obj + ' to have been called at least #{exp} times but got #{act}'
|
711 | , 'expected ' + this._obj + ' to have been called fewer than #{exp} times but got #{act}'
|
712 | , n
|
713 | , this._obj.__spy.calls.length
|
714 | );
|
715 | } else {
|
716 | _super.apply(this, arguments);
|
717 | }
|
718 | }
|
719 | }
|
720 |
|
721 | Assertion.overwriteMethod('min', min);
|
722 | Assertion.overwriteMethod('least', min);
|
723 |
|
724 | |
725 |
|
726 |
|
727 |
|
728 |
|
729 |
|
730 |
|
731 |
|
732 |
|
733 | function max (_super) {
|
734 | return function (n) {
|
735 | if ('undefined' !== typeof this._obj.__spy) {
|
736 | new Assertion(this._obj).to.be.spy;
|
737 |
|
738 | this.assert(
|
739 | this._obj.__spy.calls.length <= n
|
740 | , 'expected ' + this._obj + ' to have been called at most #{exp} times but got #{act}'
|
741 | , 'expected ' + this._obj + ' to have been called more than #{exp} times but got #{act}'
|
742 | , n
|
743 | , this._obj.__spy.calls.length
|
744 | );
|
745 | } else {
|
746 | _super.apply(this, arguments);
|
747 | }
|
748 | }
|
749 | }
|
750 |
|
751 | Assertion.overwriteMethod('max', max);
|
752 | Assertion.overwriteMethod('most', max);
|
753 | };
|