From 4b1661c1c35490b157f3878ccbeeb5866197c9e3 Mon Sep 17 00:00:00 2001 From: Carlos David Nexans Date: Tue, 27 Jun 2017 22:21:17 -0300 Subject: [PATCH] feat(spy): adds support for spy.nth call assertions --- README.md | 31 +++++++++++ chai-spies.js | 124 ++++++++++++++++++++++++++++++++++++-------- lib/spy.js | 124 ++++++++++++++++++++++++++++++++++++-------- test/spies.js | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 376 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index fe6ca67..2288ab8 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,37 @@ expect(spy).to.have.been.called.always.with.exactly('foo'); spy.should.have.been.called.always.with.exactly('foo'); ``` +#### .nth(n).called.with + +Asserts that the nth call of the spy has been made with the list of arguments provided. This assertion comes with other three flavors: + +* .first.called.with +* .second.called.with +* .third.called.with + +```js +spy('foo'); +spy('bar'); +spy('baz'); +spy('foobar'); +expect(spy).to.have.been.first.called.with('foo'); +spy.should.have.been.first.called.with('foo'); +expect(spy).on.nth(5).be.called.with('foobar'); +spy.should.on.nth(5).be.called.with('foobar'); +``` + +These assertions requires the spy to be called at least the +number of times required, for example + +```js +spy('foo'); +spy('bar'); +expect(spy).to.have.been.third.called.with('baz'); +spy.should.have.been.third.called.with('baz'); +``` + +Won't pass because the spy has not been called a third time. + #### .once Assert that a spy has been called exactly once. diff --git a/chai-spies.js b/chai-spies.js index 2fc89f3..61189c4 100644 --- a/chai-spies.js +++ b/chai-spies.js @@ -48,7 +48,7 @@ var spy = function (chai, _) { * const array = [] * const spy = chai.spy.sandbox(); * const [push, pop] = spy.on(array, ['push', 'pop']); - * + * * spy.on(array, 'push', returns => 1) * * @param {Object} object @@ -226,7 +226,7 @@ var spy = function (chai, _) { s += " }"; return s; }; - + proxy.__spy = { calls: [] , called: false @@ -439,41 +439,111 @@ var spy = function (chai, _) { }); /** - * ### .with + * # nth call (spy, n, arguments) + * + * Asserts that the nth call of the spy has been called with * */ - function assertWith () { - new Assertion(this._obj).to.be.spy; - var expArgs = [].slice.call(arguments, 0) - , calls = this._obj.__spy.calls - , always = _.flag(this, 'spy always') + function nthCallWith(spy, n, expArgs) { + if (spy.calls.length < n) return false; + + var actArgs = spy.calls[n].slice() , passed = 0; - calls.forEach(function (call) { - var actArgs = call.slice() - , found = 0; - - expArgs.forEach(function (expArg) { - for (var i = 0; i < actArgs.length; i++) { - if (_.eql(actArgs[i], expArg)) { - found++; - actArgs.splice(i, 1); - break; - } + expArgs.forEach(function (expArg) { + for (var i = 0; i < actArgs.length; i++) { + if (_.eql(actArgs[i], expArg)) { + passed++; + actArgs.splice(i, 1); + break; } - }); - if (found === expArgs.length) passed++; + } }); + return passed === expArgs.length + } + + function numberOfCallsWith(spy, expArgs) { + var found = 0 + , calls = spy.calls; + + for (var i = 0; i < calls.length; i++) { + if (nthCallWith(spy, i, expArgs)) { + found++; + } + } + + return found; + } + + Assertion.addProperty('first', function () { + if ('undefined' !== this._obj.__spy) { + _.flag(this, 'spy nth call with', 1); + } + }); + + Assertion.addProperty('second', function () { + if ('undefined' !== this._obj.__spy) { + _.flag(this, 'spy nth call with', 2); + } + }); + + Assertion.addProperty('third', function () { + if ('undefined' !== this._obj.__spy) { + _.flag(this, 'spy nth call with', 3); + } + }); + + Assertion.addProperty('on'); + + Assertion.addChainableMethod('nth', function (n) { + if ('undefined' !== this._obj.__spy) { + _.flag(this, 'spy nth call with', n); + } + }); + + function generateOrdinalNumber(n) { + if (n === 1) return 'first'; + if (n === 2) return 'second'; + if (n === 3) return 'third'; + return n + 'th'; + } + + /** + * ### .with + * + */ + + function assertWith() { + new Assertion(this._obj).to.be.spy; + var expArgs = [].slice.call(arguments, 0) + , spy = this._obj.__spy + , calls = spy.calls + , always = _.flag(this, 'spy always') + , nthCall = _.flag(this, 'spy nth call with'); + if (always) { + var passed = numberOfCallsWith(spy, expArgs); this.assert( passed === calls.length , 'expected ' + this._obj + ' to have been always called with #{exp} but got ' + passed + ' out of ' + calls.length , 'expected ' + this._obj + ' to have not always been called with #{exp}' , expArgs ); + } else if (nthCall) { + var ordinalNumber = generateOrdinalNumber(nthCall), + actArgs = calls[nthCall - 1]; + new Assertion(this._obj).to.be.have.been.called.min(nthCall); + this.assert( + nthCallWith(spy, nthCall - 1, expArgs) + , 'expected ' + this._obj + ' to have been called at the ' + ordinalNumber + ' time with #{exp} but got #{act}' + , 'expected ' + this._obj + ' to have not been called at the ' + ordinalNumber + ' time with #{exp}' + , expArgs + , actArgs + ); } else { + var passed = numberOfCallsWith(spy, expArgs); this.assert( passed > 0 , 'expected ' + this._obj + ' to have been called with #{exp}' @@ -512,6 +582,7 @@ var spy = function (chai, _) { , _with = _.flag(this, 'spy with') , args = [].slice.call(arguments, 0) , calls = this._obj.__spy.calls + , nthCall = _.flag(this, 'spy nth call with') , passed; if (always && _with) { @@ -527,6 +598,17 @@ var spy = function (chai, _) { , 'expected ' + this._obj + ' to have not always been called with exactly #{exp}' , args ); + } else if(_with && nthCall) { + var ordinalNumber = generateOrdinalNumber(nthCall), + actArgs = calls[nthCall - 1]; + new Assertion(this._obj).to.be.have.been.called.min(nthCall); + this.assert( + _.eql(actArgs, args) + , 'expected ' + this._obj + ' to have been called at the ' + ordinalNumber + ' time with exactly #{exp} but got #{act}' + , 'expected ' + this._obj + ' to have not been called at the ' + ordinalNumber + ' time with exactly #{exp}' + , args + , actArgs + ); } else if (_with) { passed = 0; calls.forEach(function (call) { diff --git a/lib/spy.js b/lib/spy.js index 96acd3d..92d3b41 100644 --- a/lib/spy.js +++ b/lib/spy.js @@ -42,7 +42,7 @@ module.exports = function (chai, _) { * const array = [] * const spy = chai.spy.sandbox(); * const [push, pop] = spy.on(array, ['push', 'pop']); - * + * * spy.on(array, 'push', returns => 1) * * @param {Object} object @@ -220,7 +220,7 @@ module.exports = function (chai, _) { s += " }"; return s; }; - + proxy.__spy = { calls: [] , called: false @@ -433,41 +433,111 @@ module.exports = function (chai, _) { }); /** - * ### .with + * # nth call (spy, n, arguments) + * + * Asserts that the nth call of the spy has been called with * */ - function assertWith () { - new Assertion(this._obj).to.be.spy; - var expArgs = [].slice.call(arguments, 0) - , calls = this._obj.__spy.calls - , always = _.flag(this, 'spy always') + function nthCallWith(spy, n, expArgs) { + if (spy.calls.length < n) return false; + + var actArgs = spy.calls[n].slice() , passed = 0; - calls.forEach(function (call) { - var actArgs = call.slice() - , found = 0; - - expArgs.forEach(function (expArg) { - for (var i = 0; i < actArgs.length; i++) { - if (_.eql(actArgs[i], expArg)) { - found++; - actArgs.splice(i, 1); - break; - } + expArgs.forEach(function (expArg) { + for (var i = 0; i < actArgs.length; i++) { + if (_.eql(actArgs[i], expArg)) { + passed++; + actArgs.splice(i, 1); + break; } - }); - if (found === expArgs.length) passed++; + } }); + return passed === expArgs.length + } + + function numberOfCallsWith(spy, expArgs) { + var found = 0 + , calls = spy.calls; + + for (var i = 0; i < calls.length; i++) { + if (nthCallWith(spy, i, expArgs)) { + found++; + } + } + + return found; + } + + Assertion.addProperty('first', function () { + if ('undefined' !== this._obj.__spy) { + _.flag(this, 'spy nth call with', 1); + } + }); + + Assertion.addProperty('second', function () { + if ('undefined' !== this._obj.__spy) { + _.flag(this, 'spy nth call with', 2); + } + }); + + Assertion.addProperty('third', function () { + if ('undefined' !== this._obj.__spy) { + _.flag(this, 'spy nth call with', 3); + } + }); + + Assertion.addProperty('on'); + + Assertion.addChainableMethod('nth', function (n) { + if ('undefined' !== this._obj.__spy) { + _.flag(this, 'spy nth call with', n); + } + }); + + function generateOrdinalNumber(n) { + if (n === 1) return 'first'; + if (n === 2) return 'second'; + if (n === 3) return 'third'; + return n + 'th'; + } + + /** + * ### .with + * + */ + + function assertWith() { + new Assertion(this._obj).to.be.spy; + var expArgs = [].slice.call(arguments, 0) + , spy = this._obj.__spy + , calls = spy.calls + , always = _.flag(this, 'spy always') + , nthCall = _.flag(this, 'spy nth call with'); + if (always) { + var passed = numberOfCallsWith(spy, expArgs); this.assert( passed === calls.length , 'expected ' + this._obj + ' to have been always called with #{exp} but got ' + passed + ' out of ' + calls.length , 'expected ' + this._obj + ' to have not always been called with #{exp}' , expArgs ); + } else if (nthCall) { + var ordinalNumber = generateOrdinalNumber(nthCall), + actArgs = calls[nthCall - 1]; + new Assertion(this._obj).to.be.have.been.called.min(nthCall); + this.assert( + nthCallWith(spy, nthCall - 1, expArgs) + , 'expected ' + this._obj + ' to have been called at the ' + ordinalNumber + ' time with #{exp} but got #{act}' + , 'expected ' + this._obj + ' to have not been called at the ' + ordinalNumber + ' time with #{exp}' + , expArgs + , actArgs + ); } else { + var passed = numberOfCallsWith(spy, expArgs); this.assert( passed > 0 , 'expected ' + this._obj + ' to have been called with #{exp}' @@ -506,6 +576,7 @@ module.exports = function (chai, _) { , _with = _.flag(this, 'spy with') , args = [].slice.call(arguments, 0) , calls = this._obj.__spy.calls + , nthCall = _.flag(this, 'spy nth call with') , passed; if (always && _with) { @@ -521,6 +592,17 @@ module.exports = function (chai, _) { , 'expected ' + this._obj + ' to have not always been called with exactly #{exp}' , args ); + } else if(_with && nthCall) { + var ordinalNumber = generateOrdinalNumber(nthCall), + actArgs = calls[nthCall - 1]; + new Assertion(this._obj).to.be.have.been.called.min(nthCall); + this.assert( + _.eql(actArgs, args) + , 'expected ' + this._obj + ' to have been called at the ' + ordinalNumber + ' time with exactly #{exp} but got #{act}' + , 'expected ' + this._obj + ' to have not been called at the ' + ordinalNumber + ' time with exactly #{exp}' + , args + , actArgs + ); } else if (_with) { passed = 0; calls.forEach(function (call) { diff --git a/test/spies.js b/test/spies.js index c4a171e..68451e4 100644 --- a/test/spies.js +++ b/test/spies.js @@ -272,6 +272,83 @@ describe('Chai Spies', function () { }); }); + describe('.first.called.with(arg, ...)', function() { + it('should pass only when called with the arguments the first time', function() { + var spy = chai.spy(); + spy(1, 2, 3) + spy(3, 4, 5) + spy.should.have.been.first.called.with(3, 2, 1); + spy.should.have.been.first.called.with(1, 2, 3) + spy.should.have.been.first.called.with(1, 2); + spy.should.not.have.been.first.called.with(4); + (function () { + spy.should.have.been.first.called.with(1, 2, 4) + }).should.throw(chai.AssertionError, /have been called at the first time with/); + (function () { + spy.should.have.not.been.first.called.with(1, 2); + }).should.throw(chai.AssertionError, /have not been called at the first time with/); + }); + }); + + describe('.second.called.with(arg, ...)', function() { + it('should pass only when called with the arguments the second time', function() { + var spy = chai.spy(); + spy(1, 2, 3) + spy(3, 4, 5) + spy.should.have.been.second.called.with(3, 4, 5) + spy.should.have.been.second.called.with(4, 5); + spy.should.not.have.been.second.called.with(1); + (function () { + spy.should.have.been.second.called.with(3, 4, 1) + }).should.throw(chai.AssertionError, /have been called at the second time with/); + (function () { + spy.should.have.not.been.second.called.with(4, 5); + }).should.throw(chai.AssertionError, /have not been called at the second time with/); + }); + }); + + describe('.third.called.with(arg, ...)', function() { + it('should pass only when called with the arguments the third time', function() { + var spy = chai.spy(); + spy(1, 2, 3) + spy(3, 4, 5) + spy(5, 6, 7) + spy.should.have.been.third.called.with(5, 6, 7) + spy.should.have.been.third.called.with(6, 5); + spy.should.not.have.been.third.called.with(1); + (function () { + spy.should.have.been.third.called.with(5, 6, 1) + }).should.throw(chai.AssertionError, /have been called at the third time with/); + (function () { + spy.should.have.not.been.third.called.with(6, 5); + }).should.throw(chai.AssertionError, /have not been called at the third time with/); + }); + }); + + describe('.nth(n).called.with(arg, ...)', function() { + it('should pass only when called with the arguments the nth time its called', function() { + var spy = chai.spy(); + spy(0); + spy(1); + spy(2); + spy(3); + spy(4, 6, 7); + spy(5, 8, 9); + spy.should.on.nth(5).be.called.with(4); + spy.should.on.nth(6).be.called.with(8, 5); + spy.should.not.on.nth(5).be.called.with(3, 4); + (function () { + spy.should.on.nth(5).be.called.with(3); + }).should.throw(chai.AssertionError, /have been called at the 5th time with/); + (function () { + spy.should.not.on.nth(6).be.called.with(5); + }).should.throw(chai.AssertionError, /have not been called at the 6th time with/); + (function () { + spy.should.on.nth(7).be.called.with(10); + }).should.throw(chai.AssertionError, /to have been called at least 7 times but got 6/); + }); + }); + describe('.always.with(arg, ...)', function () { it('should pass when called with an argument', function () { var spy = chai.spy(); @@ -357,6 +434,68 @@ describe('Chai Spies', function () { }); }); + describe('.nth(...).with.exactly(arg, ...)', function () { + it('Should work with the shorthand first for nth(1)', function() { + var spy = chai.spy(); + spy(1, 2, 3); + spy(3, 4, 5); + spy.should.have.been.first.called.with.exactly(1, 2, 3); + spy.should.have.been.not.first.called.with.exactly(3, 4, 5); + spy.should.have.been.not.first.called.with.exactly(3); + (function() { + spy.should.have.been.first.called.with.exactly(3) + }).should.throw(chai.AssertionError); + (function() { + spy.should.have.not.been.first.called.with.exactly(1, 2, 3) + }).should.throw(chai.AssertionError); + }); + it('Should work with the shorthand second for nth(2)', function() { + var spy = chai.spy(); + spy(1, 2, 3); + spy(3, 4, 5); + spy.should.have.been.second.called.with.exactly(3, 4, 5); + spy.should.have.been.not.second.called.with.exactly(1, 2, 3); + spy.should.have.been.not.second.called.with.exactly(4); + (function() { + spy.should.have.been.second.called.with.exactly(4, 5) + }).should.throw(chai.AssertionError); + (function() { + spy.should.have.not.been.second.called.with.exactly(3, 4, 5) + }).should.throw(chai.AssertionError); + }); + it('Should work with the shorthand third for nth(3)', function() { + var spy = chai.spy(); + spy(1, 2, 3); + spy(3, 4, 5); + spy(5, 6, 7); + spy.should.have.been.third.called.with.exactly(5, 6, 7); + spy.should.have.been.not.third.called.with.exactly(5); + spy.should.have.been.not.third.called.with.exactly(6, 5, 7); + (function() { + spy.should.have.been.third.called.with.exactly(7, 6, 5) + }).should.throw(chai.AssertionError); + (function() { + spy.should.have.not.been.third.called.with.exactly(5, 6, 7) + }).should.throw(chai.AssertionError); + }); + it('Should work with general nth(...) flag', function() { + var spy = chai.spy(); + spy(1, 2, 3); + spy(3, 4, 5); + spy(5, 6, 7); + spy(7, 8, 9); + spy.should.on.nth(4).be.called.with.exactly(7, 8, 9); + spy.should.not.on.nth(4).be.called.with.exactly(9, 8, 7); + spy.should.not.on.nth(4).be.called.with.exactly(7, 8); + (function() { + spy.should.on.nth(4).be.called.with.exactly(7, 6, 5); + }).should.throw(chai.AssertionError); + (function() { + spy.should.not.on.nth(4).be.called.with.exactly(7, 8, 9); + }).should.throw(chai.AssertionError); + }); + }); + describe('.always.with.exactly(arg, ...)', function () { it('should pass when called with an argument', function () { var spy = chai.spy();