Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
ChromeUtils.defineESModuleGetters(this, {
ExtensionPreferencesManager:
});
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();
}
);