feat($q): add streamlined ES6-style interface for using $q

This potentially helps lead the way towards a more performant fly-weight implementation, as discussed
earlier in the year. Using a constructor means we can put things in the prototype chain, and essentially
treat $q as a Promise class, and reuse methods as appropriate.

Short of that, I feel this style is slightly more convenient and streamlined, compared with the older
API.

Closes #8311
Closes #6427 (I know it's not really the solution asked for in #6427, sorry!)
This commit is contained in:
Caitlin Potter
2014-07-03 14:09:31 -04:00
parent c54228fbe9
commit f3a763fd2e
2 changed files with 676 additions and 6 deletions

View File

@@ -196,6 +196,605 @@ describe('q', function() {
});
describe('$Q', function() {
var resolve, reject, resolve2, reject2;
var createPromise = function() {
return q(function(resolveFn, rejectFn) {
if (resolve === null) {
resolve = resolveFn;
reject = rejectFn;
} else if (resolve2 === null) {
resolve2 = resolveFn;
reject2 = rejectFn;
}
});
};
afterEach(function() {
resolve = reject = resolve2 = reject2 = null;
});
it('should return a Promise', function() {
var promise = q(noop);
expect(typeof promise.then).toBe('function');
expect(typeof promise.catch).toBe('function');
expect(typeof promise.finally).toBe('function');
});
describe('resolve', function() {
it('should fulfill the promise and execute all success callbacks in the registration order',
function() {
var promise = createPromise();
promise.then(success(1), error());
promise.then(success(2), error());
expect(logStr()).toBe('');
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success1(foo)->foo; success2(foo)->foo');
});
it('should do nothing if a promise was previously resolved', function() {
var promise = createPromise();
promise.then(success(), error());
expect(logStr()).toBe('');
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success(foo)->foo');
log = [];
resolve('bar');
reject('baz');
expect(mockNextTick.queue.length).toBe(0);
expect(logStr()).toBe('');
});
it('should do nothing if a promise was previously rejected', function() {
var promise = createPromise();
promise.then(success(), error());
expect(logStr()).toBe('');
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('error(foo)->reject(foo)');
log = [];
resolve('bar');
reject('baz');
expect(mockNextTick.queue.length).toBe(0);
expect(logStr()).toBe('');
});
it('should allow deferred resolution with a new promise', function() {
var promise = createPromise();
promise.then(success(), error());
resolve(createPromise());
mockNextTick.flush();
expect(logStr()).toBe('');
resolve2('foo');
mockNextTick.flush();
expect(logStr()).toBe('success(foo)->foo');
});
it('should call the callback in the next turn', function() {
var promise = createPromise();
promise.then(success());
expect(logStr()).toBe('');
resolve('foo');
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('success(foo)->foo');
});
it('should not break if a callbacks registers another callback', function() {
var promise = createPromise();
promise.then(function() {
log.push('outer');
promise.then(function() {
log.push('inner');
});
});
resolve('foo');
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('outer; inner');
});
it('should not break if a callbacks tries to resolve the deferred again', function() {
var promise = createPromise();
promise.then(function(val) {
log.push('then1(' + val + ')->resolve(bar)');
deferred.resolve('bar'); // nop
});
promise.then(success(2));
resolve('foo');
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('then1(foo)->resolve(bar); success2(foo)->foo');
});
});
describe('reject', function() {
it('should reject the promise and execute all error callbacks in the registration order',
function() {
var promise = createPromise();
promise.then(success(), error(1));
promise.then(success(), error(2));
expect(logStr()).toBe('');
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
});
it('should do nothing if a promise was previously resolved', function() {
var promise = createPromise();
promise.then(success(1), error(1));
expect(logStr()).toBe('');
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success1(foo)->foo');
log = [];
reject('bar');
resolve('baz');
expect(mockNextTick.queue.length).toBe(0);
expect(logStr()).toBe('');
promise.then(success(2), error(2));
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('success2(foo)->foo');
});
it('should do nothing if a promise was previously rejected', function() {
var promise = createPromise();
promise.then(success(1), error(1));
expect(logStr()).toBe('');
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('error1(foo)->reject(foo)');
log = [];
reject('bar');
resolve('baz');
expect(mockNextTick.queue.length).toBe(0);
expect(logStr()).toBe('');
promise.then(success(2), error(2));
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('error2(foo)->reject(foo)');
});
it('should not defer rejection with a new promise', function() {
var promise = createPromise();
promise.then(success(), error());
reject(createPromise());
mockNextTick.flush();
expect(logStr()).toBe('error({})->reject({})');
});
it('should call the error callback in the next turn', function() {
var promise = createPromise();
promise.then(success(), error());
expect(logStr()).toBe('');
reject('foo');
expect(logStr()).toBe('');
mockNextTick.flush();
expect(logStr()).toBe('error(foo)->reject(foo)');
});
it('should support non-bound execution', function() {
var promise = createPromise();
promise.then(success(), error());
reject('detached');
mockNextTick.flush();
expect(logStr()).toBe('error(detached)->reject(detached)');
});
});
describe('promise', function() {
describe('then', function() {
it('should allow registration of a success callback without an errback or progressback ' +
'and resolve', function() {
var promise = createPromise();
promise.then(success());
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success(foo)->foo');
});
it('should allow registration of a success callback without an errback and reject',
function() {
var promise = createPromise();
promise.then(success());
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('');
});
it('should allow registration of an errback without a success or progress callback and ' +
' reject', function() {
var promise = createPromise();
promise.then(null, error());
reject('oops!');
mockNextTick.flush();
expect(logStr()).toBe('error(oops!)->reject(oops!)');
});
it('should allow registration of an errback without a success callback and resolve',
function() {
var promise = createPromise();
promise.then(null, error());
resolve('done');
mockNextTick.flush();
expect(logStr()).toBe('');
});
it('should allow registration of an progressback without a success callback and resolve',
function() {
var promise = createPromise();
promise.then(null, null, progress());
resolve('done');
mockNextTick.flush();
expect(logStr()).toBe('');
});
it('should allow registration of an progressback without a error callback and reject',
function() {
var promise = createPromise();
promise.then(null, null, progress());
reject('oops!');
mockNextTick.flush();
expect(logStr()).toBe('');
});
it('should resolve all callbacks with the original value', function() {
var promise = createPromise();
promise.then(success('A', 'aVal'), error(), progress());
promise.then(success('B', 'bErr', true), error(), progress());
promise.then(success('C', q.reject('cReason')), error(), progress());
promise.then(success('D', q.reject('dReason'), true), error(), progress());
promise.then(success('E', 'eVal'), error(), progress());
expect(logStr()).toBe('');
resolve('yup');
mockNextTick.flush();
expect(log).toEqual(['successA(yup)->aVal',
'successB(yup)->throw(bErr)',
'successC(yup)->{}',
'successD(yup)->throw({})',
'successE(yup)->eVal']);
});
it('should reject all callbacks with the original reason', function() {
var promise = createPromise();
promise.then(success(), error('A', 'aVal'), progress());
promise.then(success(), error('B', 'bEr', true), progress());
promise.then(success(), error('C', q.reject('cReason')), progress());
promise.then(success(), error('D', 'dVal'), progress());
expect(logStr()).toBe('');
reject('noo!');
mockNextTick.flush();
expect(logStr()).toBe('errorA(noo!)->aVal; errorB(noo!)->throw(bEr); errorC(noo!)->{}; errorD(noo!)->dVal');
});
it('should propagate resolution and rejection between dependent promises', function() {
var promise = createPromise();
promise.then(success(1, 'x'), error('1')).
then(success(2, 'y', true), error('2')).
then(success(3), error(3, 'z', true)).
then(success(4), error(4, 'done')).
then(success(5), error(5));
expect(logStr()).toBe('');
resolve('sweet!');
mockNextTick.flush();
expect(log).toEqual(['success1(sweet!)->x',
'success2(x)->throw(y)',
'error3(y)->throw(z)',
'error4(z)->done',
'success5(done)->done']);
});
it('should reject a derived promise if an exception is thrown while resolving its parent',
function() {
var promise = createPromise();
promise.then(success(1, 'oops', true), error(1)).
then(success(2), error(2));
resolve('done!');
mockNextTick.flush();
expect(logStr()).toBe('success1(done!)->throw(oops); error2(oops)->reject(oops)');
});
it('should reject a derived promise if an exception is thrown while rejecting its parent',
function() {
var promise = createPromise();
promise.then(null, error(1, 'oops', true)).
then(success(2), error(2));
reject('timeout');
mockNextTick.flush();
expect(logStr()).toBe('error1(timeout)->throw(oops); error2(oops)->reject(oops)');
});
it('should call success callback in the next turn even if promise is already resolved',
function() {
var promise = createPromise();
resolve('done!');
promise.then(success());
expect(logStr()).toBe('');
mockNextTick.flush();
expect(log).toEqual(['success(done!)->done!']);
});
it('should call error callback in the next turn even if promise is already rejected',
function() {
var promise = createPromise();
reject('oops!');
promise.then(null, error());
expect(logStr()).toBe('');
mockNextTick.flush();
expect(log).toEqual(['error(oops!)->reject(oops!)']);
});
it('should forward success resolution when success callbacks are not functions', function() {
var promise = createPromise();
resolve('yay!');
promise.then(1).
then(null).
then({}).
then('gah!').
then([]).
then(success());
expect(logStr()).toBe('');
mockNextTick.flush();
expect(log).toEqual(['success(yay!)->yay!']);
});
it('should forward error resolution when error callbacks are not functions', function() {
var promise = createPromise();
reject('oops!');
promise.then(null, 1).
then(null, null).
then(null, {}).
then(null, 'gah!').
then(null, []).
then(null, error());
expect(logStr()).toBe('');
mockNextTick.flush();
expect(log).toEqual(['error(oops!)->reject(oops!)']);
});
});
describe('finally', function() {
it('should not take an argument',
function() {
var promise = createPromise();
promise['finally'](fin(1));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()');
});
describe("when the promise is fulfilled", function () {
it('should call the callback',
function() {
var promise = createPromise();
promise.then(success(1))['finally'](fin(1));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('success1(foo)->foo; finally1()');
});
it('should fulfill with the original value',
function() {
var promise = createPromise();
promise['finally'](fin('B', 'b'), error('B')).
then(success('BB', 'bb'), error('BB'));
resolve('RESOLVED_VAL');
mockNextTick.flush();
expect(log).toEqual(['finallyB()->b',
'successBB(RESOLVED_VAL)->bb']);
});
it('should fulfill with the original value (larger test)',
function() {
var promise = createPromise();
promise.then(success('A', 'a'), error('A'));
promise['finally'](fin('B', 'b'), error('B')).
then(success('BB', 'bb'), error('BB'));
promise.then(success('C', 'c'), error('C'))['finally'](fin('CC', 'IGNORED'))
.then(success('CCC', 'cc'), error('CCC'))
.then(success('CCCC', 'ccc'), error('CCCC'));
resolve('RESOLVED_VAL');
mockNextTick.flush();
expect(log).toEqual(['successA(RESOLVED_VAL)->a',
'finallyB()->b',
'successC(RESOLVED_VAL)->c',
'successBB(RESOLVED_VAL)->bb',
'finallyCC()->IGNORED',
'successCCC(c)->cc',
'successCCCC(cc)->ccc']);
});
describe("when the callback returns a promise", function() {
describe("that is fulfilled", function() {
it("should fulfill with the original reason after that promise resolves",
function () {
var promise = createPromise();
var promise2 = createPromise();
resolve2('bar');
promise['finally'](fin(1, promise))
.then(success(2));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->{}; success2(foo)->foo');
});
});
describe("that is rejected", function() {
it("should reject with this new rejection reason",
function () {
var promise = createPromise();
var promise2 = createPromise();
reject2('bar');
promise['finally'](fin(1, promise2))
.then(success(2), error(1));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
});
});
});
describe("when the callback throws an exception", function() {
it("should reject with this new exception", function() {
var promise = createPromise();
promise['finally'](fin(1, "exception", true))
.then(success(1), error(2));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
});
});
});
describe("when the promise is rejected", function () {
it("should call the callback", function () {
var promise = createPromise();
promise['finally'](fin(1))
.then(success(2), error(1));
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1(); error1(foo)->reject(foo)');
});
it('should reject with the original reason', function() {
var promise = createPromise();
promise['finally'](fin(1), "hello")
.then(success(2), error(2));
reject('original');
mockNextTick.flush();
expect(logStr()).toBe('finally1(); error2(original)->reject(original)');
});
describe("when the callback returns a promise", function() {
describe("that is fulfilled", function() {
it("should reject with the original reason after that promise resolves", function () {
var promise = createPromise();
var promise2 = createPromise();
resolve2('bar');
promise['finally'](fin(1, promise2))
.then(success(2), error(2));
reject('original');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->{}; error2(original)->reject(original)');
});
});
describe("that is rejected", function () {
it("should reject with the new reason", function() {
var promise = createPromise();
var promise2 = createPromise();
reject2('bar');
promise['finally'](fin(1, promise2))
.then(success(2), error(1));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
});
});
});
describe("when the callback throws an exception", function() {
it("should reject with this new exception", function() {
var promise = createPromise();
promise['finally'](fin(1, "exception", true))
.then(success(1), error(2));
resolve('foo');
mockNextTick.flush();
expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
});
});
});
});
describe('catch', function() {
it('should be a shorthand for defining promise error handlers', function() {
var promise = createPromise();
promise['catch'](error(1)).then(null, error(2));
reject('foo');
mockNextTick.flush();
expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
});
});
});
});
describe('defer', function() {
it('should create a new deferred', function() {
expect(deferred.promise).toBeDefined();