/* eslint-env mocha */
import expect from 'must';
import sinon from 'sinon';
import configureMustSinon from 'must-sinon';
import { CancellationTokenSource, CancellationToken } from 'prex';
import makeChain, { Cancelled, propagate, always } from '../src/index';
configureMustSinon(expect);
const SOME_VALUE = {};
const NOOP = () => undefined;
const NOT_CALLED = () => {
throw new Error('Should not be called');
};
const UNHANDLED_REJECTION_EVENT = 'unhandledRejection';
let unhandledRejectionHandler = null;
const timeout = (duration = 0) => new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Timeout')), duration);
});
const testSilent = (runSilent) => {
it('is silent', () => {
unhandledRejectionHandler = sinon.spy();
process.on(UNHANDLED_REJECTION_EVENT, unhandledRejectionHandler);
runSilent();
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
expect(unhandledRejectionHandler).to.not.have.been.called();
resolve();
} catch (err) {
reject(err);
}
}, 10);
});
});
};
const testThenCatch = (run) => {
it('returns a promise', () => {
const promise = run(NOOP);
expect(promise).to.be.a(Promise);
});
const runSilent = () => {
run(NOOP, { parentToken: CancellationToken.canceled });
};
testSilent(runSilent);
};
const testCallback = (run, callbackName = 'callback') => {
it(`passes value to ${callbackName}`, () => {
const callback = sinon.spy();
return run(callback, { input: SOME_VALUE })
.then(() => {
expect(callback).to.have.been.calledOnce();
expect(callback).to.have.been.calledWithExactly(SOME_VALUE);
});
});
it(`rejects Cancelled exception instead of calling ${callbackName} if cancelled`, () => {
const callback = sinon.spy();
return run(callback, { parentToken: CancellationToken.canceled })
.then(NOT_CALLED, (exception) => {
expect(callback).to.not.have.been.called();
expect(exception).to.be.a(Cancelled);
});
});
it(`resolves with value returned from ${callbackName}`, () => {
const callback = sinon.stub().returns(SOME_VALUE);
return run(callback)
.then((value) => {
expect(callback).to.have.been.calledOnce();
expect(value).to.equal(SOME_VALUE);
});
});
it(`rejects with exception thrown from ${callbackName}`, () => {
const callback = sinon.stub().throws(SOME_VALUE);
return run(callback)
.then(NOT_CALLED, (value) => {
expect(callback).to.have.been.calledOnce();
expect(value).to.equal(SOME_VALUE);
});
});
it(`doesn't wait for ${callbackName} to complete if cancelled`, () => {
const callback = sinon.stub().returns(new Promise(NOOP));
const source = new CancellationTokenSource();
const promise = Promise.race([
run(callback, { parentToken: source.token }),
timeout(100),
])
.then(NOT_CALLED, (value) => {
expect(callback).to.have.been.calledOnce();
expect(value).to.be.a(Cancelled);
});
setTimeout(() => source.cancel(), 10);
return promise;
});
it(`isn't silent with rejection from ${callbackName}`, () => {
unhandledRejectionHandler = sinon.spy();
process.on(UNHANDLED_REJECTION_EVENT, unhandledRejectionHandler);
run(() => Promise.reject(SOME_VALUE));
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
expect(unhandledRejectionHandler).to.have.been.calledOnce();
expect(unhandledRejectionHandler).to.have.been.calledWith(SOME_VALUE);
resolve();
} catch (err) {
reject(err);
}
}, 10);
});
});
};
describe('cancellable-chain-of-promises', () => {
describe('always', () => {
it('is called with resolved value', () => {
const callback = sinon.spy();
return Promise.resolve(SOME_VALUE)
::always(callback)
.then(() => {
expect(callback).to.have.been.calledOnce();
expect(callback).to.have.been.calledWithExactly(undefined, SOME_VALUE);
});
});
it('is called with rejected value', () => {
const callback = sinon.spy();
return Promise.reject(SOME_VALUE)
::always(callback)
.then(NOT_CALLED, () => {
expect(callback).to.have.been.calledOnce();
expect(callback).to.have.been.calledWithExactly(SOME_VALUE, undefined);
});
});
it('is still called after then with cancelled exception', () => {
const chain = makeChain(CancellationToken.canceled);
const callback = sinon.spy();
return Promise.resolve()
::chain.then(NOT_CALLED)
::always(callback)
.then(NOT_CALLED, (cancelledError) => {
expect(cancelledError).to.be.a(Cancelled);
expect(callback).to.have.been.calledOnce();
expect(callback).to.have.been.calledWithExactly(cancelledError, undefined);
});
});
it('is still called after catch with cancelled exception', () => {
const chain = makeChain(CancellationToken.canceled);
const callback = sinon.spy();
return Promise.reject()
::chain.catch(NOT_CALLED)
::always(callback)
.then(NOT_CALLED, (cancelledError) => {
expect(cancelledError).to.be.a(Cancelled);
expect(callback).to.have.been.calledOnce();
expect(callback).to.have.been.calledWithExactly(cancelledError, undefined);
});
});
const runSilent = () =>
(Promise.reject(new Cancelled(CancellationToken.canceled))::always(NOOP));
testSilent(runSilent);
});
describe('token', () => {
describe('chain', () => {
describe('then', () => {
const run = (callback, { input, parentToken = CancellationToken.none } = {}) => {
const chain = makeChain(parentToken);
return Promise.resolve(input)
::chain.then(callback);
};
const runRejection = (callback, { input, parentToken = CancellationToken.none } = {}) => {
const chain = makeChain(parentToken);
return Promise.reject(input)
::chain.then(NOT_CALLED, callback);
};
testThenCatch(run);
testCallback(run);
testCallback(runRejection, 'rejection callback');
});
describe('catch', () => {
const run = (callback, { input, parentToken = CancellationToken.none } = {}) => {
const chain = makeChain(parentToken);
return Promise.reject(input)
::chain.catch(callback);
};
testThenCatch(run);
testCallback(run);
});
describe('ifcancelled', () => {
it('is called if cancelled', () => {
const chain = makeChain(CancellationToken.canceled);
const callback = sinon.spy();
return Promise.resolve()
::chain.ifcancelled(callback)
.then(() => {
expect(callback).to.have.been.calledOnce();
expect(callback).to.have.been.calledWithExactly(
sinon.match.instanceOf(CancellationToken));
});
});
it('resolves with returned value if cancelled', () => {
const chain = makeChain(CancellationToken.canceled);
const callback = sinon.stub().returns(SOME_VALUE);
return Promise.resolve()
::chain.ifcancelled(callback)
.then((value) => {
expect(callback).to.have.been.calledOnce();
expect(value).to.equal(SOME_VALUE);
});
});
it('rejects with thrown exception if cancelled', () => {
const chain = makeChain(CancellationToken.canceled);
const callback = sinon.stub().throws(SOME_VALUE);
return Promise.resolve()
::chain.ifcancelled(callback)
.then(NOT_CALLED, (exception) => {
expect(callback).to.have.been.calledOnce();
expect(exception).to.equal(SOME_VALUE);
});
});
it('is not called if not cancelled', () => {
const chain = makeChain(CancellationToken.none);
const callback = sinon.spy();
return Promise.resolve()
::chain.ifcancelled(callback)
.then(() => {
expect(callback).to.not.have.been.called();
});
});
});
});
describe('newPromise', () => {
it('returns a new Promise using the callback if not cancelled', () => {
const callback = sinon.spy();
const chain = makeChain(CancellationToken.none);
const promise = chain.newPromise(callback);
expect(promise).to.be.a(Promise);
expect(callback).to.have.been.calledOnce();
});
it('returns a new Promise that resolves from callback', () => {
const callback = sinon.stub().callsArgWith(0, SOME_VALUE);
const chain = makeChain(CancellationToken.none);
const promise = chain.newPromise(callback);
expect(promise).to.be.a(Promise);
expect(callback).to.have.been.calledOnce();
return expect(promise).to.resolve.to.equal(SOME_VALUE);
});
it('returns a new Promise that rejects from callback', () => {
const callback = sinon.stub().callsArgWith(1, SOME_VALUE);
const chain = makeChain(CancellationToken.none);
const promise = chain.newPromise(callback);
expect(promise).to.be.a(Promise);
expect(callback).to.have.been.calledOnce();
return expect(promise).to.reject.to.equal(SOME_VALUE);
});
it('doesn\'t call callback and return a Promise rejected with Cancelled exception if cancelled', () => {
const callback = sinon.stub().callsArgWith(0, SOME_VALUE);
const chain = makeChain(CancellationToken.canceled);
const promise = chain.newPromise(callback);
expect(promise).to.be.a(Promise);
expect(callback).to.not.have.been.called();
return expect(promise).to.reject.to.instanceof(Cancelled);
});
});
describe('resolve', () => {
it('returns a Promise resolved to the given value', () => {
const chain = makeChain(CancellationToken.none);
const promise = chain.resolve(SOME_VALUE);
expect(promise).to.be.a(Promise);
return expect(promise).to.resolve.to.equal(SOME_VALUE);
});
it('return a rejected Promise with Cancelled exception if cancelled', () => {
const chain = makeChain(CancellationToken.canceled);
const promise = chain.resolve(SOME_VALUE);
expect(promise).to.be.a(Promise);
return expect(promise).to.reject.to.instanceof(Cancelled);
});
});
describe('reject', () => {
it('returns a Promise rejected to the given value', () => {
const chain = makeChain(CancellationToken.none);
const promise = chain.resolve(SOME_VALUE);
expect(promise).to.be.a(Promise);
return expect(promise).to.resolve.to.equal(SOME_VALUE);
});
it('return a Promise rejected with Cancelled exception if cancelled', () => {
const chain = makeChain(CancellationToken.canceled);
const promise = chain.resolve(SOME_VALUE);
expect(promise).to.be.a(Promise);
return expect(promise).to.reject.to.instanceof(Cancelled);
});
});
});
describe('propagate', () => {
it('updates cancelled', () => {
const source = new CancellationTokenSource();
const chain = makeChain(source.token);
const childChain = makeChain(CancellationToken.canceled);
const callback = sinon.spy();
return Promise.resolve()
::childChain.then(NOOP)
::propagate(source)
::chain.catch(callback)
.then(NOT_CALLED, (/* cancelled */) => {
expect(callback).to.not.have.been.called();
expect(source.token.cancellationRequested).to.be.true();
});
});
it('doesn\'t update cancelled if not called', () => {
const source = new CancellationTokenSource();
const chain = makeChain(source.token);
const childChain = makeChain(CancellationToken.canceled);
const callback = sinon.spy();
return Promise.resolve()
::childChain.then(NOOP)
::chain.catch(callback)
.then(() => {
expect(callback).to.have.been.calledOnce();
expect(callback).to.have.been.calledWithExactly(sinon.match.instanceOf(Cancelled));
expect(source.token.cancellationRequested).to.be.false();
});
});
it('returns a promise', () => {
const source = new CancellationTokenSource();
const promise = Promise.resolve()
::propagate(source);
expect(promise).to.be.a(Promise);
});
const runSilent = () => {
const source = new CancellationTokenSource();
source.cancel();
return Promise.resolve()
::propagate(source);
};
testSilent(runSilent);
});
afterEach(() => {
if (unhandledRejectionHandler !== null) {
process.removeListener(UNHANDLED_REJECTION_EVENT, unhandledRejectionHandler);
}
unhandledRejectionHandler = null;
});
});
|