Commits

Etienne Folio committed ac4d7aa Merge

Merged in qraynaud/angular-extend-promises (pull request #2)

tests: add test for all missing promise methods and fix issues

Comments (0)

Files changed (30)

     "sinon": false,
     "beforeEach": false,
     "afterEach": false,
-    "inject": false
+    "inject": false,
+    "createPromiseChain": true,
   }
 }

src/newq/errors.js

   this.message = message;
   if (Error.captureStackTrace)
     Error.captureStackTrace(this, this.constructor);
+  else
+    this.stack = (new Error()).stack;
 };
 
 NewQError.prototype.toString = function() {
   NewQError.apply(this, arguments);
 };
 NewQError.subError(AggregateError);
+
+var TimeoutError = module.exports.TimeoutError = function TimeoutError() {
+  NewQError.apply(this, arguments);
+};
+NewQError.subError(TimeoutError);

src/newq/index.js

   when: require('./when'),
 
   // Errors
-  AggregateError: errors.AggregateError
+  AggregateError: errors.AggregateError,
+  TimeoutError: errors.TimeoutError
 });
 
 if (globals.$options.compatibilityAliases) {

src/promise/call.js

 
 module.exports = function() {
   var args = _.toArray(arguments);
-  var method = args.unshift();
+  var method = args.shift();
 
   return this.then(function(val) {
     return val[method].apply(val, args);

src/promise/decorate.js

   map: callNewQ('map'),
   nodeify: require('./nodeify'),
   props: callNewQ('props'),
-  spread: require('./spread'),
-  tap: require('./tap'),
-  'throw': require('./throw'),
-  timeout: require('./timeout'),
   reduce: callNewQ('reduce'),
   'return': require('./return'),
   some: callNewQ('some'),
+  spread: require('./spread'),
+  tap: require('./tap'),
   then: require('./then'),
+  'throw': require('./throw'),
+  timeout: require('./timeout'),
 
   constructor: Promise
 };

src/promise/nodeify.js

 
       if (!options.spread || !_.isArray(a))
         a = [null, a];
-      else {
-        a = _.clone(a);
-        a.unshift(null);
-      }
+      else
+        a = [null].concat(a);
 
       cb.apply(this, a);
 

src/promise/timeout.js

 'use strict';
 
 var globals = require('../globals');
+var newq = require('../newq');
 
 module.exports = function(ms, msg) {
   var def = globals.$defer(this);
-  var to = setTimeout(def.reject, ms, msg || 'Timed out after ' + ms + ' ms');
+  var to = setTimeout(function() {
+    def.reject(new newq.TimeoutError(msg || 'Timed out after ' + ms + ' ms'));
+  }, ms);
 
   this
     .then(function(val) {

tests/functionals/newq/each.js

     $rootScope.$digest();
   });
 
+  it('should call the callback with index & array length', function() {
+    var spy = sinon.spy();
+
+    newq.each([21, 42], spy);
+
+    $rootScope.$digest();
+
+    expect(spy)
+      .to.have.been.calledTwice
+      .and.to.have.been.calledWithExactly(21, 0, 2)
+      .and.to.have.been.calledWithExactly(42, 1, 2)
+    ;
+  });
+
   it('should return a decorated promise', function() {
     expect(newq.each([2], function() {}))
       .to.have.deep.property('constructor.name', 'Promise')

tests/functionals/newq/map.js

     chain.play($rootScope);
   });
 
+  it('should call the callback with index & array length', function() {
+    var spy = sinon.spy();
+
+    newq.map([21, 42], spy);
+
+    $rootScope.$digest();
+
+    expect(spy)
+      .to.have.been.calledTwice
+      .and.to.have.been.calledWithExactly(21, 0, 2)
+      .and.to.have.been.calledWithExactly(42, 1, 2)
+    ;
+  });
+
   it('should return a decorated promise', function() {
     expect(newq.map([2], function() {}))
       .to.have.deep.property('constructor.name', 'Promise')

tests/functionals/promise/all.js

+describe('Promise.all', function() {
+  'use strict';
+
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should synchronize an array of promises or values', function(done) {
+    expect(newq.resolve([newq.resolve(1), 2]).all())
+      .to.eventually.be.deep.equal([1, 2])
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/any.js

+describe('Promise.any', function() {
+  'use strict';
+
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should resolve with the first promise that resolves', function(done) {
+    expect(newq.resolve([newq.resolve(1), 2]).any())
+      .to.eventually.be.equal(1)
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/bind.js

+describe('Promise.bind', function() {
+  'use strict';
+
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should bind this in the next callback', function(done) {
+    var test = {};
+    expect(newq.resolve(42).bind(test).then(function() {
+      expect(this).to.be.equal(test);
+    }))
+      .to.eventually.be.fulfilled
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+
+  it('should also bind following promises', function(done) {
+    var test = {};
+    expect(newq.resolve(42).bind(test).then(function() {}).then(function() {
+      expect(this).to.be.equal(test);
+    }))
+      .to.eventually.be.fulfilled
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/call.js

+describe('Promise.call', function() {
+  'use strict';
+
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should call the method provided with the correct context & arguments', function(done) {
+    var test = {
+      method: sinon.spy()
+    };
+
+    expect(newq.resolve(test).call('method', 21, 42).then(function() {
+      expect(test.method)
+        .to.have.been.calledOnce
+        .and.to.have.been.calledWith(21, 42)
+        .and.to.have.been.calledOn(test)
+      ;
+    }))
+      .to.eventually.be.fulfilled
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/catch.js

+describe('Promise.catch', function() {
+  'use strict';
+
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should be called on errors', function(done) {
+    var test = new Error();
+
+    expect(newq.reject(test).catch(function(err) {
+      expect(err).to.be.equal(test);
+    }))
+      .to.eventually.be.fulfilled
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/delay.js

+describe('Promise.delay', function() {
+  'use strict';
+
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should delay by x ms the promise flow', function(done) {
+    var stub = sinon.stub().returnsArg(0);
+
+    expect(newq.resolve(42).delay(10).then(stub))
+      .to.eventually.be.equal(42)
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(stub).to.not.have.been.called;
+
+    setTimeout(function() {
+      $rootScope.$digest();
+      expect(stub).to.have.been.called;
+    }, 10);
+  });
+});

tests/functionals/promise/done.js

+describe('Promise.done', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module(function($exceptionHandlerProvider) {
+    $exceptionHandlerProvider.mode('log');
+  }));
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+  afterEach(function() {
+    if (!window.setTimeout.restore)
+      return;
+    window.setTimeout.restore();
+  });
+
+  function setTimeoutStub(error, done) {
+    var t = setTimeout(done, 100, 'Global error not sent');
+
+    sinon.stub(window, 'setTimeout', function(fn) {
+      expect(fn).to.throw(error);
+
+      clearTimeout(t);
+      done();
+    });
+  }
+
+  it('should throw a global error when a rejected value goes through done', function(done) {
+    var myError = new Error('flowTest');
+
+    setTimeoutStub(myError, done);
+
+    newq.reject(myError).done();
+
+    $rootScope.$digest();
+  });
+
+  it('should throw a global error when the callback returns a rejected promise', function(done) {
+    var myError = new Error('resolveTest');
+
+    setTimeoutStub(myError, done);
+
+    newq.resolve(42).done(function() {
+      return newq.reject(myError);
+    });
+
+    $rootScope.$digest();
+  });
+
+  it('should throw a global error when the callback throws', function(done) {
+    var myError = new Error('throwTest');
+
+    setTimeoutStub(myError, done);
+
+    newq.resolve(42).done(function() {
+      throw myError;
+    });
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/each.js

+describe('Promise.each', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  function add1(val) {
+    return val + 1;
+  }
+
+  it('should be able to iterate over a value returned by a promise', function(done) {
+    expect(newq.resolve([1, 3]).each(add1))
+      .to.eventually.deep.equal([1, 3])
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+
+  it('should be able to iterate over a value returned by newq.each', function(done) {
+    expect(newq.each([1, 3], add1).each(add1))
+      .to.eventually.deep.equal([1, 3])
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+
+  it('should be able to iterate over a value returned by another promise.each', function(done) {
+    expect(newq.resolve([1, 3]).each(add1).each(add1))
+      .to.eventually.deep.equal([1, 3])
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+
+  it('should flow promises down each asynchronously after a newq.each', function(done) {
+    newq.each([newq.defer().promise, 1], function(val) {
+      return val;
+    })
+      .each(add1)
+      .each(function(val) {
+        expect(val).to.be.equal(1);
+        done();
+      })
+    ;
+
+    $rootScope.$digest();
+  });
+
+  it('should flow promises down each asynchronously after a promise.each', function(done) {
+    newq.resolve([newq.defer().promise, 2])
+      .each(add1)
+      .each(function(val) {
+        expect(val).to.be.equal(2);
+        done();
+      })
+    ;
+
+    $rootScope.$digest();
+  });
+
+  it('should call the callback with index & array length', function() {
+    var spy = sinon.spy();
+
+    newq.resolve([21, 42]).each(spy);
+
+    $rootScope.$digest();
+
+    expect(spy)
+      .to.have.been.calledTwice
+      .and.to.have.been.calledWithExactly(21, 0, 2)
+      .and.to.have.been.calledWithExactly(42, 1, 2)
+    ;
+  });
+});

tests/functionals/promise/filter.js

+describe('Promise.filter', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  function filterEven(val) {
+    return !(val % 2);
+  }
+
+  it('should filter elements of an array', function(done) {
+    expect(newq.resolve([1, 2, 3, 4]).filter(filterEven))
+      .to.eventually.deep.equal([2, 4])
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/finally.js

+describe('Promise.finally', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should be called on normal flow without any argument', function(done) {
+    var spy = sinon.spy();
+
+    expect(newq.resolve([1, 3]).finally(spy))
+      .to.eventually.deep.equal([1, 3])
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(spy)
+      .to.have.been.calledOnce
+      .and.to.have.been.calledWithExactly()
+    ;
+  });
+
+  it('should be called on error flow without any argument', function(done) {
+    var spy = sinon.spy();
+
+    expect(newq.reject(new Error()).finally(spy))
+      .to.eventually.be.rejected
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(spy)
+      .to.have.been.calledOnce
+      .and.to.have.been.calledWithExactly()
+    ;
+  });
+
+  it('should synchronize the promise returned by finally if there is one', function(done) {
+    var stubFinally = sinon.stub().returns(newq.resolve().delay(10));
+    var stubThen = sinon.stub().returnsArg(0);
+
+    expect(newq.resolve(42).finally(stubFinally).then(stubThen))
+      .to.eventually.be.equal(42)
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(stubFinally)
+      .to.have.been.calledOnce
+      .and.to.have.been.calledWithExactly()
+    ;
+
+    expect(stubThen).to.not.have.been.called;
+
+    setTimeout(function() {
+      $rootScope.$digest();
+      expect(stubThen).to.have.been.calledWith(42);
+    }, 10);
+  });
+});

tests/functionals/promise/get.js

+describe('Promise.get', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should return the value of the given property of the resolved value', function(done) {
+    var test = {
+      prop: 42
+    };
+
+    expect(newq.resolve(test).get('prop'))
+      .to.eventually.equal(42)
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/map.js

 
     $rootScope.$digest();
   });
+
+  it('should call the callback with index & array length', function() {
+    var spy = sinon.spy();
+
+    newq.resolve([21, 42]).map(spy);
+
+    $rootScope.$digest();
+
+    expect(spy)
+      .to.have.been.calledTwice
+      .and.to.have.been.calledWithExactly(21, 0, 2)
+      .and.to.have.been.calledWithExactly(42, 1, 2)
+    ;
+  });
 });

tests/functionals/promise/nodeify.js

+describe('Promise.nodeify', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should call the provided callback with (null, value) on normal flow', function(done) {
+    var spy = sinon.spy();
+
+    expect(newq.resolve(42).nodeify(spy))
+      .to.eventually.be.fulfilled
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(spy).to.have.been.calledWithExactly(null, 42);
+  });
+
+  it('should call the provided callback with (null, [values]) on normal flow when ' +
+    'the value is an array', function(done) {
+    var spy = sinon.spy();
+
+    expect(newq.resolve([42, 21]).nodeify(spy))
+      .to.eventually.be.fulfilled
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(spy).to.have.been.calledWithExactly(null, [42, 21]);
+  });
+
+  it('should call the provided callback with (null, value...) on normal flow when ' +
+    'the value is an array and option spread is true', function(done) {
+    var spy = sinon.spy();
+
+    expect(newq.resolve([42, 21]).nodeify(spy, {spread: true}))
+      .to.eventually.be.fulfilled
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(spy).to.have.been.calledWithExactly(null, 42, 21);
+  });
+});

tests/functionals/promise/props.js

+describe('Promise.props', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it("should synchronize an object's properties", function(done) {
+    expect(newq.resolve({
+      val: 42,
+      prom: newq.resolve(2)
+    }).props())
+      .to.eventually.be.deep.equal({
+        val: 42,
+        prom: 2
+      })
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(spy).to.have.been.calledWithExactly(null, 42);
+  });
+});

tests/functionals/promise/reduce.js

+describe('Promise.reduce', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  function add(a, b) {
+    return a + b;
+  }
+
+  it('should reduce the resolved array', function(done) {
+    expect(newq.resolve([1, 2, 3]).reduce(add, 1))
+      .to.eventually.be.equal(7)
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/return.js

+describe('Promise.return', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should return the provided value discarding the previous one', function(done) {
+    expect(newq.resolve(21).return(42))
+      .to.eventually.be.equal(42)
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/some.js

+describe('Promise.some', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should return the x first promises to resolve in order of resolution', function(done) {
+    expect(newq.resolve([newq.resolve(1).delay(5), 2, newq.resolve(2).delay(10)]).some(2))
+      .to.eventually.be.deep.equal([2, 1])
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+    setTimeout($rootScope.$digest.bind($rootScope), 5);
+  });
+});

tests/functionals/promise/spread.js

+describe('Promise.spread', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should call the method with the expended array (like apply)', function(done) {
+    var spy = sinon.stub().returns(42);
+
+    expect(newq.resolve([1, 3]).spread(spy))
+      .to.eventually.be.equal(42)
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(spy).to.have.been.calledWithExactly(1, 3);
+  });
+});

tests/functionals/promise/tap.js

+describe('Promise.tap', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should call the method with the value & ignore result', function(done) {
+    var spy = sinon.stub().returns(42);
+
+    expect(newq.resolve([1, 3]).tap(spy))
+      .to.eventually.be.deep.equal([1, 3])
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    expect(spy).to.have.been.calledWithExactly([1, 3]);
+  });
+});

tests/functionals/promise/throw.js

+describe('Promise.throw', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should reject the promise with the provided value', function(done) {
+    var test = new Error();
+
+    expect(newq.resolve(42).throw(test))
+      .to.eventually.be.rejected
+      .and.to.be.equal(test)
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+});

tests/functionals/promise/timeout.js

+describe('Promise.timeout', function() {
+  var newq;
+  var $rootScope;
+
+  beforeEach(module('angular-extend-promises'));
+  beforeEach(inject(function($q, _$rootScope_) {
+    newq = $q;
+    $rootScope = _$rootScope_;
+  }));
+
+  it('should do nothing when the promise resolves before the timeout', function(done) {
+    var test = new Error();
+
+    expect(newq.resolve(42).timeout(10))
+      .to.eventually.be.equal(42)
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+  });
+
+  it('should reject the promise with a TimeoutError if the promise resolves ' +
+    'after the timeout', function(done) {
+    expect(newq.resolve(42).delay(20).timeout(10))
+      .to.eventually.be.rejectedWith(newq.TimeoutError, 'Timed out after 10 ms')
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    setTimeout($rootScope.$digest.bind($rootScope), 10);
+  });
+
+  it('should reject the promise with a TimeoutError and the provided message if ' +
+    'the promise resolves after the timeout', function(done) {
+    expect(newq.resolve(42).delay(20).timeout(10, 'message'))
+      .to.eventually.be.rejectedWith(newq.TimeoutError, 'message')
+      .notify(done)
+    ;
+
+    $rootScope.$digest();
+
+    setTimeout($rootScope.$digest.bind($rootScope), 10);
+  });
+});