ayepromise
Version:
A teeny-tiny promise library
816 lines (663 loc) • 28.5 kB
JavaScript
(function (definition) {
if (typeof module !== 'undefined') {
module.exports = definition;
} else {
this.specs = definition;
}
})(function (subject, libraryName) {
var helpers = this.helpers || require('./helpers.js');
describe(libraryName, function () {
beforeEach(function () {
helpers.setSubject(subject);
});
describe("resolve", function () {
it("should resolve 'null'", function () {
var defer = subject.defer();
defer.resolve(null);
});
it("should not fulfill an already fulfilled promise", function () {
var defer = subject.defer(),
onFulfillCallback = jasmine.createSpy('onFulfill'),
done = false;
defer.promise.then(onFulfillCallback);
defer.resolve(null);
setTimeout(function() {
onFulfillCallback.reset();
defer.resolve(42);
setTimeout(function() {
expect(onFulfillCallback).not.toHaveBeenCalled();
done = true;
}, 10);
}, 10);
waitsFor(function () {
return done;
});
});
it("should not fulfill a rejected promise", function () {
var defer = subject.defer(),
onFulfillCallback = jasmine.createSpy('onFulfill'),
done = false;
defer.promise.then(onFulfillCallback);
defer.reject(new Error('just because'));
setTimeout(function() {
defer.resolve(42);
setTimeout(function() {
expect(onFulfillCallback).not.toHaveBeenCalled();
done = true;
}, 10);
}, 10);
waitsFor(function () {
return done;
});
});
});
describe("on fulfill callback", function () {
helpers.testFulfilled("should execute a given function when promised is resolved", null, function (promise, done) {
promise.then(done);
});
it("should not execute a given function before resolved", function () {
var defer = subject.defer(),
spy = jasmine.createSpy("call me");
defer.promise.then(spy);
expect(spy).not.toHaveBeenCalled();
});
helpers.testFulfilled("should pass the result to the callback", "the value", function (promise, done) {
promise.then(function (value) {
expect(value).toBe("the value");
done();
});
});
helpers.testFulfilled("should handle multiple callbacks", null, function (promise, done) {
var spy = jasmine.createSpy("call me"),
yetAnotherSpy = jasmine.createSpy("call me too");
promise.then(spy);
promise.then(yetAnotherSpy);
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalled();
expect(yetAnotherSpy).toHaveBeenCalled();
done();
});
});
helpers.testFulfilled("should allow pipelining", null, function (promise, done) {
var spy = jasmine.createSpy("call me"),
yetAnotherSpy = jasmine.createSpy("call me after the other spy finished"),
followingPromise;
followingPromise = promise.then(spy);
followingPromise.then(yetAnotherSpy);
waitsFor(function () {
return yetAnotherSpy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalled();
expect(yetAnotherSpy).toHaveBeenCalled();
});
done();
});
helpers.testFulfilled("should pass the result of one call as argument to the following", null, function (promise, done) {
promise
.then(function () {
return 42;
})
.then(function (value) {
expect(value).toBe(42);
done();
});
});
it("should only call the next link in the call chain when a returned promise has been resolved", function () {
var defer = subject.defer(),
secondDefer = subject.defer(),
spy = jasmine.createSpy("call me").andReturn(secondDefer.promise),
yetAnotherSpy = jasmine.createSpy("call me after the other spy");
defer.promise
.then(spy)
.then(yetAnotherSpy);
defer.resolve();
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalled();
expect(yetAnotherSpy).not.toHaveBeenCalled();
});
runs(function () {
secondDefer.resolve();
});
waitsFor(function () {
return yetAnotherSpy.wasCalled;
});
runs(function () {
expect(yetAnotherSpy).toHaveBeenCalled();
});
});
helpers.testFulfilled("should pass the result from a promise on to the next function in a call chain", null, function (promise, done) {
var defer = subject.defer();
promise
.then(function () {
return defer.promise;
})
.then(function (value) {
expect(value).toBe("hey there");
done();
});
defer.resolve("hey there");
});
helpers.testFulfilled("should call the next link in the call chain with the resolved value", null, function (promise, done) {
var defer = subject.defer();
promise
.then(function () {
return defer.promise;
})
.then(function (value) {
expect(value).toBe("99");
done();
});
defer.resolve("99");
});
it("should trigger only when the promised passed to resolve has been resolved", function () {
var defer = subject.defer(),
secondDefer = subject.defer(),
spy = jasmine.createSpy("call me");
defer.promise.then(spy);
defer.resolve(secondDefer.promise);
waits(10);
runs(function () {
expect(spy).not.toHaveBeenCalled();
secondDefer.resolve("yay");
});
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalledWith("yay");
});
});
helpers.testFulfilled("should allow non-functions (an integer) in fulfill handler", "hey there", function (promise, done) {
var nonFunction = 42;
promise
.then(nonFunction)
.then(function (value) {
expect(value).toBe("hey there");
done();
});
});
helpers.testFulfilled("should allow non-functions (undefined) in fulfill handler", "hey there", function (promise, done) {
var nonFunction;
promise
.then(nonFunction)
.then(function (value) {
expect(value).toBe("hey there");
done();
});
});
helpers.testFulfilled("should return before calling handler", null, function (promise, done) {
var isDone = false;
promise
.then(function () {
expect(isDone).toBeTruthy();
done();
});
isDone = true;
});
});
describe("reject", function () {
it("should not reject a fulfilled promise", function () {
var defer = subject.defer(),
onRejectCallback = jasmine.createSpy('onReject'),
done = false;
defer.promise.then(null, onRejectCallback);
defer.resolve(null);
setTimeout(function() {
defer.reject(new Error('because'));
setTimeout(function() {
expect(onRejectCallback).not.toHaveBeenCalled();
done = true;
}, 10);
}, 10);
waitsFor(function () {
return done;
});
});
it("should not reject a rejected promise", function () {
var defer = subject.defer(),
onRejectCallback = jasmine.createSpy('onReject'),
done = false;
defer.promise.then(null, onRejectCallback);
defer.reject(new Error('original reason'));
setTimeout(function() {
onRejectCallback.reset();
defer.reject(new Error('because'));
setTimeout(function() {
expect(onRejectCallback).not.toHaveBeenCalled();
done = true;
}, 10);
}, 10);
waitsFor(function () {
return done;
});
});
});
describe("on reject callback", function () {
var error = new Error("didn't work out, sorry");
helpers.testRejected("should execute a fail callback", null, function (promise, done) {
promise.then(null, function () {
done();
});
});
helpers.testRejected("should pass the error to the fail callback", error, function (promise, done) {
promise.then(null, function (value) {
expect(value).toBe(error);
done();
});
});
helpers.testFulfilled("should not trigger fail callback passed after promise has been fulfilled", null, function (promise, done) {
var spy = jasmine.createSpy("don't call me");
promise.then(function () {
expect(spy).not.toHaveBeenCalled();
done();
}, spy);
});
helpers.testRejected("should not trigger fulfilled callback passed after promise has been rejected", null, function (promise, done) {
var spy = jasmine.createSpy("don't call me");
promise.then(spy, function () {
expect(spy).not.toHaveBeenCalled();
done();
});
});
helpers.testRejected("should handle multiple fail callbacks", null, function (promise, done) {
var spy = jasmine.createSpy("call me"),
yetAnotherSpy = jasmine.createSpy("call me too");
promise.then(null, spy);
promise.then(null, yetAnotherSpy);
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalled();
expect(yetAnotherSpy).toHaveBeenCalled();
done();
});
});
helpers.testRejected("should allow pipelining with a fail callback", null, function (promise, done) {
var spy = jasmine.createSpy("call me"),
followingPromise;
followingPromise = promise.then(null, spy);
followingPromise.then(function () {
expect(spy).toHaveBeenCalled();
done();
});
});
helpers.testFulfilled("should trigger a fail callback in a chain on a raised exception", null, function (promise, done) {
var error = new Error("meh");
promise
.then(function () {
throw error;
})
.then(null, function (value) {
expect(value).toBe(error);
done();
});
});
helpers.testFulfilled("should not trigger the call chain on a raised exception", null, function (promise, done) {
var spy = jasmine.createSpy("call me").andThrow(new Error()),
anotherSpy = jasmine.createSpy("another spy");
promise
.then(spy)
.then(anotherSpy);
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(anotherSpy).not.toHaveBeenCalled();
done();
});
});
helpers.testFulfilled("should trigger a fail callback in a chain on a failed promise", null, function (promise, done) {
var defer = subject.defer(),
error = new Error("meh");
promise
.then(function () {
return defer.promise;
})
.then(null, function (value) {
expect(value).toBe(error);
done();
});
defer.reject(error);
});
helpers.testFulfilled("should not trigger a then callback in a chain on a failed promise", null, function (promise, done) {
var defer = subject.defer(),
anotherSpy = jasmine.createSpy("another spy");
promise
.then(function () {
return defer.promise;
})
.then(anotherSpy, function () {
expect(anotherSpy).not.toHaveBeenCalled();
done();
});
defer.reject(new Error());
});
helpers.testFulfilled("should handle a 'catch-all' fail at the end of a call chain", null, function (promise, done) {
var error = new Error("oopsie");
promise
.then(function () {
throw error;
})
.then(function () {})
.then(null, function (value) {
expect(value).toBe(error);
done();
});
});
helpers.testFulfilled("should handle a call chain appended after a fail handler", null, function (promise, done) {
promise
.then(null, function () {})
.then(function () {
done();
});
});
it("should trigger fail callback when the promised passed to resolve has been rejected", function () {
var defer = subject.defer(),
secondDefer = subject.defer(),
error = new Error("noes"),
spy = jasmine.createSpy("call me");
defer.promise.then(null, spy);
defer.resolve(secondDefer.promise);
// Didn't know a better way to wait for Q triggering its internals
waits(10);
runs(function () {
expect(spy).not.toHaveBeenCalled();
secondDefer.reject(error);
});
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalledWith(error);
});
});
it("should reject immediately even when given a promise", function () {
var defer = subject.defer(),
secondDefer = subject.defer(),
spy = jasmine.createSpy("call me");
defer.promise.then(null, spy);
defer.reject(secondDefer.promise);
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalledWith(secondDefer.promise);
});
});
helpers.testRejected("should allow non-functions (an integer) in reject handler", error, function (promise, done) {
var nonFunction = 42;
promise
.then(null, nonFunction)
.then(null, function (value) {
expect(value).toBe(error);
done();
});
});
helpers.testRejected("should allow non-functions (undefined) in reject handler", error, function (promise, done) {
var nonFunction;
promise
.then(null, nonFunction)
.then(null, function (value) {
expect(value).toBe(error);
done();
});
});
helpers.testRejected("should return before calling handler", error, function (promise, done) {
var isDone = false;
promise
.then(null, function () {
expect(isDone).toBeTruthy();
done();
});
isDone = true;
});
});
describe("fail shorthand", function () {
it("should act as a shorthand to then", function () {
var defer = subject.defer(),
error = new Error("fail"),
spy = jasmine.createSpy("call me");
defer.promise.fail(spy);
defer.reject(error);
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalledWith(error);
});
});
});
describe("resolve thenables", function () {
helpers.testFulfilled("should accept a pseudo promise", null, function (promise, done) {
promise
.then(function () {
return {
then: function (f) {
f(42);
}
};
})
.then(function (value) {
expect(value).toBe(42);
done();
});
});
helpers.testFulfilled("should recursively resolve promises", null, function (promise, done) {
promise
.then(function () {
return {
then: function (f) {
f({
then: function (f) {
f(42);
}
});
}
};
})
.then(function (value) {
expect(value).toBe(42);
done();
});
});
helpers.testFulfilledIfNotQ('should access "then" getter only once', libraryName, null, function (promise, done) {
var getterCallCount = 0;
promise
.then(function () {
return Object.create(null, {
then: {
get: function () {
getterCallCount += 1;
return function (onFulfilled) {
onFulfilled();
};
}
}
});
})
.then(function () {
expect(getterCallCount).toBe(1);
done();
});
});
it('should not resolve twice when waiting for a thenable', function () {
var defer = subject.defer(),
fulfillSpy = jasmine.createSpy("fulfill"),
fulfill;
defer.promise.then(fulfillSpy);
defer.resolve({
then: function (onFulfill) {
fulfill = onFulfill;
}
});
defer.resolve();
waitsFor(function () {
return fulfill !== undefined;
});
runs(function () {
fulfill();
});
waits(50);
runs(function () {
expect(fulfillSpy.callCount).toBe(1);
});
});
it('should not reject promise when waiting for a thenable from fulfilling', function () {
var defer = subject.defer(),
fulfillSpy = jasmine.createSpy('fulfill'),
rejectSpy = jasmine.createSpy('reject'),
fulfill;
defer.promise.then(fulfillSpy, rejectSpy);
defer.resolve({
then: function (onFulfill) {
fulfill = onFulfill;
}
});
defer.reject();
waitsFor(function () {
return fulfill !== undefined;
});
runs(function () {
fulfill();
});
waits(50);
runs(function () {
expect(fulfillSpy).toHaveBeenCalled();
expect(rejectSpy).not.toHaveBeenCalled();
});
});
});
describe("rogue thenables", function () {
it('should not call fulfill more than once', function () {
var defer = subject.defer(),
spy = jasmine.createSpy("call me"),
p = defer.promise.then(function () {
return {
then: function (onFulfill) {
onFulfill(1);
onFulfill(2);
}
};
});
p.then(spy);
defer.resolve();
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalledWith(1);
expect(spy).not.toHaveBeenCalledWith(2);
});
});
it('should not call reject more than once', function () {
var defer = subject.defer(),
spy = jasmine.createSpy("call me"),
p = defer.promise.then(function () {
return {
then: function (onFulfill, onReject) {
onReject(1);
onReject(2);
}
};
});
p.then(null, spy);
defer.resolve();
waitsFor(function () {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalledWith(1);
expect(spy).not.toHaveBeenCalledWith(2);
});
});
it('should not call fulfill and reject together', function () {
var defer = subject.defer(),
fulfillSpy = jasmine.createSpy("fulfill"),
rejectSpy = jasmine.createSpy("reject"),
p = defer.promise.then(function () {
return {
then: function (onFulfill, onReject) {
onFulfill(1);
onReject(2);
}
};
});
p.then(fulfillSpy, rejectSpy);
defer.resolve();
waitsFor(function () {
return fulfillSpy.wasCalled;
});
runs(function () {
expect(fulfillSpy).toHaveBeenCalledWith(1);
expect(rejectSpy).not.toHaveBeenCalled();
});
});
helpers.testFulfilled("should handle a failing thenable", null, function (promise, done) {
var error = new Error("error");
promise.then(function () {
return {
then: function () {
throw error;
}
};
}).then(null, function (e) {
expect(e).toBe(error);
done();
});
});
helpers.ifNotQIt('should reject when thenable accessor throws an error', libraryName, function () {
var defer = subject.defer(),
spy = jasmine.createSpy('call me'),
e = new Error('error');
defer.resolve();
defer.promise
.then(function () {
return Object.create(null, {
then: {
get: function () {
throw e;
}
}
});
})
.then(null, spy);
waitsFor(function() {
return spy.wasCalled;
});
runs(function () {
expect(spy).toHaveBeenCalledWith(e);
});
});
it('should not reject a failing thenable after it fulfilled', function () {
var defer = subject.defer(),
fulfillSpy = jasmine.createSpy("fulfill"),
rejectSpy = jasmine.createSpy("reject"),
p = defer.promise.then(function () {
return {
then: function (onFulfill) {
onFulfill(1);
throw new Error("an error");
}
};
});
p.then(fulfillSpy, rejectSpy);
defer.resolve();
waitsFor(function () {
return fulfillSpy.wasCalled;
});
runs(function () {
expect(fulfillSpy).toHaveBeenCalledWith(1);
expect(rejectSpy).not.toHaveBeenCalled();
});
});
});
});
});