diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 016ffccc8..7c925489e 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -185,6 +185,10 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { return j$.isA_('Function', value); }; + j$.isAsyncFunction_ = function(value) { + return j$.isA_('AsyncFunction', value); + }; + j$.isA_ = function(typeName, value) { return j$.getType_(value) === '[object ' + typeName + ']'; }; @@ -935,6 +939,12 @@ getJasmineRequireObj().Env = function(j$) { } }; + var ensureIsFunctionOrAsync = function(fn, caller) { + if (!j$.isFunction_(fn) && !j$.isAsyncFunction_(fn)) { + throw new Error(caller + ' expects a function argument; received ' + j$.getType_(fn)); + } + }; + var suiteFactory = function(description) { var suite = new j$.Suite({ env: self, @@ -1073,7 +1083,7 @@ getJasmineRequireObj().Env = function(j$) { // it() sometimes doesn't have a fn argument, so only check the type if // it's given. if (arguments.length > 1 && typeof fn !== 'undefined') { - ensureIsFunction(fn, 'it'); + ensureIsFunctionOrAsync(fn, 'it'); } var spec = specFactory(description, fn, currentDeclarationSuite, timeout); if (currentDeclarationSuite.markedPending) { @@ -1087,7 +1097,7 @@ getJasmineRequireObj().Env = function(j$) { // xit(), like it(), doesn't always have a fn argument, so only check the // type when needed. if (arguments.length > 1 && typeof fn !== 'undefined') { - ensureIsFunction(fn, 'xit'); + ensureIsFunctionOrAsync(fn, 'xit'); } var spec = this.it.apply(this, arguments); spec.pend('Temporarily disabled with xit'); @@ -1095,7 +1105,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.fit = function(description, fn, timeout){ - ensureIsFunction(fn, 'fit'); + ensureIsFunctionOrAsync(fn, 'fit'); var spec = specFactory(description, fn, currentDeclarationSuite, timeout); currentDeclarationSuite.addChild(spec); focusedRunnables.push(spec.id); @@ -1112,7 +1122,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.beforeEach = function(beforeEachFunction, timeout) { - ensureIsFunction(beforeEachFunction, 'beforeEach'); + ensureIsFunctionOrAsync(beforeEachFunction, 'beforeEach'); currentDeclarationSuite.beforeEach({ fn: beforeEachFunction, timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } @@ -1120,7 +1130,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.beforeAll = function(beforeAllFunction, timeout) { - ensureIsFunction(beforeAllFunction, 'beforeAll'); + ensureIsFunctionOrAsync(beforeAllFunction, 'beforeAll'); currentDeclarationSuite.beforeAll({ fn: beforeAllFunction, timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } @@ -1128,7 +1138,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.afterEach = function(afterEachFunction, timeout) { - ensureIsFunction(afterEachFunction, 'afterEach'); + ensureIsFunctionOrAsync(afterEachFunction, 'afterEach'); currentDeclarationSuite.afterEach({ fn: afterEachFunction, timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } @@ -1136,7 +1146,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.afterAll = function(afterAllFunction, timeout) { - ensureIsFunction(afterAllFunction, 'afterAll'); + ensureIsFunctionOrAsync(afterAllFunction, 'afterAll'); currentDeclarationSuite.afterAll({ fn: afterAllFunction, timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } @@ -3859,11 +3869,10 @@ getJasmineRequireObj().QueueRunner = function(j$) { for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) { var queueableFn = queueableFns[iterativeIndex]; - if (queueableFn.fn.length > 0) { - attemptAsync(queueableFn); + var completedSynchronously = attempt(queueableFn); + + if (!completedSynchronously) { return; - } else { - attemptSync(queueableFn); } } @@ -3872,15 +3881,7 @@ getJasmineRequireObj().QueueRunner = function(j$) { self.onComplete(); }); - function attemptSync(queueableFn) { - try { - queueableFn.fn.call(self.userContext); - } catch (e) { - handleException(e, queueableFn); - } - } - - function attemptAsync(queueableFn) { + function attempt(queueableFn) { var clearTimeout = function () { Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]); }, @@ -3888,9 +3889,12 @@ getJasmineRequireObj().QueueRunner = function(j$) { onException(error); next(); }, - next = once(function () { + cleanup = once(function() { clearTimeout(timeoutId); self.globalErrors.popListener(handleError); + }), + next = once(function () { + cleanup(); self.run(queueableFns, iterativeIndex + 1); }), timeoutId; @@ -3911,11 +3915,23 @@ getJasmineRequireObj().QueueRunner = function(j$) { } try { - queueableFn.fn.call(self.userContext, next); + if (queueableFn.fn.length === 0) { + var maybeThenable = queueableFn.fn.call(self.userContext); + + if (maybeThenable && j$.isFunction_(maybeThenable.then)) { + maybeThenable.then(next, next.fail); + return false; + } + } else { + queueableFn.fn.call(self.userContext, next); + return false; + } } catch (e) { handleException(e, queueableFn); - next(); } + + cleanup(); + return true; } function onException(e) { diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index 61c525114..bf239707d 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -91,6 +91,13 @@ describe("Env", function() { env.it('pending spec'); }).not.toThrow(); }); + + it('accepts an async function', function() { + jasmine.getEnv().requireAsyncAwait(); + expect(function() { + env.it('async', jasmine.getEnv().makeAsyncAwaitFunction()); + }).not.toThrow(); + }); }); describe('#xit', function() { @@ -114,6 +121,13 @@ describe("Env", function() { env.xit('pending spec'); }).not.toThrow(); }); + + it('accepts an async function', function() { + jasmine.getEnv().requireAsyncAwait(); + expect(function() { + env.xit('async', jasmine.getEnv().makeAsyncAwaitFunction()); + }).not.toThrow(); + }); }); describe('#fit', function () { @@ -130,6 +144,13 @@ describe("Env", function() { env.beforeEach(undefined); }).toThrowError(/beforeEach expects a function argument; received \[object (Undefined|DOMWindow|Object)\]/); }); + + it('accepts an async function', function() { + jasmine.getEnv().requireAsyncAwait(); + expect(function() { + env.beforeEach(jasmine.getEnv().makeAsyncAwaitFunction()); + }).not.toThrow(); + }); }); describe('#beforeAll', function () { @@ -138,6 +159,13 @@ describe("Env", function() { env.beforeAll(undefined); }).toThrowError(/beforeAll expects a function argument; received \[object (Undefined|DOMWindow|Object)\]/); }); + + it('accepts an async function', function() { + jasmine.getEnv().requireAsyncAwait(); + expect(function() { + env.beforeAll(jasmine.getEnv().makeAsyncAwaitFunction()); + }).not.toThrow(); + }); }); describe('#afterEach', function () { @@ -146,6 +174,13 @@ describe("Env", function() { env.afterEach(undefined); }).toThrowError(/afterEach expects a function argument; received \[object (Undefined|DOMWindow|Object)\]/); }); + + it('accepts an async function', function() { + jasmine.getEnv().requireAsyncAwait(); + expect(function() { + env.afterEach(jasmine.getEnv().makeAsyncAwaitFunction()); + }).not.toThrow(); + }); }); describe('#afterAll', function () { @@ -154,5 +189,12 @@ describe("Env", function() { env.afterAll(undefined); }).toThrowError(/afterAll expects a function argument; received \[object (Undefined|DOMWindow|Object)\]/); }); + + it('accepts an async function', function() { + jasmine.getEnv().requireAsyncAwait(); + expect(function() { + env.afterAll(jasmine.getEnv().makeAsyncAwaitFunction()); + }).not.toThrow(); + }); }); }); diff --git a/spec/core/QueueRunnerSpec.js b/spec/core/QueueRunnerSpec.js index 3f1242def..ee0605385 100644 --- a/spec/core/QueueRunnerSpec.js +++ b/spec/core/QueueRunnerSpec.js @@ -273,6 +273,84 @@ describe("QueueRunner", function() { }); }); + describe("with a function that returns a promise", function() { + function StubPromise() {} + + StubPromise.prototype.then = function(resolve, reject) { + this.resolveHandler = resolve; + this.rejectHandler = reject; + }; + + beforeEach(function() { + jasmine.clock().install(); + }); + + afterEach(function() { + jasmine.clock().uninstall(); + }); + + it("runs the function asynchronously, advancing once the promise is settled", function() { + var onComplete = jasmine.createSpy('onComplete'), + fnCallback = jasmine.createSpy('fnCallback'), + p1 = new StubPromise(), + p2 = new StubPromise(), + queueableFn1 = { fn: function() { + setTimeout(function() { + p1.resolveHandler(); + }, 100); + return p1; + } }; + queueableFn2 = { fn: function() { + fnCallback(); + setTimeout(function() { + p2.resolveHandler(); + }, 100); + return p2; + } }, + queueRunner = new jasmineUnderTest.QueueRunner({ + queueableFns: [queueableFn1, queueableFn2], + onComplete: onComplete + }); + + queueRunner.execute(); + expect(fnCallback).not.toHaveBeenCalled(); + expect(onComplete).not.toHaveBeenCalled(); + + jasmine.clock().tick(100); + + expect(fnCallback).toHaveBeenCalled(); + expect(onComplete).not.toHaveBeenCalled(); + + jasmine.clock().tick(100); + + expect(onComplete).toHaveBeenCalled(); + }); + + it("fails the function when the promise is rejected", function() { + var promise = new StubPromise(), + queueableFn1 = { fn: function() { + setTimeout(function() { promise.rejectHandler('foo'); }, 100); + return promise; + } }, + queueableFn2 = { fn: jasmine.createSpy('fn2') }, + failFn = jasmine.createSpy('fail'), + queueRunner = new jasmineUnderTest.QueueRunner({ + queueableFns: [queueableFn1, queueableFn2], + fail: failFn + }); + + queueRunner.execute(); + + expect(failFn).not.toHaveBeenCalled(); + expect(queueableFn2.fn).not.toHaveBeenCalled(); + + jasmine.clock().tick(100); + + expect(failFn).toHaveBeenCalledWith('foo'); + expect(queueableFn2.fn).toHaveBeenCalled(); + }); + }); + it("calls exception handlers when an exception is thrown in a fn", function() { var queueableFn = { type: 'queueable', fn: function() { diff --git a/spec/helpers/asyncAwait.js b/spec/helpers/asyncAwait.js new file mode 100644 index 000000000..866d95264 --- /dev/null +++ b/spec/helpers/asyncAwait.js @@ -0,0 +1,27 @@ +(function(env) { + function getAsyncCtor() { + try { + eval("var func = async function(){};"); + } catch (e) { + return null; + } + + return Object.getPrototypeOf(func).constructor; + } + + function hasAsyncAwaitSupport() { + return getAsyncCtor() !== null; + } + + env.makeAsyncAwaitFunction = function() { + var AsyncFunction = getAsyncCtor(); + return new AsyncFunction(""); + }; + + env.requireAsyncAwait = function() { + if (!hasAsyncAwaitSupport()) { + env.pending("Environment does not support async/await functions"); + } + }; +})(jasmine.getEnv()); + diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index 83b19dbba..01f1d8282 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -6,6 +6,7 @@ "npmPackage/**/*.js" ], "helpers": [ + "helpers/asyncAwait.js", "helpers/checkForSet.js", "helpers/nodeDefineJasmineUnderTest.js" ], diff --git a/spec/support/jasmine.yml b/spec/support/jasmine.yml index da9590317..053f67e8e 100644 --- a/spec/support/jasmine.yml +++ b/spec/support/jasmine.yml @@ -16,6 +16,7 @@ src_files: - '**/*.js' stylesheets: helpers: + - 'helpers/asyncAwait.js' - 'helpers/BrowserFlags.js' - 'helpers/checkForSet.js' - 'helpers/defineJasmineUnderTest.js' diff --git a/src/core/Env.js b/src/core/Env.js index 81927ed94..6d7ae7557 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -319,6 +319,12 @@ getJasmineRequireObj().Env = function(j$) { } }; + var ensureIsFunctionOrAsync = function(fn, caller) { + if (!j$.isFunction_(fn) && !j$.isAsyncFunction_(fn)) { + throw new Error(caller + ' expects a function argument; received ' + j$.getType_(fn)); + } + }; + var suiteFactory = function(description) { var suite = new j$.Suite({ env: self, @@ -457,7 +463,7 @@ getJasmineRequireObj().Env = function(j$) { // it() sometimes doesn't have a fn argument, so only check the type if // it's given. if (arguments.length > 1 && typeof fn !== 'undefined') { - ensureIsFunction(fn, 'it'); + ensureIsFunctionOrAsync(fn, 'it'); } var spec = specFactory(description, fn, currentDeclarationSuite, timeout); if (currentDeclarationSuite.markedPending) { @@ -471,7 +477,7 @@ getJasmineRequireObj().Env = function(j$) { // xit(), like it(), doesn't always have a fn argument, so only check the // type when needed. if (arguments.length > 1 && typeof fn !== 'undefined') { - ensureIsFunction(fn, 'xit'); + ensureIsFunctionOrAsync(fn, 'xit'); } var spec = this.it.apply(this, arguments); spec.pend('Temporarily disabled with xit'); @@ -479,7 +485,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.fit = function(description, fn, timeout){ - ensureIsFunction(fn, 'fit'); + ensureIsFunctionOrAsync(fn, 'fit'); var spec = specFactory(description, fn, currentDeclarationSuite, timeout); currentDeclarationSuite.addChild(spec); focusedRunnables.push(spec.id); @@ -496,7 +502,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.beforeEach = function(beforeEachFunction, timeout) { - ensureIsFunction(beforeEachFunction, 'beforeEach'); + ensureIsFunctionOrAsync(beforeEachFunction, 'beforeEach'); currentDeclarationSuite.beforeEach({ fn: beforeEachFunction, timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } @@ -504,7 +510,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.beforeAll = function(beforeAllFunction, timeout) { - ensureIsFunction(beforeAllFunction, 'beforeAll'); + ensureIsFunctionOrAsync(beforeAllFunction, 'beforeAll'); currentDeclarationSuite.beforeAll({ fn: beforeAllFunction, timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } @@ -512,7 +518,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.afterEach = function(afterEachFunction, timeout) { - ensureIsFunction(afterEachFunction, 'afterEach'); + ensureIsFunctionOrAsync(afterEachFunction, 'afterEach'); currentDeclarationSuite.afterEach({ fn: afterEachFunction, timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } @@ -520,7 +526,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.afterAll = function(afterAllFunction, timeout) { - ensureIsFunction(afterAllFunction, 'afterAll'); + ensureIsFunctionOrAsync(afterAllFunction, 'afterAll'); currentDeclarationSuite.afterAll({ fn: afterAllFunction, timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } diff --git a/src/core/QueueRunner.js b/src/core/QueueRunner.js index 8cafd26f4..59f6691c6 100644 --- a/src/core/QueueRunner.js +++ b/src/core/QueueRunner.js @@ -40,11 +40,10 @@ getJasmineRequireObj().QueueRunner = function(j$) { for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) { var queueableFn = queueableFns[iterativeIndex]; - if (queueableFn.fn.length > 0) { - attemptAsync(queueableFn); + var completedSynchronously = attempt(queueableFn); + + if (!completedSynchronously) { return; - } else { - attemptSync(queueableFn); } } @@ -53,15 +52,7 @@ getJasmineRequireObj().QueueRunner = function(j$) { self.onComplete(); }); - function attemptSync(queueableFn) { - try { - queueableFn.fn.call(self.userContext); - } catch (e) { - handleException(e, queueableFn); - } - } - - function attemptAsync(queueableFn) { + function attempt(queueableFn) { var clearTimeout = function () { Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]); }, @@ -69,9 +60,12 @@ getJasmineRequireObj().QueueRunner = function(j$) { onException(error); next(); }, - next = once(function () { + cleanup = once(function() { clearTimeout(timeoutId); self.globalErrors.popListener(handleError); + }), + next = once(function () { + cleanup(); self.run(queueableFns, iterativeIndex + 1); }), timeoutId; @@ -92,11 +86,23 @@ getJasmineRequireObj().QueueRunner = function(j$) { } try { - queueableFn.fn.call(self.userContext, next); + if (queueableFn.fn.length === 0) { + var maybeThenable = queueableFn.fn.call(self.userContext); + + if (maybeThenable && j$.isFunction_(maybeThenable.then)) { + maybeThenable.then(next, next.fail); + return false; + } + } else { + queueableFn.fn.call(self.userContext, next); + return false; + } } catch (e) { handleException(e, queueableFn); - next(); } + + cleanup(); + return true; } function onException(e) { diff --git a/src/core/base.js b/src/core/base.js index 89b945557..bbd811702 100644 --- a/src/core/base.js +++ b/src/core/base.js @@ -58,6 +58,10 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) { return j$.isA_('Function', value); }; + j$.isAsyncFunction_ = function(value) { + return j$.isA_('AsyncFunction', value); + }; + j$.isA_ = function(typeName, value) { return j$.getType_(value) === '[object ' + typeName + ']'; };