diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 6d0f4c95..fbd9dd17 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2008-2017 Pivotal Labs +Copyright (c) 2008-2018 Pivotal Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -4937,7 +4937,7 @@ getJasmineRequireObj().Spy = function (j$) { wrapper = makeFunc(numArgs, function () { return spy.apply(this, Array.prototype.slice.call(arguments)); }), - spyStrategy = new j$.SpyStrategy({ + strategyDispatcher = new SpyStrategyDispatcher({ name: name, fn: originalFn, getSpy: function () { @@ -4959,7 +4959,7 @@ getJasmineRequireObj().Spy = function (j$) { }; callTracker.track(callData); - var returnValue = spyStrategy.exec(this, arguments); + var returnValue = strategyDispatcher.exec(this, arguments); callData.returnValue = returnValue; return returnValue; @@ -4988,12 +4988,95 @@ getJasmineRequireObj().Spy = function (j$) { wrapper[prop] = originalFn[prop]; } - wrapper.and = spyStrategy; + /** + * @member {SpyStrategy} - Accesses the default strategy for the spy. This strategy will be used + * whenever the spy is called with arguments that don't match any strategy + * created with {@link Spy#withArgs}. + * @name Spy#and + * @example + * spyOn(someObj, 'func').and.returnValue(42); + */ + wrapper.and = strategyDispatcher.and; + /** + * Specifies a strategy to be used for calls to the spy that have the + * specified arguments. + * @name Spy#withArgs + * @function + * @param {...*} args - The arguments to match + * @type {SpyStrategy} + * @example + * spyOn(someObj, 'func').withArgs(1, 2, 3).and.returnValue(42); + * someObj.func(1, 2, 3); // returns 42 + */ + wrapper.withArgs = function() { + return strategyDispatcher.withArgs.apply(strategyDispatcher, arguments); + }; wrapper.calls = callTracker; return wrapper; } + + function SpyStrategyDispatcher(strategyArgs) { + var baseStrategy = new j$.SpyStrategy(strategyArgs); + var argsStrategies = new StrategyDict(function() { + return new j$.SpyStrategy(strategyArgs); + }); + + this.and = baseStrategy; + + this.exec = function(spy, args) { + var strategy = argsStrategies.get(args); + + if (!strategy) { + if (argsStrategies.any() && !baseStrategy.isConfigured()) { + throw new Error('Spy \'' + strategyArgs.name + '\' receieved a call with arguments ' + j$.pp(Array.prototype.slice.call(args)) + ' but all configured strategies specify other arguments.'); + } else { + strategy = baseStrategy; + } + } + + return strategy.exec(spy, args); + }; + + this.withArgs = function() { + return { and: argsStrategies.getOrCreate(arguments) }; + }; + } + + function StrategyDict(strategyFactory) { + this.strategies = []; + this.strategyFactory = strategyFactory; + } + + StrategyDict.prototype.any = function() { + return this.strategies.length > 0; + }; + + StrategyDict.prototype.getOrCreate = function(args) { + var strategy = this.get(args); + + if (!strategy) { + strategy = this.strategyFactory(); + this.strategies.push({ + args: args, + strategy: strategy + }); + } + + return strategy; + }; + + StrategyDict.prototype.get = function(args) { + var i; + + for (i = 0; i < this.strategies.length; i++) { + if (j$.matchersUtil.equals(args, this.strategies[i].args)) { + return this.strategies[i].strategy; + } + } + }; + return Spy; }; @@ -5132,26 +5215,26 @@ getJasmineRequireObj().SpyRegistry = function(j$) { getJasmineRequireObj().SpyStrategy = function(j$) { /** - * @namespace Spy#and + * @interface SpyStrategy */ function SpyStrategy(options) { options = options || {}; /** * Get the identifying information for the spy. - * @name Spy#and#identity + * @name SpyStrategy#identity * @member * @type {String} */ this.identity = options.name || 'unknown'; this.originalFn = options.fn || function() {}; this.getSpy = options.getSpy || function() {}; - this.plan = function() {}; + this.plan = this._defaultPlan = function() {}; } /** * Execute the current spy strategy. - * @name Spy#and#exec + * @name SpyStrategy#exec * @function */ SpyStrategy.prototype.exec = function(context, args) { @@ -5160,7 +5243,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to call through to the real implementation when invoked. - * @name Spy#and#callThrough + * @name SpyStrategy#callThrough * @function */ SpyStrategy.prototype.callThrough = function() { @@ -5170,7 +5253,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to return the value when invoked. - * @name Spy#and#returnValue + * @name SpyStrategy#returnValue * @function * @param {*} value The value to return. */ @@ -5183,7 +5266,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to return one of the specified values (sequentially) each time the spy is invoked. - * @name Spy#and#returnValues + * @name SpyStrategy#returnValues * @function * @param {...*} values - Values to be returned on subsequent calls to the spy. */ @@ -5197,7 +5280,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to throw an error when invoked. - * @name Spy#and#throwError + * @name SpyStrategy#throwError * @function * @param {Error|String} something Thing to throw */ @@ -5211,7 +5294,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to call a fake implementation when invoked. - * @name Spy#and#callFake + * @name SpyStrategy#callFake * @function * @param {Function} fn The function to invoke with the passed parameters. */ @@ -5225,7 +5308,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to do nothing when invoked. This is the default. - * @name Spy#and#stub + * @name SpyStrategy#stub * @function */ SpyStrategy.prototype.stub = function(fn) { @@ -5233,6 +5316,10 @@ getJasmineRequireObj().SpyStrategy = function(j$) { return this.getSpy(); }; + SpyStrategy.prototype.isConfigured = function() { + return this.plan !== this._defaultPlan; + }; + return SpyStrategy; }; diff --git a/spec/core/SpySpec.js b/spec/core/SpySpec.js index f3266ae5..562f684b 100644 --- a/spec/core/SpySpec.js +++ b/spec/core/SpySpec.js @@ -124,4 +124,48 @@ describe('Spies', function () { }).toThrow("createSpyObj requires a non-empty array or object of method names to create spies for"); }); }); + + it("can use different strategies for different arguments", function() { + var spy = jasmineUnderTest.createSpy('foo'); + spy.and.returnValue(42); + spy.withArgs('baz', 'grault').and.returnValue(-1); + spy.withArgs('thud').and.returnValue('bob'); + + expect(spy('foo')).toEqual(42); + expect(spy('baz', 'grault')).toEqual(-1); + expect(spy('thud')).toEqual('bob'); + expect(spy('baz', 'grault', 'waldo')).toEqual(42); + }); + + it("uses custom equality testers when selecting a strategy", function() { + var spy = jasmineUnderTest.createSpy('foo'); + spy.and.returnValue(42); + spy.withArgs(jasmineUnderTest.any(String)).and.returnValue(-1); + + expect(spy('foo')).toEqual(-1); + expect(spy({})).toEqual(42); + }); + + it("can reconfigure an argument-specific strategy", function() { + var spy = jasmineUnderTest.createSpy('foo'); + spy.withArgs('foo').and.returnValue(42); + spy.withArgs('foo').and.returnValue(17); + expect(spy('foo')).toEqual(17); + }); + + describe("When withArgs is used without a base strategy", function() { + it("uses the matching strategy", function() { + var spy = jasmineUnderTest.createSpy('foo'); + spy.withArgs('baz').and.returnValue(-1); + + expect(spy('baz')).toEqual(-1); + }); + + it("throws if the args don't match", function() { + var spy = jasmineUnderTest.createSpy('foo'); + spy.withArgs('bar').and.returnValue(-1); + + expect(function() { spy('baz', {qux: 42}); }).toThrowError('Spy \'foo\' receieved a call with arguments [ \'baz\', Object({ qux: 42 }) ] but all configured strategies specify other arguments.'); + }); + }); }); diff --git a/src/core/Spy.js b/src/core/Spy.js index 97fb50ab..c99ff191 100644 --- a/src/core/Spy.js +++ b/src/core/Spy.js @@ -18,7 +18,7 @@ getJasmineRequireObj().Spy = function (j$) { wrapper = makeFunc(numArgs, function () { return spy.apply(this, Array.prototype.slice.call(arguments)); }), - spyStrategy = new j$.SpyStrategy({ + strategyDispatcher = new SpyStrategyDispatcher({ name: name, fn: originalFn, getSpy: function () { @@ -40,7 +40,7 @@ getJasmineRequireObj().Spy = function (j$) { }; callTracker.track(callData); - var returnValue = spyStrategy.exec(this, arguments); + var returnValue = strategyDispatcher.exec(this, arguments); callData.returnValue = returnValue; return returnValue; @@ -69,11 +69,94 @@ getJasmineRequireObj().Spy = function (j$) { wrapper[prop] = originalFn[prop]; } - wrapper.and = spyStrategy; + /** + * @member {SpyStrategy} - Accesses the default strategy for the spy. This strategy will be used + * whenever the spy is called with arguments that don't match any strategy + * created with {@link Spy#withArgs}. + * @name Spy#and + * @example + * spyOn(someObj, 'func').and.returnValue(42); + */ + wrapper.and = strategyDispatcher.and; + /** + * Specifies a strategy to be used for calls to the spy that have the + * specified arguments. + * @name Spy#withArgs + * @function + * @param {...*} args - The arguments to match + * @type {SpyStrategy} + * @example + * spyOn(someObj, 'func').withArgs(1, 2, 3).and.returnValue(42); + * someObj.func(1, 2, 3); // returns 42 + */ + wrapper.withArgs = function() { + return strategyDispatcher.withArgs.apply(strategyDispatcher, arguments); + }; wrapper.calls = callTracker; return wrapper; } + + function SpyStrategyDispatcher(strategyArgs) { + var baseStrategy = new j$.SpyStrategy(strategyArgs); + var argsStrategies = new StrategyDict(function() { + return new j$.SpyStrategy(strategyArgs); + }); + + this.and = baseStrategy; + + this.exec = function(spy, args) { + var strategy = argsStrategies.get(args); + + if (!strategy) { + if (argsStrategies.any() && !baseStrategy.isConfigured()) { + throw new Error('Spy \'' + strategyArgs.name + '\' receieved a call with arguments ' + j$.pp(Array.prototype.slice.call(args)) + ' but all configured strategies specify other arguments.'); + } else { + strategy = baseStrategy; + } + } + + return strategy.exec(spy, args); + }; + + this.withArgs = function() { + return { and: argsStrategies.getOrCreate(arguments) }; + }; + } + + function StrategyDict(strategyFactory) { + this.strategies = []; + this.strategyFactory = strategyFactory; + } + + StrategyDict.prototype.any = function() { + return this.strategies.length > 0; + }; + + StrategyDict.prototype.getOrCreate = function(args) { + var strategy = this.get(args); + + if (!strategy) { + strategy = this.strategyFactory(); + this.strategies.push({ + args: args, + strategy: strategy + }); + } + + return strategy; + }; + + StrategyDict.prototype.get = function(args) { + var i; + + for (i = 0; i < this.strategies.length; i++) { + if (j$.matchersUtil.equals(args, this.strategies[i].args)) { + return this.strategies[i].strategy; + } + } + }; + return Spy; }; diff --git a/src/core/SpyStrategy.js b/src/core/SpyStrategy.js index 773957e2..643fe73f 100644 --- a/src/core/SpyStrategy.js +++ b/src/core/SpyStrategy.js @@ -1,26 +1,26 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** - * @namespace Spy#and + * @interface SpyStrategy */ function SpyStrategy(options) { options = options || {}; /** * Get the identifying information for the spy. - * @name Spy#and#identity + * @name SpyStrategy#identity * @member * @type {String} */ this.identity = options.name || 'unknown'; this.originalFn = options.fn || function() {}; this.getSpy = options.getSpy || function() {}; - this.plan = function() {}; + this.plan = this._defaultPlan = function() {}; } /** * Execute the current spy strategy. - * @name Spy#and#exec + * @name SpyStrategy#exec * @function */ SpyStrategy.prototype.exec = function(context, args) { @@ -29,7 +29,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to call through to the real implementation when invoked. - * @name Spy#and#callThrough + * @name SpyStrategy#callThrough * @function */ SpyStrategy.prototype.callThrough = function() { @@ -39,7 +39,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to return the value when invoked. - * @name Spy#and#returnValue + * @name SpyStrategy#returnValue * @function * @param {*} value The value to return. */ @@ -52,7 +52,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to return one of the specified values (sequentially) each time the spy is invoked. - * @name Spy#and#returnValues + * @name SpyStrategy#returnValues * @function * @param {...*} values - Values to be returned on subsequent calls to the spy. */ @@ -66,7 +66,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to throw an error when invoked. - * @name Spy#and#throwError + * @name SpyStrategy#throwError * @function * @param {Error|String} something Thing to throw */ @@ -80,7 +80,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to call a fake implementation when invoked. - * @name Spy#and#callFake + * @name SpyStrategy#callFake * @function * @param {Function} fn The function to invoke with the passed parameters. */ @@ -94,7 +94,7 @@ getJasmineRequireObj().SpyStrategy = function(j$) { /** * Tell the spy to do nothing when invoked. This is the default. - * @name Spy#and#stub + * @name SpyStrategy#stub * @function */ SpyStrategy.prototype.stub = function(fn) { @@ -102,5 +102,9 @@ getJasmineRequireObj().SpyStrategy = function(j$) { return this.getSpy(); }; + SpyStrategy.prototype.isConfigured = function() { + return this.plan !== this._defaultPlan; + }; + return SpyStrategy; };