"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( "resource://testing-common/ExtensionTestCommon.sys.mjs" ); 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": ``, "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"); }); }, }); });