michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: * http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: "use strict"; michael@0: michael@0: Components.utils.import("resource://gre/modules/Promise.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: Components.utils.import("resource://gre/modules/Task.jsm"); michael@0: michael@0: // Deactivate the standard xpcshell observer, as it turns uncaught michael@0: // rejections into failures, which we don't want here. michael@0: Promise.Debugging.clearUncaughtErrorObservers(); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Test runner michael@0: michael@0: let run_promise_tests = function run_promise_tests(tests, cb) { michael@0: let loop = function loop(index) { michael@0: if (index >= tests.length) { michael@0: if (cb) { michael@0: cb.call(); michael@0: } michael@0: return; michael@0: } michael@0: do_print("Launching test " + (index + 1) + "/" + tests.length); michael@0: let test = tests[index]; michael@0: // Execute from an empty stack michael@0: let next = function next() { michael@0: do_print("Test " + (index + 1) + "/" + tests.length + " complete"); michael@0: do_execute_soon(function() { michael@0: loop(index + 1); michael@0: }); michael@0: }; michael@0: let result = test(); michael@0: result.then(next, next); michael@0: }; michael@0: return loop(0); michael@0: }; michael@0: michael@0: let make_promise_test = function(test) { michael@0: return function runtest() { michael@0: do_print("Test starting: " + test.name); michael@0: try { michael@0: let result = test(); michael@0: if (result && "promise" in result) { michael@0: result = result.promise; michael@0: } michael@0: if (!result || !("then" in result)) { michael@0: let exn; michael@0: try { michael@0: do_throw("Test " + test.name + " did not return a promise: " + result); michael@0: } catch (x) { michael@0: exn = x; michael@0: } michael@0: return Promise.reject(exn); michael@0: } michael@0: // The test returns a promise michael@0: result = result.then( michael@0: // Test complete michael@0: function onResolve() { michael@0: do_print("Test complete: " + test.name); michael@0: }, michael@0: // The test failed with an unexpected error michael@0: function onReject(err) { michael@0: let detail; michael@0: if (err && typeof err == "object" && "stack" in err) { michael@0: detail = err.stack; michael@0: } else { michael@0: detail = "(no stack)"; michael@0: } michael@0: do_throw("Test " + test.name + " rejected with the following reason: " michael@0: + err + detail); michael@0: }); michael@0: return result; michael@0: } catch (x) { michael@0: // The test failed because of an error outside of a promise michael@0: do_throw("Error in body of test " + test.name + ": " + x + " at " + x.stack); michael@0: return Promise.reject(); michael@0: } michael@0: }; michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Tests michael@0: michael@0: let tests = []; michael@0: michael@0: // Utility function to observe an failures in a promise michael@0: // This function is useful if the promise itself is michael@0: // not returned. michael@0: let observe_failures = function observe_failures(promise) { michael@0: promise.catch(function onReject(reason) { michael@0: test.do_throw("Observed failure in test " + test + ": " + reason); michael@0: }); michael@0: }; michael@0: michael@0: // Test that all observers are notified michael@0: tests.push(make_promise_test( michael@0: function notification(test) { michael@0: // The size of the test michael@0: const SIZE = 10; michael@0: const RESULT = "this is an arbitrary value"; michael@0: michael@0: // Number of observers that yet need to be notified michael@0: let expected = SIZE; michael@0: michael@0: // |true| once an observer has been notified michael@0: let notified = []; michael@0: michael@0: // The promise observed michael@0: let source = Promise.defer(); michael@0: let result = Promise.defer(); michael@0: michael@0: let install_observer = function install_observer(i) { michael@0: observe_failures(source.promise.then( michael@0: function onSuccess(value) { michael@0: do_check_true(!notified[i], "Ensuring that observer is notified at most once"); michael@0: notified[i] = true; michael@0: michael@0: do_check_eq(value, RESULT, "Ensuring that the observed value is correct"); michael@0: if (--expected == 0) { michael@0: result.resolve(); michael@0: } michael@0: })); michael@0: }; michael@0: michael@0: // Install a number of observers before resolving michael@0: let i; michael@0: for (i = 0; i < SIZE/2; ++i) { michael@0: install_observer(i); michael@0: } michael@0: michael@0: source.resolve(RESULT); michael@0: michael@0: // Install remaining observers michael@0: for(;i < SIZE; ++i) { michael@0: install_observer(i); michael@0: } michael@0: michael@0: return result; michael@0: })); michael@0: michael@0: // Test that observers get the correct "this" value in strict mode. michael@0: tests.push( michael@0: make_promise_test(function handlers_this_value(test) { michael@0: return Promise.resolve().then( michael@0: function onResolve() { michael@0: // Since this file is in strict mode, the correct value is "undefined". michael@0: do_check_eq(this, undefined); michael@0: throw "reject"; michael@0: } michael@0: ).then( michael@0: null, michael@0: function onReject() { michael@0: // Since this file is in strict mode, the correct value is "undefined". michael@0: do_check_eq(this, undefined); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that observers registered on a pending promise are notified in order. michael@0: tests.push( michael@0: make_promise_test(function then_returns_before_callbacks(test) { michael@0: let deferred = Promise.defer(); michael@0: let promise = deferred.promise; michael@0: michael@0: let order = 0; michael@0: michael@0: promise.then( michael@0: function onResolve() { michael@0: do_check_eq(order, 0); michael@0: order++; michael@0: } michael@0: ); michael@0: michael@0: promise.then( michael@0: function onResolve() { michael@0: do_check_eq(order, 1); michael@0: order++; michael@0: } michael@0: ); michael@0: michael@0: let newPromise = promise.then( michael@0: function onResolve() { michael@0: do_check_eq(order, 2); michael@0: } michael@0: ); michael@0: michael@0: deferred.resolve(); michael@0: michael@0: // This test finishes after the last handler succeeds. michael@0: return newPromise; michael@0: })); michael@0: michael@0: // Test that observers registered on a resolved promise are notified in order. michael@0: tests.push( michael@0: make_promise_test(function then_returns_before_callbacks(test) { michael@0: let promise = Promise.resolve(); michael@0: michael@0: let order = 0; michael@0: michael@0: promise.then( michael@0: function onResolve() { michael@0: do_check_eq(order, 0); michael@0: order++; michael@0: } michael@0: ); michael@0: michael@0: promise.then( michael@0: function onResolve() { michael@0: do_check_eq(order, 1); michael@0: order++; michael@0: } michael@0: ); michael@0: michael@0: // This test finishes after the last handler succeeds. michael@0: return promise.then( michael@0: function onResolve() { michael@0: do_check_eq(order, 2); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that all observers are notified at most once, even if source michael@0: // is resolved/rejected several times michael@0: tests.push(make_promise_test( michael@0: function notification_once(test) { michael@0: // The size of the test michael@0: const SIZE = 10; michael@0: const RESULT = "this is an arbitrary value"; michael@0: michael@0: // Number of observers that yet need to be notified michael@0: let expected = SIZE; michael@0: michael@0: // |true| once an observer has been notified michael@0: let notified = []; michael@0: michael@0: // The promise observed michael@0: let observed = Promise.defer(); michael@0: let result = Promise.defer(); michael@0: michael@0: let install_observer = function install_observer(i) { michael@0: observe_failures(observed.promise.then( michael@0: function onSuccess(value) { michael@0: do_check_true(!notified[i], "Ensuring that observer is notified at most once"); michael@0: notified[i] = true; michael@0: michael@0: do_check_eq(value, RESULT, "Ensuring that the observed value is correct"); michael@0: if (--expected == 0) { michael@0: result.resolve(); michael@0: } michael@0: })); michael@0: }; michael@0: michael@0: // Install a number of observers before resolving michael@0: let i; michael@0: for (i = 0; i < SIZE/2; ++i) { michael@0: install_observer(i); michael@0: } michael@0: michael@0: observed.resolve(RESULT); michael@0: michael@0: // Install remaining observers michael@0: for(;i < SIZE; ++i) { michael@0: install_observer(i); michael@0: } michael@0: michael@0: // Resolve some more michael@0: for (i = 0; i < 10; ++i) { michael@0: observed.resolve(RESULT); michael@0: observed.reject(); michael@0: } michael@0: michael@0: return result; michael@0: })); michael@0: michael@0: // Test that throwing an exception from a onResolve listener michael@0: // does not prevent other observers from receiving the notification michael@0: // of success. michael@0: tests.push( michael@0: make_promise_test(function exceptions_do_not_stop_notifications(test) { michael@0: let source = Promise.defer(); michael@0: michael@0: let exception_thrown = false; michael@0: let exception_content = new Error("Boom!"); michael@0: michael@0: let observer_1 = source.promise.then( michael@0: function onResolve() { michael@0: exception_thrown = true; michael@0: throw exception_content; michael@0: }); michael@0: michael@0: let observer_2 = source.promise.then( michael@0: function onResolve() { michael@0: do_check_true(exception_thrown, "Second observer called after first observer has thrown"); michael@0: } michael@0: ); michael@0: michael@0: let result = observer_1.then( michael@0: function onResolve() { michael@0: do_throw("observer_1 should not have resolved"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_true(reason == exception_content, "Obtained correct rejection"); michael@0: } michael@0: ); michael@0: michael@0: source.resolve(); michael@0: return result; michael@0: } michael@0: )); michael@0: michael@0: // Test that, once a promise is resolved, further resolve/reject michael@0: // are ignored. michael@0: tests.push( michael@0: make_promise_test(function subsequent_resolves_are_ignored(test) { michael@0: let deferred = Promise.defer(); michael@0: deferred.resolve(1); michael@0: deferred.resolve(2); michael@0: deferred.reject(3); michael@0: michael@0: let result = deferred.promise.then( michael@0: function onResolve(value) { michael@0: do_check_eq(value, 1, "Resolution chose the first value"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_throw("Obtained a rejection while the promise was already resolved"); michael@0: } michael@0: ); michael@0: michael@0: return result; michael@0: })); michael@0: michael@0: // Test that, once a promise is rejected, further resolve/reject michael@0: // are ignored. michael@0: tests.push( michael@0: make_promise_test(function subsequent_rejects_are_ignored(test) { michael@0: let deferred = Promise.defer(); michael@0: deferred.reject(1); michael@0: deferred.reject(2); michael@0: deferred.resolve(3); michael@0: michael@0: let result = deferred.promise.then( michael@0: function onResolve() { michael@0: do_throw("Obtained a resolution while the promise was already rejected"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_eq(reason, 1, "Rejection chose the first value"); michael@0: } michael@0: ); michael@0: michael@0: return result; michael@0: })); michael@0: michael@0: // Test that returning normally from a rejection recovers from the error michael@0: // and that listeners are informed of a success. michael@0: tests.push( michael@0: make_promise_test(function recovery(test) { michael@0: let boom = new Error("Boom!"); michael@0: let deferred = Promise.defer(); michael@0: const RESULT = "An arbitrary value"; michael@0: michael@0: let promise = deferred.promise.then( michael@0: function onResolve() { michael@0: do_throw("A rejected promise should not resolve"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_true(reason == boom, "Promise was rejected with the correct error"); michael@0: return RESULT; michael@0: } michael@0: ); michael@0: michael@0: promise = promise.then( michael@0: function onResolve(value) { michael@0: do_check_eq(value, RESULT, "Promise was recovered with the correct value"); michael@0: } michael@0: ); michael@0: michael@0: deferred.reject(boom); michael@0: return promise; michael@0: })); michael@0: michael@0: // Test that returning a resolved promise from a onReject causes a resolution michael@0: // (recovering from the error) and that returning a rejected promise michael@0: // from a onResolve listener causes a rejection (raising an error). michael@0: tests.push( michael@0: make_promise_test(function recovery_with_promise(test) { michael@0: let boom = new Error("Arbitrary error"); michael@0: let deferred = Promise.defer(); michael@0: const RESULT = "An arbitrary value"; michael@0: const boom2 = new Error("Another arbitrary error"); michael@0: michael@0: // return a resolved promise from a onReject listener michael@0: let promise = deferred.promise.then( michael@0: function onResolve() { michael@0: do_throw("A rejected promise should not resolve"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_true(reason == boom, "Promise was rejected with the correct error"); michael@0: return Promise.resolve(RESULT); michael@0: } michael@0: ); michael@0: michael@0: // return a rejected promise from a onResolve listener michael@0: promise = promise.then( michael@0: function onResolve(value) { michael@0: do_check_eq(value, RESULT, "Promise was recovered with the correct value"); michael@0: return Promise.reject(boom2); michael@0: } michael@0: ); michael@0: michael@0: promise = promise.catch( michael@0: function onReject(reason) { michael@0: do_check_eq(reason, boom2, "Rejection was propagated with the correct " + michael@0: "reason, through a promise"); michael@0: } michael@0: ); michael@0: michael@0: deferred.reject(boom); michael@0: return promise; michael@0: })); michael@0: michael@0: // Test that we can resolve with promises of promises michael@0: tests.push( michael@0: make_promise_test(function test_propagation(test) { michael@0: const RESULT = "Yet another arbitrary value"; michael@0: let d1 = Promise.defer(); michael@0: let d2 = Promise.defer(); michael@0: let d3 = Promise.defer(); michael@0: michael@0: d3.resolve(d2.promise); michael@0: d2.resolve(d1.promise); michael@0: d1.resolve(RESULT); michael@0: michael@0: return d3.promise.then( michael@0: function onSuccess(value) { michael@0: do_check_eq(value, RESULT, "Resolution with a promise eventually yielded " michael@0: + " the correct result"); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test sequences of |then| and |catch| michael@0: tests.push( michael@0: make_promise_test(function test_chaining(test) { michael@0: let error_1 = new Error("Error 1"); michael@0: let error_2 = new Error("Error 2"); michael@0: let result_1 = "First result"; michael@0: let result_2 = "Second result"; michael@0: let result_3 = "Third result"; michael@0: michael@0: let source = Promise.defer(); michael@0: michael@0: let promise = source.promise.then().then(); michael@0: michael@0: source.resolve(result_1); michael@0: michael@0: // Check that result_1 is correctly propagated michael@0: promise = promise.then( michael@0: function onSuccess(result) { michael@0: do_check_eq(result, result_1, "Result was propagated correctly through " + michael@0: " several applications of |then|"); michael@0: return result_2; michael@0: } michael@0: ); michael@0: michael@0: // Check that returning from the promise produces a resolution michael@0: promise = promise.catch( michael@0: function onReject() { michael@0: do_throw("Incorrect rejection"); michael@0: } michael@0: ); michael@0: michael@0: // ... and that the check did not alter the value michael@0: promise = promise.then( michael@0: function onResolve(value) { michael@0: do_check_eq(value, result_2, "Result was propagated correctly once again"); michael@0: } michael@0: ); michael@0: michael@0: // Now the same kind of tests for rejections michael@0: promise = promise.then( michael@0: function onResolve() { michael@0: throw error_1; michael@0: } michael@0: ); michael@0: michael@0: promise = promise.then( michael@0: function onResolve() { michael@0: do_throw("Incorrect resolution: the exception should have caused a rejection"); michael@0: } michael@0: ); michael@0: michael@0: promise = promise.catch( michael@0: function onReject(reason) { michael@0: do_check_true(reason == error_1, "Reason was propagated correctly"); michael@0: throw error_2; michael@0: } michael@0: ); michael@0: michael@0: promise = promise.catch( michael@0: function onReject(reason) { michael@0: do_check_true(reason == error_2, "Throwing an error altered the reason " + michael@0: "as expected"); michael@0: return result_3; michael@0: } michael@0: ); michael@0: michael@0: promise = promise.then( michael@0: function onResolve(result) { michael@0: do_check_eq(result, result_3, "Error was correctly recovered"); michael@0: } michael@0: ); michael@0: michael@0: return promise; michael@0: })); michael@0: michael@0: // Test that resolving with a rejected promise actually rejects michael@0: tests.push( michael@0: make_promise_test(function resolve_to_rejected(test) { michael@0: let source = Promise.defer(); michael@0: let error = new Error("Boom"); michael@0: michael@0: let promise = source.promise.then( michael@0: function onResolve() { michael@0: do_throw("Incorrect call to onResolve listener"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_eq(reason, error, "Rejection lead to the expected reason"); michael@0: } michael@0: ); michael@0: michael@0: source.resolve(Promise.reject(error)); michael@0: michael@0: return promise; michael@0: })); michael@0: michael@0: // Test that Promise.resolve resolves as expected michael@0: tests.push( michael@0: make_promise_test(function test_resolve(test) { michael@0: const RESULT = "arbitrary value"; michael@0: let p1 = Promise.resolve(RESULT); michael@0: let p2 = Promise.resolve(p1); michael@0: do_check_eq(p1, p2, "Promise.resolve used on a promise just returns the promise"); michael@0: michael@0: return p1.then( michael@0: function onResolve(result) { michael@0: do_check_eq(result, RESULT, "Promise.resolve propagated the correct result"); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that Promise.resolve throws when its argument is an async function. michael@0: tests.push( michael@0: make_promise_test(function test_promise_resolve_throws_with_async_function(test) { michael@0: Assert.throws(() => Promise.resolve(Task.async(function* () {})), michael@0: /Cannot resolve a promise with an async function/); michael@0: return Promise.resolve(); michael@0: })); michael@0: michael@0: // Test that the code after "then" is always executed before the callbacks michael@0: tests.push( michael@0: make_promise_test(function then_returns_before_callbacks(test) { michael@0: let promise = Promise.resolve(); michael@0: michael@0: let thenExecuted = false; michael@0: michael@0: promise = promise.then( michael@0: function onResolve() { michael@0: thenExecuted = true; michael@0: } michael@0: ); michael@0: michael@0: do_check_false(thenExecuted); michael@0: michael@0: return promise; michael@0: })); michael@0: michael@0: // Test that chaining promises does not generate long stack traces michael@0: tests.push( michael@0: make_promise_test(function chaining_short_stack(test) { michael@0: let source = Promise.defer(); michael@0: let promise = source.promise; michael@0: michael@0: const NUM_ITERATIONS = 100; michael@0: michael@0: for (let i = 0; i < NUM_ITERATIONS; i++) { michael@0: promise = promise.then( michael@0: function onResolve(result) { michael@0: return result + "."; michael@0: } michael@0: ); michael@0: } michael@0: michael@0: promise = promise.then( michael@0: function onResolve(result) { michael@0: // Check that the execution went as expected. michael@0: let expectedString = new Array(1 + NUM_ITERATIONS).join("."); michael@0: do_check_true(result == expectedString); michael@0: michael@0: // Check that we didn't generate one or more stack frames per iteration. michael@0: let stackFrameCount = 0; michael@0: let stackFrame = Components.stack; michael@0: while (stackFrame) { michael@0: stackFrameCount++; michael@0: stackFrame = stackFrame.caller; michael@0: } michael@0: michael@0: do_check_true(stackFrameCount < NUM_ITERATIONS); michael@0: } michael@0: ); michael@0: michael@0: source.resolve(""); michael@0: michael@0: return promise; michael@0: })); michael@0: michael@0: // Test that the values of the promise return by Promise.all() are kept in the michael@0: // given order even if the given promises are resolved in arbitrary order michael@0: tests.push( michael@0: make_promise_test(function all_resolve(test) { michael@0: let d1 = Promise.defer(); michael@0: let d2 = Promise.defer(); michael@0: let d3 = Promise.defer(); michael@0: michael@0: d3.resolve(4); michael@0: d2.resolve(2); michael@0: do_execute_soon(() => d1.resolve(1)); michael@0: michael@0: let promises = [d1.promise, d2.promise, 3, d3.promise]; michael@0: michael@0: return Promise.all(promises).then( michael@0: function onResolve([val1, val2, val3, val4]) { michael@0: do_check_eq(val1, 1); michael@0: do_check_eq(val2, 2); michael@0: do_check_eq(val3, 3); michael@0: do_check_eq(val4, 4); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that rejecting one of the promises passed to Promise.all() michael@0: // rejects the promise return by Promise.all() michael@0: tests.push( michael@0: make_promise_test(function all_reject(test) { michael@0: let error = new Error("Boom"); michael@0: michael@0: let d1 = Promise.defer(); michael@0: let d2 = Promise.defer(); michael@0: let d3 = Promise.defer(); michael@0: michael@0: d3.resolve(3); michael@0: d2.resolve(2); michael@0: do_execute_soon(() => d1.reject(error)); michael@0: michael@0: let promises = [d1.promise, d2.promise, d3.promise]; michael@0: michael@0: return Promise.all(promises).then( michael@0: function onResolve() { michael@0: do_throw("Incorrect call to onResolve listener"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_eq(reason, error, "Rejection lead to the expected reason"); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that passing only values (not promises) to Promise.all() michael@0: // forwards them all as resolution values. michael@0: tests.push( michael@0: make_promise_test(function all_resolve_no_promises(test) { michael@0: try { michael@0: Promise.all(null); michael@0: do_check_true(false, "all() should only accept iterables"); michael@0: } catch (e) { michael@0: do_check_true(true, "all() fails when first the arg is not an iterable"); michael@0: } michael@0: michael@0: let p1 = Promise.all([]).then( michael@0: function onResolve(val) { michael@0: do_check_true(Array.isArray(val) && val.length == 0); michael@0: } michael@0: ); michael@0: michael@0: let p2 = Promise.all([1, 2, 3]).then( michael@0: function onResolve([val1, val2, val3]) { michael@0: do_check_eq(val1, 1); michael@0: do_check_eq(val2, 2); michael@0: do_check_eq(val3, 3); michael@0: } michael@0: ); michael@0: michael@0: return Promise.all([p1, p2]); michael@0: })); michael@0: michael@0: // Test that Promise.all() handles non-array iterables michael@0: tests.push( michael@0: make_promise_test(function all_iterable(test) { michael@0: function* iterable() { michael@0: yield 1; michael@0: yield 2; michael@0: yield 3; michael@0: } michael@0: michael@0: return Promise.all(iterable()).then( michael@0: function onResolve([val1, val2, val3]) { michael@0: do_check_eq(val1, 1); michael@0: do_check_eq(val2, 2); michael@0: do_check_eq(val3, 3); michael@0: }, michael@0: function onReject() { michael@0: do_throw("all() unexpectedly rejected"); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that throwing from the iterable passed to Promise.all() rejects the michael@0: // promise returned by Promise.all() michael@0: tests.push( michael@0: make_promise_test(function all_iterable_throws(test) { michael@0: function* iterable() { michael@0: throw 1; michael@0: } michael@0: michael@0: return Promise.all(iterable()).then( michael@0: function onResolve() { michael@0: do_throw("all() unexpectedly resolved"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_eq(reason, 1, "all() rejects when the iterator throws"); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that Promise.race() resolves with the first available resolution value michael@0: tests.push( michael@0: make_promise_test(function race_resolve(test) { michael@0: let p1 = Promise.resolve(1); michael@0: let p2 = Promise.resolve().then(() => 2); michael@0: michael@0: return Promise.race([p1, p2]).then( michael@0: function onResolve(value) { michael@0: do_check_eq(value, 1); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that passing only values (not promises) to Promise.race() works michael@0: tests.push( michael@0: make_promise_test(function race_resolve_no_promises(test) { michael@0: try { michael@0: Promise.race(null); michael@0: do_check_true(false, "race() should only accept iterables"); michael@0: } catch (e) { michael@0: do_check_true(true, "race() fails when first the arg is not an iterable"); michael@0: } michael@0: michael@0: return Promise.race([1, 2, 3]).then( michael@0: function onResolve(value) { michael@0: do_check_eq(value, 1); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that Promise.race() never resolves when passed an empty iterable michael@0: tests.push( michael@0: make_promise_test(function race_resolve_never(test) { michael@0: return new Promise(resolve => { michael@0: Promise.race([]).then( michael@0: function onResolve() { michael@0: do_throw("race() unexpectedly resolved"); michael@0: }, michael@0: function onReject() { michael@0: do_throw("race() unexpectedly rejected"); michael@0: } michael@0: ); michael@0: michael@0: // Approximate "never" so we don't have to solve the halting problem. michael@0: do_timeout(200, resolve); michael@0: }); michael@0: })); michael@0: michael@0: // Test that Promise.race() handles non-array iterables. michael@0: tests.push( michael@0: make_promise_test(function race_iterable(test) { michael@0: function* iterable() { michael@0: yield 1; michael@0: yield 2; michael@0: yield 3; michael@0: } michael@0: michael@0: return Promise.race(iterable()).then( michael@0: function onResolve(value) { michael@0: do_check_eq(value, 1); michael@0: }, michael@0: function onReject() { michael@0: do_throw("race() unexpectedly rejected"); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that throwing from the iterable passed to Promise.race() rejects the michael@0: // promise returned by Promise.race() michael@0: tests.push( michael@0: make_promise_test(function race_iterable_throws(test) { michael@0: function* iterable() { michael@0: throw 1; michael@0: } michael@0: michael@0: return Promise.race(iterable()).then( michael@0: function onResolve() { michael@0: do_throw("race() unexpectedly resolved"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_eq(reason, 1, "race() rejects when the iterator throws"); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test that rejecting one of the promises passed to Promise.race() rejects the michael@0: // promise returned by Promise.race() michael@0: tests.push( michael@0: make_promise_test(function race_reject(test) { michael@0: let p1 = Promise.reject(1); michael@0: let p2 = Promise.resolve(2); michael@0: let p3 = Promise.resolve(3); michael@0: michael@0: return Promise.race([p1, p2, p3]).then( michael@0: function onResolve() { michael@0: do_throw("race() unexpectedly resolved"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_eq(reason, 1, "race() rejects when given a rejected promise"); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: // Test behavior of the Promise constructor. michael@0: tests.push( michael@0: make_promise_test(function test_constructor(test) { michael@0: try { michael@0: new Promise(null); michael@0: do_check_true(false, "Constructor should fail when not passed a function"); michael@0: } catch (e) { michael@0: do_check_true(true, "Constructor fails when not passed a function"); michael@0: } michael@0: michael@0: let executorRan = false; michael@0: let promise = new Promise( michael@0: function executor(resolve, reject) { michael@0: executorRan = true; michael@0: do_check_eq(this, undefined); michael@0: do_check_eq(typeof resolve, "function", michael@0: "resolve function should be passed to the executor"); michael@0: do_check_eq(typeof reject, "function", michael@0: "reject function should be passed to the executor"); michael@0: } michael@0: ); michael@0: do_check_instanceof(promise, Promise); michael@0: do_check_true(executorRan, "Executor should execute synchronously"); michael@0: michael@0: // resolve a promise from the executor michael@0: let resolvePromise = new Promise( michael@0: function executor(resolve) { michael@0: resolve(1); michael@0: } michael@0: ).then( michael@0: function onResolve(value) { michael@0: do_check_eq(value, 1, "Executor resolved with correct value"); michael@0: }, michael@0: function onReject() { michael@0: do_throw("Executor unexpectedly rejected"); michael@0: } michael@0: ); michael@0: michael@0: // reject a promise from the executor michael@0: let rejectPromise = new Promise( michael@0: function executor(_, reject) { michael@0: reject(1); michael@0: } michael@0: ).then( michael@0: function onResolve() { michael@0: do_throw("Executor unexpectedly resolved"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_eq(reason, 1, "Executor rejected with correct value"); michael@0: } michael@0: ); michael@0: michael@0: // throw from the executor, causing a rejection michael@0: let throwPromise = new Promise( michael@0: function executor() { michael@0: throw 1; michael@0: } michael@0: ).then( michael@0: function onResolve() { michael@0: do_throw("Throwing inside an executor should not resolve the promise"); michael@0: }, michael@0: function onReject(reason) { michael@0: do_check_eq(reason, 1, "Executor rejected with correct value"); michael@0: } michael@0: ); michael@0: michael@0: return Promise.all([resolvePromise, rejectPromise, throwPromise]); michael@0: })); michael@0: michael@0: // Test deadlock in Promise.jsm with nested event loops michael@0: // The scenario being tested is: michael@0: // promise_1.then({ michael@0: // do some work that will asynchronously signal done michael@0: // start an event loop waiting for the done signal michael@0: // } michael@0: // where the async work uses resolution of a second promise to michael@0: // trigger the "done" signal. While this would likely work in a michael@0: // naive implementation, our constant-stack implementation needs michael@0: // a special case to avoid deadlock. Note that this test is michael@0: // sensitive to the implementation-dependent order in which then() michael@0: // clauses for two different promises are executed, so it is michael@0: // possible for other implementations to pass this test and still michael@0: // have similar deadlocks. michael@0: tests.push( michael@0: make_promise_test(function promise_nested_eventloop_deadlock(test) { michael@0: // Set up a (long enough to be noticeable) timeout to michael@0: // exit the nested event loop and throw if the test run is hung michael@0: let shouldExitNestedEventLoop = false; michael@0: michael@0: function event_loop() { michael@0: let thr = Services.tm.mainThread; michael@0: while(!shouldExitNestedEventLoop) { michael@0: thr.processNextEvent(true); michael@0: } michael@0: } michael@0: michael@0: // I wish there was a way to cancel xpcshell do_timeout()s michael@0: do_timeout(2000, () => { michael@0: if (!shouldExitNestedEventLoop) { michael@0: shouldExitNestedEventLoop = true; michael@0: do_throw("Test timed out"); michael@0: } michael@0: }); michael@0: michael@0: let promise1 = Promise.resolve(1); michael@0: let promise2 = Promise.resolve(2); michael@0: michael@0: do_print("Setting wait for first promise"); michael@0: promise1.then(value => { michael@0: do_print("Starting event loop"); michael@0: event_loop(); michael@0: }, null); michael@0: michael@0: do_print("Setting wait for second promise"); michael@0: return promise2.catch(error => {return 3;}) michael@0: .then( michael@0: count => { michael@0: shouldExitNestedEventLoop = true; michael@0: }); michael@0: })); michael@0: michael@0: function wait_for_uncaught(aMustAppear, aTimeout = undefined) { michael@0: let remaining = new Set(); michael@0: for (let k of aMustAppear) { michael@0: remaining.add(k); michael@0: } michael@0: let deferred = Promise.defer(); michael@0: let print = do_print; michael@0: let execute_soon = do_execute_soon; michael@0: let observer = function({message, stack}) { michael@0: let data = message + stack; michael@0: print("Observing " + message + ", looking for " + aMustAppear.join(", ")); michael@0: for (let expected of remaining) { michael@0: if (data.indexOf(expected) != -1) { michael@0: print("I found " + expected); michael@0: remaining.delete(expected); michael@0: } michael@0: if (remaining.size == 0 && observer) { michael@0: Promise.Debugging.removeUncaughtErrorObserver(observer); michael@0: observer = null; michael@0: deferred.resolve(); michael@0: } michael@0: } michael@0: }; michael@0: Promise.Debugging.addUncaughtErrorObserver(observer); michael@0: if (aTimeout) { michael@0: do_timeout(aTimeout, function timeout() { michael@0: if (observer) { michael@0: Promise.Debugging.removeUncaughtErrorObserver(observer); michael@0: observer = null; michael@0: } michael@0: deferred.reject(new Error("Timeout")); michael@0: }); michael@0: } michael@0: return deferred.promise; michael@0: } michael@0: michael@0: // Test that uncaught errors are reported as uncaught michael@0: (function() { michael@0: let make_string_rejection = function make_string_rejection() { michael@0: let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); michael@0: let string = "This is an uncaught rejection " + salt; michael@0: // Our error is not Error-like nor an nsIException, so the stack will michael@0: // include the closure doing the actual rejection. michael@0: return {mustFind: ["test_rejection_closure", string], error: string}; michael@0: }; michael@0: let make_num_rejection = function make_num_rejection() { michael@0: let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); michael@0: // Our error is not Error-like nor an nsIException, so the stack will michael@0: // include the closure doing the actual rejection. michael@0: return {mustFind: ["test_rejection_closure", salt], error: salt}; michael@0: }; michael@0: let make_undefined_rejection = function make_undefined_rejection() { michael@0: // Our error is not Error-like nor an nsIException, so the stack will michael@0: // include the closure doing the actual rejection. michael@0: return {mustFind: ["test_rejection_closure"], error: undefined}; michael@0: }; michael@0: let make_error_rejection = function make_error_rejection() { michael@0: let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); michael@0: let error = new Error("This is an uncaught error " + salt); michael@0: return { michael@0: mustFind: [error.message, error.fileName, error.lineNumber, error.stack], michael@0: error: error michael@0: }; michael@0: }; michael@0: let make_exception_rejection = function make_exception_rejection() { michael@0: let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); michael@0: let exn = new Components.Exception("This is an uncaught exception " + salt, michael@0: Components.results.NS_ERROR_NOT_AVAILABLE); michael@0: return { michael@0: mustFind: [exn.message, exn.filename, exn.lineNumber, exn.location.toString()], michael@0: error: exn michael@0: }; michael@0: }; michael@0: for (let make_rejection of [make_string_rejection, michael@0: make_num_rejection, michael@0: make_undefined_rejection, michael@0: make_error_rejection, michael@0: make_exception_rejection]) { michael@0: let {mustFind, error} = make_rejection(); michael@0: let name = make_rejection.name; michael@0: tests.push(make_promise_test(function test_uncaught_is_reported() { michael@0: do_print("Testing with rejection " + name); michael@0: let promise = wait_for_uncaught(mustFind); michael@0: (function test_rejection_closure() { michael@0: // For the moment, we cannot be absolutely certain that a value is michael@0: // garbage-collected, even if it is not referenced anymore, due to michael@0: // the conservative stack-scanning algorithm. michael@0: // michael@0: // To be _almost_ certain that a value will be garbage-collected, we michael@0: // 1. isolate that value in an anonymous closure; michael@0: // 2. allocate 100 values instead of 1 (gc-ing a single value from michael@0: // these is sufficient for the test); michael@0: // 3. place everything in a loop, as the JIT typically reuses memory; michael@0: // 4. call all the GC methods we can. michael@0: // michael@0: // Unfortunately, we might still have intermittent failures, michael@0: // materialized as timeouts. michael@0: // michael@0: for (let i = 0; i < 100; ++i) { michael@0: Promise.reject(error); michael@0: } michael@0: })(); michael@0: do_print("Posted all rejections"); michael@0: Components.utils.forceGC(); michael@0: Components.utils.forceCC(); michael@0: Components.utils.forceShrinkingGC(); michael@0: return promise; michael@0: })); michael@0: } michael@0: })(); michael@0: michael@0: michael@0: // Test that caught errors are not reported as uncaught michael@0: tests.push( michael@0: make_promise_test(function test_caught_is_not_reported() { michael@0: let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); michael@0: let promise = wait_for_uncaught([salt], 500); michael@0: (function() { michael@0: let uncaught = Promise.reject("This error, on the other hand, is caught " + salt); michael@0: uncaught.catch(function() { /* ignore rejection */}); michael@0: uncaught = null; michael@0: })(); michael@0: // Isolate this in a function to increase likelihood that the gc will michael@0: // realise that |uncaught| has remained uncaught. michael@0: Components.utils.forceGC(); michael@0: michael@0: return promise.then(function onSuccess() { michael@0: throw new Error("This error was caught and should not have been reported"); michael@0: }, function onError() { michael@0: do_print("The caught error was not reported, all is fine"); michael@0: } michael@0: ); michael@0: })); michael@0: michael@0: function run_test() michael@0: { michael@0: do_test_pending(); michael@0: run_promise_tests(tests, do_test_finished); michael@0: }