Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
// This test checks that the suspension of the event page is delayed when the
// runtime.onConnect / runtime.onMessage events are involved.
//
// Another test (test_ext_eventpage_messaging_wakeup.js) verifies that the event
// page wakes up when these events are to be triggered.
const { ExtensionTestCommon } = ChromeUtils.importESModule(
);
add_setup(() => {
// In this test, we want to verify that an idle timeout is reset when
// extension messages are set. To avoid waiting for too long, reduce the
// default timeout to a short value. To avoid premature test termination,
// this timeout should be sufficiently large to run the relevant logic that
// is supposed to postpone event page termination:
// - The idle timer starts when the background has loaded.
// - The idle timer should reset when expected by tests.
Services.prefs.setIntPref("extensions.background.idle.timeout", 1000);
});
async function loadEventPageAndExtensionPage({
backgroundScript,
extensionPageScript,
}) {
let extension = ExtensionTestUtils.loadExtension({
// Delay startup, to ensure that the event page does not suspend until we
// have started the extension page that runs extensionPageScript.
startupReason: "APP_STARTUP",
// APP_STARTUP is not enough, delayedStartup is needed (bug 1756225).
delayedStartup: true,
manifest: {
background: { persistent: false },
},
background: backgroundScript,
files: {
"page.html": `<!DOCTYPE html><script src="page.js"></script>`,
"page.js": extensionPageScript,
},
});
// Delay event page startup until notifyEarlyStartup+notifyLateStartup below.
await ExtensionTestCommon.resetStartupPromises();
await extension.startup();
// Start extension page first, so that it can register runtime.onSuspend
// before there is any chance of encountering a suspended event page.
let contentPage = await ExtensionTestUtils.loadContentPage(
`moz-extension://${extension.uuid}/page.html`
);
info("Extension page loaded, permitting event page to start up");
await ExtensionTestCommon.notifyEarlyStartup();
await ExtensionTestCommon.notifyLateStartup();
await extension.awaitMessage("FINAL_SUSPEND");
info("Received FINAL_SUSPEND, awaiting full event page shutdown.");
await promiseExtensionEvent(extension, "shutdown-background-script");
await contentPage.close();
await extension.unload();
}
add_task(async function test_runtime_onMessage_cancels_suspend() {
await loadEventPageAndExtensionPage({
backgroundScript() {
// This background script registers listeners without calling any other
// extension API. This ensures that if the event page suspend is canceled,
// that it was intentionally done by the listener, and not as a side
// effect of an unrelated extension API call.
browser.runtime.onMessage.addListener(msg => {
return Promise.resolve(`bg_pong:${msg}`);
});
},
extensionPageScript() {
let cancelCount = 0;
browser.runtime.onSuspendCanceled.addListener(() => {
browser.test.assertEq(1, ++cancelCount, "onSuspendCanceled 1x");
});
let suspendCount = 0;
let firstSuspendTestCompleted = false;
browser.runtime.onSuspend.addListener(async () => {
// We expect 2x suspend: first one we cancel, second one is final.
if (++suspendCount === 1) {
// First suspend attempt.
browser.test.assertEq(0, cancelCount, "Not suspended yet");
let res = await browser.runtime.sendMessage("ping");
browser.test.assertEq(1, cancelCount, "onMessage cancels suspend");
browser.test.assertEq("bg_pong:ping", res, "onMessage result");
firstSuspendTestCompleted = true;
} else {
browser.test.assertTrue(firstSuspendTestCompleted, "First test done");
browser.test.assertEq(2, suspendCount, "Second onSuspend");
browser.test.sendMessage("FINAL_SUSPEND");
}
});
browser.test.log("Waiting for background to be suspended");
},
});
});
add_task(async function test_runtime_onConnect_cancels_suspend() {
await loadEventPageAndExtensionPage({
backgroundScript() {
// This background script registers listeners without calling any other
// extension API. This ensures that if the event page suspend is canceled,
// that it was intentionally done by the listener, and not as a side
// effect of an unrelated extension API call.
browser.runtime.onConnect.addListener(() => {
// Set by extensionPageScript before runtime.connect():
globalThis.notify_extensionPage_got_onConnect();
});
},
extensionPageScript() {
let cancelCount = 0;
browser.runtime.onSuspendCanceled.addListener(() => {
browser.test.assertEq(1, ++cancelCount, "onSuspendCanceled 1x");
});
let suspendCount = 0;
let firstSuspendTestCompleted = false;
let port; // Prevent port from being gc'd during test.
browser.runtime.onSuspend.addListener(async () => {
// We expect 2x suspend: first one we cancel, second one is final.
if (++suspendCount === 1) {
// First suspend attempt.
browser.test.assertEq(0, cancelCount, "Not suspended yet");
// Call runtime.connect() twice:
// 1. First connect() should be triggering the reset.
// 2. We are immediately notified when runtime.onConnect is called.
// 2. We call connect() again to have another page->parent->background
// roundtrip. This ensures that enough time to have been passed to
// allow the first runtime.onConnect handling to have finished,
// and to have triggeres onSuspendCanceled as desired.
for (let i = 0; i < 2; ++i) {
await new Promise(resolve => {
let bgGlobal = browser.extension.getBackgroundPage();
browser.test.assertTrue(!!bgGlobal, "Event page still running");
bgGlobal.notify_extensionPage_got_onConnect = resolve;
port = browser.runtime.connect({});
});
}
browser.test.assertEq(1, cancelCount, "onConnect cancels suspend");
firstSuspendTestCompleted = true;
} else {
browser.test.assertTrue(firstSuspendTestCompleted, "First test done");
browser.test.assertEq(2, suspendCount, "Second onSuspend");
browser.test.assertEq(null, port.error, "port has no error");
browser.test.sendMessage("FINAL_SUSPEND");
}
});
browser.test.log("Waiting for background to be suspended");
},
});
});
add_task(async function test_runtime_Port_onMessage_cancels_suspend() {
await loadEventPageAndExtensionPage({
backgroundScript() {
// This background script registers listeners without calling any other
// extension API. This ensures that if the event page suspend is canceled,
// that it was intentionally done by the listener, and not as a side
// effect of an unrelated extension API call.
browser.runtime.onConnect.addListener(port => {
port.onMessage.addListener(() => {
// Set by extensionPageScript before runtime.connect():
globalThis.notify_extensionPage_got_port_onMessage();
});
});
},
extensionPageScript() {
let cancelCount = 0;
browser.runtime.onSuspendCanceled.addListener(() => {
browser.test.assertEq(1, ++cancelCount, "onSuspendCanceled 1x");
});
let suspendCount = 0;
let firstSuspendTestCompleted = false;
let port;
browser.runtime.onSuspend.addListener(async () => {
// We expect 2x suspend: first one we cancel, second one is final.
if (++suspendCount === 1) {
// First suspend attempt.
browser.test.assertEq(0, cancelCount, "Not suspended yet");
browser.test.assertTrue(!!port, "Should run after we opened a port");
// Call port.postMessage() twice:
// 1. First port.postMessage() should be triggering the reset.
// 2. We are immediately notified when runtime.onMessage is called.
// 2. We postMessage() again to have another page->parent->background
// roundtrip. This ensures that enough time to have been passed to
// allow the first port.onMessage handling to have finished,
// and to have triggeres onSuspendCanceled as desired.
for (let i = 0; i < 2; ++i) {
await new Promise(resolve => {
let bgGlobal = browser.extension.getBackgroundPage();
browser.test.assertTrue(!!bgGlobal, "Event page still running");
bgGlobal.notify_extensionPage_got_port_onMessage = resolve;
port.postMessage("");
});
}
browser.test.assertEq(
1,
cancelCount,
"port.onMessage cancels suspend"
);
firstSuspendTestCompleted = true;
} else {
browser.test.assertTrue(firstSuspendTestCompleted, "First test done");
browser.test.assertEq(2, suspendCount, "Second onSuspend");
browser.test.sendMessage("FINAL_SUSPEND");
}
});
browser.runtime.getBackgroundPage(bgGlobal => {
browser.test.assertTrue(!!bgGlobal, "Event page has started");
// Since the event page has started, this should trigger onConnect in
// the event page. If somehow the event page has suspended in the
// meantime, then we will detect that in runtime.onSuspend (and fail).
port = browser.runtime.connect({});
// Assuming that runtime.onConnect in the event page has received the
// port and started listening, we should now wait for an attempt to
// suspend the event page (and try to cancel that via port.onMessage).
browser.test.log("Waiting for background to be suspended");
});
},
});
});