describe('QueueRunner', function() { it('validates that queueableFns are truthy', function() { expect(function() { new privateUnderTest.QueueRunner({ queueableFns: [undefined] }); }).toThrowError('Received a falsy queueableFn'); }); it('validates that queueableFns have fn properties', function() { expect(function() { new privateUnderTest.QueueRunner({ queueableFns: [{ fn: undefined }] }); }).toThrowError('Received a queueableFn with no fn'); }); it("runs all the functions it's passed", function() { const calls = []; const queueableFn1 = { fn: jasmine.createSpy('fn1') }; const queueableFn2 = { fn: jasmine.createSpy('fn2') }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1, queueableFn2] }); queueableFn1.fn.and.callFake(function() { calls.push('fn1'); }); queueableFn2.fn.and.callFake(function() { calls.push('fn2'); }); queueRunner.execute(); expect(calls).toEqual(['fn1', 'fn2']); }); it("calls each function with a consistent 'this'-- an empty object", function() { const queueableFn1 = { fn: jasmine.createSpy('fn1') }; const queueableFn2 = { fn: jasmine.createSpy('fn2') }; let asyncContext; const queueableFn3 = { fn: function(done) { asyncContext = this; done(); } }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1, queueableFn2, queueableFn3] }); queueRunner.execute(); const context = queueableFn1.fn.calls.first().object; expect(context).toEqual(new privateUnderTest.UserContext()); expect(queueableFn2.fn.calls.first().object).toBe(context); expect(asyncContext).toBe(context); }); describe('with an asynchronous function', function() { beforeEach(function() { jasmine.clock().install(); }); afterEach(function() { jasmine.clock().uninstall(); }); it('supports asynchronous functions, only advancing to next function after a done() callback', function() { //TODO: it would be nice if spy arity could match the fake, so we could do something like: //createSpy('asyncfn').and.callFake(function(done) {}); const onComplete = jasmine.createSpy('onComplete'); const beforeCallback = jasmine.createSpy('beforeCallback'); const fnCallback = jasmine.createSpy('fnCallback'); const afterCallback = jasmine.createSpy('afterCallback'); const queueableFn1 = { fn: function(done) { beforeCallback(); setTimeout(done, 100); } }; const queueableFn2 = { fn: function(done) { fnCallback(); setTimeout(done, 100); } }; const queueableFn3 = { fn: function(done) { afterCallback(); setTimeout(done, 100); } }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1, queueableFn2, queueableFn3], onComplete: onComplete }); queueRunner.execute(); expect(beforeCallback).toHaveBeenCalled(); expect(fnCallback).not.toHaveBeenCalled(); expect(afterCallback).not.toHaveBeenCalled(); expect(onComplete).not.toHaveBeenCalled(); jasmine.clock().tick(100); expect(fnCallback).toHaveBeenCalled(); expect(afterCallback).not.toHaveBeenCalled(); expect(onComplete).not.toHaveBeenCalled(); jasmine.clock().tick(100); expect(afterCallback).toHaveBeenCalled(); expect(onComplete).not.toHaveBeenCalled(); jasmine.clock().tick(100); expect(onComplete).toHaveBeenCalled(); }); it('explicitly fails an async function with a provided fail function and moves to the next function', function() { const queueableFn1 = { fn: function(done) { setTimeout(function() { done.fail('foo'); }, 100); } }; const queueableFn2 = { fn: jasmine.createSpy('fn2') }; const failFn = jasmine.createSpy('fail'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1, queueableFn2], fail: failFn }); queueRunner.execute(); expect(failFn).not.toHaveBeenCalled(); expect(queueableFn2.fn).not.toHaveBeenCalled(); jasmine.clock().tick(100); expect(failFn).toHaveBeenCalledWith('foo'); expect(queueableFn2.fn).toHaveBeenCalled(); }); describe('When next is called with an argument', function() { it('explicitly fails and moves to the next function', function() { const err = 'anything except undefined'; const queueableFn1 = { fn: function(done) { setTimeout(function() { done(err); }, 100); } }; const queueableFn2 = { fn: jasmine.createSpy('fn2') }; const failFn = jasmine.createSpy('fail'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1, queueableFn2], fail: failFn }); queueRunner.execute(); expect(failFn).not.toHaveBeenCalled(); expect(queueableFn2.fn).not.toHaveBeenCalled(); jasmine.clock().tick(100); expect(failFn).toHaveBeenCalledWith(err); expect(queueableFn2.fn).toHaveBeenCalled(); }); describe('as a result of a promise', function() { describe('and the argument is an Error', function() { // Since promise support was added, Jasmine has failed specs that // return a promise that resolves to an error. That's probably not // the desired behavior but it's also not something we should change // except on a major release and with a deprecation warning in // advance. it('explicitly fails and moves to the next function', function(done) { const err = new Error('foo'); const queueableFn1 = { fn: function() { return Promise.resolve(err); } }; const queueableFn2 = { fn: jasmine.createSpy('fn2') }; const failFn = jasmine.createSpy('fail'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1, queueableFn2], fail: failFn, onComplete: function() { expect(failFn).toHaveBeenCalledWith(err); expect(queueableFn2.fn).toHaveBeenCalled(); done(); } }); queueRunner.execute(); }); }); describe('and the argument is not an Error', function() { it('does not report a failure', function(done) { const queueableFn1 = { fn: function() { return Promise.resolve('not an error'); } }; const failFn = jasmine.createSpy('fail'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1], fail: failFn, onComplete: function() { expect(failFn).not.toHaveBeenCalled(); done(); } }); queueRunner.execute(); }); }); }); }); it('does not cause an explicit fail if execution is being stopped', function() { const err = new privateUnderTest.StopExecutionError('foo'); const queueableFn1 = { fn: function(done) { setTimeout(function() { done(err); }, 100); } }; const queueableFn2 = { fn: jasmine.createSpy('fn2') }; const failFn = jasmine.createSpy('fail'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1, queueableFn2], fail: failFn }); queueRunner.execute(); expect(failFn).not.toHaveBeenCalled(); expect(queueableFn2.fn).not.toHaveBeenCalled(); jasmine.clock().tick(100); expect(failFn).not.toHaveBeenCalled(); expect(queueableFn2.fn).toHaveBeenCalled(); }); it("sets a timeout if requested for asynchronous functions so they don't go on forever", function() { const timeout = 3; const beforeFn = { fn: function(done) {}, type: 'before', timeout: timeout }; const queueableFn = { fn: jasmine.createSpy('fn'), type: 'queueable' }; const onComplete = jasmine.createSpy('onComplete'); const onException = jasmine.createSpy('onException'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [beforeFn, queueableFn], onComplete: onComplete, onException: onException }); queueRunner.execute(); expect(queueableFn.fn).not.toHaveBeenCalled(); jasmine.clock().tick(timeout); expect(onException).toHaveBeenCalledWith(jasmine.any(Error)); expect(queueableFn.fn).toHaveBeenCalled(); expect(onComplete).toHaveBeenCalled(); }); it('does not call onMultipleDone if an asynchrnous function completes after timing out', function() { const timeout = 3; let queueableFnDone; const queueableFn = { fn: function(done) { queueableFnDone = done; }, type: 'queueable', timeout: timeout }; const onComplete = jasmine.createSpy('onComplete'); const onMultipleDone = jasmine.createSpy('onMultipleDone'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn], onComplete: onComplete, onMultipleDone: onMultipleDone }); queueRunner.execute(); jasmine.clock().tick(timeout); queueableFnDone(); expect(onComplete).toHaveBeenCalled(); expect(onMultipleDone).not.toHaveBeenCalled(); }); it('by default does not set a timeout for asynchronous functions', function() { const beforeFn = { fn: function(done) {} }; const queueableFn = { fn: jasmine.createSpy('fn') }; const onComplete = jasmine.createSpy('onComplete'); const onException = jasmine.createSpy('onException'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [beforeFn, queueableFn], onComplete: onComplete, onException: onException }); queueRunner.execute(); expect(queueableFn.fn).not.toHaveBeenCalled(); jasmine.clock().tick(jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL); expect(onException).not.toHaveBeenCalled(); expect(queueableFn.fn).not.toHaveBeenCalled(); expect(onComplete).not.toHaveBeenCalled(); }); it('clears the timeout when an async function throws an exception, to prevent additional exception reporting', function() { const queueableFn = { fn: function(done) { throw new Error('error!'); } }; const onComplete = jasmine.createSpy('onComplete'); const onException = jasmine.createSpy('onException'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn], onComplete: onComplete, onException: onException }); queueRunner.execute(); expect(onComplete).toHaveBeenCalled(); expect(onException).toHaveBeenCalled(); jasmine.clock().tick(jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL); expect(onException.calls.count()).toEqual(1); }); it('clears the timeout when the done callback is called', function() { const queueableFn = { fn: function(done) { done(); } }; const onComplete = jasmine.createSpy('onComplete'); const onException = jasmine.createSpy('onException'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn], onComplete: onComplete, onException: onException }); queueRunner.execute(); jasmine.clock().tick(1); expect(onComplete).toHaveBeenCalled(); jasmine.clock().tick(jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL); expect(onException).not.toHaveBeenCalled(); }); it('only moves to the next spec the first time you call done', function() { const queueableFn = { fn: function(done) { done(); done(); } }; const nextQueueableFn = { fn: jasmine.createSpy('nextFn') }; const onMultipleDone = jasmine.createSpy('onMultipleDone'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn, nextQueueableFn], onMultipleDone: onMultipleDone }); queueRunner.execute(); jasmine.clock().tick(1); expect(nextQueueableFn.fn.calls.count()).toEqual(1); expect(onMultipleDone).toHaveBeenCalled(); }); it('does not move to the next spec if done is called after an exception has ended the spec', function() { const queueableFn = { fn: function(done) { setTimeout(done, 1); throw new Error('error!'); } }; const nextQueueableFn = { fn: jasmine.createSpy('nextFn') }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn, nextQueueableFn] }); queueRunner.execute(); jasmine.clock().tick(1); expect(nextQueueableFn.fn.calls.count()).toEqual(1); }); it('should return a null when you call done', function() { // Some promises want handlers to return anything but undefined to help catch "forgotten returns". let doneReturn; const queueableFn = { fn: function(done) { doneReturn = done(); } }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn] }); queueRunner.execute(); expect(doneReturn).toBe(null); }); it('continues running functions when an exception is thrown in async code without timing out', function() { const queueableFn = { fn: function(done) { throwAsync(); }, timeout: 1 }; const nextQueueableFn = { fn: jasmine.createSpy('nextFunction') }; const onException = jasmine.createSpy('onException'); const globalErrors = { pushListener: jasmine.createSpy('pushListener'), popListener: jasmine.createSpy('popListener') }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn, nextQueueableFn], onException: onException, globalErrors: globalErrors }); const throwAsync = function() { globalErrors.pushListener.calls.mostRecent().args[0](new Error('foo')); jasmine.clock().tick(2); }; nextQueueableFn.fn.and.callFake(function() { // should remove the same function that was added expect(globalErrors.popListener).toHaveBeenCalledWith( globalErrors.pushListener.calls.argsFor(1)[0] ); }); queueRunner.execute(); function errorWithMessage(message) { return { asymmetricMatch: function(other) { return new RegExp(message).test(other.message); }, toString: function() { return ''; } }; } expect(onException).not.toHaveBeenCalledWith( errorWithMessage(/DEFAULT_TIMEOUT_INTERVAL/) ); expect(onException).toHaveBeenCalledWith(errorWithMessage(/^foo$/)); expect(nextQueueableFn.fn).toHaveBeenCalled(); }); it('handles exceptions thrown while waiting for the stack to clear', function() { const queueableFn = { fn: function(done) { done(); } }; const errorListeners = []; const globalErrors = { pushListener: function(f) { errorListeners.push(f); }, popListener: function() { errorListeners.pop(); } }; const clearStack = jasmine.createSpyObj('clearStack', ['clearStack']); const onException = jasmine.createSpy('onException'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn], globalErrors: globalErrors, clearStack: clearStack, onException: onException }); const error = new Error('nope'); queueRunner.execute(); jasmine.clock().tick(); expect(clearStack.clearStack).toHaveBeenCalled(); expect(errorListeners.length).toEqual(1); errorListeners[0](error); clearStack.clearStack.calls.argsFor(0)[0](); expect(onException).toHaveBeenCalledWith(error); }); }); describe('with a function that returns a promise', function() { function StubPromise() {} StubPromise.prototype.then = function(resolve, reject) { this.resolveHandler = resolve; this.rejectHandler = reject; }; beforeEach(function() { jasmine.clock().install(); }); afterEach(function() { jasmine.clock().uninstall(); }); it('runs the function asynchronously, advancing once the promise is settled', function() { const onComplete = jasmine.createSpy('onComplete'); const fnCallback = jasmine.createSpy('fnCallback'); const p1 = new StubPromise(); const p2 = new StubPromise(); const queueableFn1 = { fn: function() { setTimeout(function() { p1.resolveHandler(); }, 100); return p1; } }; const queueableFn2 = { fn: function() { fnCallback(); setTimeout(function() { p2.resolveHandler(); }, 100); return p2; } }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1, queueableFn2], onComplete: onComplete }); queueRunner.execute(); expect(fnCallback).not.toHaveBeenCalled(); expect(onComplete).not.toHaveBeenCalled(); jasmine.clock().tick(100); expect(fnCallback).toHaveBeenCalled(); expect(onComplete).not.toHaveBeenCalled(); jasmine.clock().tick(100); expect(onComplete).toHaveBeenCalled(); }); it('handles a rejected promise like an unhandled exception', function() { const promise = new StubPromise(); const queueableFn1 = { fn: function() { setTimeout(function() { promise.rejectHandler('foo'); }, 100); return promise; } }; const queueableFn2 = { fn: jasmine.createSpy('fn2') }; const onExceptionCallback = jasmine.createSpy('on exception callback'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn1, queueableFn2], onException: onExceptionCallback }); queueRunner.execute(); expect(onExceptionCallback).not.toHaveBeenCalled(); expect(queueableFn2.fn).not.toHaveBeenCalled(); jasmine.clock().tick(100); expect(onExceptionCallback).toHaveBeenCalledWith('foo'); expect(queueableFn2.fn).toHaveBeenCalled(); }); it('issues an error if the function also takes a parameter', function() { const queueableFn = { fn: function(done) { return new StubPromise(); } }; const onException = jasmine.createSpy('onException'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn], onException: onException }); queueRunner.execute(); expect(onException).toHaveBeenCalledWith( new Error( 'An asynchronous ' + 'before/it/after function took a done callback but also returned a ' + 'promise. ' + 'Either remove the done callback (recommended) or change the function ' + 'to not return a promise.' ) ); }); it('issues a more specific error if the function is `async`', function() { async function fn(done) {} const onException = jasmine.createSpy('onException'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [{ fn: fn }], onException: onException }); queueRunner.execute(); expect(onException).toHaveBeenCalledWith( new Error( 'An asynchronous ' + 'before/it/after function was defined with the async keyword but ' + 'also took a done callback. Either remove the done callback ' + '(recommended) or remove the async keyword.' ) ); }); }); it('passes final errors to exception handlers', function() { const error = new Error('fake error'); const onExceptionCallback = jasmine.createSpy('on exception callback'); const queueRunner = new privateUnderTest.QueueRunner({ onException: onExceptionCallback }); queueRunner.execute(); queueRunner.handleFinalError(error); expect(onExceptionCallback).toHaveBeenCalledWith(error); }); it('calls exception handlers when an exception is thrown in a fn', function() { const queueableFn = { type: 'queueable', fn: function() { throw new Error('fake error'); } }; const onExceptionCallback = jasmine.createSpy('on exception callback'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn], onException: onExceptionCallback }); queueRunner.execute(); expect(onExceptionCallback).toHaveBeenCalledWith(jasmine.any(Error)); }); it('continues running the functions even after an exception is thrown in an async spec', function() { const queueableFn = { fn: function(done) { throw new Error('error'); } }; const nextQueueableFn = { fn: jasmine.createSpy('nextFunction') }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn, nextQueueableFn] }); queueRunner.execute(); expect(nextQueueableFn.fn).toHaveBeenCalled(); }); describe('When configured with a skip policy', function() { it('instantiates the skip policy', function() { const SkipPolicy = jasmine.createSpy('SkipPolicy ctor'); const queueableFns = [{ fn: () => {} }, { fn: () => {} }]; new privateUnderTest.QueueRunner({ queueableFns, SkipPolicy }); expect(SkipPolicy).toHaveBeenCalledWith(queueableFns); }); it('uses the skip policy to determine which fn to run next', function() { const queueableFns = [ { fn: jasmine.createSpy('fn0') }, { fn: jasmine.createSpy('fn1') }, { fn: jasmine.createSpy('fn2').and.throwError(new Error('nope')) }, { fn: jasmine.createSpy('fn3') } ]; const skipPolicy = jasmine.createSpyObj('skipPolicy', [ 'skipTo', 'fnErrored' ]); skipPolicy.skipTo.and.callFake(function(lastRanIx) { return lastRanIx === 0 ? 2 : lastRanIx + 1; }); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns, SkipPolicy: function() { return skipPolicy; } }); queueRunner.execute(); expect(skipPolicy.skipTo).toHaveBeenCalledWith(0); expect(skipPolicy.skipTo).toHaveBeenCalledWith(2); expect(skipPolicy.fnErrored).toHaveBeenCalledWith(2); expect(queueableFns[0].fn).toHaveBeenCalled(); expect(queueableFns[1].fn).not.toHaveBeenCalled(); expect(queueableFns[2].fn).toHaveBeenCalled(); expect(queueableFns[3].fn).toHaveBeenCalled(); }); it('throws if the skip policy returns the current fn', function() { const skipPolicy = { skipTo: i => i }; const queueableFns = [{ fn: () => {} }]; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns, SkipPolicy: function() { return skipPolicy; } }); expect(function() { queueRunner.execute(); }).toThrowError("Can't skip to the same queueable fn that just finished"); }); }); describe('When configured to complete on first error', function() { it('skips to cleanup functions on the first exception', function() { const queueableFn = { fn: function() { throw new Error('error'); } }; const nextQueueableFn = { fn: jasmine.createSpy('nextFunction') }; const cleanupFn = { fn: jasmine.createSpy('cleanup'), type: 'specCleanup' }; const onComplete = jasmine.createSpy('onComplete'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn, nextQueueableFn, cleanupFn], onComplete: onComplete, SkipPolicy: privateUnderTest.CompleteOnFirstErrorSkipPolicy }); queueRunner.execute(); expect(nextQueueableFn.fn).not.toHaveBeenCalled(); expect(cleanupFn.fn).toHaveBeenCalled(); expect(onComplete).toHaveBeenCalledWith( jasmine.any(privateUnderTest.StopExecutionError) ); }); it('does not skip when a cleanup function throws', function() { const queueableFn = { fn: function() {} }; const cleanupFn1 = { fn: function() { throw new Error('error'); }, type: 'afterEach' }; const cleanupFn2 = { fn: jasmine.createSpy('cleanupFn2'), type: 'afterEach' }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn, cleanupFn1, cleanupFn2], SkipPolicy: privateUnderTest.CompleteOnFirstErrorSkipPolicy }); queueRunner.execute(); expect(cleanupFn2.fn).toHaveBeenCalled(); }); describe('with an asynchronous function', function() { beforeEach(function() { jasmine.clock().install(); }); afterEach(function() { jasmine.clock().uninstall(); }); it('skips to cleanup functions once the fn completes after an unhandled exception', function() { const errorListeners = []; let queueableFnDone; const queueableFn = { fn: function(done) { queueableFnDone = done; } }; const nextQueueableFn = { fn: jasmine.createSpy('nextFunction') }; const cleanupFn = { fn: jasmine.createSpy('cleanup'), type: 'specCleanup' }; const queueRunner = new privateUnderTest.QueueRunner({ globalErrors: { pushListener: function(f) { errorListeners.push(f); }, popListener: function() { errorListeners.pop(); } }, queueableFns: [queueableFn, nextQueueableFn, cleanupFn], SkipPolicy: privateUnderTest.CompleteOnFirstErrorSkipPolicy }); queueRunner.execute(); errorListeners[errorListeners.length - 1](new Error('error')); expect(cleanupFn.fn).not.toHaveBeenCalled(); queueableFnDone(); expect(nextQueueableFn.fn).not.toHaveBeenCalled(); expect(cleanupFn.fn).toHaveBeenCalled(); }); it('skips to cleanup functions when next.fail is called', function() { const queueableFn = { fn: function(done) { done.fail('nope'); } }; const nextQueueableFn = { fn: jasmine.createSpy('nextFunction') }; const cleanupFn = { fn: jasmine.createSpy('cleanup'), type: 'specCleanup' }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn, nextQueueableFn, cleanupFn], SkipPolicy: privateUnderTest.CompleteOnFirstErrorSkipPolicy }); queueRunner.execute(); jasmine.clock().tick(); expect(nextQueueableFn.fn).not.toHaveBeenCalled(); expect(cleanupFn.fn).toHaveBeenCalled(); }); it('skips to cleanup functions when next is called with an Error', function() { const queueableFn = { fn: function(done) { done(new Error('nope')); } }; const nextQueueableFn = { fn: jasmine.createSpy('nextFunction') }; const cleanupFn = { fn: jasmine.createSpy('cleanup'), type: 'specCleanup' }; const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn, nextQueueableFn, cleanupFn], SkipPolicy: privateUnderTest.CompleteOnFirstErrorSkipPolicy }); queueRunner.execute(); jasmine.clock().tick(); expect(nextQueueableFn.fn).not.toHaveBeenCalled(); expect(cleanupFn.fn).toHaveBeenCalled(); }); }); }); it('calls a provided complete callback when done', function() { const queueableFn = { fn: jasmine.createSpy('fn') }; const completeCallback = jasmine.createSpy('completeCallback'); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [queueableFn], onComplete: completeCallback }); queueRunner.execute(); expect(completeCallback).toHaveBeenCalled(); }); describe('clearing the stack', function() { beforeEach(function() { jasmine.clock().install(); }); afterEach(function() { jasmine.clock().uninstall(); }); it('calls a provided stack clearing function when done', function() { const asyncFn = { fn: function(done) { done(); } }; const afterFn = { fn: jasmine.createSpy('afterFn') }; const completeCallback = jasmine.createSpy('completeCallback'); const clearStack = jasmine.createSpyObj('clearStack', ['clearStack']); const queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [asyncFn, afterFn], clearStack: clearStack, onComplete: completeCallback }); clearStack.clearStack.and.callFake(function(fn) { fn(); }); queueRunner.execute(); jasmine.clock().tick(); expect(afterFn.fn).toHaveBeenCalled(); expect(clearStack.clearStack).toHaveBeenCalled(); clearStack.clearStack.calls.argsFor(0)[0](); expect(completeCallback).toHaveBeenCalled(); }); }); describe('when user context has not been defined', function() { beforeEach(function() { const fn = jasmine.createSpy('fn1'); this.fn = fn; this.queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [{ fn: fn }] }); }); it('runs the functions on the scope of a UserContext', function() { let context; this.fn.and.callFake(function() { context = this; }); this.queueRunner.execute(); expect(context.constructor).toBe(privateUnderTest.UserContext); }); }); describe('when user context has been defined', function() { beforeEach(function() { const fn = jasmine.createSpy('fn1'); let context; this.fn = fn; this.context = context = new privateUnderTest.UserContext(); this.queueRunner = new privateUnderTest.QueueRunner({ queueableFns: [{ fn: fn }], userContext: context }); }); it('runs the functions on the scope of a UserContext', function() { let context; this.fn.and.callFake(function() { context = this; }); this.queueRunner.execute(); expect(context).toBe(this.context); }); }); });