// Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The SFC licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. 'use strict'; const assert = require('assert'); const testutil = require('./testutil'); const promise = require('../../lib/promise'); // Aliases for readability. const NativePromise = Promise; const StubError = testutil.StubError; const assertIsStubError = testutil.assertIsStubError; const callbackHelper = testutil.callbackHelper; const callbackPair = testutil.callbackPair; const throwStubError = testutil.throwStubError; const fail = () => assert.fail(); // Refer to promise_aplus_test for promise compliance with standard behavior. describe('promise', function() { var app, uncaughtExceptions; beforeEach(function setUp() { promise.LONG_STACK_TRACES = false; uncaughtExceptions = []; app = promise.controlFlow(); app.on(promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, (e) => uncaughtExceptions.push(e)); }); afterEach(function tearDown() { app.reset(); promise.setDefaultFlow(new promise.ControlFlow); assert.deepEqual([], uncaughtExceptions, 'Did not expect any uncaught exceptions'); promise.LONG_STACK_TRACES = false; }); const assertIsPromise = (p) => assert.ok(promise.isPromise(p)); const assertNotPromise = (v) => assert.ok(!promise.isPromise(v)); function createRejectedPromise(reason) { var p = promise.rejected(reason); p.catch(function() {}); return p; } it('testCanDetectPromiseLikeObjects', function() { assertIsPromise(new promise.Promise(function(fulfill) { fulfill(); })); assertIsPromise(new promise.Deferred()); assertIsPromise(new promise.Deferred().promise); assertIsPromise({then:function() {}}); assertNotPromise(undefined); assertNotPromise(null); assertNotPromise(''); assertNotPromise(true); assertNotPromise(false); assertNotPromise(1); assertNotPromise({}); assertNotPromise({then:1}); assertNotPromise({then:true}); assertNotPromise({then:''}); }); describe('then', function() { it('returnsOwnPromiseIfNoCallbacksWereGiven', function() { var deferred = new promise.Deferred(); assert.equal(deferred.promise, deferred.promise.then()); assert.equal(deferred.promise, deferred.promise.catch()); assert.equal(deferred.promise, promise.when(deferred.promise)); }); it('stillConsideredUnHandledIfNoCallbacksWereGivenOnCallsToThen', function() { promise.rejected(new StubError).then(); var handler = callbackHelper(assertIsStubError); // so tearDown() doesn't throw app.removeAllListeners(); app.on(promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, handler); return NativePromise.resolve().then(() => handler.assertCalled()); }); }); describe('thenFinally', function() { it('nonFailingCallbackDoesNotSuppressOriginalError', function() { var done = callbackHelper(assertIsStubError); return promise.rejected(new StubError). thenFinally(function() {}). catch(done). thenFinally(done.assertCalled); }); it('failingCallbackSuppressesOriginalError', function() { var done = callbackHelper(assertIsStubError); return promise.rejected(new Error('original')). thenFinally(throwStubError). catch(done). thenFinally(done.assertCalled); }); it('callbackThrowsAfterFulfilledPromise', function() { var done = callbackHelper(assertIsStubError); return promise.fulfilled(). thenFinally(throwStubError). catch(done). thenFinally(done.assertCalled); }); it('callbackReturnsRejectedPromise', function() { var done = callbackHelper(assertIsStubError); return promise.fulfilled(). thenFinally(function() { return promise.rejected(new StubError); }). catch(done). thenFinally(done.assertCalled); }); }); describe('cancel', function() { it('passesTheCancellationReasonToReject', function() { var d = new promise.Deferred(); var res = d.then(assert.fail, function(e) { assert.ok(e instanceof promise.CancellationError); assert.equal('because i said so', e.message); }); d.cancel('because i said so'); return res; }); it('canCancelADeferredFromAChainedPromise', function() { var d = new promise.Deferred(); var p = d.then(assert.fail, function(e) { assert.ok(e instanceof promise.CancellationError); assert.equal('because i said so', e.message); }); var p2 = p.then(function() {}, assert.fail); p.cancel('because i said so'); return p2; }); it('canCancelATimeout', function() { var p = promise.delayed(25) .then(assert.fail, (e) => e instanceof promise.CancellationError); setTimeout(() => p.cancel(), 20); p.cancel(); return p; }); it('cancelIsANoopOnceAPromiseHasBeenFulfilled', function() { var p = promise.fulfilled(123); p.cancel(); return p.then((v) => assert.equal(123, v)); }); it('cancelIsANoopOnceAPromiseHasBeenRejected', function() { var p = promise.rejected(new StubError); p.cancel(); var pair = callbackPair(null, assertIsStubError); return p.then(assert.fail, assertIsStubError); }); it('noopCancelTriggeredOnCallbackOfResolvedPromise', function() { var d = promise.defer(); var p = d.promise.then(); d.fulfill(); p.cancel(); // This should not throw. return p; // This should not trigger a failure. }); }); describe('when', function() { it('ReturnsAResolvedPromiseIfGivenANonPromiseValue', function() { var ret = promise.when('abc'); assertIsPromise(ret); return ret.then((value) => assert.equal('abc', value)); }); it('PassesRawErrorsToCallbacks', function() { var error = new Error('boo!'); return promise.when(error, function(value) { assert.equal(error, value); }); }); it('WaitsForValueToBeResolvedBeforeInvokingCallback', function() { var d = new promise.Deferred(), callback; let result = promise.when(d, callback = callbackHelper(function(value) { assert.equal('hi', value); })); callback.assertNotCalled(); d.fulfill('hi'); return result.then(callback.assertCalled); }); it('canCancelReturnedPromise', function() { var callbacks = callbackPair(null, function(e) { assert.ok(e instanceof promise.CancellationError); assert.equal('just because', e.message); }); var promiseLike = { then: function(cb, eb) { this.callback = cb; this.errback = eb; } }; var aPromise = promise.when(promiseLike, callbacks.callback, callbacks.errback); assert.ok(aPromise.isPending()); aPromise.cancel('just because'); return aPromise.thenFinally(callbacks.assertErrback); }); }); it('firesUncaughtExceptionEventIfRejectionNeverHandled', function() { promise.rejected(new StubError); var handler = callbackHelper(assertIsStubError); // so tearDown() doesn't throw app.removeAllListeners(); app.on(promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, handler); return NativePromise.resolve().then(handler.assertCalled); }); it('cannotResolveADeferredWithItself', function() { var deferred = new promise.Deferred(); assert.throws(() => deferred.fulfill(deferred)); assert.throws(() => deferred.reject(deferred)); }); describe('fullyResolved', function() { it('primitives', function() { function runTest(value) { var callback, errback; return promise.fullyResolved(value) .then((resolved) => assert.equal(value, resolved)); } return runTest(true) .then(() => runTest(function() {})) .then(() => runTest(null)) .then(() => runTest(123)) .then(() => runTest('foo bar')) .then(() => runTest(undefined)); }); it('arrayOfPrimitives', function() { var fn = function() {}; var array = [true, fn, null, 123, '', undefined, 1]; return promise.fullyResolved(array).then(function(resolved) { assert.equal(array, resolved); assert.deepEqual([true, fn, null, 123, '', undefined, 1], resolved); }); }); it('nestedArrayOfPrimitives', function() { var fn = function() {}; var array = [true, [fn, null, 123], '', undefined]; return promise.fullyResolved(array) .then(function(resolved) { assert.equal(array, resolved); assert.deepEqual([true, [fn, null, 123], '', undefined], resolved); assert.deepEqual([fn, null, 123], resolved[1]); }); }); it('arrayWithPromisedPrimitive', function() { return promise.fullyResolved([promise.fulfilled(123)]) .then(function(resolved) { assert.deepEqual([123], resolved); }); }); it('promiseResolvesToPrimitive', function() { return promise.fullyResolved(promise.fulfilled(123)) .then((resolved) => assert.equal(123, resolved)); }); it('promiseResolvesToArray', function() { var fn = function() {}; var array = [true, [fn, null, 123], '', undefined]; var aPromise = promise.fulfilled(array); var result = promise.fullyResolved(aPromise); return result.then(function(resolved) { assert.equal(array, resolved); assert.deepEqual([true, [fn, null, 123], '', undefined], resolved); assert.deepEqual([fn, null, 123], resolved[1]); }); }); it('promiseResolvesToArrayWithPromises', function() { var nestedPromise = promise.fulfilled(123); var aPromise = promise.fulfilled([true, nestedPromise]); return promise.fullyResolved(aPromise) .then(function(resolved) { assert.deepEqual([true, 123], resolved); }); }); it('rejectsIfArrayPromiseRejects', function() { var nestedPromise = createRejectedPromise(new StubError); var aPromise = promise.fulfilled([true, nestedPromise]); var pair = callbackPair(null, assertIsStubError); return promise.fullyResolved(aPromise) .then(assert.fail, assertIsStubError); }); it('rejectsOnFirstArrayRejection', function() { var e1 = new Error('foo'); var e2 = new Error('bar'); var aPromise = promise.fulfilled([ createRejectedPromise(e1), createRejectedPromise(e2) ]); return promise.fullyResolved(aPromise) .then(assert.fail, function(error) { assert.strictEqual(e1, error); }); }); it('rejectsIfNestedArrayPromiseRejects', function() { var aPromise = promise.fulfilled([ promise.fulfilled([ createRejectedPromise(new StubError) ]) ]); return promise.fullyResolved(aPromise) .then(assert.fail, assertIsStubError); }); it('simpleHash', function() { var hash = {'a': 123}; return promise.fullyResolved(hash) .then(function(resolved) { assert.strictEqual(hash, resolved); assert.deepEqual(hash, {'a': 123}); }); }); it('nestedHash', function() { var nestedHash = {'foo':'bar'}; var hash = {'a': 123, 'b': nestedHash}; return promise.fullyResolved(hash) .then(function(resolved) { assert.strictEqual(hash, resolved); assert.deepEqual({'a': 123, 'b': {'foo': 'bar'}}, resolved); assert.strictEqual(nestedHash, resolved['b']); }); }); it('promiseResolvesToSimpleHash', function() { var hash = {'a': 123}; var aPromise = promise.fulfilled(hash); return promise.fullyResolved(aPromise) .then((resolved) => assert.strictEqual(hash, resolved)); }); it('promiseResolvesToNestedHash', function() { var nestedHash = {'foo':'bar'}; var hash = {'a': 123, 'b': nestedHash}; var aPromise = promise.fulfilled(hash); return promise.fullyResolved(aPromise) .then(function(resolved) { assert.strictEqual(hash, resolved); assert.strictEqual(nestedHash, resolved['b']); assert.deepEqual(hash, {'a': 123, 'b': {'foo': 'bar'}}); }); }); it('promiseResolvesToHashWithPromises', function() { var aPromise = promise.fulfilled({ 'a': promise.fulfilled(123) }); return promise.fullyResolved(aPromise) .then(function(resolved) { assert.deepEqual({'a': 123}, resolved); }); }); it('rejectsIfHashPromiseRejects', function() { var aPromise = promise.fulfilled({ 'a': createRejectedPromise(new StubError) }); return promise.fullyResolved(aPromise) .then(assert.fail, assertIsStubError); }); it('rejectsIfNestedHashPromiseRejects', function() { var aPromise = promise.fulfilled({ 'a': {'b': createRejectedPromise(new StubError)} }); return promise.fullyResolved(aPromise) .then(assert.fail, assertIsStubError); }); it('instantiatedObject', function() { function Foo() { this.bar = 'baz'; } var foo = new Foo; return promise.fullyResolved(foo).then(function(resolvedFoo) { assert.equal(foo, resolvedFoo); assert.ok(resolvedFoo instanceof Foo); assert.deepEqual(new Foo, resolvedFoo); }); }); it('withEmptyArray', function() { return promise.fullyResolved([]).then(function(resolved) { assert.deepEqual([], resolved); }); }); it('withEmptyHash', function() { return promise.fullyResolved({}).then(function(resolved) { assert.deepEqual({}, resolved); }); }); it('arrayWithPromisedHash', function() { var obj = {'foo': 'bar'}; var array = [promise.fulfilled(obj)]; return promise.fullyResolved(array).then(function(resolved) { assert.deepEqual(resolved, [obj]); }); }); }); describe('checkedNodeCall', function() { it('functionThrows', function() { return promise.checkedNodeCall(throwStubError) .then(assert.fail, assertIsStubError); }); it('functionReturnsAnError', function() { return promise.checkedNodeCall(function(callback) { callback(new StubError); }).then(assert.fail, assertIsStubError); }); it('functionReturnsSuccess', function() { var success = 'success!'; return promise.checkedNodeCall(function(callback) { callback(null, success); }).then((value) => assert.equal(success, value)); }); it('functionReturnsAndThrows', function() { var error = new Error('boom'); var error2 = new Error('boom again'); return promise.checkedNodeCall(function(callback) { callback(error); throw error2; }).then(assert.fail, (e) => assert.equal(error, e)); }); it('functionThrowsAndReturns', function() { var error = new Error('boom'); var error2 = new Error('boom again'); return promise.checkedNodeCall(function(callback) { setTimeout(() => callback(error), 10); throw error2; }).then(assert.fail, (e) => assert.equal(error2, e)); }); }); describe('all', function() { it('(base case)', function() { var a = [ 0, 1, promise.defer(), promise.defer(), 4, 5, 6 ]; delete a[5]; var pair = callbackPair(function(value) { assert.deepEqual([0, 1, 2, 3, 4, undefined, 6], value); }); var result = promise.all(a).then(pair.callback, pair.errback); pair.assertNeither(); a[2].fulfill(2); pair.assertNeither(); a[3].fulfill(3); return result.then(() => pair.assertCallback()); }); it('empty array', function() { return promise.all([]).then((a) => assert.deepEqual([], a)); }); it('usesFirstRejection', function() { var a = [ promise.defer(), promise.defer() ]; var result = promise.all(a).then(assert.fail, assertIsStubError); a[1].reject(new StubError); setTimeout(() => a[0].reject(Error('ignored')), 0); return result; }); }); describe('map', function() { it('(base case)', function() { var a = [1, 2, 3]; return promise.map(a, function(value, index, a2) { assert.equal(a, a2); assert.equal('number', typeof index, 'not a number'); return value + 1; }).then(function(value) { assert.deepEqual([2, 3, 4], value); }); }); it('omitsDeleted', function() { var a = [0, 1, 2, 3, 4, 5, 6]; delete a[1]; delete a[3]; delete a[4]; delete a[6]; var expected = [0, 1, 4, 9, 16, 25, 36]; delete expected[1]; delete expected[3]; delete expected[4]; delete expected[6]; return promise.map(a, function(value) { return value * value; }).then(function(value) { assert.deepEqual(expected, value); }); }); it('emptyArray', function() { return promise.map([], function(value) { return value + 1; }).then(function(value) { assert.deepEqual([], value); }); }); it('inputIsPromise', function() { var input = promise.defer(); var result = promise.map(input, function(value) { return value + 1; }); var pair = callbackPair(function(value) { assert.deepEqual([2, 3, 4], value); }); result = result.then(pair.callback, pair.errback); setTimeout(function() { pair.assertNeither(); input.fulfill([1, 2, 3]); }, 10); return result; }); it('waitsForFunctionResultToResolve', function() { var innerResults = [ promise.defer(), promise.defer() ]; var result = promise.map([1, 2], function(value, index) { return innerResults[index].promise; }); var pair = callbackPair(function(value) { assert.deepEqual(['a', 'b'], value); }); result = result.then(pair.callback, pair.errback); return NativePromise.resolve() .then(function() { pair.assertNeither(); innerResults[0].fulfill('a'); }) .then(function() { pair.assertNeither(); innerResults[1].fulfill('b'); return result; }) .then(pair.assertCallback); }); it('rejectsPromiseIfFunctionThrows', function() { return promise.map([1], throwStubError) .then(assert.fail, assertIsStubError); }); it('rejectsPromiseIfFunctionReturnsRejectedPromise', function() { return promise.map([1], function() { return promise.rejected(new StubError); }).then(assert.fail, assertIsStubError); }); it('stopsCallingFunctionIfPreviousIterationFailed', function() { var count = 0; return promise.map([1, 2, 3, 4], function() { count++; if (count == 3) { throw new StubError; } }).then(assert.fail, function(e) { assertIsStubError(e); assert.equal(3, count); }); }); it('rejectsWithFirstRejectedPromise', function() { var innerResult = [ promise.fulfilled(), createRejectedPromise(new StubError), createRejectedPromise(Error('should be ignored')) ]; var count = 0; return promise.map([1, 2, 3, 4], function(value, index) { count += 1; return innerResult[index]; }).then(assert.fail, function(e) { assertIsStubError(e); assert.equal(2, count); }); }); it('preservesOrderWhenMapReturnsPromise', function() { var deferreds = [ promise.defer(), promise.defer(), promise.defer(), promise.defer() ]; var result = promise.map(deferreds, function(value) { return value.promise; }); var pair = callbackPair(function(value) { assert.deepEqual([0, 1, 2, 3], value); }); result = result.then(pair.callback, pair.errback); return NativePromise.resolve() .then(function() { pair.assertNeither(); for (let i = deferreds.length; i > 0; i -= 1) { deferreds[i - 1].fulfill(i - 1); } return result; }).then(pair.assertCallback); }); }); describe('filter', function() { it('basicFiltering', function() { var a = [0, 1, 2, 3]; return promise.filter(a, function(val, index, a2) { assert.equal(a, a2); assert.equal('number', typeof index, 'not a number'); return val > 1; }).then(function(val) { assert.deepEqual([2, 3], val); }); }); it('omitsDeleted', function() { var a = [0, 1, 2, 3, 4, 5, 6]; delete a[3]; delete a[4]; return promise.filter(a, function(value) { return value > 1 && value < 6; }).then(function(val) { assert.deepEqual([2, 5], val); }); }); it('preservesInputs', function() { var a = [0, 1, 2, 3]; return promise.filter(a, function(value, i, a2) { assert.equal(a, a2); // Even if a function modifies the input array, the original value // should be inserted into the new array. a2[i] = a2[i] - 1; return a2[i] >= 1; }).then(function(val) { assert.deepEqual([2, 3], val); }); }); it('inputIsPromise', function() { var input = promise.defer(); var result = promise.filter(input, function(value) { return value > 1 && value < 3; }); var pair = callbackPair(function(value) { assert.deepEqual([2], value); }); result = result.then(pair.callback, pair.errback); return NativePromise.resolve() .then(function() { pair.assertNeither(); input.fulfill([1, 2, 3]); return result; }) .then(pair.assertCallback); }); it('waitsForFunctionResultToResolve', function() { var innerResults = [ promise.defer(), promise.defer() ]; var result = promise.filter([1, 2], function(value, index) { return innerResults[index].promise; }); var pair = callbackPair(function(value) { assert.deepEqual([2], value); }); result = result.then(pair.callback, pair.errback); return NativePromise.resolve() .then(function() { pair.assertNeither(); innerResults[0].fulfill(false); }) .then(function() { pair.assertNeither(); innerResults[1].fulfill(true); return result; }) .then(pair.assertCallback); }); it('rejectsPromiseIfFunctionReturnsRejectedPromise', function() { return promise.filter([1], function() { return promise.rejected(new StubError); }).then(assert.fail, assertIsStubError); }); it('stopsCallingFunctionIfPreviousIterationFailed', function() { var count = 0; return promise.filter([1, 2, 3, 4], function() { count++; if (count == 3) { throw new StubError; } }).then(assert.fail, function(e) { assertIsStubError(e); assert.equal(3, count); }); }); it('rejectsWithFirstRejectedPromise', function() { var innerResult = [ promise.fulfilled(), createRejectedPromise(new StubError), createRejectedPromise(Error('should be ignored')) ]; return promise.filter([1, 2, 3, 4], function(value, index) { assert.ok(index < innerResult.length); return innerResult[index]; }).then(assert.fail, assertIsStubError); }); it('preservesOrderWhenFilterReturnsPromise', function() { var deferreds = [ promise.defer(), promise.defer(), promise.defer(), promise.defer() ]; var result = promise.filter([0, 1, 2, 3], function(value, index) { return deferreds[index].promise; }); var pair = callbackPair(function(value) { assert.deepEqual([1, 2], value); }); result = result.then(pair.callback, pair.errback); return NativePromise.resolve() .then(function() { pair.assertNeither(); for (let i = deferreds.length - 1; i >= 0; i -= 1) { deferreds[i].fulfill(i > 0 && i < 3); } return result; }).then(pair.assertCallback); }); }); it('testAddThenableImplementation', function() { function tmp() {} assert.ok(!promise.Thenable.isImplementation(new tmp())); promise.Thenable.addImplementation(tmp); assert.ok(promise.Thenable.isImplementation(new tmp())); class tmpClass {} assert.ok(!promise.Thenable.isImplementation(new tmpClass())); promise.Thenable.addImplementation(tmpClass); assert.ok(promise.Thenable.isImplementation(new tmpClass())); }); describe('testLongStackTraces', function() { beforeEach(() => promise.LONG_STACK_TRACES = false); afterEach(() => promise.LONG_STACK_TRACES = false); it('doesNotAppendStackIfFeatureDisabled', function() { promise.LONG_STACK_TRACES = false; var error = Error('hello'); var originalStack = error.stack; return promise.rejected(error). then(fail). then(fail). then(fail). then(fail, function(e) { assert.equal(error, e); assert.equal(originalStack, e.stack); }); }); function getStackMessages(error) { return error.stack.split(/\n/).filter(function(line) { return /^From: /.test(line); }); } it('appendsInitialPromiseCreation_resolverThrows', function() { promise.LONG_STACK_TRACES = true; var error = Error('hello'); var originalStack = '(placeholder; will be overwritten later)'; return new promise.Promise(function() { try { throw error; } catch (e) { originalStack = e.stack; throw e; } }).then(fail, function(e) { assert.strictEqual(error, e); if (typeof originalStack !== 'string') { return; } assert.notEqual(originalStack, e.stack); assert.equal(e.stack.indexOf(originalStack), 0, 'should start with original stack'); assert.deepEqual(['From: ManagedPromise: new'], getStackMessages(e)); }); }); it('appendsInitialPromiseCreation_rejectCalled', function() { promise.LONG_STACK_TRACES = true; var error = Error('hello'); var originalStack = error.stack; return new promise.Promise(function(_, reject) { reject(error); }).then(fail, function(e) { assert.equal(error, e); if (typeof originalStack !== 'string') { return; } assert.notEqual(originalStack, e.stack); assert.equal(e.stack.indexOf(originalStack), 0, 'should start with original stack'); assert.deepEqual(['From: ManagedPromise: new'], getStackMessages(e)); }); }); it('appendsEachStepToRejectionError', function() { promise.LONG_STACK_TRACES = true; var error = Error('hello'); var originalStack = '(placeholder; will be overwritten later)'; return new promise.Promise(function() { try { throw error; } catch (e) { originalStack = e.stack; throw e; } }). then(fail). catch(function(e) { throw e; }). then(fail). catch(function(e) { throw e; }). then(fail, function(e) { assert.equal(error, e); if (typeof originalStack !== 'string') { return; } assert.notEqual(originalStack, e.stack); assert.equal(e.stack.indexOf(originalStack), 0, 'should start with original stack'); assert.deepEqual([ 'From: ManagedPromise: new', 'From: Promise: then', 'From: Promise: catch', 'From: Promise: then', 'From: Promise: catch', ], getStackMessages(e)); }); }); it('errorOccursInCallbackChain', function() { promise.LONG_STACK_TRACES = true; var error = Error('hello'); var originalStack = '(placeholder; will be overwritten later)'; return promise.fulfilled(). then(function() {}). then(function() {}). then(function() { try { throw error; } catch (e) { originalStack = e.stack; throw e; } }). catch(function(e) { throw e; }). then(fail, function(e) { assert.equal(error, e); if (typeof originalStack !== 'string') { return; } assert.notEqual(originalStack, e.stack); assert.equal(e.stack.indexOf(originalStack), 0, 'should start with original stack'); assert.deepEqual([ 'From: Promise: then', 'From: Promise: catch', ], getStackMessages(e)); }); }); }); });