File: test/test.js

Recommend this page to a friend!
  Classes of Jeremy Judeaux   Cancellable chain of promises   test/test.js   Download  
File: test/test.js
Role: Example script
Content type: text/plain
Description: Example script
Class: Cancellable chain of promises
Run chains of actions based on asynchronous events
Author: By
Last change: Update of test/test.js
Date: 3 years ago
Size: 13,188 bytes
 

Contents

Class file image Download
/* 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; }); });