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