/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; requestLongerTimeout(10); /** * The recently closed tab list is populated on a per-window basis. * * By default, the withFirefoxView helper opens fx view in the current window. * This ensures that the add_new_tab, close_tab, * and open_then_close functions are creating sessionstore entries * associated with the correct window where the tests are run. */ ChromeUtils.defineESModuleGetters(globalThis, { SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", }); const RECENTLY_CLOSED_EVENT = [ ["firefoxview", "entered", "firefoxview", undefined], ["firefoxview", "recently_closed", "tabs", undefined], ]; const CLOSED_TABS_OPEN_EVENT = [ ["firefoxview", "closed_tabs_open", "tabs", "false"], ]; const RECENTLY_CLOSED_DISMISS_EVENT = [ ["firefoxview", "dismiss_closed_tab", "tabs", undefined], ]; async function add_new_tab(URL) { let tab = BrowserTestUtils.addTab(gBrowser, URL); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); return tab; } async function close_tab(tab) { const sessionStorePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab); BrowserTestUtils.removeTab(tab); await sessionStorePromise; } async function dismiss_tab(tab, content) { info(`Dismissing tab ${tab.dataset.targeturi}`); const closedObjectsChanged = () => TestUtils.topicObserved("sessionstore-closed-objects-changed"); let dismissButton = tab.querySelector(".closed-tab-li-dismiss"); EventUtils.synthesizeMouseAtCenter(dismissButton, {}, content); await closedObjectsChanged(); } add_setup(async function setup() { // set updateTimeMs to 0 to prevent unexpected/unrelated DOM mutations during testing await SpecialPowers.pushPrefEnv({ set: [["browser.tabs.firefox-view.updateTimeMs", 100000]], }); }); add_task(async function test_empty_list() { clearHistory(); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; let container = document.querySelector("#collapsible-tabs-container"); ok( container.classList.contains("empty-container"), "collapsible container should have correct styling when the list is empty" ); Assert.ok( document.getElementById("recently-closed-tabs-placeholder"), "The empty message is displayed." ); Assert.ok( !document.querySelector("ol.closed-tabs-list"), "The recently closed tabs list is not displayed." ); const tab1 = await add_new_tab(URLs[0]); await close_tab(tab1); // The UI update happens asynchronously as we learn of the new closed tab. await BrowserTestUtils.waitForMutationCondition( container, { attributeFilter: ["class"] }, () => !container.classList.contains("empty-container") ); ok( !container.classList.contains("empty-container"), "collapsible container should have correct styling when the list is not empty" ); Assert.ok( !document.getElementById("recently-closed-tabs-placeholder"), "The empty message is not displayed." ); Assert.ok( document.querySelector("ol.closed-tabs-list"), "The recently closed tabs list is displayed." ); is( document.querySelector("ol.closed-tabs-list").children.length, 1, "recently-closed-tabs-list should have one list item" ); }); }); add_task(async function test_list_ordering() { Services.obs.notifyObservers(null, "browser:purge-session-history"); is( SessionStore.getClosedTabCountForWindow(window), 0, "Closed tab count after purging session history" ); await clearAllParentTelemetryEvents(); const closedObjectsChanged = () => TestUtils.topicObserved("sessionstore-closed-objects-changed"); const tab1 = await add_new_tab(URLs[0]); const tab2 = await add_new_tab(URLs[1]); const tab3 = await add_new_tab(URLs[2]); gBrowser.selectedTab = tab3; await close_tab(tab3); await closedObjectsChanged(); await close_tab(tab2); await closedObjectsChanged(); await close_tab(tab1); await closedObjectsChanged(); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; const tabsList = document.querySelector("ol.closed-tabs-list"); await BrowserTestUtils.waitForMutationCondition( tabsList, { childList: true }, () => tabsList.children.length > 1 ); is( document.querySelector("ol.closed-tabs-list").children.length, 3, "recently-closed-tabs-list should have three list items" ); // check that the ordering is correct when user navigates to another tab, and then closes multiple tabs. ok( document .querySelector("ol.closed-tabs-list") .children[0].textContent.includes("mochi.test"), "first list item in recently-closed-tabs-list is in the correct order" ); ok( document .querySelector("ol.closed-tabs-list") .children[2].textContent.includes("example.net"), "last list item in recently-closed-tabs-list is in the correct order" ); let ele = document.querySelector("ol.closed-tabs-list").firstElementChild; let uri = ele.getAttribute("data-targeturi"); let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, uri); ele.querySelector(".closed-tab-li-main").click(); await newTabPromise; await TestUtils.waitForCondition( () => { let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ).parent; return events && events.length >= 2; }, "Waiting for entered and recently_closed firefoxview telemetry events.", 200, 100 ); TelemetryTestUtils.assertEvents( RECENTLY_CLOSED_EVENT, { category: "firefoxview" }, { clear: true, process: "parent" } ); gBrowser.removeTab(gBrowser.selectedTab); await clearAllParentTelemetryEvents(); await waitForElementVisible( browser, "#recently-closed-tabs-container > summary" ); document.querySelector("#recently-closed-tabs-container > summary").click(); await TestUtils.waitForCondition( () => { let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ).parent; return events && events.length >= 1; }, "Waiting for closed_tabs_open firefoxview telemetry event.", 200, 100 ); TelemetryTestUtils.assertEvents( CLOSED_TABS_OPEN_EVENT, { category: "firefoxview" }, { clear: true, process: "parent" } ); }); }); add_task(async function test_max_list_items() { Services.obs.notifyObservers(null, "browser:purge-session-history"); is( SessionStore.getClosedTabCountForWindow(window), 0, "Closed tab count after purging session history" ); await open_then_close(URLs[0]); await open_then_close(URLs[1]); await open_then_close(URLs[2]); // Seed the closed tabs count. We've assured that we've opened and // closed at least three tabs because of the calls to open_then_close // above. let mockMaxTabsLength = 3; await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; // override this value for testing purposes document.querySelector("recently-closed-tabs-list").maxTabsLength = mockMaxTabsLength; ok( !document .querySelector("#collapsible-tabs-container") .classList.contains("empty-container"), "collapsible container should have correct styling when the list is not empty" ); Assert.ok( !document.getElementById("recently-closed-tabs-placeholder"), "The empty message is not displayed." ); Assert.ok( document.querySelector("ol.closed-tabs-list"), "The recently closed tabs list is displayed." ); is( document.querySelector("ol.closed-tabs-list").children.length, mockMaxTabsLength, `recently-closed-tabs-list should have ${mockMaxTabsLength} list items` ); const closedObjectsChanged = TestUtils.topicObserved( "sessionstore-closed-objects-changed" ); // add another tab const tab = await add_new_tab(URLs[3]); await close_tab(tab); await closedObjectsChanged; let firstListItem = document.querySelector("ol.closed-tabs-list") .children[0]; await BrowserTestUtils.waitForMutationCondition( firstListItem, { characterData: true, childList: true, subtree: true }, () => firstListItem.textContent.includes(".org") ); ok( firstListItem.textContent.includes("example.org"), "first list item in recently-closed-tabs-list should have been updated" ); is( document.querySelector("ol.closed-tabs-list").children.length, mockMaxTabsLength, `recently-closed-tabs-list should still have ${mockMaxTabsLength} list items` ); }); }); add_task(async function test_time_updates_correctly() { clearHistory(); is( SessionStore.getClosedTabCountForWindow(window), 0, "Closed tab count after purging session history" ); // Set the closed tabs state to include one tab that was closed 2 seconds ago. // This is well below the initial threshold for displaying the 'Just now' timestamp. // It is also much greater than the 5ms threshold we use for the updated pref value, // which results in the timestamp text changing after the pref value is changed. const TAB_CLOSED_AGO_MS = 2000; const TAB_UPDATE_TIME_MS = 5; const TAB_CLOSED_STATE = { windows: [ { tabs: [{ entries: [] }], _closedTabs: [ { state: { entries: [{ url: "https://www.example.com/" }] }, closedId: 0, closedAt: Date.now() - TAB_CLOSED_AGO_MS, image: null, title: "Example", }, ], }, ], }; await SessionStore.setBrowserState(JSON.stringify(TAB_CLOSED_STATE)); is( SessionStore.getClosedTabCountForWindow(window), 1, "Closed tab count after setting browser state" ); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; const tabsList = document.querySelector("ol.closed-tabs-list"); const numOfListItems = tabsList.children.length; const lastListItem = tabsList.children[numOfListItems - 1]; const timeLabel = lastListItem.querySelector("span.closed-tab-li-time"); let initialTimeText = timeLabel.textContent; Assert.stringContains( initialTimeText, "Just now", "recently-closed-tabs list item time is 'Just now'" ); await SpecialPowers.pushPrefEnv({ set: [["browser.tabs.firefox-view.updateTimeMs", TAB_UPDATE_TIME_MS]], }); await BrowserTestUtils.waitForMutationCondition( timeLabel, { childList: true }, () => !timeLabel.textContent.includes("now") ); isnot( timeLabel.textContent, initialTimeText, "recently-closed-tabs list item time has updated" ); await SpecialPowers.popPrefEnv(); }); // Cleanup recently closed tab data. clearHistory(); }); add_task(async function test_list_maintains_focus_when_restoring_tab() { await SpecialPowers.clearUserPref(RECENTLY_CLOSED_STATE_PREF); Services.obs.notifyObservers(null, "browser:purge-session-history"); is( SessionStore.getClosedTabCountForWindow(window), 0, "Closed tab count after purging session history" ); const sandbox = sinon.createSandbox(); let setupCompleteStub = sandbox.stub( TabsSetupFlowManager, "isTabSyncSetupComplete" ); setupCompleteStub.returns(true); await open_then_close(URLs[0]); await open_then_close(URLs[1]); await open_then_close(URLs[2]); await withFirefoxView({}, async browser => { let gBrowser = browser.getTabBrowser(); const { document } = browser.contentWindow; const list = document.querySelectorAll(".closed-tab-li"); list[0].querySelector(".closed-tab-li-main").focus(); EventUtils.synthesizeKey("KEY_Enter"); let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View"); await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab); Assert.ok( document.activeElement.textContent.includes("mochitest index"), "Focus should be on the first item in the recently closed list" ); }); // clean up extra tabs while (gBrowser.tabs.length > 1) { BrowserTestUtils.removeTab(gBrowser.tabs.at(-1)); } clearHistory(); await open_then_close(URLs[2]); await withFirefoxView({}, async browser => { let gBrowser = browser.getTabBrowser(); const { document } = browser.contentWindow; let expectedFocusedElement = document.getElementById( "recently-closed-tabs-header-section" ); const list = document.querySelectorAll(".closed-tab-li"); list[0].querySelector(".closed-tab-li-main").focus(); EventUtils.synthesizeKey("KEY_Enter"); let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View"); await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab); is( document.activeElement, expectedFocusedElement, "Focus should be on the section header" ); }); // clean up extra tabs while (gBrowser.tabs.length > 1) { BrowserTestUtils.removeTab(gBrowser.tabs.at(-1)); } }); add_task(async function test_switch_before_closing() { clearHistory(); const INITIAL_URL = "https://example.org/iwilldisappear"; const FINAL_URL = "https://example.com/ishouldappear"; await withFirefoxView({}, async function (browser) { let gBrowser = browser.getTabBrowser(); let newTab = await BrowserTestUtils.openNewForegroundTab( gBrowser, INITIAL_URL ); // Switch back to FxView: await BrowserTestUtils.switchTab( gBrowser, gBrowser.getTabForBrowser(browser) ); // Update the tab we opened to a different site: let loadPromise = BrowserTestUtils.browserLoaded( newTab.linkedBrowser, null, FINAL_URL ); BrowserTestUtils.loadURIString(newTab.linkedBrowser, FINAL_URL); await loadPromise; // Close the added tab BrowserTestUtils.removeTab(newTab); const { document } = browser.contentWindow; await BrowserTestUtils.waitForMutationCondition( document.querySelector("recently-closed-tabs-list"), { childList: true, subtree: true }, () => document.querySelector("ol.closed-tabs-list") ); const tabsList = document.querySelector("ol.closed-tabs-list"); info("A tab appeared in the list, ensure it has the right URL."); let urlBit = tabsList.children[0].querySelector(".closed-tab-li-url"); await BrowserTestUtils.waitForMutationCondition( urlBit, { characterData: true, attributeFilter: ["title"] }, () => urlBit.textContent.includes(".com") ); Assert.ok( urlBit.textContent.includes("example.com"), "Item should end up with the correct URL." ); }); }); add_task(async function test_alt_click_no_launch() { Services.obs.notifyObservers(null, "browser:purge-session-history"); is( SessionStore.getClosedTabCountForWindow(window), 0, "Closed tab count after purging session history" ); await open_then_close(URLs[0]); await withFirefoxView({}, async browser => { let gBrowser = browser.getTabBrowser(); let originalTabsLength = gBrowser.tabs.length; await BrowserTestUtils.synthesizeMouseAtCenter( ".closed-tab-li .closed-tab-li-main", { altKey: true }, browser ); is( gBrowser.tabs.length, originalTabsLength, `Opened tabs length should still be ${originalTabsLength}` ); }); }); /** * Asserts that tabs that have been recently closed can be * restored by clicking on them, using the Enter key, * and using the Space bar. */ add_task(async function test_restore_recently_closed_tabs() { clearHistory(); await open_then_close(URLs[0]); await open_then_close(URLs[1]); await open_then_close(URLs[2]); await EventUtils.synthesizeMouseAtCenter( gBrowser.ownerDocument.getElementById("firefox-view-button"), { type: "mousedown" }, window ); // Wait for Firefox View to be loaded before interacting // with the page. await BrowserTestUtils.browserLoaded( window.FirefoxViewHandler.tab.linkedBrowser ); let { document } = gBrowser.contentWindow; let tabRestored = BrowserTestUtils.waitForNewTab(gBrowser, URLs[2]); EventUtils.synthesizeMouseAtCenter( document.querySelector(".closed-tab-li"), {}, gBrowser.contentWindow ); await tabRestored; ok(true, "Tab was restored by mouse click"); await EventUtils.synthesizeMouseAtCenter( gBrowser.ownerDocument.getElementById("firefox-view-button"), { type: "mousedown" }, window ); tabRestored = BrowserTestUtils.waitForNewTab(gBrowser, URLs[1]); document.querySelector(".closed-tab-li .closed-tab-li-main").focus(); EventUtils.synthesizeKey("KEY_Enter", {}, gBrowser.contentWindow); await tabRestored; ok(true, "Tab was restored by using the Enter key"); // clean up extra tabs while (gBrowser.tabs.length > 1) { BrowserTestUtils.removeTab(gBrowser.tabs.at(-1)); } }); /** * Asserts that tabs are removed from Recently Closed tabs in * Fx View when tabs are removed from latest closed tab data. * Ex: Selecting "Reopen Closed Tab" from the tabs toolbar * context menu */ add_task(async function test_reopen_recently_closed_tabs() { clearHistory(); await open_then_close(URLs[0]); await open_then_close(URLs[1]); await open_then_close(URLs[2]); await EventUtils.synthesizeMouseAtCenter( gBrowser.ownerDocument.getElementById("firefox-view-button"), { type: "mousedown" }, window ); // Wait for Firefox View to be loaded before interacting // with the page. await BrowserTestUtils.browserLoaded( window.FirefoxViewHandler.tab.linkedBrowser ); let { document } = gBrowser.contentWindow; let tabReopened = BrowserTestUtils.waitForNewTab(gBrowser, URLs[2]); SessionStore.undoCloseTab(window); await tabReopened; const tabsList = document.querySelector("ol.closed-tabs-list"); await EventUtils.synthesizeMouseAtCenter( gBrowser.ownerDocument.getElementById("firefox-view-button"), { type: "mousedown" }, window ); await BrowserTestUtils.waitForMutationCondition( tabsList, { childList: true }, () => tabsList.children.length === 2 ); Assert.equal( tabsList.children[0].dataset.targeturi, URLs[1], `First recently closed item should be ${URLs[1]}` ); await close_tab(gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1]); await BrowserTestUtils.waitForMutationCondition( tabsList, { childList: true }, () => tabsList.children.length === 3 ); Assert.equal( tabsList.children[0].dataset.targeturi, URLs[2], `First recently closed item should be ${URLs[2]}` ); await dismiss_tab(tabsList.children[0], content); await BrowserTestUtils.waitForMutationCondition( tabsList, { childList: true }, () => tabsList.children.length === 2 ); Assert.equal( tabsList.children[0].dataset.targeturi, URLs[1], `First recently closed item should be ${URLs[1]}` ); const contextMenu = gBrowser.ownerDocument.getElementById( "contentAreaContextMenu" ); const promisePopup = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); EventUtils.synthesizeMouseAtCenter( tabsList.querySelector(".closed-tab-li-title"), { button: 2, type: "contextmenu", }, gBrowser.contentWindow ); await promisePopup; const promiseNewTab = BrowserTestUtils.waitForNewTab(gBrowser, URLs[1]); contextMenu.activateItem( gBrowser.ownerDocument.getElementById("context-openlinkintab") ); await promiseNewTab; await BrowserTestUtils.waitForMutationCondition( tabsList, { childList: true }, () => tabsList.children.length === 1 ); Assert.equal( tabsList.children[0].dataset.targeturi, URLs[0], `First recently closed item should be ${URLs[0]}` ); // clean up extra tabs while (gBrowser.tabs.length > 1) { BrowserTestUtils.removeTab(gBrowser.tabs.at(-1)); } }); /** * Asserts that tabs that have been recently closed can be * dismissed by clicking on their respective dismiss buttons. */ add_task(async function test_dismiss_tab() { const TAB_UPDATE_TIME_MS = 5; Services.obs.notifyObservers(null, "browser:purge-session-history"); Assert.equal( SessionStore.getClosedTabCountForWindow(window), 0, "Closed tab count after purging session history" ); await clearAllParentTelemetryEvents(); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; const closedObjectsChanged = () => TestUtils.topicObserved("sessionstore-closed-objects-changed"); const tab1 = await add_new_tab(URLs[0]); const tab2 = await add_new_tab(URLs[1]); const tab3 = await add_new_tab(URLs[2]); await close_tab(tab3); await closedObjectsChanged(); await close_tab(tab2); await closedObjectsChanged(); await close_tab(tab1); await closedObjectsChanged(); await clearAllParentTelemetryEvents(); const tabsList = document.querySelector("ol.closed-tabs-list"); const numOfListItems = tabsList.children.length; const lastListItem = tabsList.children[numOfListItems - 1]; const timeLabel = lastListItem.querySelector("span.closed-tab-li-time"); let initialTimeText = timeLabel.textContent; Assert.stringContains( initialTimeText, "Just now", "recently-closed-tabs list item time is 'Just now'" ); await SpecialPowers.pushPrefEnv({ set: [["browser.tabs.firefox-view.updateTimeMs", TAB_UPDATE_TIME_MS]], }); await BrowserTestUtils.waitForMutationCondition( timeLabel, { childList: true }, () => !timeLabel.textContent.includes("Just now") ); isnot( timeLabel.textContent, initialTimeText, "recently-closed-tabs list item time has updated" ); await dismiss_tab(tabsList.children[0], content); await TestUtils.waitForCondition( () => { let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ).parent; return events && events.length >= 1; }, "Waiting for dismiss_closed_tab firefoxview telemetry event.", 200, 100 ); TelemetryTestUtils.assertEvents( RECENTLY_CLOSED_DISMISS_EVENT, { category: "firefoxview" }, { clear: true, process: "parent" } ); Assert.equal( tabsList.children[0].dataset.targeturi, URLs[1], `First recently closed item should be ${URLs[1]}` ); Assert.equal( tabsList.children.length, 2, "recently-closed-tabs-list should have two list items" ); await clearAllParentTelemetryEvents(); await dismiss_tab(tabsList.children[0], content); await TestUtils.waitForCondition( () => { let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ).parent; return events && events.length >= 1; }, "Waiting for dismiss_closed_tab firefoxview telemetry event.", 200, 100 ); TelemetryTestUtils.assertEvents( RECENTLY_CLOSED_DISMISS_EVENT, { category: "firefoxview" }, { clear: true, process: "parent" } ); Assert.equal( tabsList.children[0].dataset.targeturi, URLs[2], `First recently closed item should be ${URLs[2]}` ); Assert.equal( tabsList.children.length, 1, "recently-closed-tabs-list should have one list item" ); await clearAllParentTelemetryEvents(); await dismiss_tab(tabsList.children[0], content); await TestUtils.waitForCondition( () => { let events = Services.telemetry.snapshotEvents( Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, false ).parent; return events && events.length >= 1; }, "Waiting for dismiss_closed_tab firefoxview telemetry event.", 200, 100 ); TelemetryTestUtils.assertEvents( RECENTLY_CLOSED_DISMISS_EVENT, { category: "firefoxview" }, { clear: true, process: "parent" } ); Assert.ok( document.getElementById("recently-closed-tabs-placeholder"), "The empty message is displayed." ); Assert.ok( !document.querySelector("ol.closed-tabs-list"), "The recently closed tabs list is not displayed." ); await SpecialPowers.popPrefEnv(); }); }); /** * Asserts that the actionable part of each list item is role="button". * Discussion on why we want a button role can be seen here: * https://bugzilla.mozilla.org/show_bug.cgi?id=1789875#c1 */ add_task(async function test_button_role() { Services.obs.notifyObservers(null, "browser:purge-session-history"); Assert.equal( SessionStore.getClosedTabCountForWindow(window), 0, "Closed tab count after purging session history" ); await withFirefoxView({}, async browser => { const { document } = browser.contentWindow; clearHistory(); await open_then_close(URLs[0]); await open_then_close(URLs[1]); await open_then_close(URLs[2]); await EventUtils.synthesizeMouseAtCenter( gBrowser.ownerDocument.getElementById("firefox-view-button"), { type: "mousedown" }, window ); const tabsList = document.querySelector("ol.closed-tabs-list"); Array.from(tabsList.children).forEach(tabItem => { let actionableElement = tabItem.querySelector(".closed-tab-li-main"); Assert.ok(actionableElement.getAttribute("role"), "button"); }); }); });