/* 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(() => {});
let p2 =
new Promise(() => {});
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) {
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 => 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;
});