diff options
Diffstat (limited to 'dom/promise/tests/unit')
-rw-r--r-- | dom/promise/tests/unit/test_monitor_uncaught.js | 322 | ||||
-rw-r--r-- | dom/promise/tests/unit/test_promise_job_across_sandbox.js | 221 | ||||
-rw-r--r-- | dom/promise/tests/unit/test_promise_unhandled_rejection.js | 139 | ||||
-rw-r--r-- | dom/promise/tests/unit/xpcshell.ini | 6 |
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] |