/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * Test tabmail behaviour when tabs close. */ "use strict"; var { assert_selected_tab, be_in_folder, collapse_all_threads, create_folder, make_display_threaded, make_message_sets_in_folders, mc, open_selected_message_in_new_tab, open_selected_messages, select_click_row, switch_tab, } = ChromeUtils.import( "resource://testing-common/mozmill/FolderDisplayHelpers.jsm" ); var gFolder; var MSGS_PER_THREAD = 3; add_setup(async function () { gFolder = await create_folder("test-tabmail-closing folder"); await make_message_sets_in_folders( [gFolder], [{ msgsPerThread: MSGS_PER_THREAD }] ); }); /** * Test that if we open up a message in a tab from the inbox tab, that * if we immediately close that tab, we switch back to the inbox tab. */ add_task(async function test_closed_single_message_tab_returns_to_inbox() { await be_in_folder(gFolder); make_display_threaded(); let inboxTab = mc.window.document.getElementById("tabmail").currentTabInfo; select_click_row(0); // Open a message in a new tab... await open_selected_message_in_new_tab(false); // Open a second message in a new tab... await switch_tab(0); select_click_row(1); await open_selected_message_in_new_tab(false); // Close the second tab mc.window.document.getElementById("tabmail").closeTab(2); // We should have gone back to the inbox tab assert_selected_tab(inboxTab); // Close the first tab mc.window.document.getElementById("tabmail").closeTab(1); }); /** * Test that if we open up some message tabs from the inbox tab, and then * switch around in those tabs, closing the tabs doesn't immediately jump * you back to the inbox tab. */ add_task(async function test_does_not_go_to_opener_if_switched() { await be_in_folder(gFolder); make_display_threaded(); select_click_row(0); // Open a message in a new tab... await open_selected_message_in_new_tab(false); // Open a second message in a new tab... await switch_tab(0); select_click_row(1); await open_selected_message_in_new_tab(false); // Switch to the first tab await switch_tab(1); let firstTab = mc.window.document.getElementById("tabmail").currentTabInfo; // Switch back to the second tab await switch_tab(2); // Close the second tab mc.window.document.getElementById("tabmail").closeTab(2); // We should have gone back to the second tab assert_selected_tab(firstTab); // Close the first tab mc.window.document.getElementById("tabmail").closeTab(1); }); /** * Test that if we open a whole thread up in message tabs, closing * the last message tab takes us to the second last message tab as opposed * to the inbox tab. */ add_task(async function test_opening_thread_in_tabs_closing_behaviour() { await be_in_folder(gFolder); make_display_threaded(); collapse_all_threads(); // Open a thread as a series of message tabs. select_click_row(0); open_selected_messages(mc); // At this point, the last message tab should be selected already. We // close that tab, and the second last message tab should be selected. // We should close that tab, and the third last tab should be selected, // etc. for (let i = MSGS_PER_THREAD; i > 0; --i) { let previousTab = mc.window.document .getElementById("tabmail") .tabContainer.getItemAtIndex(i - 1); mc.window.document.getElementById("tabmail").closeTab(i); Assert.equal( previousTab, mc.window.document.getElementById("tabmail").tabContainer.selectedItem, "Expected tab at index " + (i - 1) + " to be selected." ); } }).skip(); /** * @typedef {object} TestTab * @property {Element} node - The tab's DOM node. * @property {number} index - The tab's index. * @property {object} info - The tabInfo for this tab, as used in #tabmail. */ /** * Open some message tabs in the background from the folder tab. * * @param {number} numAdd - The number of tabs to add. * * @param {TestTab[]} An array of tab objects corresponding to all the open * tabs. */ async function openTabs(numAdd) { await be_in_folder(gFolder); select_click_row(0); for (let i = 0; i < numAdd; i++) { await open_selected_message_in_new_tab(true); } let tabs = mc.window.document .getElementById("tabmail") .tabInfo.map((info, index) => { return { info, index, node: info.tabNode, }; }); Assert.equal(tabs.length, numAdd + 1, "Have expected number of tabs"); return tabs; } /** * Assert that a tab is closed. * * @param {TestTab} fromTab - The tab to close from. * @param {Function} closeMethod - The (async) method to call on fromTab.node in * order to perform the tab close. * @param {TestTab} switchToTab - The tab we expect to switch to after closing * tab. * @param {TestTab[]} [closingTabs] - The tabs we expect to close after calling * the closeMethod. This is just fromTab by default. */ async function assertClose(fromTab, closeMethod, switchToTab, closingTabs) { let desc; if (closingTabs) { let closingIndices = closingTabs.map(t => t.index).join(","); desc = `closing tab #${closingIndices} using tab #${fromTab.index}`; } else { closingTabs = [fromTab]; desc = `closing tab #${fromTab.index}`; } let numTabsBefore = mc.window.document.getElementById("tabmail").tabInfo.length; for (let tab of closingTabs) { Assert.ok( tab.node.parentNode, `tab #${tab.index} should be in the DOM tree before ${desc}` ); } fromTab.node.scrollIntoView(); await closeMethod(fromTab.node); for (let tab of closingTabs) { Assert.ok( !tab.node.parentNode, `tab #${tab.index} should be removed from the DOM tree after ${desc}` ); } Assert.equal( mc.window.document.getElementById("tabmail").tabInfo.length, numTabsBefore - closingTabs.length, `Number of tabs after ${desc}` ); assert_selected_tab( switchToTab.info, `tab #${switchToTab.index} is selected after ${desc}` ); } /** * Close a tab using its close button. * * @param {Element} tab - The tab to close. */ function closeWithButton(tab) { EventUtils.synthesizeMouseAtCenter( tab.querySelector(".tab-close-button"), {}, tab.ownerGlobal ); } /** * Close a tab using a middle mouse click. * * @param {Element} tab - The tab to close. */ function closeWithMiddleClick(tab) { EventUtils.synthesizeMouseAtCenter(tab, { button: 1 }, tab.ownerGlobal); } /** * Close the currently selected tab. * * @param {Element} tab - The tab to close. */ function closeWithKeyboard(tab) { if (AppConstants.platform == "macosx") { EventUtils.synthesizeKey("w", { accelKey: true }, tab.ownerGlobal); } else { EventUtils.synthesizeKey("w", { ctrlKey: true }, tab.ownerGlobal); } } /** * Open the context menu of a tab. * * @param {Element} tab - The tab to open the context menu of. */ async function openContextMenu(tab) { let win = tab.ownerGlobal; let contextMenu = win.document.getElementById("tabContextMenu"); let shownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); EventUtils.synthesizeMouseAtCenter( tab, { type: "contextmenu", button: 2 }, win ); await shownPromise; } /** * Close the context menu, without selecting anything. * * @param {Element} tab - The tab to close the context menu of. */ async function closeContextMenu(tab) { let win = tab.ownerGlobal; let contextMenu = win.document.getElementById("tabContextMenu"); let hiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden"); contextMenu.hidePopup(); await hiddenPromise; } /** * Open a tab's context menu and select an item. * * @param {Element} tab - The tab to open the context menu on. * @param {string} itemId - The id of the menu item to select. */ async function selectFromContextMenu(tab, itemId) { let doc = tab.ownerDocument; let contextMenu = doc.getElementById("tabContextMenu"); let item = doc.getElementById(itemId); await openContextMenu(tab); let hiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden"); contextMenu.activateItem(item); await hiddenPromise; } /** * Close a tab using its context menu. * * @param {Element} tab - The tab to close. */ async function closeWithContextMenu(tab) { await selectFromContextMenu(tab, "tabContextMenuClose"); } /** * Close all other tabs using a tab's context menu. * * @param {Element} tab - The tab to not close. */ async function closeOtherTabsWithContextMenu(tab) { await selectFromContextMenu(tab, "tabContextMenuCloseOtherTabs"); } /** * Test closing unselected tabs with the mouse or keyboard. */ add_task(async function test_close_unselected_tab_methods() { let tabs = await openTabs(3); // Can't close the first tab. Assert.ok( BrowserTestUtils.is_hidden(tabs[0].node.querySelector(".tab-close-button")), "Close button should be hidden for the first tab" ); // Middle click does nothing. closeWithMiddleClick(tabs[0].node); assert_selected_tab(tabs[0].info); // Keyboard shortcut does nothing. closeWithKeyboard(tabs[0].node); assert_selected_tab(tabs[0].info); // Context close item is disabled. await openContextMenu(tabs[0].node); Assert.ok( mc.window.document.getElementById("tabContextMenuClose").disabled, "Close context menu item should be disabled for the first tab" ); await closeContextMenu(tabs[0].node); // Close unselected tabs. The selected tab should stay the same. await assertClose(tabs[3], closeWithButton, tabs[0]); await assertClose(tabs[1], closeWithMiddleClick, tabs[0]); await assertClose(tabs[2], closeWithContextMenu, tabs[0]); // Keyboard shortcut cannot be used to close an unselected tab. }); /** * Test closing selected tabs with the mouse or keyboard. */ add_task(async function test_close_selected_tab_methods() { let tabs = await openTabs(4); // Select tab by clicking it. EventUtils.synthesizeMouseAtCenter(tabs[4].node, {}, mc.window); assert_selected_tab(tabs[4].info); await assertClose(tabs[4], closeWithButton, tabs[3]); // Select tab #2 by clicking tab #3 and using the shortcut to go back. EventUtils.synthesizeMouseAtCenter(tabs[3].node, {}, mc.window); assert_selected_tab(tabs[3].info); EventUtils.synthesizeKey( "VK_TAB", { ctrlKey: true, shiftKey: true }, mc.window ); assert_selected_tab(tabs[2].info); await assertClose(tabs[2], closeWithKeyboard, tabs[3]); // Note: Current open tabs is: #0, #1, #2, #3. // Select tab #1 by using the shortcut to go forward from tab #0. EventUtils.synthesizeMouseAtCenter(tabs[0].node, {}, mc.window); assert_selected_tab(tabs[0].info); EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true }, mc.window); assert_selected_tab(tabs[1].info); await assertClose(tabs[1], closeWithMiddleClick, tabs[3]); // Note: Current open tabs is: #0, #3. // Close tabs #3 using the context menu. await assertClose(tabs[3], closeWithContextMenu, tabs[0]); }); /** * Test closing other tabs with the context menu. */ add_task(async function test_close_other_tabs() { let tabs = await openTabs(3); EventUtils.synthesizeMouseAtCenter(tabs[3].node, {}, mc.window); assert_selected_tab(tabs[3].info); // Close tabs #1 and #2 using the context menu of #3. await assertClose(tabs[3], closeOtherTabsWithContextMenu, tabs[3], [ tabs[1], tabs[2], ]); // Note: Current open tabs is: #0 #3. // The tab #3 closeOtherItem is now disabled since only tab #0 is left, which // cannot be closed. await openContextMenu(tabs[3].node); Assert.ok( mc.window.document.getElementById("tabContextMenuCloseOtherTabs").disabled, "Close context menu item should be disabled for the first tab" ); await closeContextMenu(tabs[3].node); // But we can close tab #3 using tab #0 context menu. await assertClose(tabs[0], closeOtherTabsWithContextMenu, tabs[0], [tabs[3]]); });