summaryrefslogtreecommitdiffstats
path: root/toolkit/components/asyncshutdown/tests
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/asyncshutdown/tests')
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/head.js180
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js348
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_blocker_error_annotations.js55
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js105
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js94
-rw-r--r--toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.toml11
6 files changed, 793 insertions, 0 deletions
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/head.js b/toolkit/components/asyncshutdown/tests/xpcshell/head.js
new file mode 100644
index 0000000000..6b8c756122
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/head.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var { AsyncShutdown } = ChromeUtils.importESModule(
+ "resource://gre/modules/AsyncShutdown.sys.mjs"
+);
+
+var asyncShutdownService = Cc[
+ "@mozilla.org/async-shutdown-service;1"
+].getService(Ci.nsIAsyncShutdownService);
+
+Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+
+/**
+ * Utility function used to provide the same API for various sources
+ * of async shutdown barriers.
+ *
+ * @param {string} kind One of
+ * - "phase" to test an AsyncShutdown phase;
+ * - "barrier" to test an instance of AsyncShutdown.Barrier;
+ * - "xpcom-barrier" to test an instance of nsIAsyncShutdownBarrier;
+ * - "xpcom-barrier-unwrapped" to test the field `jsclient` of a nsIAsyncShutdownClient.
+ *
+ * @return An object with the following methods:
+ * - addBlocker() - the same method as AsyncShutdown phases and barrier clients
+ * - wait() - trigger the resolution of the lock
+ */
+function makeLock(kind) {
+ if (kind == "phase") {
+ let topic = "test-Phase-" + ++makeLock.counter;
+ let phase = AsyncShutdown._getPhase(topic);
+ return {
+ addBlocker(...args) {
+ return phase.addBlocker(...args);
+ },
+ removeBlocker(blocker) {
+ return phase.removeBlocker(blocker);
+ },
+ wait() {
+ Services.obs.notifyObservers(null, topic);
+ return Promise.resolve();
+ },
+ get isClosed() {
+ return phase.isClosed;
+ },
+ };
+ } else if (kind == "barrier") {
+ let name = "test-Barrier-" + ++makeLock.counter;
+ let barrier = new AsyncShutdown.Barrier(name);
+ return {
+ addBlocker: barrier.client.addBlocker,
+ removeBlocker: barrier.client.removeBlocker,
+ wait() {
+ return barrier.wait();
+ },
+ get isClosed() {
+ return barrier.client.isClosed;
+ },
+ };
+ } else if (kind == "xpcom-barrier") {
+ let name = "test-xpcom-Barrier-" + ++makeLock.counter;
+ let barrier = asyncShutdownService.makeBarrier(name);
+ return {
+ addBlocker(blockerName, condition, state) {
+ if (condition == null) {
+ // Slight trick as `null` or `undefined` cannot be used as keys
+ // for `xpcomMap`. Note that this has no incidence on the result
+ // of the test as the XPCOM interface imposes that the condition
+ // is a method, so it cannot be `null`/`undefined`.
+ condition = "<this case can't happen with the xpcom interface>";
+ }
+ let blocker = makeLock.xpcomMap.get(condition);
+ if (!blocker) {
+ blocker = {
+ name: blockerName,
+ state,
+ blockShutdown(aBarrierClient) {
+ return (async function () {
+ try {
+ if (typeof condition == "function") {
+ await Promise.resolve(condition());
+ } else {
+ await Promise.resolve(condition);
+ }
+ } finally {
+ aBarrierClient.removeBlocker(blocker);
+ }
+ })();
+ },
+ };
+ makeLock.xpcomMap.set(condition, blocker);
+ }
+ let { fileName, lineNumber, stack } = new Error();
+ return barrier.client.addBlocker(blocker, fileName, lineNumber, stack);
+ },
+ removeBlocker(condition) {
+ let blocker = makeLock.xpcomMap.get(condition);
+ if (!blocker) {
+ return;
+ }
+ barrier.client.removeBlocker(blocker);
+ },
+ wait() {
+ return new Promise(resolve => {
+ barrier.wait(resolve);
+ });
+ },
+ get isClosed() {
+ return barrier.client.isClosed;
+ },
+ };
+ } else if ("unwrapped-xpcom-barrier") {
+ let name = "unwrapped-xpcom-barrier-" + ++makeLock.counter;
+ let barrier = asyncShutdownService.makeBarrier(name);
+ let client = barrier.client.jsclient;
+ return {
+ addBlocker: client.addBlocker,
+ removeBlocker: client.removeBlocker,
+ wait() {
+ return new Promise(resolve => {
+ barrier.wait(resolve);
+ });
+ },
+ get isClosed() {
+ return client.isClosed;
+ },
+ };
+ }
+ throw new TypeError("Unknown kind " + kind);
+}
+makeLock.counter = 0;
+makeLock.xpcomMap = new Map(); // Note: Not a WeakMap as we wish to handle non-gc-able keys (e.g. strings)
+
+/**
+ * An asynchronous task that takes several ticks to complete.
+ *
+ * @param {*=} resolution The value with which the resulting promise will be
+ * resolved once the task is complete. This may be a rejected promise,
+ * in which case the resulting promise will itself be rejected.
+ * @param {object=} outResult An object modified by side-effect during the task.
+ * Initially, its field |isFinished| is set to |false|. Once the task is
+ * complete, its field |isFinished| is set to |true|.
+ *
+ * @return {promise} A promise fulfilled once the task is complete
+ */
+function longRunningAsyncTask(resolution = undefined, outResult = {}) {
+ outResult.isFinished = false;
+ if (!("countFinished" in outResult)) {
+ outResult.countFinished = 0;
+ }
+ return new Promise(resolve => {
+ do_timeout(100, function () {
+ ++outResult.countFinished;
+ outResult.isFinished = true;
+ resolve(resolution);
+ });
+ });
+}
+
+function get_exn(f) {
+ try {
+ f();
+ return null;
+ } catch (ex) {
+ return ex;
+ }
+}
+
+function do_check_exn(exn, constructor) {
+ Assert.notEqual(exn, null);
+ if (exn.name == constructor) {
+ Assert.equal(exn.constructor.name, constructor);
+ return;
+ }
+ info("Wrong error constructor");
+ info(exn.constructor.name);
+ info(exn.stack);
+ Assert.ok(false);
+}
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js
new file mode 100644
index 0000000000..27c3a26985
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown.js
@@ -0,0 +1,348 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+add_task(async function test_no_condition() {
+ for (let kind of [
+ "phase",
+ "barrier",
+ "xpcom-barrier",
+ "xpcom-barrier-unwrapped",
+ ]) {
+ info("Testing a barrier with no condition (" + kind + ")");
+ let lock = makeLock(kind);
+ await lock.wait();
+ info("Barrier with no condition didn't lock");
+ }
+});
+
+add_task(async function test_phase_various_failures() {
+ for (let kind of [
+ "phase",
+ "barrier",
+ "xpcom-barrier",
+ "xpcom-barrier-unwrapped",
+ ]) {
+ info("Kind: " + kind);
+ // Testing with wrong arguments
+ let lock = makeLock(kind);
+
+ Assert.throws(
+ () => lock.addBlocker(),
+ /TypeError|NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS/
+ );
+ Assert.throws(
+ () => lock.addBlocker(null, true),
+ /TypeError|NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS/
+ );
+
+ if (kind != "xpcom-barrier") {
+ // xpcom-barrier actually expects a string in that position
+ Assert.throws(
+ () => lock.addBlocker("Test 2", () => true, "not a function"),
+ /TypeError/
+ );
+ }
+
+ if (kind == "xpcom-barrier") {
+ const blocker = () => true;
+ lock.addBlocker("Test 3", blocker);
+ Assert.throws(
+ () => lock.addBlocker("Test 3", blocker),
+ /We have already registered the blocker \(Test 3\)/
+ );
+ }
+
+ // Attempting to add a blocker after we are done waiting
+ Assert.ok(!lock.isClosed, "Barrier is open");
+ await lock.wait();
+ Assert.throws(() => lock.addBlocker("Test 4", () => true), /is finished/);
+ Assert.ok(lock.isClosed, "Barrier is closed");
+ }
+});
+
+add_task(async function test_reentrant() {
+ info("Ensure that we can call addBlocker from within a blocker");
+
+ for (let kind of [
+ "phase",
+ "barrier",
+ "xpcom-barrier",
+ "xpcom-barrier-unwrapped",
+ ]) {
+ info("Kind: " + kind);
+ let lock = makeLock(kind);
+
+ let deferredOuter = Promise.withResolvers();
+ let deferredInner = Promise.withResolvers();
+ let deferredBlockInner = Promise.withResolvers();
+
+ lock.addBlocker("Outer blocker", () => {
+ info("Entering outer blocker");
+ deferredOuter.resolve();
+ lock.addBlocker("Inner blocker", () => {
+ info("Entering inner blocker");
+ deferredInner.resolve();
+ return deferredBlockInner.promise;
+ });
+ });
+
+ // Note that phase-style locks spin the event loop and do not return from
+ // `lock.wait()` until after all blockers have been resolved. Therefore,
+ // to be able to test them, we need to dispatch the following steps to the
+ // event loop before calling `lock.wait()`, which we do by forcing
+ // a Promise.resolve().
+ //
+ let promiseSteps = (async function () {
+ await Promise.resolve();
+
+ info("Waiting until we have entered the outer blocker");
+ await deferredOuter.promise;
+
+ info("Waiting until we have entered the inner blocker");
+ await deferredInner.promise;
+
+ info("Allowing the lock to resolve");
+ deferredBlockInner.resolve();
+ })();
+
+ info("Starting wait");
+ await lock.wait();
+
+ info("Waiting until all steps have been walked");
+ await promiseSteps;
+ }
+});
+
+add_task(async function test_phase_removeBlocker() {
+ info(
+ "Testing that we can call removeBlocker before, during and after the call to wait()"
+ );
+
+ for (let kind of [
+ "phase",
+ "barrier",
+ "xpcom-barrier",
+ "xpcom-barrier-unwrapped",
+ ]) {
+ info("Switching to kind " + kind);
+ info("Attempt to add then remove a blocker before wait()");
+ let lock = makeLock(kind);
+ let blocker = () => {
+ info("This promise will never be resolved");
+ return Promise.withResolvers().promise;
+ };
+
+ lock.addBlocker("Wait forever", blocker);
+ let do_remove_blocker = function (aLock, aBlocker, aShouldRemove) {
+ info(
+ "Attempting to remove blocker " +
+ aBlocker +
+ ", expecting result " +
+ aShouldRemove
+ );
+ if (kind == "xpcom-barrier") {
+ // The xpcom variant always returns `undefined`, so we can't
+ // check its result.
+ aLock.removeBlocker(aBlocker);
+ return;
+ }
+ Assert.equal(aLock.removeBlocker(aBlocker), aShouldRemove);
+ };
+ do_remove_blocker(lock, blocker, true);
+ do_remove_blocker(lock, blocker, false);
+ info("Attempt to remove non-registered blockers before wait()");
+ do_remove_blocker(lock, "foo", false);
+ do_remove_blocker(lock, null, false);
+ info("Waiting (should lift immediately)");
+ await lock.wait();
+
+ info("Attempt to add a blocker then remove it during wait()");
+ lock = makeLock(kind);
+ let blockers = [
+ () => {
+ info("This blocker will self-destruct");
+ do_remove_blocker(lock, blockers[0], true);
+ return Promise.withResolvers().promise;
+ },
+ () => {
+ info("This blocker will self-destruct twice");
+ do_remove_blocker(lock, blockers[1], true);
+ do_remove_blocker(lock, blockers[1], false);
+ return Promise.withResolvers().promise;
+ },
+ () => {
+ info("Attempt to remove non-registered blockers during wait()");
+ do_remove_blocker(lock, "foo", false);
+ do_remove_blocker(lock, null, false);
+ },
+ ];
+ for (let i in blockers) {
+ lock.addBlocker("Wait forever again: " + i, blockers[i]);
+ }
+ info("Waiting (should lift very quickly)");
+ await lock.wait();
+ do_remove_blocker(lock, blockers[0], false);
+
+ info("Attempt to remove a blocker after wait");
+ lock = makeLock(kind);
+ blocker = Promise.resolve.bind(Promise);
+ await lock.wait();
+ do_remove_blocker(lock, blocker, false);
+
+ info("Attempt to remove non-registered blocker after wait()");
+ do_remove_blocker(lock, "foo", false);
+ do_remove_blocker(lock, null, false);
+ }
+});
+
+add_task(async function test_addBlocker_noDistinctNamesConstraint() {
+ info("Testing that we can add two distinct blockers with identical names");
+
+ for (let kind of [
+ "phase",
+ "barrier",
+ "xpcom-barrier",
+ "xpcom-barrier-unwrapped",
+ ]) {
+ info("Switching to kind " + kind);
+ let lock = makeLock(kind);
+ let deferred1 = Promise.withResolvers();
+ let resolved1 = false;
+ let deferred2 = Promise.withResolvers();
+ let resolved2 = false;
+ let blocker1 = () => {
+ info("Entering blocker1");
+ return deferred1.promise;
+ };
+ let blocker2 = () => {
+ info("Entering blocker2");
+ return deferred2.promise;
+ };
+
+ info("Attempt to add two distinct blockers with identical names");
+ lock.addBlocker("Blocker", blocker1);
+ lock.addBlocker("Blocker", blocker2);
+
+ // Note that phase-style locks spin the event loop and do not return from
+ // `lock.wait()` until after all blockers have been resolved. Therefore,
+ // to be able to test them, we need to dispatch the following steps to the
+ // event loop before calling `lock.wait()`, which we do by forcing
+ // a Promise.resolve().
+ //
+ let promiseSteps = (async () => {
+ info("Waiting for an event-loop spin");
+ await Promise.resolve();
+
+ info("Resolving blocker1");
+ deferred1.resolve();
+ resolved1 = true;
+
+ info("Waiting for an event-loop spin");
+ await Promise.resolve();
+
+ info("Resolving blocker2");
+ deferred2.resolve();
+ resolved2 = true;
+ })();
+
+ info("Waiting for lock");
+ await lock.wait();
+
+ Assert.ok(resolved1);
+ Assert.ok(resolved2);
+ await promiseSteps;
+ }
+});
+
+add_task(async function test_state() {
+ info("Testing information contained in `state`");
+
+ let BLOCKER_NAME = "test_state blocker " + Math.random();
+
+ // Set up the barrier. Note that we cannot test `barrier.state`
+ // immediately, as it initially contains "Not started"
+ let barrier = new AsyncShutdown.Barrier("test_filename");
+ let deferred = Promise.withResolvers();
+ let { filename, lineNumber } = Components.stack;
+ barrier.client.addBlocker(BLOCKER_NAME, function () {
+ return deferred.promise;
+ });
+
+ let promiseDone = barrier.wait();
+
+ // Now that we have called `wait()`, the state contains interesting things
+ info("State: " + JSON.stringify(barrier.state, null, "\t"));
+ let state = barrier.state[0];
+ Assert.equal(state.filename, filename);
+ Assert.equal(state.lineNumber, lineNumber + 1);
+ Assert.equal(state.name, BLOCKER_NAME);
+ Assert.ok(
+ state.stack.some(x => x.includes("test_state")),
+ "The stack contains the caller function's name"
+ );
+ Assert.ok(
+ state.stack.some(x => x.includes(filename)),
+ "The stack contains the calling file's name"
+ );
+
+ deferred.resolve();
+ await promiseDone;
+});
+
+add_task(async function test_multistate() {
+ info("Testing information contained in multiple `state`");
+
+ let BLOCKER_NAMES = [
+ "test_state blocker " + Math.random(),
+ "test_state blocker " + Math.random(),
+ ];
+
+ // Set up the barrier. Note that we cannot test `barrier.state`
+ // immediately, as it initially contains "Not started"
+ let barrier = asyncShutdownService.makeBarrier("test_filename");
+ let deferred = Promise.withResolvers();
+ let { filename, lineNumber } = Components.stack;
+ for (let name of BLOCKER_NAMES) {
+ barrier.client.jsclient.addBlocker(name, () => deferred.promise, {
+ fetchState: () => ({ progress: name }),
+ });
+ }
+
+ let promiseDone = new Promise(r => barrier.wait(r));
+
+ // Now that we have called `wait()`, the state contains interesting things.
+ Assert.ok(
+ barrier.state instanceof Ci.nsIPropertyBag,
+ "State is a PropertyBag"
+ );
+ for (let i = 0; i < BLOCKER_NAMES.length; ++i) {
+ let state = barrier.state.getProperty(i.toString());
+ Assert.equal(typeof state, "string", "state is a string");
+ info("State: " + state + "\t");
+ state = JSON.parse(state);
+ Assert.equal(state.filename, filename);
+ Assert.equal(state.lineNumber, lineNumber + 2);
+ Assert.equal(state.name, BLOCKER_NAMES[i]);
+ Assert.ok(
+ state.stack.some(x => x.includes("test_multistate")),
+ "The stack contains the caller function's name"
+ );
+ Assert.ok(
+ state.stack.some(x => x.includes(filename)),
+ "The stack contains the calling file's name"
+ );
+ Assert.equal(
+ state.state.progress,
+ BLOCKER_NAMES[i],
+ "The state contains the fetchState provided value"
+ );
+ }
+
+ deferred.resolve();
+ await promiseDone;
+});
+
+add_task(async function () {
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+});
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_blocker_error_annotations.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_blocker_error_annotations.js
new file mode 100644
index 0000000000..669164f68e
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_blocker_error_annotations.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that when addBlocker fails, we store that failure internally
+ * and include its information in crash report annotation information.
+ */
+add_task(async function test_addBlockerFailureState() {
+ info("Testing addBlocker information reported to crash reporter");
+
+ let BLOCKER_NAME = "test_addBlocker_state blocker " + Math.random();
+
+ // Set up the barrier. Note that we cannot test `barrier.state`
+ // immediately, as it initially contains "Not started"
+ let barrier = new AsyncShutdown.Barrier("test_addBlocker_failure");
+ let deferred = Promise.withResolvers();
+ barrier.client.addBlocker(BLOCKER_NAME, function () {
+ return deferred.promise;
+ });
+
+ // Add a blocker and confirm that throws.
+ const THROWING_BLOCKER_NAME = "test_addBlocker_throws blocker";
+ Assert.throws(() => {
+ barrier.client.addBlocker(THROWING_BLOCKER_NAME, Promise.resolve(), 5);
+ }, /object as third argument/);
+
+ let promiseDone = barrier.wait();
+
+ // Now that we have called `wait()`, the state should match crash
+ // reporting info
+ let crashInfo = barrier._gatherCrashReportTimeoutData(
+ barrier._name,
+ barrier.state
+ );
+ Assert.deepEqual(
+ crashInfo.conditions,
+ barrier.state,
+ "Barrier state should match crash info."
+ );
+ Assert.equal(
+ crashInfo.brokenAddBlockers.length,
+ 1,
+ "Should have registered the broken addblocker call."
+ );
+ Assert.stringMatches(
+ crashInfo.brokenAddBlockers?.[0] || "undefined",
+ THROWING_BLOCKER_NAME,
+ "Throwing call's blocker name should be listed in message."
+ );
+
+ deferred.resolve();
+ await promiseDone;
+});
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js
new file mode 100644
index 0000000000..b37416d7a7
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_AsyncShutdown_leave_uncaught.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+//
+// This file contains tests that need to leave uncaught asynchronous
+// errors. If your test catches all its asynchronous errors, please
+// put it in another file.
+//
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed();
+
+add_task(async function test_phase_simple_async() {
+ info("Testing various combinations of a phase with a single condition");
+ for (let kind of [
+ "phase",
+ "barrier",
+ "xpcom-barrier",
+ "xpcom-barrier-unwrapped",
+ ]) {
+ for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
+ for (let resolution of [arg, Promise.reject(arg)]) {
+ for (let success of [false, true]) {
+ for (let state of [
+ [null],
+ [],
+ [() => "some state"],
+ [
+ function () {
+ throw new Error("State BOOM");
+ },
+ ],
+ [
+ function () {
+ return {
+ toJSON() {
+ throw new Error("State.toJSON BOOM");
+ },
+ };
+ },
+ ],
+ ]) {
+ // Asynchronous phase
+ info(
+ "Asynchronous test with " + arg + ", " + resolution + ", " + kind
+ );
+ let lock = makeLock(kind);
+ let outParam = { isFinished: false };
+ lock.addBlocker(
+ "Async test",
+ function () {
+ if (success) {
+ return longRunningAsyncTask(resolution, outParam);
+ }
+ throw resolution;
+ },
+ ...state
+ );
+ Assert.ok(!outParam.isFinished);
+ await lock.wait();
+ Assert.equal(outParam.isFinished, success);
+ }
+ }
+
+ // Synchronous phase - just test that we don't throw/freeze
+ info("Synchronous test with " + arg + ", " + resolution + ", " + kind);
+ let lock = makeLock(kind);
+ lock.addBlocker("Sync test", resolution);
+ await lock.wait();
+ }
+ }
+ }
+});
+
+add_task(async function test_phase_many() {
+ info("Testing various combinations of a phase with many conditions");
+ for (let kind of [
+ "phase",
+ "barrier",
+ "xpcom-barrier",
+ "xpcom-barrier-unwrapped",
+ ]) {
+ let lock = makeLock(kind);
+ let outParams = [];
+ for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
+ for (let resolve of [true, false]) {
+ info("Testing with " + kind + ", " + arg + ", " + resolve);
+ let resolution = resolve ? arg : Promise.reject(arg);
+ let outParam = { isFinished: false };
+ lock.addBlocker("Test " + Math.random(), () =>
+ longRunningAsyncTask(resolution, outParam)
+ );
+ }
+ }
+ Assert.ok(outParams.every(x => !x.isFinished));
+ await lock.wait();
+ Assert.ok(outParams.every(x => x.isFinished));
+ }
+});
+
+add_task(async function () {
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+});
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js b/toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js
new file mode 100644
index 0000000000..1e4740ddc1
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/test_converters.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test conversion between nsIPropertyBag and JS values.
+ */
+
+var PropertyBagConverter =
+ new asyncShutdownService.wrappedJSObject._propertyBagConverter();
+
+function run_test() {
+ test_conversions();
+}
+
+function normalize(obj) {
+ if (obj === undefined) {
+ return null;
+ }
+ if (obj == null || typeof obj != "object") {
+ return obj;
+ }
+ if (Array.isArray(obj)) {
+ return obj.map(normalize);
+ }
+ let result = {};
+ for (let k of Object.keys(obj).sort()) {
+ result[k] = normalize(obj[k]);
+ }
+ return result;
+}
+
+function test_conversions() {
+ const SAMPLES = [
+ // Simple values
+ 1,
+ true,
+ "string",
+ null,
+ undefined,
+ // Object
+ {
+ a: 1,
+ b: true,
+ c: "string",
+ d: 0.5,
+ e: [2, false, "another string", 0.3],
+ f: [],
+ g: {
+ a2: 1,
+ b2: true,
+ c2: "string",
+ d2: 0.5,
+ e2: [2, false, "another string", 0.3],
+ f2: [],
+ g2: [
+ {
+ a3: 1,
+ b3: true,
+ c3: "string",
+ d3: 0.5,
+ e3: [2, false, "another string", 0.3],
+ f3: [],
+ g3: {},
+ },
+ ],
+ h2: null,
+ },
+ h: null,
+ },
+ // Array
+ [1, 2, 3],
+ // Array of objects
+ [[1, 2], { a: 1, b: "string" }, null],
+ ];
+
+ for (let sample of SAMPLES) {
+ let stringified = JSON.stringify(normalize(sample), null, "\t");
+ info("Testing conversions of " + stringified);
+ let rewrites = [sample];
+ for (let i = 1; i < 3; ++i) {
+ let source = rewrites[i - 1];
+ let bag = PropertyBagConverter.jsValueToPropertyBag(source);
+ Assert.ok(bag instanceof Ci.nsIPropertyBag, "The bag is a property bag");
+ let dest = PropertyBagConverter.propertyBagToJsValue(bag);
+ let restringified = JSON.stringify(normalize(dest), null, "\t");
+ info("Comparing");
+ info(stringified);
+ info(restringified);
+ Assert.deepEqual(sample, dest, "Testing after " + i + " conversions");
+ rewrites.push(dest);
+ }
+ }
+}
diff --git a/toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.toml b/toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..365af63451
--- /dev/null
+++ b/toolkit/components/asyncshutdown/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+head = "head.js"
+skip-if = ["os == 'android'"]
+
+["test_AsyncShutdown.js"]
+
+["test_AsyncShutdown_blocker_error_annotations.js"]
+
+["test_AsyncShutdown_leave_uncaught.js"]
+
+["test_converters.js"]