"use strict"; /** * These tests the behaviour of the browser when background tabs crash, * while the foreground tab remains. * * The current behavioural rule is this: if only background tabs crash, * then only the first tab shown of that group should show the tab crash * page, and subsequent ones should restore on demand. */ /** * Makes the current browser tab non-remote, and then sets up two remote * background tabs, ensuring that both belong to the same content process. * Callers should pass in a testing function that will execute (and possibly * yield Promises) taking the created background tabs as arguments. Once * the testing function completes, this function will take care of closing * the opened tabs. * * @param testFn (function) * A Promise-generating function that will be called once the tabs * are opened and ready. * @return Promise * Resolves once the testing function completes and the opened tabs * have been completely closed. */ async function setupBackgroundTabs(testFn) { const REMOTE_PAGE = "http://www.example.com"; const NON_REMOTE_PAGE = "about:mozilla"; // Browse the initial tab to a non-remote page, which we'll have in the // foreground. let initialTab = gBrowser.selectedTab; let initialBrowser = initialTab.linkedBrowser; BrowserTestUtils.loadURIString(initialBrowser, NON_REMOTE_PAGE); await BrowserTestUtils.browserLoaded(initialBrowser); // Quick sanity check - the browser should be non remote. Assert.ok( !initialBrowser.isRemoteBrowser, "Initial browser should not be remote." ); // Open some tabs that should be running in the content process. let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REMOTE_PAGE); let remoteBrowser1 = tab1.linkedBrowser; await TabStateFlusher.flush(remoteBrowser1); let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REMOTE_PAGE); let remoteBrowser2 = tab2.linkedBrowser; await TabStateFlusher.flush(remoteBrowser2); // Quick sanity check - the two browsers should be remote and share the // same childID, or else this test is not going to work. Assert.ok( remoteBrowser1.isRemoteBrowser, "Browser should be remote in order to crash." ); Assert.ok( remoteBrowser2.isRemoteBrowser, "Browser should be remote in order to crash." ); Assert.equal( remoteBrowser1.frameLoader.childID, remoteBrowser2.frameLoader.childID, "Both remote browsers should share the same content process." ); // Now switch back to the non-remote browser... await BrowserTestUtils.switchTab(gBrowser, initialTab); await testFn([tab1, tab2]); BrowserTestUtils.removeTab(tab1); BrowserTestUtils.removeTab(tab2); } /** * Takes some set of background tabs that are assumed to all belong to * the same content process, and crashes them. * * @param tabs (Array()) * The tabs to crash. * @return Promise * Resolves once the tabs have crashed and entered the pending * background state. */ async function crashBackgroundTabs(tabs) { Assert.ok(!!tabs.length, "Need to crash at least one tab."); for (let tab of tabs) { Assert.ok(tab.linkedBrowser.isRemoteBrowser, "tab is remote"); } let remotenessChangePromises = tabs.map(t => { return BrowserTestUtils.waitForEvent(t, "TabRemotenessChange"); }); let tabsRevived = tabs.map(t => { return promiseTabRestoring(t); }); await BrowserTestUtils.crashFrame(tabs[0].linkedBrowser, false); await Promise.all(remotenessChangePromises); await Promise.all(tabsRevived); // Both background tabs should now be in the pending restore // state. for (let tab of tabs) { Assert.ok(!tab.linkedBrowser.isRemoteBrowser, "tab is not remote"); Assert.ok(!tab.linkedBrowser.hasAttribute("crashed"), "tab is not crashed"); Assert.ok(tab.hasAttribute("pending"), "tab is pending"); } } add_setup(async function () { // We'll simplify by making sure we only ever one content process for this // test. await SpecialPowers.pushPrefEnv({ set: [ ["dom.ipc.processCount", 1], ["dom.ipc.processCount.webIsolated", 1], ], }); // On debug builds, crashing tabs results in much thinking, which // slows down the test and results in intermittent test timeouts, // so we'll pump up the expected timeout for this test. requestLongerTimeout(5); }); /** * Tests that if a content process crashes taking down only * background tabs, then the first of those tabs that the user * selects will show the tab crash page, but the rest will restore * on demand. */ add_task(async function test_background_crash_simple() { await setupBackgroundTabs(async function ([tab1, tab2]) { // Let's crash one of those background tabs now... await crashBackgroundTabs([tab1, tab2]); // Selecting the first tab should now send it to the tab crashed page. let tabCrashedPagePromise = BrowserTestUtils.waitForContentEvent( tab1.linkedBrowser, "AboutTabCrashedReady", false, null, true ); await BrowserTestUtils.switchTab(gBrowser, tab1); await tabCrashedPagePromise; // Selecting the second tab should restore it. let tabRestored = promiseTabRestored(tab2); await BrowserTestUtils.switchTab(gBrowser, tab2); await tabRestored; }); }); /** * Tests that if a content process crashes taking down only * background tabs, and the user is configured to send backlogged * crash reports automatically, that the tab crashed page is not * shown. */ add_task(async function test_background_crash_autosubmit_backlogged() { await SpecialPowers.pushPrefEnv({ set: [["browser.crashReports.unsubmittedCheck.autoSubmit2", true]], }); await setupBackgroundTabs(async function ([tab1, tab2]) { // Let's crash one of those background tabs now... await crashBackgroundTabs([tab1, tab2]); // Selecting the first tab should restore it. let tabRestored = promiseTabRestored(tab1); await BrowserTestUtils.switchTab(gBrowser, tab1); await tabRestored; // Selecting the second tab should restore it. tabRestored = promiseTabRestored(tab2); await BrowserTestUtils.switchTab(gBrowser, tab2); await tabRestored; }); await SpecialPowers.popPrefEnv(); }); /** * Tests that if there are two background tab crashes in a row, that * the two sets of background crashes don't interfere with one another. * * Specifically, if we start with two background tabs (1, 2) which crash, * and we visit 1, 1 should go to the tab crashed page. If we then have * two new background tabs (3, 4) crash, visiting 2 should still restore. * Visiting 4 should show us the tab crashed page, and then visiting 3 * should restore. */ add_task(async function test_background_crash_multiple() { let initialTab = gBrowser.selectedTab; await setupBackgroundTabs(async function ([tab1, tab2]) { // Let's crash one of those background tabs now... await crashBackgroundTabs([tab1, tab2]); // Selecting the first tab should now send it to the tab crashed page. let tabCrashedPagePromise = BrowserTestUtils.waitForContentEvent( tab1.linkedBrowser, "AboutTabCrashedReady", false, null, true ); await BrowserTestUtils.switchTab(gBrowser, tab1); await tabCrashedPagePromise; // Now switch back to the original non-remote tab... await BrowserTestUtils.switchTab(gBrowser, initialTab); await setupBackgroundTabs(async function ([tab3, tab4]) { await crashBackgroundTabs([tab3, tab4]); // Selecting the second tab should restore it. let tabRestored = promiseTabRestored(tab2); await BrowserTestUtils.switchTab(gBrowser, tab2); await tabRestored; // Selecting the fourth tab should now send it to the tab crashed page. tabCrashedPagePromise = BrowserTestUtils.waitForContentEvent( tab4.linkedBrowser, "AboutTabCrashedReady", false, null, true ); await BrowserTestUtils.switchTab(gBrowser, tab4); await tabCrashedPagePromise; // Selecting the third tab should restore it. tabRestored = promiseTabRestored(tab3); await BrowserTestUtils.switchTab(gBrowser, tab3); await tabRestored; }); }); }); // Tests that crashed preloaded tabs are removed and no unexpected errors are // thrown. add_task(async function test_preload_crash() { if (!Services.prefs.getBoolPref("browser.newtab.preload")) { return; } // Release any existing preloaded browser NewTabPagePreloading.removePreloadedBrowser(window); // Create a fresh preloaded browser await BrowserTestUtils.maybeCreatePreloadedBrowser(gBrowser); await BrowserTestUtils.crashFrame(gBrowser.preloadedBrowser, false); Assert.ok(!gBrowser.preloadedBrowser); });