summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_eventpage_idle.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_eventpage_idle.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_eventpage_idle.js774
1 files changed, 774 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_eventpage_idle.js b/toolkit/components/extensions/test/xpcshell/test_ext_eventpage_idle.js
new file mode 100644
index 0000000000..46d34f1bcb
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_eventpage_idle.js
@@ -0,0 +1,774 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionPreferencesManager:
+ "resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+Services.prefs.setBoolPref("extensions.eventPages.enabled", true);
+// Set minimum idle timeout for testing
+Services.prefs.setIntPref("extensions.background.idle.timeout", 0);
+
+// Expected rejection from the test cases defined in this file.
+PromiseTestUtils.allowMatchingRejectionsGlobally(/expected-test-rejection/);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Actor 'Conduits' destroyed before query 'RunListener' was resolved/
+);
+
+add_setup(async () => {
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_eventpage_idle() {
+ const { GleanCustomDistribution } = globalThis;
+
+ resetTelemetryData();
+
+ assertHistogramEmpty(WEBEXT_EVENTPAGE_RUNNING_TIME_MS);
+ assertKeyedHistogramEmpty(WEBEXT_EVENTPAGE_RUNNING_TIME_MS_BY_ADDONID);
+ assertHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT);
+ assertKeyedHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID);
+ assertGleanMetricsNoSamples({
+ metricId: "eventPageRunningTime",
+ gleanMetric: Glean.extensionsTiming.eventPageRunningTime,
+ gleanMetricConstructor: GleanCustomDistribution,
+ });
+ assertGleanLabeledCounterEmpty({
+ metricId: "eventPageIdleResult",
+ gleanMetric: Glean.extensionsCounters.eventPageIdleResult,
+ gleanMetricLabels: GLEAN_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ permissions: ["browserSettings"],
+ background: { persistent: false },
+ },
+ background() {
+ browser.browserSettings.allowPopupsForUserEvents.onChange.addListener(
+ () => {
+ browser.test.sendMessage("allowPopupsForUserEvents");
+ }
+ );
+ browser.runtime.onSuspend.addListener(async () => {
+ let setting =
+ await browser.browserSettings.allowPopupsForUserEvents.get({});
+ browser.test.sendMessage("suspended", setting);
+ });
+ },
+ });
+ await extension.startup();
+ assertPersistentListeners(
+ extension,
+ "browserSettings",
+ "allowPopupsForUserEvents",
+ {
+ primed: false,
+ }
+ );
+
+ info(`test idle timeout after startup`);
+ await extension.awaitMessage("suspended");
+ await promiseExtensionEvent(extension, "shutdown-background-script");
+ assertPersistentListeners(
+ extension,
+ "browserSettings",
+ "allowPopupsForUserEvents",
+ {
+ primed: true,
+ }
+ );
+ ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "allowPopupsForUserEvents",
+ "click"
+ );
+ await extension.awaitMessage("allowPopupsForUserEvents");
+ ok(true, "allowPopupsForUserEvents.onChange fired");
+
+ // again after the event is fired
+ info(`test idle timeout after wakeup`);
+ let setting = await extension.awaitMessage("suspended");
+ equal(setting.value, true, "verify simple async wait works in onSuspend");
+
+ await promiseExtensionEvent(extension, "shutdown-background-script");
+ assertPersistentListeners(
+ extension,
+ "browserSettings",
+ "allowPopupsForUserEvents",
+ {
+ primed: true,
+ }
+ );
+ ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "allowPopupsForUserEvents",
+ false
+ );
+ await extension.awaitMessage("allowPopupsForUserEvents");
+ ok(true, "allowPopupsForUserEvents.onChange fired");
+
+ const { id } = extension;
+ await extension.unload();
+
+ info("Verify eventpage telemetry recorded");
+
+ assertHistogramSnapshot(
+ WEBEXT_EVENTPAGE_RUNNING_TIME_MS,
+ {
+ keyed: false,
+ processSnapshot: snapshot => snapshot.sum > 0,
+ expectedValue: true,
+ },
+ `Expect stored values in the eventpage running time non-keyed histogram snapshot`
+ );
+
+ assertHistogramSnapshot(
+ WEBEXT_EVENTPAGE_RUNNING_TIME_MS_BY_ADDONID,
+ {
+ keyed: true,
+ processSnapshot: snapshot => snapshot[id]?.sum > 0,
+ expectedValue: true,
+ },
+ `Expect stored values for addon with id ${id} in the eventpage running time keyed histogram snapshot`
+ );
+
+ assertHistogramCategoryNotEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT, {
+ category: "suspend",
+ categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ });
+ assertGleanLabeledCounterNotEmpty({
+ metricId: "eventPageIdleResult",
+ gleanMetric: Glean.extensionsCounters.eventPageIdleResult,
+ expectedNotEmptyLabels: ["suspend"],
+ });
+
+ assertHistogramCategoryNotEmpty(
+ WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID,
+ {
+ keyed: true,
+ key: id,
+ category: "suspend",
+ categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ }
+ );
+
+ Assert.greater(
+ Glean.extensionsTiming.eventPageRunningTime.testGetValue()?.sum,
+ 0,
+ `Expect stored values in the eventPageRunningTime Glean metric`
+ );
+});
+
+add_task(
+ { pref_set: [["extensions.background.idle.timeout", 500]] },
+ async function test_eventpage_runtime_parentApiCall_resets_timeout() {
+ resetTelemetryData();
+
+ assertHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT);
+ assertKeyedHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID);
+ assertGleanLabeledCounterEmpty({
+ metricId: "eventPageIdleResult",
+ gleanMetric: Glean.extensionsCounters.eventPageIdleResult,
+ gleanMetricLabels: GLEAN_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ background: { persistent: false },
+ },
+ async background() {
+ let start = Date.now();
+
+ browser.runtime.onSuspend.addListener(() => {
+ browser.test.sendMessage("done", Date.now() - start);
+ });
+
+ browser.runtime.getBrowserInfo();
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => browser.runtime.getBrowserInfo(), 50);
+ },
+ });
+
+ await extension.startup();
+ let [, resetData] = await promiseExtensionEvent(
+ extension,
+ "background-script-reset-idle"
+ );
+
+ equal(resetData.reason, "parentApiCall", "Got the expected idle reset.");
+
+ await promiseExtensionEvent(extension, "shutdown-background-script");
+
+ let time = await extension.awaitMessage("done");
+ Assert.greater(time, 100, `Background script suspended after ${time}ms.`);
+
+ // Disabled because the telemetry is too chatty, see bug 1868960.
+ // assertHistogramCategoryNotEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT, {
+ // category: "reset_parentapicall",
+ // categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ // });
+
+ // assertHistogramCategoryNotEmpty(
+ // WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID,
+ // {
+ // keyed: true,
+ // key: extension.id,
+ // category: "reset_parentapicall",
+ // categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ // }
+ // );
+
+ // assertGleanLabeledCounterNotEmpty({
+ // metricId: "eventPageIdleResult",
+ // gleanMetric: Glean.extensionsCounters.eventPageIdleResult,
+ // expectedNotEmptyLabels: ["reset_parentapicall"],
+ // });
+
+ await extension.unload();
+ }
+);
+
+add_task(
+ { pref_set: [["extensions.background.idle.timeout", 500]] },
+ async function test_extension_page_reset_idle() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ background: { persistent: false },
+ },
+ background() {
+ browser.test.log("background script start");
+ browser.runtime.onSuspend.addListener(() => {
+ browser.test.sendMessage("suspended");
+ });
+ browser.test.sendMessage("ready");
+ },
+ files: {
+ "page.html": "<meta charset=utf-8><script src=page.js></script>",
+ async "page.js"() {
+ await browser.runtime.getBrowserInfo();
+ browser.test.sendMessage("page-done");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ // Need to set up the listener as early as possible.
+ let closed = promiseExtensionEvent(extension, "shutdown-background-script");
+
+ await extension.awaitMessage("ready");
+ info("Background script ready.");
+
+ extension.extension.once("background-script-reset-idle", () => {
+ ok(false, "background-script-reset-idle emitted from an extension page.");
+ });
+
+ let page = await ExtensionTestUtils.loadContentPage(
+ extension.extension.baseURI.resolve("page.html")
+ );
+ await extension.awaitMessage("page-done");
+ info("Test page loaded.");
+
+ await closed;
+ await extension.awaitMessage("suspended");
+
+ ok(true, "API call from extension page did not reset idle timeout.");
+
+ await page.close();
+ await extension.unload();
+ }
+);
+
+add_task(async function test_persistent_background_reset_idle() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ background: { persistent: true },
+ },
+ background() {
+ browser.test.onMessage.addListener(async () => {
+ await browser.runtime.getBrowserInfo();
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ extension.extension.once("background-script-reset-idle", () => {
+ ok(false, "background-script-reset-idle from persistent background page.");
+ });
+
+ extension.sendMessage("call-parent-api");
+ ok(true, "API call from persistent background did not reset idle timeout.");
+
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(
+ { pref_set: [["extensions.webextensions.runtime.timeout", 500]] },
+ async function test_eventpage_runtime_onSuspend_timeout() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ background: { persistent: false },
+ },
+ background() {
+ browser.runtime.onSuspend.addListener(() => {
+ // return a promise that never resolves
+ return new Promise(() => {});
+ });
+ },
+ });
+ await extension.startup();
+ await promiseExtensionEvent(extension, "shutdown-background-script");
+ ok(true, "onSuspend did not block background shutdown");
+ await extension.unload();
+ }
+);
+
+add_task(
+ { pref_set: [["extensions.webextensions.runtime.timeout", 500]] },
+ async function test_eventpage_runtime_onSuspend_reject() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ background: { persistent: false },
+ },
+ background() {
+ browser.runtime.onSuspend.addListener(() => {
+ // Raise an error to test error handling in onSuspend
+ return Promise.reject("testing reject");
+ });
+ },
+ });
+ await extension.startup();
+ await promiseExtensionEvent(extension, "shutdown-background-script");
+ ok(true, "onSuspend did not block background shutdown");
+ await extension.unload();
+ }
+);
+
+add_task(
+ { pref_set: [["extensions.webextensions.runtime.timeout", 1000]] },
+ async function test_eventpage_runtime_onSuspend_canceled() {
+ resetTelemetryData();
+
+ assertHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT);
+ assertKeyedHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID);
+ assertGleanLabeledCounterEmpty({
+ metricId: "eventPageIdleResult",
+ gleanMetric: Glean.extensionsCounters.eventPageIdleResult,
+ gleanMetricLabels: GLEAN_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ permissions: ["browserSettings"],
+ background: { persistent: false },
+ },
+ background() {
+ let resolveSuspend;
+ browser.browserSettings.allowPopupsForUserEvents.onChange.addListener(
+ () => {
+ browser.test.sendMessage("allowPopupsForUserEvents");
+ }
+ );
+ browser.runtime.onSuspend.addListener(() => {
+ browser.test.sendMessage("suspending");
+ return new Promise(resolve => {
+ resolveSuspend = resolve;
+ });
+ });
+ browser.runtime.onSuspendCanceled.addListener(() => {
+ browser.test.sendMessage("suspendCanceled");
+ });
+ browser.test.onMessage.addListener(() => {
+ resolveSuspend();
+ });
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("suspending");
+ // While suspending, cause an event
+ ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "allowPopupsForUserEvents",
+ "click"
+ );
+ extension.sendMessage("resolveSuspend");
+ await extension.awaitMessage("allowPopupsForUserEvents");
+ await extension.awaitMessage("suspendCanceled");
+ ok(true, "event caused suspend-canceled");
+
+ // Disabled because the telemetry is too chatty, see bug 1868960.
+ // assertHistogramCategoryNotEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT, {
+ // category: "reset_event",
+ // categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ // });
+ // assertGleanLabeledCounterNotEmpty({
+ // metricId: "eventPageIdleResult",
+ // gleanMetric: Glean.extensionsCounters.eventPageIdleResult,
+ // expectedNotEmptyLabels: ["reset_event"],
+ // });
+
+ // assertHistogramCategoryNotEmpty(
+ // WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID,
+ // {
+ // keyed: true,
+ // key: extension.id,
+ // category: "reset_event",
+ // categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ // }
+ // );
+
+ await extension.awaitMessage("suspending");
+ await promiseExtensionEvent(extension, "shutdown-background-script");
+ await extension.unload();
+ }
+);
+
+add_task(async function test_terminateBackground_after_extension_hasShutdown() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ background: { persistent: false },
+ },
+ async background() {
+ browser.runtime.onSuspend.addListener(() => {
+ browser.test.fail(
+ `runtime.onSuspend listener should have not been called`
+ );
+ });
+
+ // Call an API method implemented in the parent process (to be sure runtime.onSuspend
+ // listener is going to be fully registered from a parent process perspective by the
+ // time we will send the "bg-ready" test message).
+ await browser.runtime.getBrowserInfo();
+
+ browser.test.sendMessage("bg-ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bg-ready");
+
+ // Fake suspending event page on idle while the extension was being shutdown by manually
+ // setting the hasShutdown flag to true on the Extension class instance object.
+ extension.extension.hasShutdown = true;
+ await extension.terminateBackground();
+ extension.extension.hasShutdown = false;
+
+ await extension.unload();
+});
+
+add_task(async function test_wakeupBackground_after_extension_hasShutdown() {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ background: { persistent: false },
+ },
+ async background() {
+ browser.test.sendMessage("bg-ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bg-ready");
+ await extension.terminateBackground();
+
+ // Fake suspending event page on idle while the extension was being shutdown by manually
+ // setting the hasShutdown flag to true on the Extension class instance object.
+ extension.extension.hasShutdown = true;
+ await Assert.rejects(
+ extension.wakeupBackground(),
+ /wakeupBackground called while the extension was already shutting down/,
+ "Got the expected rejection when wakeupBackground is called after extension shutdown"
+ );
+ extension.extension.hasShutdown = false;
+
+ await extension.unload();
+});
+
+async function testSuspendShutdownRace({ manifest_version }) {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version,
+ background: manifest_version === 2 ? { persistent: false } : {},
+ permissions: ["webRequest", "webRequestBlocking"],
+ host_permissions: ["*://example.com/*"],
+ granted_host_permissions: true,
+ },
+ // Define an empty background script.
+ background() {},
+ });
+
+ await extension.startup();
+ await extension.extension.promiseBackgroundStarted();
+ const promiseTerminateBackground = extension.extension.terminateBackground();
+ // Wait one tick to leave to terminateBackground async method time to get
+ // past the first check that returns earlier if extension.hasShutdown is true.
+ await Promise.resolve();
+ const promiseUnload = extension.unload();
+
+ await promiseUnload;
+ try {
+ await promiseTerminateBackground;
+ ok(true, "extension.terminateBackground should not have been rejected");
+ } catch (err) {
+ ok(
+ false,
+ `extension.terminateBackground should not have been rejected: ${err} :: ${err.stack}`
+ );
+ }
+}
+
+add_task(function test_mv2_suspend_shutdown_race() {
+ return testSuspendShutdownRace({ manifest_version: 2 });
+});
+
+add_task(
+ {
+ pref_set: [["extensions.manifestV3.enabled", true]],
+ },
+ function test_mv3_suspend_shutdown_race() {
+ return testSuspendShutdownRace({ manifest_version: 3 });
+ }
+);
+
+function createPendingListenerTestExtension() {
+ return ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ permissions: ["browserSettings"],
+ background: { persistent: false },
+ },
+ background() {
+ let idx = 0;
+ browser.browserSettings.allowPopupsForUserEvents.onChange.addListener(
+ async () => {
+ const currIdx = idx++;
+ await new Promise((resolve, reject) => {
+ browser.test.onMessage.addListener(msg => {
+ switch (`${msg}-${currIdx}`) {
+ case "unblock-promise-0":
+ resolve();
+ browser.test.sendMessage("allowPopupsForUserEvents:resolved");
+ break;
+ case "unblock-promise-1":
+ reject(new Error("expected-test-rejection"));
+ browser.test.sendMessage("allowPopupsForUserEvents:rejected");
+ break;
+ default:
+ browser.test.fail(`Unexpected test message: ${msg}`);
+ }
+ });
+ browser.test.sendMessage("allowPopupsForUserEvents:awaiting");
+ });
+ }
+ );
+
+ browser.runtime.onSuspend.addListener(() => {
+ // Raise an error to test error handling in onSuspend
+ return browser.test.sendMessage("runtime-on-suspend");
+ });
+
+ browser.test.sendMessage("bg-script-ready");
+ },
+ });
+}
+
+add_task(
+ { pref_set: [["extensions.background.idle.timeout", 500]] },
+ async function test_eventpage_idle_reset_on_async_listener_unresolved() {
+ resetTelemetryData();
+
+ assertHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT);
+ assertKeyedHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID);
+ assertGleanLabeledCounterEmpty({
+ metricId: "eventPageIdleResult",
+ gleanMetric: Glean.extensionsCounters.eventPageIdleResult,
+ gleanMetricLabels: GLEAN_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ });
+
+ let extension = createPendingListenerTestExtension();
+ await extension.startup();
+ await extension.awaitMessage("bg-script-ready");
+
+ info("Trigger the first API event listener call");
+ ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "allowPopupsForUserEvents",
+ "click"
+ );
+
+ await extension.awaitMessage("allowPopupsForUserEvents:awaiting");
+
+ info("Trigger the second API event listener call");
+ ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "allowPopupsForUserEvents",
+ "click"
+ );
+
+ await extension.awaitMessage("allowPopupsForUserEvents:awaiting");
+
+ info("Wait for suspend on idle to be reset");
+ const [, resetIdleData] = await promiseExtensionEvent(
+ extension,
+ "background-script-reset-idle"
+ );
+
+ Assert.deepEqual(
+ resetIdleData,
+ {
+ reason: "pendingListeners",
+ pendingListeners: 2,
+ },
+ "Got the expected idle reset reason and pendingListeners count"
+ );
+
+ assertHistogramCategoryNotEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT, {
+ category: "reset_listeners",
+ categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ });
+
+ assertGleanLabeledCounter({
+ metricId: "eventPageIdleResult",
+ gleanMetric: Glean.extensionsCounters.eventPageIdleResult,
+ gleanMetricLabels: GLEAN_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ ignoreNonExpectedLabels: true, // Only check values on the labels listed below.
+ expectedLabelsValue: {
+ reset_listeners: 1,
+ },
+ });
+
+ assertHistogramCategoryNotEmpty(
+ WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID,
+ {
+ keyed: true,
+ key: extension.id,
+ category: "reset_listeners",
+ categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ }
+ );
+
+ info(
+ "Resolve the async listener pending on a promise and expect the event page to suspend after the idle timeout"
+ );
+ extension.sendMessage("unblock-promise");
+ // Expect the two promises to be resolved and rejected respectively.
+ await extension.awaitMessage("allowPopupsForUserEvents:resolved");
+ await extension.awaitMessage("allowPopupsForUserEvents:rejected");
+
+ info("Await for the runtime.onSuspend event to be emitted");
+ await extension.awaitMessage("runtime-on-suspend");
+ await extension.unload();
+ }
+);
+
+add_task(
+ { pref_set: [["extensions.background.idle.timeout", 500]] },
+ async function test_pending_async_listeners_promises_rejected_on_shutdown() {
+ let extension = createPendingListenerTestExtension();
+ await extension.startup();
+ await extension.awaitMessage("bg-script-ready");
+
+ info("Trigger the API event listener call");
+ ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "allowPopupsForUserEvents",
+ "click"
+ );
+
+ await extension.awaitMessage("allowPopupsForUserEvents:awaiting");
+
+ const { runListenerPromises } = extension.extension.backgroundContext;
+ equal(
+ runListenerPromises.size,
+ 1,
+ "Got the expected number of pending runListener promises"
+ );
+
+ const pendingPromise = Array.from(runListenerPromises)[0];
+
+ // Shutdown the extension while there is still a pending promises being tracked
+ // to verify they gets rejected as expected when the background page browser element
+ // is going to be destroyed.
+ await extension.unload();
+
+ await Assert.rejects(
+ pendingPromise,
+ /Actor 'Conduits' destroyed before query 'RunListener' was resolved/,
+ "Previously pending runListener promise rejected with the expected error"
+ );
+
+ equal(
+ runListenerPromises.size,
+ 0,
+ "Expect no remaining pending runListener promises"
+ );
+ }
+);
+
+add_task(
+ { pref_set: [["extensions.background.idle.timeout", 500]] },
+ async function test_eventpage_idle_reset_once_on_pending_async_listeners() {
+ let extension = createPendingListenerTestExtension();
+ await extension.startup();
+ await extension.awaitMessage("bg-script-ready");
+
+ info("Trigger the API event listener call");
+ ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "allowPopupsForUserEvents",
+ "click"
+ );
+
+ await extension.awaitMessage("allowPopupsForUserEvents:awaiting");
+
+ info("Wait for suspend on the first idle timeout to be reset");
+ const [, resetIdleData] = await promiseExtensionEvent(
+ extension,
+ "background-script-reset-idle"
+ );
+
+ Assert.deepEqual(
+ resetIdleData,
+ {
+ reason: "pendingListeners",
+ pendingListeners: 1,
+ },
+ "Got the expected idle reset reason and pendingListeners count"
+ );
+
+ info(
+ "Await for the runtime.onSuspend event to be emitted on the second idle timeout hit"
+ );
+ // We expect this part of the test to trigger a uncaught rejection for the
+ // "Actor 'Conduits' destroyed before query 'RunListener' was resolved" error,
+ // due to the listener left purposely pending in this test
+ // and so that expected rejection is ignored using PromiseTestUtils in the preamble
+ // of this test file.
+ await extension.awaitMessage("runtime-on-suspend");
+ await extension.unload();
+ }
+);