diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index f6e2d8dc..4504ab8a 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -90,6 +90,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$ ); j$.ReportDispatcher = jRequire.ReportDispatcher(j$); + j$.RunnableResources = jRequire.RunnableResources(j$); j$.Spec = jRequire.Spec(j$); j$.Spy = jRequire.Spy(j$); j$.SpyFactory = jRequire.SpyFactory(j$); @@ -1112,7 +1113,10 @@ getJasmineRequireObj().Env = function(j$) { new j$.MockDate(global) ); - const runnableResources = {}; + const runnableResources = new j$.RunnableResources(function() { + const r = currentRunnable(); + return r ? r.id : null; + }); let topSuite; let currentSpec = null; @@ -1122,7 +1126,6 @@ getJasmineRequireObj().Env = function(j$) { let hasFailures = false; let deprecator; let reporter; - let spyRegistry; /** * This represents the available options to configure Jasmine. @@ -1316,74 +1319,27 @@ getJasmineRequireObj().Env = function(j$) { }; this.setDefaultSpyStrategy = function(defaultStrategyFn) { - if (!currentRunnable()) { - throw new Error( - 'Default spy strategy must be set in a before function or a spec' - ); - } - runnableResources[ - currentRunnable().id - ].defaultStrategyFn = defaultStrategyFn; + runnableResources.setDefaultSpyStrategy(defaultStrategyFn); }; this.addSpyStrategy = function(name, fn) { - if (!currentRunnable()) { - throw new Error( - 'Custom spy strategies must be added in a before function or a spec' - ); - } - runnableResources[currentRunnable().id].customSpyStrategies[name] = fn; + runnableResources.customSpyStrategies()[name] = fn; }; this.addCustomEqualityTester = function(tester) { - if (!currentRunnable()) { - throw new Error( - 'Custom Equalities must be added in a before function or a spec' - ); - } - runnableResources[currentRunnable().id].customEqualityTesters.push( - tester - ); + runnableResources.customEqualityTesters().push(tester); }; this.addMatchers = function(matchersToAdd) { - if (!currentRunnable()) { - throw new Error( - 'Matchers must be added in a before function or a spec' - ); - } - const customMatchers = - runnableResources[currentRunnable().id].customMatchers; - - for (const matcherName in matchersToAdd) { - customMatchers[matcherName] = matchersToAdd[matcherName]; - } + runnableResources.addCustomMatchers(matchersToAdd); }; this.addAsyncMatchers = function(matchersToAdd) { - if (!currentRunnable()) { - throw new Error( - 'Async Matchers must be added in a before function or a spec' - ); - } - const customAsyncMatchers = - runnableResources[currentRunnable().id].customAsyncMatchers; - - for (const matcherName in matchersToAdd) { - customAsyncMatchers[matcherName] = matchersToAdd[matcherName]; - } + runnableResources.addCustomAsyncMatchers(matchersToAdd); }; this.addCustomObjectFormatter = function(formatter) { - if (!currentRunnable()) { - throw new Error( - 'Custom object formatters must be added in a before function or a spec' - ); - } - - runnableResources[currentRunnable().id].customObjectFormatters.push( - formatter - ); + runnableResources.customObjectFormatters().push(formatter); }; j$.Expectation.addCoreMatchers(j$.matchers); @@ -1401,31 +1357,10 @@ getJasmineRequireObj().Env = function(j$) { return 'suite' + nextSuiteId++; } - function makePrettyPrinter() { - const customObjectFormatters = - runnableResources[currentRunnable().id].customObjectFormatters; - return j$.makePrettyPrinter(customObjectFormatters); - } - - function makeMatchersUtil() { - const cr = currentRunnable(); - - if (cr) { - const customEqualityTesters = - runnableResources[cr.id].customEqualityTesters; - return new j$.MatchersUtil({ - customTesters: customEqualityTesters, - pp: makePrettyPrinter() - }); - } else { - return new j$.MatchersUtil({ pp: j$.basicPrettyPrinter_ }); - } - } - const expectationFactory = function(actual, spec) { return j$.Expectation.factory({ - matchersUtil: makeMatchersUtil(), - customMatchers: runnableResources[spec.id].customMatchers, + matchersUtil: runnableResources.makeMatchersUtil(), + customMatchers: runnableResources.customMatchers(), actual: actual, addExpectationResult: addExpectationResult }); @@ -1502,8 +1437,8 @@ getJasmineRequireObj().Env = function(j$) { const asyncExpectationFactory = function(actual, spec, runableType) { return j$.Expectation.asyncFactory({ - matchersUtil: makeMatchersUtil(), - customAsyncMatchers: runnableResources[spec.id].customAsyncMatchers, + matchersUtil: runnableResources.makeMatchersUtil(), + customAsyncMatchers: runnableResources.customAsyncMatchers(), actual: actual, addExpectationResult: addExpectationResult }); @@ -1523,45 +1458,6 @@ getJasmineRequireObj().Env = function(j$) { return asyncExpectationFactory(actual, suite, 'Spec'); }; - function defaultResourcesForRunnable(id, parentRunnableId) { - const resources = { - spies: [], - customEqualityTesters: [], - customMatchers: {}, - customAsyncMatchers: {}, - customSpyStrategies: {}, - defaultStrategyFn: undefined, - customObjectFormatters: [] - }; - - if (runnableResources[parentRunnableId]) { - resources.customEqualityTesters = j$.util.clone( - runnableResources[parentRunnableId].customEqualityTesters - ); - resources.customMatchers = j$.util.clone( - runnableResources[parentRunnableId].customMatchers - ); - resources.customAsyncMatchers = j$.util.clone( - runnableResources[parentRunnableId].customAsyncMatchers - ); - resources.customObjectFormatters = j$.util.clone( - runnableResources[parentRunnableId].customObjectFormatters - ); - resources.customSpyStrategies = j$.util.clone( - runnableResources[parentRunnableId].customSpyStrategies - ); - resources.defaultStrategyFn = - runnableResources[parentRunnableId].defaultStrategyFn; - } - - runnableResources[id] = resources; - } - - function clearResourcesForRunnable(id) { - spyRegistry.clearSpies(); - delete runnableResources[id]; - } - function beforeAndAfterFns(targetSuite) { return function() { let befores = [], @@ -1787,7 +1683,7 @@ getJasmineRequireObj().Env = function(j$) { topSuite.reset(); } this._executedBefore = true; - defaultResourcesForRunnable(topSuite.id); + runnableResources.initForRunnable(topSuite.id); installGlobalErrors(); if (!runnablesToRun) { @@ -1810,7 +1706,7 @@ getJasmineRequireObj().Env = function(j$) { failSpecWithNoExpectations: config.failSpecWithNoExpectations, nodeStart: function(suite, next) { currentlyExecutingSuites.push(suite); - defaultResourcesForRunnable(suite.id, suite.parentSuite.id); + runnableResources.initForRunnable(suite.id, suite.parentSuite.id); reporter.suiteStarted(suite.result, next); suite.startTimer(); }, @@ -1819,7 +1715,7 @@ getJasmineRequireObj().Env = function(j$) { throw new Error('Tried to complete the wrong suite'); } - clearResourcesForRunnable(suite.id); + runnableResources.clearForRunnable(suite.id); currentlyExecutingSuites.pop(); if (result.status === 'failed') { @@ -1884,7 +1780,7 @@ getJasmineRequireObj().Env = function(j$) { await reportChildrenOfBeforeAllFailure(topSuite); } - clearResourcesForRunnable(topSuite.id); + runnableResources.clearForRunnable(topSuite.id); currentlyExecutingSuites.pop(); let overallStatus, incompleteReason; @@ -2008,42 +1904,6 @@ getJasmineRequireObj().Env = function(j$) { reporter.clearReporters(); }; - const spyFactory = new j$.SpyFactory( - function getCustomStrategies() { - const runnable = currentRunnable(); - - if (runnable) { - return runnableResources[runnable.id].customSpyStrategies; - } - - return {}; - }, - function getDefaultStrategyFn() { - const runnable = currentRunnable(); - - if (runnable) { - return runnableResources[runnable.id].defaultStrategyFn; - } - - return undefined; - }, - makeMatchersUtil - ); - - spyRegistry = new j$.SpyRegistry({ - currentSpies: function() { - if (!currentRunnable()) { - throw new Error( - 'Spies must be created in a before function or a spec' - ); - } - return runnableResources[currentRunnable().id].spies; - }, - createSpy: function(name, originalFn) { - return self.createSpy(name, originalFn); - } - }); - /** * Configures whether Jasmine should allow the same function to be spied on * more than once during the execution of a spec. By default, spying on @@ -2054,32 +1914,40 @@ getJasmineRequireObj().Env = function(j$) { * @param {boolean} allow Whether to allow respying */ this.allowRespy = function(allow) { - spyRegistry.allowRespy(allow); + runnableResources.spyRegistry.allowRespy(allow); }; this.spyOn = function() { - return spyRegistry.spyOn.apply(spyRegistry, arguments); + return runnableResources.spyRegistry.spyOn.apply( + runnableResources.spyRegistry, + arguments + ); }; this.spyOnProperty = function() { - return spyRegistry.spyOnProperty.apply(spyRegistry, arguments); + return runnableResources.spyRegistry.spyOnProperty.apply( + runnableResources.spyRegistry, + arguments + ); }; this.spyOnAllFunctions = function() { - return spyRegistry.spyOnAllFunctions.apply(spyRegistry, arguments); + return runnableResources.spyRegistry.spyOnAllFunctions.apply( + runnableResources.spyRegistry, + arguments + ); }; this.createSpy = function(name, originalFn) { - if (arguments.length === 1 && j$.isFunction_(name)) { - originalFn = name; - name = originalFn.name; - } - - return spyFactory.createSpy(name, originalFn); + return runnableResources.spyFactory.createSpy(name, originalFn); }; this.createSpyObj = function(baseName, methodNames, propertyNames) { - return spyFactory.createSpyObj(baseName, methodNames, propertyNames); + return runnableResources.spyFactory.createSpyObj( + baseName, + methodNames, + propertyNames + ); }; function ensureIsFunction(fn, caller) { @@ -2234,7 +2102,7 @@ getJasmineRequireObj().Env = function(j$) { return spec; function specResultCallback(result, next) { - clearResourcesForRunnable(spec.id); + runnableResources.clearForRunnable(spec.id); currentSpec = null; if (result.status === 'failed') { @@ -2246,7 +2114,7 @@ getJasmineRequireObj().Env = function(j$) { function specStarted(spec, next) { currentSpec = spec; - defaultResourcesForRunnable(spec.id, suite.id); + runnableResources.initForRunnable(spec.id, suite.id); reporter.specStarted(spec.result, next); } }; @@ -2468,7 +2336,8 @@ getJasmineRequireObj().Env = function(j$) { message += error; } else { // pretty print all kind of objects. This includes arrays. - message += makePrettyPrinter()(error); + const pp = runnableResources.makePrettyPrinter(); + message += pp(error); } } @@ -8664,6 +8533,161 @@ getJasmineRequireObj().interface = function(jasmine, env) { return jasmineInterface; }; +getJasmineRequireObj().RunnableResources = function(j$) { + class RunnableResources { + constructor(getCurrentRunnableId) { + this.byRunnableId_ = {}; + this.getCurrentRunnableId_ = getCurrentRunnableId; + + this.spyFactory = new j$.SpyFactory( + () => { + if (this.getCurrentRunnableId_()) { + return this.customSpyStrategies(); + } else { + return {}; + } + }, + () => this.defaultSpyStrategy(), + () => this.makeMatchersUtil() + ); + + this.spyRegistry = new j$.SpyRegistry({ + currentSpies: () => this.spies(), + createSpy: (name, originalFn) => + this.spyFactory.createSpy(name, originalFn) + }); + } + + initForRunnable(runnableId, parentId) { + const newRes = (this.byRunnableId_[runnableId] = { + customEqualityTesters: [], + customMatchers: {}, + customAsyncMatchers: {}, + customSpyStrategies: {}, + customObjectFormatters: [], + defaultSpyStrategy: undefined, + spies: [] + }); + + const parentRes = this.byRunnableId_[parentId]; + + if (parentRes) { + newRes.defaultSpyStrategy = parentRes.defaultSpyStrategy; + const toClone = [ + 'customEqualityTesters', + 'customMatchers', + 'customAsyncMatchers', + 'customObjectFormatters', + 'customSpyStrategies' + ]; + + for (const k of toClone) { + newRes[k] = j$.util.clone(parentRes[k]); + } + } + } + + clearForRunnable(runnableId) { + this.spyRegistry.clearSpies(); + delete this.byRunnableId_[runnableId]; + } + + spies() { + return this.forCurrentRunnable_( + 'Spies must be created in a before function or a spec' + ).spies; + } + + defaultSpyStrategy() { + if (!this.getCurrentRunnableId_()) { + return undefined; + } + + return this.byRunnableId_[this.getCurrentRunnableId_()] + .defaultSpyStrategy; + } + + setDefaultSpyStrategy(fn) { + this.forCurrentRunnable_( + 'Default spy strategy must be set in a before function or a spec' + ).defaultSpyStrategy = fn; + } + + customSpyStrategies() { + return this.forCurrentRunnable_( + 'Custom spy strategies must be added in a before function or a spec' + ).customSpyStrategies; + } + + customEqualityTesters() { + return this.forCurrentRunnable_( + 'Custom Equalities must be added in a before function or a spec' + ).customEqualityTesters; + } + + customMatchers() { + return this.forCurrentRunnable_( + 'Matchers must be added in a before function or a spec' + ).customMatchers; + } + + addCustomMatchers(matchersToAdd) { + const matchers = this.customMatchers(); + + for (const name in matchersToAdd) { + matchers[name] = matchersToAdd[name]; + } + } + + customAsyncMatchers() { + return this.forCurrentRunnable_( + 'Async Matchers must be added in a before function or a spec' + ).customAsyncMatchers; + } + + addCustomAsyncMatchers(matchersToAdd) { + const matchers = this.customAsyncMatchers(); + + for (const name in matchersToAdd) { + matchers[name] = matchersToAdd[name]; + } + } + + customObjectFormatters() { + return this.forCurrentRunnable_( + 'Custom object formatters must be added in a before function or a spec' + ).customObjectFormatters; + } + + makePrettyPrinter() { + return j$.makePrettyPrinter(this.customObjectFormatters()); + } + + makeMatchersUtil() { + if (this.getCurrentRunnableId_()) { + return new j$.MatchersUtil({ + customTesters: this.customEqualityTesters(), + pp: this.makePrettyPrinter() + }); + } else { + return new j$.MatchersUtil({ pp: j$.basicPrettyPrinter_ }); + } + } + + forCurrentRunnable_(errorMsg) { + const resources = this.byRunnableId_[this.getCurrentRunnableId_()]; + + if (!resources && errorMsg) { + throw new Error(errorMsg); + } + + return resources; + } + } + + return RunnableResources; +}; + getJasmineRequireObj().SkipAfterBeforeAllErrorPolicy = function(j$) { function SkipAfterBeforeAllErrorPolicy(queueableFns) { this.queueableFns_ = queueableFns; @@ -8923,6 +8947,11 @@ getJasmineRequireObj().SpyFactory = function(j$) { getMatchersUtil ) { this.createSpy = function(name, originalFn) { + if (j$.isFunction_(name) && originalFn === undefined) { + originalFn = name; + name = originalFn.name; + } + return j$.Spy(name, getMatchersUtil(), { originalFn, customStrategies: getCustomStrategies(), diff --git a/spec/core/RunnableResourcesSpec.js b/spec/core/RunnableResourcesSpec.js new file mode 100644 index 00000000..a33f95f6 --- /dev/null +++ b/spec/core/RunnableResourcesSpec.js @@ -0,0 +1,503 @@ +describe('RunnableResources', function() { + describe('#spies', function() { + behavesLikeAPerRunnableMutableArray( + 'spies', + 'Spies must be created in a before function or a spec', + false + ); + }); + + describe('#customSpyStrategies', function() { + behavesLikeAPerRunnableMutableObject( + 'customSpyStrategies', + 'Custom spy strategies must be added in a before function or a spec' + ); + }); + + describe('#customEqualityTesters', function() { + behavesLikeAPerRunnableMutableArray( + 'customEqualityTesters', + 'Custom Equalities must be added in a before function or a spec' + ); + }); + + describe('#customObjectFormatters', function() { + behavesLikeAPerRunnableMutableArray( + 'customObjectFormatters', + 'Custom object formatters must be added in a before function or a spec' + ); + }); + + describe('#customMatchers', function() { + behavesLikeAPerRunnableMutableObject( + 'customMatchers', + 'Matchers must be added in a before function or a spec' + ); + }); + + describe('#addCustomMatchers', function() { + it("adds all properties to the current runnable's matchers", function() { + const currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + + function toBeFoo() {} + function toBeBar() {} + function toBeBaz() {} + + runnableResources.addCustomMatchers({ toBeFoo }); + expect(runnableResources.customMatchers()).toEqual({ toBeFoo }); + + runnableResources.addCustomMatchers({ toBeBar, toBeBaz }); + expect(runnableResources.customMatchers()).toEqual({ + toBeFoo, + toBeBar, + toBeBaz + }); + }); + }); + + describe('#customAsyncMatchers', function() { + behavesLikeAPerRunnableMutableObject( + 'customAsyncMatchers', + 'Async Matchers must be added in a before function or a spec' + ); + }); + + describe('#addCustomAsyncMatchers', function() { + it("adds all properties to the current runnable's matchers", function() { + const currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + + function toBeFoo() {} + function toBeBar() {} + function toBeBaz() {} + + runnableResources.addCustomAsyncMatchers({ toBeFoo }); + expect(runnableResources.customAsyncMatchers()).toEqual({ toBeFoo }); + + runnableResources.addCustomAsyncMatchers({ toBeBar, toBeBaz }); + expect(runnableResources.customAsyncMatchers()).toEqual({ + toBeFoo, + toBeBar, + toBeBaz + }); + }); + }); + + describe('#defaultSpyStrategy', function() { + it('returns undefined for a newly initialized resource', function() { + let currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + + expect(runnableResources.defaultSpyStrategy()).toBeUndefined(); + }); + + it('returns the value previously set by #setDefaultSpyStrategy', function() { + let currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + const fn = () => {}; + runnableResources.setDefaultSpyStrategy(fn); + + expect(runnableResources.defaultSpyStrategy()).toBe(fn); + }); + + it('is per-runnable', function() { + let currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + runnableResources.setDefaultSpyStrategy(() => {}); + currentRunnableId = 2; + runnableResources.initForRunnable(2); + + expect(runnableResources.defaultSpyStrategy()).toBeUndefined(); + }); + + it('does not require a current runnable', function() { + const runnableResources = new jasmineUnderTest.RunnableResources( + () => null + ); + expect(runnableResources.defaultSpyStrategy()).toBeUndefined(); + }); + + it("inherits the parent runnable's value", function() { + let currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + const fn = () => {}; + runnableResources.setDefaultSpyStrategy(fn); + currentRunnableId = 2; + runnableResources.initForRunnable(2, 1); + + expect(runnableResources.defaultSpyStrategy()).toBe(fn); + }); + }); + + describe('#setDefaultSpyStrategy', function() { + it('throws a user-facing error when there is no current runnable', function() { + const runnableResources = new jasmineUnderTest.RunnableResources( + () => null + ); + expect(function() { + runnableResources.setDefaultSpyStrategy(); + }).toThrowError( + 'Default spy strategy must be set in a before function or a spec' + ); + }); + }); + + describe('#makePrettyPrinter', function() { + it('returns a pretty printer configured with the current customObjectFormatters', function() { + const runnableResources = new jasmineUnderTest.RunnableResources(() => 1); + runnableResources.initForRunnable(1); + function cof() {} + runnableResources.customObjectFormatters().push(cof); + spyOn(jasmineUnderTest, 'makePrettyPrinter').and.callThrough(); + const pp = runnableResources.makePrettyPrinter(); + + expect(jasmineUnderTest.makePrettyPrinter).toHaveBeenCalledOnceWith([ + cof + ]); + expect(pp).toBe( + jasmineUnderTest.makePrettyPrinter.calls.first().returnValue + ); + }); + }); + + describe('#makeMatchersUtil', function() { + describe('When there is a current runnable', function() { + it('returns a MatchersUtil configured with the current resources', function() { + const runnableResources = new jasmineUnderTest.RunnableResources( + () => 1 + ); + runnableResources.initForRunnable(1); + function cof() {} + runnableResources.customObjectFormatters().push(cof); + function ceq() {} + runnableResources.customEqualityTesters().push(ceq); + const expectedPP = {}; + const expectedMatchersUtil = {}; + spyOn(jasmineUnderTest, 'makePrettyPrinter').and.returnValue( + expectedPP + ); + spyOn(jasmineUnderTest, 'MatchersUtil').and.returnValue( + expectedMatchersUtil + ); + + const matchersUtil = runnableResources.makeMatchersUtil(); + + expect(matchersUtil).toBe(expectedMatchersUtil); + expect(jasmineUnderTest.makePrettyPrinter).toHaveBeenCalledOnceWith([ + cof + ]); + // We need === equality on the pp passed to MatchersUtil + expect(jasmineUnderTest.MatchersUtil).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + customTesters: [ceq] + }) + ); + expect(jasmineUnderTest.MatchersUtil.calls.argsFor(0)[0].pp).toBe( + expectedPP + ); + }); + }); + + describe('When there is no current runnable', function() { + it('returns a MatchersUtil configured with defaults', function() { + const runnableResources = new jasmineUnderTest.RunnableResources( + () => null + ); + const expectedMatchersUtil = {}; + spyOn(jasmineUnderTest, 'MatchersUtil').and.returnValue( + expectedMatchersUtil + ); + + const matchersUtil = runnableResources.makeMatchersUtil(); + + expect(matchersUtil).toBe(expectedMatchersUtil); + // We need === equality on the pp passed to MatchersUtil + expect(jasmineUnderTest.MatchersUtil).toHaveBeenCalledTimes(1); + expect(jasmineUnderTest.MatchersUtil.calls.argsFor(0)[0].pp).toBe( + jasmineUnderTest.basicPrettyPrinter_ + ); + expect( + jasmineUnderTest.MatchersUtil.calls.argsFor(0)[0].customTesters + ).toBeUndefined(); + }); + }); + }); + + describe('.spyFactory', function() { + describe('When there is no current runnable', function() { + it('is configured with default strategies and matchersUtil', function() { + const runnableResources = new jasmineUnderTest.RunnableResources( + () => null + ); + spyOn(jasmineUnderTest, 'Spy'); + const matchersUtil = {}; + spyOn(runnableResources, 'makeMatchersUtil').and.returnValue( + matchersUtil + ); + + runnableResources.spyFactory.createSpy('foo'); + + expect(jasmineUnderTest.Spy).toHaveBeenCalledWith( + 'foo', + is(matchersUtil), + jasmine.objectContaining({ + customStrategies: {}, + defaultStrategyFn: undefined + }) + ); + }); + }); + + describe('When there is a current runnable', function() { + it("is configured with the current runnable's strategies and matchersUtil", function() { + const runnableResources = new jasmineUnderTest.RunnableResources( + () => 1 + ); + runnableResources.initForRunnable(1); + function customStrategy() {} + function defaultStrategy() {} + runnableResources.customSpyStrategies().foo = customStrategy; + runnableResources.setDefaultSpyStrategy(defaultStrategy); + spyOn(jasmineUnderTest, 'Spy'); + const matchersUtil = {}; + spyOn(runnableResources, 'makeMatchersUtil').and.returnValue( + matchersUtil + ); + + runnableResources.spyFactory.createSpy('foo'); + + expect(jasmineUnderTest.Spy).toHaveBeenCalledWith( + 'foo', + is(matchersUtil), + jasmine.objectContaining({ + customStrategies: { foo: customStrategy }, + defaultStrategyFn: defaultStrategy + }) + ); + }); + }); + + function is(expected) { + return { + asymmetricMatch: function(actual) { + return actual === expected; + }, + jasmineToString: function(pp) { + return ''; + } + }; + } + }); + + describe('.spyRegistry', function() { + it("writes to the current runnable's spies", function() { + const runnableResources = new jasmineUnderTest.RunnableResources(() => 1); + runnableResources.initForRunnable(1); + function foo() {} + const spyObj = { foo }; + runnableResources.spyRegistry.spyOn(spyObj, 'foo'); + + expect(runnableResources.spies()).toEqual([ + jasmine.objectContaining({ + restoreObjectToOriginalState: jasmine.any(Function) + }) + ]); + expect(jasmineUnderTest.isSpy(spyObj.foo)).toBeTrue(); + + runnableResources.spyRegistry.clearSpies(); + expect(spyObj.foo).toBe(foo); + }); + }); + + describe('#clearForRunnable', function() { + it('removes resources for the specified runnable', function() { + const runnableResources = new jasmineUnderTest.RunnableResources(() => 1); + runnableResources.initForRunnable(1); + expect(function() { + runnableResources.spies(); + }).not.toThrow(); + runnableResources.clearForRunnable(1); + expect(function() { + runnableResources.spies(); + }).toThrowError('Spies must be created in a before function or a spec'); + }); + + it('clears spies', function() { + const runnableResources = new jasmineUnderTest.RunnableResources(() => 1); + runnableResources.initForRunnable(1); + function foo() {} + const spyObj = { foo }; + runnableResources.spyRegistry.spyOn(spyObj, 'foo'); + expect(spyObj.foo).not.toBe(foo); + + runnableResources.clearForRunnable(1); + expect(spyObj.foo).toBe(foo); + }); + + it('does not remove resources for other runnables', function() { + const runnableResources = new jasmineUnderTest.RunnableResources(() => 1); + runnableResources.initForRunnable(1); + function cof() {} + runnableResources.customObjectFormatters().push(cof); + runnableResources.clearForRunnable(2); + expect(runnableResources.customObjectFormatters()).toEqual([cof]); + }); + }); + + function behavesLikeAPerRunnableMutableArray( + methodName, + errorMsg, + inherits = true + ) { + it('is initially empty', function() { + const currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + + expect(runnableResources[methodName]()).toEqual([]); + }); + + it('is mutable', function() { + const currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + function newItem() {} + runnableResources[methodName]().push(newItem); + expect(runnableResources[methodName]()).toEqual([newItem]); + }); + + it('is per-runnable', function() { + let currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + runnableResources[methodName]().push(() => {}); + runnableResources.initForRunnable(2); + currentRunnableId = 2; + expect(runnableResources[methodName]()).toEqual([]); + }); + + it('throws a user-facing error when there is no current runnable', function() { + const runnableResources = new jasmineUnderTest.RunnableResources( + () => null + ); + expect(function() { + runnableResources[methodName](); + }).toThrowError(errorMsg); + }); + + if (inherits) { + it('inherits from the parent runnable', function() { + let currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + function parentItem() {} + runnableResources[methodName]().push(parentItem); + runnableResources.initForRunnable(2, 1); + currentRunnableId = 2; + function childItem() {} + runnableResources[methodName]().push(childItem); + expect(runnableResources[methodName]()).toEqual([ + parentItem, + childItem + ]); + + currentRunnableId = 1; + expect(runnableResources[methodName]()).toEqual([parentItem]); + }); + } + } + + function behavesLikeAPerRunnableMutableObject(methodName, errorMsg) { + it('is initially empty', function() { + const currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + + expect(runnableResources[methodName]()).toEqual({}); + }); + + it('is mutable', function() { + const currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + function newItem() {} + runnableResources[methodName]().foo = newItem; + expect(runnableResources[methodName]()).toEqual({ foo: newItem }); + }); + + it('is per-runnable', function() { + let currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + runnableResources[methodName]().foo = function() {}; + runnableResources.initForRunnable(2); + currentRunnableId = 2; + expect(runnableResources[methodName]()).toEqual({}); + }); + + it('throws a user-facing error when there is no current runnable', function() { + const runnableResources = new jasmineUnderTest.RunnableResources( + () => null + ); + expect(function() { + runnableResources[methodName](); + }).toThrowError(errorMsg); + }); + + it('inherits from the parent runnable', function() { + let currentRunnableId = 1; + const runnableResources = new jasmineUnderTest.RunnableResources( + () => currentRunnableId + ); + runnableResources.initForRunnable(1); + function parentItem() {} + runnableResources[methodName]().parentName = parentItem; + runnableResources.initForRunnable(2, 1); + currentRunnableId = 2; + function childItem() {} + runnableResources[methodName]().childName = childItem; + expect(runnableResources[methodName]()).toEqual({ + parentName: parentItem, + childName: childItem + }); + + currentRunnableId = 1; + expect(runnableResources[methodName]()).toEqual({ + parentName: parentItem + }); + }); + } +}); diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index 217488ee..1aa43fb6 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -1354,6 +1354,7 @@ describe('Env integration', function() { env.it('spec 1', secondSpec); }); + env.configure({ random: false }); await env.execute(); expect(firstSpec).toHaveBeenCalled(); diff --git a/src/core/Env.js b/src/core/Env.js index 3b8c2961..debe99e2 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -26,7 +26,10 @@ getJasmineRequireObj().Env = function(j$) { new j$.MockDate(global) ); - const runnableResources = {}; + const runnableResources = new j$.RunnableResources(function() { + const r = currentRunnable(); + return r ? r.id : null; + }); let topSuite; let currentSpec = null; @@ -36,7 +39,6 @@ getJasmineRequireObj().Env = function(j$) { let hasFailures = false; let deprecator; let reporter; - let spyRegistry; /** * This represents the available options to configure Jasmine. @@ -230,74 +232,27 @@ getJasmineRequireObj().Env = function(j$) { }; this.setDefaultSpyStrategy = function(defaultStrategyFn) { - if (!currentRunnable()) { - throw new Error( - 'Default spy strategy must be set in a before function or a spec' - ); - } - runnableResources[ - currentRunnable().id - ].defaultStrategyFn = defaultStrategyFn; + runnableResources.setDefaultSpyStrategy(defaultStrategyFn); }; this.addSpyStrategy = function(name, fn) { - if (!currentRunnable()) { - throw new Error( - 'Custom spy strategies must be added in a before function or a spec' - ); - } - runnableResources[currentRunnable().id].customSpyStrategies[name] = fn; + runnableResources.customSpyStrategies()[name] = fn; }; this.addCustomEqualityTester = function(tester) { - if (!currentRunnable()) { - throw new Error( - 'Custom Equalities must be added in a before function or a spec' - ); - } - runnableResources[currentRunnable().id].customEqualityTesters.push( - tester - ); + runnableResources.customEqualityTesters().push(tester); }; this.addMatchers = function(matchersToAdd) { - if (!currentRunnable()) { - throw new Error( - 'Matchers must be added in a before function or a spec' - ); - } - const customMatchers = - runnableResources[currentRunnable().id].customMatchers; - - for (const matcherName in matchersToAdd) { - customMatchers[matcherName] = matchersToAdd[matcherName]; - } + runnableResources.addCustomMatchers(matchersToAdd); }; this.addAsyncMatchers = function(matchersToAdd) { - if (!currentRunnable()) { - throw new Error( - 'Async Matchers must be added in a before function or a spec' - ); - } - const customAsyncMatchers = - runnableResources[currentRunnable().id].customAsyncMatchers; - - for (const matcherName in matchersToAdd) { - customAsyncMatchers[matcherName] = matchersToAdd[matcherName]; - } + runnableResources.addCustomAsyncMatchers(matchersToAdd); }; this.addCustomObjectFormatter = function(formatter) { - if (!currentRunnable()) { - throw new Error( - 'Custom object formatters must be added in a before function or a spec' - ); - } - - runnableResources[currentRunnable().id].customObjectFormatters.push( - formatter - ); + runnableResources.customObjectFormatters().push(formatter); }; j$.Expectation.addCoreMatchers(j$.matchers); @@ -315,31 +270,10 @@ getJasmineRequireObj().Env = function(j$) { return 'suite' + nextSuiteId++; } - function makePrettyPrinter() { - const customObjectFormatters = - runnableResources[currentRunnable().id].customObjectFormatters; - return j$.makePrettyPrinter(customObjectFormatters); - } - - function makeMatchersUtil() { - const cr = currentRunnable(); - - if (cr) { - const customEqualityTesters = - runnableResources[cr.id].customEqualityTesters; - return new j$.MatchersUtil({ - customTesters: customEqualityTesters, - pp: makePrettyPrinter() - }); - } else { - return new j$.MatchersUtil({ pp: j$.basicPrettyPrinter_ }); - } - } - const expectationFactory = function(actual, spec) { return j$.Expectation.factory({ - matchersUtil: makeMatchersUtil(), - customMatchers: runnableResources[spec.id].customMatchers, + matchersUtil: runnableResources.makeMatchersUtil(), + customMatchers: runnableResources.customMatchers(), actual: actual, addExpectationResult: addExpectationResult }); @@ -416,8 +350,8 @@ getJasmineRequireObj().Env = function(j$) { const asyncExpectationFactory = function(actual, spec, runableType) { return j$.Expectation.asyncFactory({ - matchersUtil: makeMatchersUtil(), - customAsyncMatchers: runnableResources[spec.id].customAsyncMatchers, + matchersUtil: runnableResources.makeMatchersUtil(), + customAsyncMatchers: runnableResources.customAsyncMatchers(), actual: actual, addExpectationResult: addExpectationResult }); @@ -437,45 +371,6 @@ getJasmineRequireObj().Env = function(j$) { return asyncExpectationFactory(actual, suite, 'Spec'); }; - function defaultResourcesForRunnable(id, parentRunnableId) { - const resources = { - spies: [], - customEqualityTesters: [], - customMatchers: {}, - customAsyncMatchers: {}, - customSpyStrategies: {}, - defaultStrategyFn: undefined, - customObjectFormatters: [] - }; - - if (runnableResources[parentRunnableId]) { - resources.customEqualityTesters = j$.util.clone( - runnableResources[parentRunnableId].customEqualityTesters - ); - resources.customMatchers = j$.util.clone( - runnableResources[parentRunnableId].customMatchers - ); - resources.customAsyncMatchers = j$.util.clone( - runnableResources[parentRunnableId].customAsyncMatchers - ); - resources.customObjectFormatters = j$.util.clone( - runnableResources[parentRunnableId].customObjectFormatters - ); - resources.customSpyStrategies = j$.util.clone( - runnableResources[parentRunnableId].customSpyStrategies - ); - resources.defaultStrategyFn = - runnableResources[parentRunnableId].defaultStrategyFn; - } - - runnableResources[id] = resources; - } - - function clearResourcesForRunnable(id) { - spyRegistry.clearSpies(); - delete runnableResources[id]; - } - function beforeAndAfterFns(targetSuite) { return function() { let befores = [], @@ -701,7 +596,7 @@ getJasmineRequireObj().Env = function(j$) { topSuite.reset(); } this._executedBefore = true; - defaultResourcesForRunnable(topSuite.id); + runnableResources.initForRunnable(topSuite.id); installGlobalErrors(); if (!runnablesToRun) { @@ -724,7 +619,7 @@ getJasmineRequireObj().Env = function(j$) { failSpecWithNoExpectations: config.failSpecWithNoExpectations, nodeStart: function(suite, next) { currentlyExecutingSuites.push(suite); - defaultResourcesForRunnable(suite.id, suite.parentSuite.id); + runnableResources.initForRunnable(suite.id, suite.parentSuite.id); reporter.suiteStarted(suite.result, next); suite.startTimer(); }, @@ -733,7 +628,7 @@ getJasmineRequireObj().Env = function(j$) { throw new Error('Tried to complete the wrong suite'); } - clearResourcesForRunnable(suite.id); + runnableResources.clearForRunnable(suite.id); currentlyExecutingSuites.pop(); if (result.status === 'failed') { @@ -798,7 +693,7 @@ getJasmineRequireObj().Env = function(j$) { await reportChildrenOfBeforeAllFailure(topSuite); } - clearResourcesForRunnable(topSuite.id); + runnableResources.clearForRunnable(topSuite.id); currentlyExecutingSuites.pop(); let overallStatus, incompleteReason; @@ -922,42 +817,6 @@ getJasmineRequireObj().Env = function(j$) { reporter.clearReporters(); }; - const spyFactory = new j$.SpyFactory( - function getCustomStrategies() { - const runnable = currentRunnable(); - - if (runnable) { - return runnableResources[runnable.id].customSpyStrategies; - } - - return {}; - }, - function getDefaultStrategyFn() { - const runnable = currentRunnable(); - - if (runnable) { - return runnableResources[runnable.id].defaultStrategyFn; - } - - return undefined; - }, - makeMatchersUtil - ); - - spyRegistry = new j$.SpyRegistry({ - currentSpies: function() { - if (!currentRunnable()) { - throw new Error( - 'Spies must be created in a before function or a spec' - ); - } - return runnableResources[currentRunnable().id].spies; - }, - createSpy: function(name, originalFn) { - return self.createSpy(name, originalFn); - } - }); - /** * Configures whether Jasmine should allow the same function to be spied on * more than once during the execution of a spec. By default, spying on @@ -968,32 +827,40 @@ getJasmineRequireObj().Env = function(j$) { * @param {boolean} allow Whether to allow respying */ this.allowRespy = function(allow) { - spyRegistry.allowRespy(allow); + runnableResources.spyRegistry.allowRespy(allow); }; this.spyOn = function() { - return spyRegistry.spyOn.apply(spyRegistry, arguments); + return runnableResources.spyRegistry.spyOn.apply( + runnableResources.spyRegistry, + arguments + ); }; this.spyOnProperty = function() { - return spyRegistry.spyOnProperty.apply(spyRegistry, arguments); + return runnableResources.spyRegistry.spyOnProperty.apply( + runnableResources.spyRegistry, + arguments + ); }; this.spyOnAllFunctions = function() { - return spyRegistry.spyOnAllFunctions.apply(spyRegistry, arguments); + return runnableResources.spyRegistry.spyOnAllFunctions.apply( + runnableResources.spyRegistry, + arguments + ); }; this.createSpy = function(name, originalFn) { - if (arguments.length === 1 && j$.isFunction_(name)) { - originalFn = name; - name = originalFn.name; - } - - return spyFactory.createSpy(name, originalFn); + return runnableResources.spyFactory.createSpy(name, originalFn); }; this.createSpyObj = function(baseName, methodNames, propertyNames) { - return spyFactory.createSpyObj(baseName, methodNames, propertyNames); + return runnableResources.spyFactory.createSpyObj( + baseName, + methodNames, + propertyNames + ); }; function ensureIsFunction(fn, caller) { @@ -1148,7 +1015,7 @@ getJasmineRequireObj().Env = function(j$) { return spec; function specResultCallback(result, next) { - clearResourcesForRunnable(spec.id); + runnableResources.clearForRunnable(spec.id); currentSpec = null; if (result.status === 'failed') { @@ -1160,7 +1027,7 @@ getJasmineRequireObj().Env = function(j$) { function specStarted(spec, next) { currentSpec = spec; - defaultResourcesForRunnable(spec.id, suite.id); + runnableResources.initForRunnable(spec.id, suite.id); reporter.specStarted(spec.result, next); } }; @@ -1382,7 +1249,8 @@ getJasmineRequireObj().Env = function(j$) { message += error; } else { // pretty print all kind of objects. This includes arrays. - message += makePrettyPrinter()(error); + const pp = runnableResources.makePrettyPrinter(); + message += pp(error); } } diff --git a/src/core/RunnableResources.js b/src/core/RunnableResources.js new file mode 100644 index 00000000..112a07e1 --- /dev/null +++ b/src/core/RunnableResources.js @@ -0,0 +1,154 @@ +getJasmineRequireObj().RunnableResources = function(j$) { + class RunnableResources { + constructor(getCurrentRunnableId) { + this.byRunnableId_ = {}; + this.getCurrentRunnableId_ = getCurrentRunnableId; + + this.spyFactory = new j$.SpyFactory( + () => { + if (this.getCurrentRunnableId_()) { + return this.customSpyStrategies(); + } else { + return {}; + } + }, + () => this.defaultSpyStrategy(), + () => this.makeMatchersUtil() + ); + + this.spyRegistry = new j$.SpyRegistry({ + currentSpies: () => this.spies(), + createSpy: (name, originalFn) => + this.spyFactory.createSpy(name, originalFn) + }); + } + + initForRunnable(runnableId, parentId) { + const newRes = (this.byRunnableId_[runnableId] = { + customEqualityTesters: [], + customMatchers: {}, + customAsyncMatchers: {}, + customSpyStrategies: {}, + customObjectFormatters: [], + defaultSpyStrategy: undefined, + spies: [] + }); + + const parentRes = this.byRunnableId_[parentId]; + + if (parentRes) { + newRes.defaultSpyStrategy = parentRes.defaultSpyStrategy; + const toClone = [ + 'customEqualityTesters', + 'customMatchers', + 'customAsyncMatchers', + 'customObjectFormatters', + 'customSpyStrategies' + ]; + + for (const k of toClone) { + newRes[k] = j$.util.clone(parentRes[k]); + } + } + } + + clearForRunnable(runnableId) { + this.spyRegistry.clearSpies(); + delete this.byRunnableId_[runnableId]; + } + + spies() { + return this.forCurrentRunnable_( + 'Spies must be created in a before function or a spec' + ).spies; + } + + defaultSpyStrategy() { + if (!this.getCurrentRunnableId_()) { + return undefined; + } + + return this.byRunnableId_[this.getCurrentRunnableId_()] + .defaultSpyStrategy; + } + + setDefaultSpyStrategy(fn) { + this.forCurrentRunnable_( + 'Default spy strategy must be set in a before function or a spec' + ).defaultSpyStrategy = fn; + } + + customSpyStrategies() { + return this.forCurrentRunnable_( + 'Custom spy strategies must be added in a before function or a spec' + ).customSpyStrategies; + } + + customEqualityTesters() { + return this.forCurrentRunnable_( + 'Custom Equalities must be added in a before function or a spec' + ).customEqualityTesters; + } + + customMatchers() { + return this.forCurrentRunnable_( + 'Matchers must be added in a before function or a spec' + ).customMatchers; + } + + addCustomMatchers(matchersToAdd) { + const matchers = this.customMatchers(); + + for (const name in matchersToAdd) { + matchers[name] = matchersToAdd[name]; + } + } + + customAsyncMatchers() { + return this.forCurrentRunnable_( + 'Async Matchers must be added in a before function or a spec' + ).customAsyncMatchers; + } + + addCustomAsyncMatchers(matchersToAdd) { + const matchers = this.customAsyncMatchers(); + + for (const name in matchersToAdd) { + matchers[name] = matchersToAdd[name]; + } + } + + customObjectFormatters() { + return this.forCurrentRunnable_( + 'Custom object formatters must be added in a before function or a spec' + ).customObjectFormatters; + } + + makePrettyPrinter() { + return j$.makePrettyPrinter(this.customObjectFormatters()); + } + + makeMatchersUtil() { + if (this.getCurrentRunnableId_()) { + return new j$.MatchersUtil({ + customTesters: this.customEqualityTesters(), + pp: this.makePrettyPrinter() + }); + } else { + return new j$.MatchersUtil({ pp: j$.basicPrettyPrinter_ }); + } + } + + forCurrentRunnable_(errorMsg) { + const resources = this.byRunnableId_[this.getCurrentRunnableId_()]; + + if (!resources && errorMsg) { + throw new Error(errorMsg); + } + + return resources; + } + } + + return RunnableResources; +}; diff --git a/src/core/SpyFactory.js b/src/core/SpyFactory.js index d20d6539..a9768e1e 100644 --- a/src/core/SpyFactory.js +++ b/src/core/SpyFactory.js @@ -5,6 +5,11 @@ getJasmineRequireObj().SpyFactory = function(j$) { getMatchersUtil ) { this.createSpy = function(name, originalFn) { + if (j$.isFunction_(name) && originalFn === undefined) { + originalFn = name; + name = originalFn.name; + } + return j$.Spy(name, getMatchersUtil(), { originalFn, customStrategies: getCustomStrategies(), diff --git a/src/core/requireCore.js b/src/core/requireCore.js index da49dc83..2ef0636e 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -68,6 +68,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { j$ ); j$.ReportDispatcher = jRequire.ReportDispatcher(j$); + j$.RunnableResources = jRequire.RunnableResources(j$); j$.Spec = jRequire.Spec(j$); j$.Spy = jRequire.Spy(j$); j$.SpyFactory = jRequire.SpyFactory(j$);