diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 712a8bf4..9642905b 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -66,6 +66,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) { j$.QueueRunner = jRequire.QueueRunner(j$); j$.ReportDispatcher = jRequire.ReportDispatcher(); j$.Spec = jRequire.Spec(j$); + j$.Spy = jRequire.Spy(j$); j$.SpyRegistry = jRequire.SpyRegistry(j$); j$.SpyStrategy = jRequire.SpyStrategy(j$); j$.StringMatching = jRequire.StringMatching(j$); @@ -189,38 +190,7 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { }; j$.createSpy = function(name, originalFn) { - - var spyStrategy = new j$.SpyStrategy({ - name: name, - fn: originalFn, - getSpy: function() { return spy; } - }), - callTracker = new j$.CallTracker(), - spy = function() { - var callData = { - object: this, - args: Array.prototype.slice.apply(arguments) - }; - - callTracker.track(callData); - var returnValue = spyStrategy.exec.apply(this, arguments); - callData.returnValue = returnValue; - - return returnValue; - }; - - for (var prop in originalFn) { - if (prop === 'and' || prop === 'calls') { - throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object being spied upon'); - } - - spy[prop] = originalFn[prop]; - } - - spy.and = spyStrategy; - spy.calls = callTracker; - - return spy; + return j$.Spy(name, originalFn); }; j$.isSpy = function(putativeSpy) { @@ -2078,6 +2048,65 @@ getJasmineRequireObj().ReportDispatcher = function() { }; +getJasmineRequireObj().Spy = function (j$) { + + function Spy(name, originalFn) { + var args = buildArgs(), + /*`eval` is the only option to preserve both this and context: + - former is needed to work as expected with methods, + - latter is needed to access real spy function and allows to reduce eval'ed code to absolute minimum + More explanation here (look at comments): http://www.bennadel.com/blog/1909-javascript-function-constructor-does-not-create-a-closure.htm + */ + /* jshint evil: true */ + wrapper = eval('(function (' + args + ') { return spy.apply(this, Array.prototype.slice.call(arguments)); })'), + spyStrategy = new j$.SpyStrategy({ + name: name, + fn: originalFn, + getSpy: function () { + return wrapper; + } + }), + callTracker = new j$.CallTracker(), + spy = function () { + var callData = { + object: this, + args: Array.prototype.slice.apply(arguments) + }; + + callTracker.track(callData); + var returnValue = spyStrategy.exec.apply(this, arguments); + callData.returnValue = returnValue; + + return returnValue; + }; + + function buildArgs() { + var args = []; + + while (originalFn instanceof Function && args.length < originalFn.length) { + args.push('arg' + args.length); + } + + return args.join(', '); + } + + for (var prop in originalFn) { + if (prop === 'and' || prop === 'calls') { + throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object being spied upon'); + } + + wrapper[prop] = originalFn[prop]; + } + + wrapper.and = spyStrategy; + wrapper.calls = callTracker; + + return wrapper; + } + + return Spy; +}; + getJasmineRequireObj().SpyRegistry = function(j$) { var getErrorMsg = j$.formatErrorMsg('', 'spyOn(, )'); diff --git a/spec/core/SpySpec.js b/spec/core/SpySpec.js index a5f9105e..e472aa9c 100644 --- a/spec/core/SpySpec.js +++ b/spec/core/SpySpec.js @@ -57,6 +57,24 @@ describe('Spies', function () { expect(trackSpy.calls.mostRecent().args[0].returnValue).toEqual("return value"); }); + + it("preserves arity of original function", function () { + var functions = [ + function nullary () {}, + function unary (arg) {}, + function binary (arg1, arg2) {}, + function ternary (arg1, arg2, arg3) {}, + function quaternary (arg1, arg2, arg3, arg4) {}, + function quinary (arg1, arg2, arg3, arg4, arg5) {}, + function senary (arg1, arg2, arg3, arg4, arg5, arg6) {} + ]; + + functions.forEach(function (someFunction, arity) { + var spy = jasmineUnderTest.createSpy(someFunction.name, someFunction); + + expect(spy.length).toEqual(arity); + }); + }); }); describe("createSpyObj", function() { diff --git a/src/core/Spy.js b/src/core/Spy.js new file mode 100644 index 00000000..a2a09b33 --- /dev/null +++ b/src/core/Spy.js @@ -0,0 +1,58 @@ +getJasmineRequireObj().Spy = function (j$) { + + function Spy(name, originalFn) { + var args = buildArgs(), + /*`eval` is the only option to preserve both this and context: + - former is needed to work as expected with methods, + - latter is needed to access real spy function and allows to reduce eval'ed code to absolute minimum + More explanation here (look at comments): http://www.bennadel.com/blog/1909-javascript-function-constructor-does-not-create-a-closure.htm + */ + /* jshint evil: true */ + wrapper = eval('(function (' + args + ') { return spy.apply(this, Array.prototype.slice.call(arguments)); })'), + spyStrategy = new j$.SpyStrategy({ + name: name, + fn: originalFn, + getSpy: function () { + return wrapper; + } + }), + callTracker = new j$.CallTracker(), + spy = function () { + var callData = { + object: this, + args: Array.prototype.slice.apply(arguments) + }; + + callTracker.track(callData); + var returnValue = spyStrategy.exec.apply(this, arguments); + callData.returnValue = returnValue; + + return returnValue; + }; + + function buildArgs() { + var args = []; + + while (originalFn instanceof Function && args.length < originalFn.length) { + args.push('arg' + args.length); + } + + return args.join(', '); + } + + for (var prop in originalFn) { + if (prop === 'and' || prop === 'calls') { + throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object being spied upon'); + } + + wrapper[prop] = originalFn[prop]; + } + + wrapper.and = spyStrategy; + wrapper.calls = callTracker; + + return wrapper; + } + + return Spy; +}; diff --git a/src/core/base.js b/src/core/base.js index 27f0069d..75a8a0f8 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -71,38 +71,7 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { }; j$.createSpy = function(name, originalFn) { - - var spyStrategy = new j$.SpyStrategy({ - name: name, - fn: originalFn, - getSpy: function() { return spy; } - }), - callTracker = new j$.CallTracker(), - spy = function() { - var callData = { - object: this, - args: Array.prototype.slice.apply(arguments) - }; - - callTracker.track(callData); - var returnValue = spyStrategy.exec.apply(this, arguments); - callData.returnValue = returnValue; - - return returnValue; - }; - - for (var prop in originalFn) { - if (prop === 'and' || prop === 'calls') { - throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object being spied upon'); - } - - spy[prop] = originalFn[prop]; - } - - spy.and = spyStrategy; - spy.calls = callTracker; - - return spy; + return j$.Spy(name, originalFn); }; j$.isSpy = function(putativeSpy) { diff --git a/src/core/requireCore.js b/src/core/requireCore.js index 28cd2914..08660781 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -44,6 +44,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) { j$.QueueRunner = jRequire.QueueRunner(j$); j$.ReportDispatcher = jRequire.ReportDispatcher(); j$.Spec = jRequire.Spec(j$); + j$.Spy = jRequire.Spy(j$); j$.SpyRegistry = jRequire.SpyRegistry(j$); j$.SpyStrategy = jRequire.SpyStrategy(j$); j$.StringMatching = jRequire.StringMatching(j$);