summaryrefslogtreecommitdiffstats
path: root/dom/promise/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'dom/promise/tests/unit')
-rw-r--r--dom/promise/tests/unit/test_monitor_uncaught.js322
-rw-r--r--dom/promise/tests/unit/test_promise_job_across_sandbox.js221
-rw-r--r--dom/promise/tests/unit/test_promise_unhandled_rejection.js139
-rw-r--r--dom/promise/tests/unit/xpcshell.ini6
4 files changed, 688 insertions, 0 deletions
diff --git a/dom/promise/tests/unit/test_monitor_uncaught.js b/dom/promise/tests/unit/test_monitor_uncaught.js
new file mode 100644
index 0000000000..afa328cb97
--- /dev/null
+++ b/dom/promise/tests/unit/test_monitor_uncaught.js
@@ -0,0 +1,322 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+// Prevent test failures due to the unhandled rejections in this test file.
+PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest();
+
+add_task(async function test_globals() {
+ Assert.notEqual(
+ PromiseDebugging,
+ undefined,
+ "PromiseDebugging is available."
+ );
+});
+
+add_task(async function test_promiseID() {
+ let p1 = new Promise(resolve => {});
+ let p2 = new Promise(resolve => {});
+ let p3 = p2.catch(null);
+ let promise = [p1, p2, p3];
+
+ let identifiers = promise.map(PromiseDebugging.getPromiseID);
+ info("Identifiers: " + JSON.stringify(identifiers));
+ let idSet = new Set(identifiers);
+ Assert.equal(
+ idSet.size,
+ identifiers.length,
+ "PromiseDebugging.getPromiseID returns a distinct id per promise"
+ );
+
+ let identifiers2 = promise.map(PromiseDebugging.getPromiseID);
+ Assert.equal(
+ JSON.stringify(identifiers),
+ JSON.stringify(identifiers2),
+ "Successive calls to PromiseDebugging.getPromiseID return the same id for the same promise"
+ );
+});
+
+add_task(async function test_observe_uncaught() {
+ // The names of Promise instances
+ let names = new Map();
+
+ // The results for UncaughtPromiseObserver callbacks.
+ let CallbackResults = function (name) {
+ this.name = name;
+ this.expected = new Set();
+ this.observed = new Set();
+ this.blocker = new Promise(resolve => (this.resolve = resolve));
+ };
+ CallbackResults.prototype = {
+ observe(promise) {
+ info(this.name + " observing Promise " + names.get(promise));
+ Assert.equal(
+ PromiseDebugging.getState(promise).state,
+ "rejected",
+ this.name + " observed a rejected Promise"
+ );
+ if (!this.expected.has(promise)) {
+ Assert.ok(
+ false,
+ this.name +
+ " observed a Promise that it expected to observe, " +
+ names.get(promise) +
+ " (" +
+ PromiseDebugging.getPromiseID(promise) +
+ ", " +
+ PromiseDebugging.getAllocationStack(promise) +
+ ")"
+ );
+ }
+ Assert.ok(
+ this.expected.delete(promise),
+ this.name +
+ " observed a Promise that it expected to observe, " +
+ names.get(promise) +
+ " (" +
+ PromiseDebugging.getPromiseID(promise) +
+ ")"
+ );
+ Assert.ok(
+ !this.observed.has(promise),
+ this.name + " observed a Promise that it has not observed yet"
+ );
+ this.observed.add(promise);
+ if (this.expected.size == 0) {
+ this.resolve();
+ } else {
+ info(
+ this.name +
+ " is still waiting for " +
+ this.expected.size +
+ " observations:"
+ );
+ info(
+ JSON.stringify(Array.from(this.expected.values(), x => names.get(x)))
+ );
+ }
+ },
+ };
+
+ let onLeftUncaught = new CallbackResults("onLeftUncaught");
+ let onConsumed = new CallbackResults("onConsumed");
+
+ let observer = {
+ onLeftUncaught(promise, data) {
+ onLeftUncaught.observe(promise);
+ },
+ onConsumed(promise) {
+ onConsumed.observe(promise);
+ },
+ };
+
+ let resolveLater = function (delay = 20) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ return new Promise((resolve, reject) => setTimeout(resolve, delay));
+ };
+ let rejectLater = function (delay = 20) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ return new Promise((resolve, reject) => setTimeout(reject, delay));
+ };
+ let makeSamples = function* () {
+ yield {
+ promise: Promise.resolve(0),
+ name: "Promise.resolve",
+ };
+ yield {
+ promise: Promise.resolve(resolve => resolve(0)),
+ name: "Resolution callback",
+ };
+ yield {
+ promise: Promise.resolve(0).catch(null),
+ name: "`catch(null)`",
+ };
+ yield {
+ promise: Promise.reject(0).catch(() => {}),
+ name: "Reject and catch immediately",
+ };
+ yield {
+ promise: resolveLater(),
+ name: "Resolve later",
+ };
+ yield {
+ promise: Promise.reject("Simple rejection"),
+ leftUncaught: true,
+ consumed: false,
+ name: "Promise.reject",
+ };
+
+ // Reject a promise now, consume it later.
+ let p = Promise.reject("Reject now, consume later");
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(
+ () =>
+ p.catch(() => {
+ info("Consumed promise");
+ }),
+ 200
+ );
+ yield {
+ promise: p,
+ leftUncaught: true,
+ consumed: true,
+ name: "Reject now, consume later",
+ };
+
+ yield {
+ promise: Promise.all([Promise.resolve("Promise.all"), rejectLater()]),
+ leftUncaught: true,
+ name: "Rejecting through Promise.all",
+ };
+ yield {
+ promise: Promise.race([resolveLater(500), Promise.reject()]),
+ leftUncaught: true, // The rejection wins the race.
+ name: "Rejecting through Promise.race",
+ };
+ yield {
+ promise: Promise.race([Promise.resolve(), rejectLater(500)]),
+ leftUncaught: false, // The resolution wins the race.
+ name: "Resolving through Promise.race",
+ };
+
+ let boom = new Error("`throw` in the constructor");
+ yield {
+ promise: new Promise(() => {
+ throw boom;
+ }),
+ leftUncaught: true,
+ name: "Throwing in the constructor",
+ };
+
+ let rejection = Promise.reject("`reject` during resolution");
+ yield {
+ promise: rejection,
+ leftUncaught: false,
+ consumed: false, // `rejection` is consumed immediately (see below)
+ name: "Promise.reject, again",
+ };
+
+ yield {
+ promise: new Promise(resolve => resolve(rejection)),
+ leftUncaught: true,
+ consumed: false,
+ name: "Resolving with a rejected promise",
+ };
+
+ yield {
+ promise: Promise.resolve(0).then(() => rejection),
+ leftUncaught: true,
+ consumed: false,
+ name: "Returning a rejected promise from success handler",
+ };
+
+ yield {
+ promise: Promise.resolve(0).then(() => {
+ throw new Error();
+ }),
+ leftUncaught: true,
+ consumed: false,
+ name: "Throwing during the call to the success callback",
+ };
+ };
+ let samples = [];
+ for (let s of makeSamples()) {
+ samples.push(s);
+ info(
+ "Promise '" +
+ s.name +
+ "' has id " +
+ PromiseDebugging.getPromiseID(s.promise)
+ );
+ }
+
+ PromiseDebugging.addUncaughtRejectionObserver(observer);
+
+ for (let s of samples) {
+ names.set(s.promise, s.name);
+ if (s.leftUncaught || false) {
+ onLeftUncaught.expected.add(s.promise);
+ }
+ if (s.consumed || false) {
+ onConsumed.expected.add(s.promise);
+ }
+ }
+
+ info("Test setup, waiting for callbacks.");
+ await onLeftUncaught.blocker;
+
+ info("All calls to onLeftUncaught are complete.");
+ if (onConsumed.expected.size != 0) {
+ info("onConsumed is still waiting for the following Promise:");
+ info(
+ JSON.stringify(
+ Array.from(onConsumed.expected.values(), x => names.get(x))
+ )
+ );
+ await onConsumed.blocker;
+ }
+
+ info("All calls to onConsumed are complete.");
+ let removed = PromiseDebugging.removeUncaughtRejectionObserver(observer);
+ Assert.ok(removed, "removeUncaughtRejectionObserver succeeded");
+ removed = PromiseDebugging.removeUncaughtRejectionObserver(observer);
+ Assert.ok(
+ !removed,
+ "second call to removeUncaughtRejectionObserver didn't remove anything"
+ );
+});
+
+add_task(async function test_uninstall_observer() {
+ let Observer = function () {
+ this.blocker = new Promise(resolve => (this.resolve = resolve));
+ this.active = true;
+ };
+ Observer.prototype = {
+ set active(x) {
+ this._active = x;
+ if (x) {
+ PromiseDebugging.addUncaughtRejectionObserver(this);
+ } else {
+ PromiseDebugging.removeUncaughtRejectionObserver(this);
+ }
+ },
+ onLeftUncaught() {
+ Assert.ok(this._active, "This observer is active.");
+ this.resolve();
+ },
+ onConsumed() {
+ Assert.ok(false, "We should not consume any Promise.");
+ },
+ };
+
+ info("Adding an observer.");
+ let deactivate = new Observer();
+ Promise.reject("I am an uncaught rejection.");
+ await deactivate.blocker;
+ Assert.ok(true, "The observer has observed an uncaught Promise.");
+ deactivate.active = false;
+ info(
+ "Removing the observer, it should not observe any further uncaught Promise."
+ );
+
+ info(
+ "Rejecting a Promise and waiting a little to give a chance to observers."
+ );
+ let wait = new Observer();
+ Promise.reject("I am another uncaught rejection.");
+ await wait.blocker;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+ // Normally, `deactivate` should not be notified of the uncaught rejection.
+ wait.active = false;
+});
diff --git a/dom/promise/tests/unit/test_promise_job_across_sandbox.js b/dom/promise/tests/unit/test_promise_job_across_sandbox.js
new file mode 100644
index 0000000000..ff1d1575e3
--- /dev/null
+++ b/dom/promise/tests/unit/test_promise_job_across_sandbox.js
@@ -0,0 +1,221 @@
+function createSandbox() {
+ const uri = Services.io.newURI("https://example.com");
+ const principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ return new Cu.Sandbox(principal, {});
+}
+
+add_task(async function testReactionJob() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var testPromise = Promise.resolve(10);
+`);
+
+ // Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
+ // on wrapped `resolve` in sandbox realm, and it fails to unwrap the security
+ // wrapper. The reaction job should be created with sandbox realm.
+ const p = new Promise(resolve => {
+ sandbox.resolve = resolve;
+
+ sandbox.eval(`
+testPromise.then(resolve);
+`);
+ });
+
+ const result = await p;
+
+ equal(result, 10);
+});
+
+add_task(async function testReactionJobNuked() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var testPromise = Promise.resolve(10);
+`);
+
+ // Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
+ // on wrapped `resolve` in sandbox realm, and it fails to unwrap the security
+ // wrapper. The reaction job should be created with sandbox realm.
+ const p1 = new Promise(resolve => {
+ sandbox.resolve = resolve;
+
+ sandbox.eval(`
+testPromise.then(resolve);
+`);
+
+ // Given the reaction job is created with the sandbox realm, nuking the
+ // sandbox prevents the job gets executed.
+ Cu.nukeSandbox(sandbox);
+ });
+
+ const p2 = Promise.resolve(11);
+
+ // Given the p1 doesn't get resolved, p2 should win.
+ const result = await Promise.race([p1, p2]);
+
+ equal(result, 11);
+});
+
+add_task(async function testReactionJobWithXray() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var testPromise = Promise.resolve(10);
+`);
+
+ // Calling `Promise.prototype.then` from privileged realm via Xray uses
+ // privileged `Promise.prototype.then` function, and GetFunctionRealm
+ // performed there successfully gets top-level realm. The reaction job
+ // should be created with top-level realm.
+ const result = await new Promise(resolve => {
+ sandbox.testPromise.then(resolve);
+
+ // Given the reaction job is created with the top-level realm, nuking the
+ // sandbox doesn't affect the reaction job.
+ Cu.nukeSandbox(sandbox);
+ });
+
+ equal(result, 10);
+});
+
+add_task(async function testBoundReactionJob() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var resolve = undefined;
+var callbackPromise = new Promise(r => { resolve = r; });
+var callback = function (v) { resolve(v + 1); };
+`);
+
+ // Create a bound function where its realm is privileged realm, and
+ // its target is from sandbox realm.
+ sandbox.bound_callback = Function.prototype.bind.call(
+ sandbox.callback,
+ sandbox
+ );
+
+ // Calling `Promise.prototype.then` from sandbox performs GetFunctionRealm
+ // and it fails. The reaction job should be created with sandbox realm.
+ sandbox.eval(`
+Promise.resolve(10).then(bound_callback);
+`);
+
+ const result = await sandbox.callbackPromise;
+ equal(result, 11);
+});
+
+add_task(async function testThenableJob() {
+ const sandbox = createSandbox();
+
+ const p = new Promise(resolve => {
+ // Create a bound function where its realm is privileged realm, and
+ // its target is from sandbox realm.
+ sandbox.then = function (onFulfilled, onRejected) {
+ resolve(10);
+ };
+ });
+
+ // Creating a promise thenable job in the following `Promise.resolve` performs
+ // GetFunctionRealm on the bound thenable.then and fails. The reaction job
+ // should be created with sandbox realm.
+ sandbox.eval(`
+var thenable = {
+ then: then,
+};
+
+Promise.resolve(thenable);
+`);
+
+ const result = await p;
+ equal(result, 10);
+});
+
+add_task(async function testThenableJobNuked() {
+ const sandbox = createSandbox();
+
+ let called = false;
+ sandbox.then = function (onFulfilled, onRejected) {
+ called = true;
+ };
+
+ // Creating a promise thenable job in the following `Promise.resolve` performs
+ // GetFunctionRealm on the bound thenable.then and fails. The reaction job
+ // should be created with sandbox realm.
+ sandbox.eval(`
+var thenable = {
+ then: then,
+};
+
+Promise.resolve(thenable);
+`);
+
+ Cu.nukeSandbox(sandbox);
+
+ // Drain the job queue, to make sure we hit dead object error inside the
+ // thenable job.
+ await Promise.resolve(10);
+
+ equal(
+ Services.console.getMessageArray().find(x => {
+ return x.toString().includes("can't access dead object");
+ }) !== undefined,
+ true
+ );
+ equal(called, false);
+});
+
+add_task(async function testThenableJobAccessError() {
+ const sandbox = createSandbox();
+
+ let accessed = false;
+ sandbox.thenable = {
+ get then() {
+ accessed = true;
+ },
+ };
+
+ // The following operation silently fails when accessing `then` property.
+ sandbox.eval(`
+var x = typeof thenable.then;
+
+Promise.resolve(thenable);
+`);
+
+ equal(accessed, false);
+});
+
+add_task(async function testBoundThenableJob() {
+ const sandbox = createSandbox();
+
+ sandbox.eval(`
+var resolve = undefined;
+var callbackPromise = new Promise(r => { resolve = r; });
+var callback = function (v) { resolve(v + 1); };
+
+var then = function(onFulfilled, onRejected) {
+ onFulfilled(10);
+};
+`);
+
+ // Create a bound function where its realm is privileged realm, and
+ // its target is from sandbox realm.
+ sandbox.bound_then = Function.prototype.bind.call(sandbox.then, sandbox);
+
+ // Creating a promise thenable job in the following `Promise.resolve` performs
+ // GetFunctionRealm on the bound thenable.then and fails. The reaction job
+ // should be created with sandbox realm.
+ sandbox.eval(`
+var thenable = {
+ then: bound_then,
+};
+
+Promise.resolve(thenable).then(callback);
+`);
+
+ const result = await sandbox.callbackPromise;
+ equal(result, 11);
+});
diff --git a/dom/promise/tests/unit/test_promise_unhandled_rejection.js b/dom/promise/tests/unit/test_promise_unhandled_rejection.js
new file mode 100644
index 0000000000..68471569ec
--- /dev/null
+++ b/dom/promise/tests/unit/test_promise_unhandled_rejection.js
@@ -0,0 +1,139 @@
+"use strict";
+
+// Tests that unhandled promise rejections generate the appropriate
+// console messages.
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+PromiseTestUtils.expectUncaughtRejection(/could not be cloned/);
+PromiseTestUtils.expectUncaughtRejection(/An exception was thrown/);
+PromiseTestUtils.expectUncaughtRejection(/Bleah/);
+
+const filename = "resource://foo/Bar.jsm";
+
+async function getSandboxMessages(sandbox, code) {
+ let { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ Cu.evalInSandbox(code, sandbox, null, filename, 1);
+
+ // We need two trips through the event loop for this error to be reported.
+ await new Promise(executeSoon);
+ await new Promise(executeSoon);
+ });
+
+ // xpcshell tests on OS-X sometimes include an extra warning, which we
+ // unfortunately need to ignore:
+ return messages.filter(
+ msg =>
+ !msg.message.includes(
+ "No chrome package registered for chrome://branding/locale/brand.properties"
+ )
+ );
+}
+
+add_task(async function test_unhandled_dom_exception() {
+ let sandbox = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal());
+ sandbox.StructuredCloneHolder = StructuredCloneHolder;
+
+ let messages = await getSandboxMessages(
+ sandbox,
+ `new Promise(() => {
+ new StructuredCloneHolder("", "", () => {});
+ });`
+ );
+
+ equal(messages.length, 1, "Got one console message");
+
+ let [msg] = messages;
+ ok(msg instanceof Ci.nsIScriptError, "Message is a script error");
+ equal(msg.sourceName, filename, "Got expected filename");
+ equal(msg.lineNumber, 2, "Got expected line number");
+ equal(
+ msg.errorMessage,
+ "DataCloneError: Function object could not be cloned.",
+ "Got expected error message"
+ );
+});
+
+add_task(async function test_unhandled_dom_exception_wrapped() {
+ let sandbox = Cu.Sandbox(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com/"
+ )
+ );
+ Cu.exportFunction(
+ function frick() {
+ throw new Components.Exception(
+ "Bleah.",
+ Cr.NS_ERROR_FAILURE,
+ Components.stack.caller
+ );
+ },
+ sandbox,
+ { defineAs: "frick" }
+ );
+
+ let messages = await getSandboxMessages(
+ sandbox,
+ `new Promise(() => {
+ frick();
+ });`
+ );
+
+ equal(messages.length, 2, "Got two console messages");
+
+ let [msg1, msg2] = messages;
+ ok(msg1 instanceof Ci.nsIScriptError, "Message is a script error");
+ equal(msg1.sourceName, filename, "Got expected filename");
+ equal(msg1.lineNumber, 2, "Got expected line number");
+ equal(
+ msg1.errorMessage,
+ "NS_ERROR_FAILURE: Bleah.",
+ "Got expected error message"
+ );
+
+ ok(msg2 instanceof Ci.nsIScriptError, "Message is a script error");
+ equal(msg2.sourceName, filename, "Got expected filename");
+ equal(msg2.lineNumber, 2, "Got expected line number");
+ equal(
+ msg2.errorMessage,
+ "InvalidStateError: An exception was thrown",
+ "Got expected error message"
+ );
+});
+
+add_task(async function test_unhandled_dom_exception_from_sandbox() {
+ let sandbox = Cu.Sandbox(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.com/"
+ ),
+ { wantGlobalProperties: ["DOMException"] }
+ );
+ let ctor = Cu.evalInSandbox("DOMException", sandbox);
+ Cu.exportFunction(
+ function frick() {
+ throw new ctor("Bleah.");
+ },
+ sandbox,
+ { defineAs: "frick" }
+ );
+
+ let messages = await getSandboxMessages(
+ sandbox,
+ `new Promise(() => {
+ frick();
+ });`
+ );
+
+ equal(messages.length, 1, "Got one console messages");
+
+ let [msg] = messages;
+ ok(msg instanceof Ci.nsIScriptError, "Message is a script error");
+ equal(msg.sourceName, filename, "Got expected filename");
+ equal(msg.lineNumber, 2, "Got expected line number");
+ equal(msg.errorMessage, "Error: Bleah.", "Got expected error message");
+});
diff --git a/dom/promise/tests/unit/xpcshell.ini b/dom/promise/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..4da333a9b2
--- /dev/null
+++ b/dom/promise/tests/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head =
+
+[test_monitor_uncaught.js]
+[test_promise_unhandled_rejection.js]
+[test_promise_job_across_sandbox.js]