The old style of merging all of a function's variable declarations into a single statement made some sense back in the days of var, but there's no reason to keep doing it now that we use const and let.
983 lines
31 KiB
JavaScript
983 lines
31 KiB
JavaScript
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 '<Error with message like "' + message + '">';
|
|
}
|
|
};
|
|
}
|
|
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);
|
|
});
|
|
});
|
|
});
|