diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/base/content/test/tabs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/base/content/test/tabs')
144 files changed, 14487 insertions, 0 deletions
diff --git a/browser/base/content/test/tabs/204.sjs b/browser/base/content/test/tabs/204.sjs new file mode 100644 index 0000000000..22b1d300e3 --- /dev/null +++ b/browser/base/content/test/tabs/204.sjs @@ -0,0 +1,3 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 204, "No Content"); +} diff --git a/browser/base/content/test/tabs/blank.html b/browser/base/content/test/tabs/blank.html new file mode 100644 index 0000000000..bcc2e389b8 --- /dev/null +++ b/browser/base/content/test/tabs/blank.html @@ -0,0 +1,2 @@ +<!doctype html> +This page intentionally left blank. diff --git a/browser/base/content/test/tabs/browser.toml b/browser/base/content/test/tabs/browser.toml new file mode 100644 index 0000000000..8008d70f0c --- /dev/null +++ b/browser/base/content/test/tabs/browser.toml @@ -0,0 +1,345 @@ +[DEFAULT] +support-files = [ + "head.js", + "dummy_page.html", + "../general/audio.ogg", + "file_mediaPlayback.html", + "test_process_flags_chrome.html", + "helper_origin_attrs_testing.js", + "file_about_srcdoc.html", +] +prefs = [ + "browser.sessionstore.closedTabsFromAllWindows=true", + "browser.sessionstore.closedTabsFromClosedWindows=true", +] + +["browser_addAdjacentNewTab.js"] + +["browser_addTab_index.js"] + +["browser_adoptTab_failure.js"] + +["browser_allow_process_switches_despite_related_browser.js"] + +["browser_audioTabIcon.js"] +tags = "audiochannel" +skip-if = [ + "apple_silicon && !debug" # Bug 1862716 +] + +["browser_bfcache_exemption_about_pages.js"] +skip-if = ["!fission"] + +["browser_bug580956.js"] + +["browser_bug_1387976_restore_lazy_tab_browser_muted_state.js"] + +["browser_close_during_beforeunload.js"] +https_first_disabled = true + +["browser_close_tab_by_dblclick.js"] + +["browser_contextmenu_openlink_after_tabnavigated.js"] +https_first_disabled = true +skip-if = ["verify && debug && os == 'linux'"] +support-files = ["test_bug1358314.html"] + +["browser_dont_process_switch_204.js"] +support-files = [ + "blank.html", + "204.sjs", +] + +["browser_e10s_about_page_triggeringprincipal.js"] +https_first_disabled = true +skip-if = ["verify"] +support-files = [ + "file_about_child.html", + "file_about_parent.html", +] + +["browser_e10s_about_process.js"] + +["browser_e10s_chrome_process.js"] +skip-if = ["debug"] # Bug 1444565, Bug 1457887 + +["browser_e10s_javascript.js"] + +["browser_e10s_mozillaweb_process.js"] + +["browser_e10s_switchbrowser.js"] + +["browser_file_to_http_named_popup.js"] + +["browser_file_to_http_script_closable.js"] +support-files = ["tab_that_closes.html"] + +["browser_hiddentab_contextmenu.js"] + +["browser_lazy_tab_browser_events.js"] + +["browser_link_in_tab_title_and_url_prefilled_blank_page.js"] +support-files = [ + "common_link_in_tab_title_and_url_prefilled.js", + "link_in_tab_title_and_url_prefilled.html", + "request-timeout.sjs", + "wait-a-bit.sjs", +] + +["browser_link_in_tab_title_and_url_prefilled_new_window.js"] +support-files = [ + "common_link_in_tab_title_and_url_prefilled.js", + "link_in_tab_title_and_url_prefilled.html", + "request-timeout.sjs", + "wait-a-bit.sjs", +] + +["browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js"] +support-files = [ + "common_link_in_tab_title_and_url_prefilled.js", + "link_in_tab_title_and_url_prefilled.html", + "request-timeout.sjs", + "wait-a-bit.sjs", +] + +["browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js"] +support-files = [ + "common_link_in_tab_title_and_url_prefilled.js", + "link_in_tab_title_and_url_prefilled.html", + "request-timeout.sjs", + "wait-a-bit.sjs", +] + +["browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js"] +support-files = [ + "common_link_in_tab_title_and_url_prefilled.js", + "link_in_tab_title_and_url_prefilled.html", + "request-timeout.sjs", + "wait-a-bit.sjs", +] + +["browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js"] +support-files = [ + "common_link_in_tab_title_and_url_prefilled.js", + "link_in_tab_title_and_url_prefilled.html", + "request-timeout.sjs", + "wait-a-bit.sjs", +] + +["browser_long_data_url_label_truncation.js"] + +["browser_middle_click_new_tab_button_loads_clipboard.js"] + +["browser_multiselect_tabs_active_tab_selected_by_default.js"] + +["browser_multiselect_tabs_bookmark.js"] + +["browser_multiselect_tabs_clear_selection_when_tab_switch.js"] + +["browser_multiselect_tabs_close.js"] + +["browser_multiselect_tabs_close_other_tabs.js"] + +["browser_multiselect_tabs_close_tabs_to_the_left.js"] + +["browser_multiselect_tabs_close_tabs_to_the_right.js"] + +["browser_multiselect_tabs_close_using_shortcuts.js"] + +["browser_multiselect_tabs_copy_through_drag_and_drop.js"] + +["browser_multiselect_tabs_drag_to_bookmarks_toolbar.js"] + +["browser_multiselect_tabs_duplicate.js"] + +["browser_multiselect_tabs_event.js"] + +["browser_multiselect_tabs_move.js"] + +["browser_multiselect_tabs_move_to_another_window_drag.js"] + +["browser_multiselect_tabs_move_to_new_window_contextmenu.js"] +https_first_disabled = true + +["browser_multiselect_tabs_mute_unmute.js"] + +["browser_multiselect_tabs_open_related.js"] + +["browser_multiselect_tabs_pin_unpin.js"] + +["browser_multiselect_tabs_play.js"] + +["browser_multiselect_tabs_reload.js"] + +["browser_multiselect_tabs_reopen_in_container.js"] + +["browser_multiselect_tabs_reorder.js"] + +["browser_multiselect_tabs_using_Ctrl.js"] + +["browser_multiselect_tabs_using_Shift.js"] + +["browser_multiselect_tabs_using_Shift_and_Ctrl.js"] + +["browser_multiselect_tabs_using_keyboard.js"] +skip-if = ["os == 'mac'"] # Skipped because macOS keyboard support requires changing system settings + +["browser_multiselect_tabs_using_selectedTabs.js"] + +["browser_navigatePinnedTab.js"] +https_first_disabled = true + +["browser_navigate_home_focuses_addressbar.js"] + +["browser_navigate_through_urls_origin_attributes.js"] +skip-if = ["verify && os == 'mac'"] + +["browser_new_file_whitelisted_http_tab.js"] +https_first_disabled = true + +["browser_new_tab_bookmarks_toolbar_height.js"] +skip-if = ["!verify && os == 'mac'"] # Bug 1872477 +support-files = ["file_observe_height_changes.html"] + +["browser_new_tab_in_privilegedabout_process_pref.js"] +https_first_disabled = true +skip-if = ["os == 'linux' && debug"] # Bug 1581500. + +["browser_new_tab_insert_position.js"] +https_first_disabled = true +support-files = ["file_new_tab_page.html"] + +["browser_new_tab_url.js"] +support-files = ["file_new_tab_page.html"] + +["browser_newwindow_tabstrip_overflow.js"] + +["browser_openURI_background.js"] + +["browser_open_newtab_start_observer_notification.js"] + +["browser_opened_file_tab_navigated_to_web.js"] +https_first_disabled = true + +["browser_origin_attrs_in_remote_type.js"] + +["browser_origin_attrs_rel.js"] +skip-if = ["verify && os == 'mac'"] +support-files = ["file_rel_opener_noopener.html"] + +["browser_originalURI.js"] +support-files = [ + "page_with_iframe.html", + "redirect_via_header.html", + "redirect_via_header.html^headers^", + "redirect_via_meta_tag.html", +] + +["browser_overflowScroll.js"] +fail-if = ["a11y_checks"] # Bugs 1854233 and 1873049 scrollbutton-down/up are not labeled +skip-if = [ + "win11_2009", # Bug 1797751 +] + +["browser_paste_event_at_middle_click_on_link.js"] +support-files = ["file_anchor_elements.html"] + +["browser_pinnedTabs.js"] + +["browser_pinnedTabs_clickOpen.js"] + +["browser_pinnedTabs_closeByKeyboard.js"] + +["browser_positional_attributes.js"] +skip-if = [ + "verify && os == 'win'", + "verify && os == 'mac'", +] + +["browser_preloadedBrowser_zoom.js"] + +["browser_privilegedmozilla_process_pref.js"] +https_first_disabled = true + +["browser_progress_keyword_search_handling.js"] +https_first_disabled = true + +["browser_relatedTabs_reset.js"] + +["browser_reload_deleted_file.js"] +skip-if = [ + "debug && os == 'mac'", #Bug 1421183, disabled on Linux/OSX for leaked windows + "debug && os == 'linux'", #Bug 1421183, disabled on Linux/OSX for leaked windows +] + +["browser_removeTabsToTheEnd.js"] + +["browser_removeTabsToTheStart.js"] + +["browser_removeTabs_order.js"] + +["browser_removeTabs_skipPermitUnload.js"] + +["browser_replacewithwindow_commands.js"] + +["browser_switch_by_scrolling.js"] + +["browser_tabCloseProbes.js"] + +["browser_tabCloseSpacer.js"] +skip-if = ["true"] # Bug 1616418 Bug 1549985 + +["browser_tabContextMenu_keyboard.js"] + +["browser_tabReorder.js"] + +["browser_tabReorder_overflow.js"] + +["browser_tabSpinnerProbe.js"] + +["browser_tabSuccessors.js"] + +["browser_tab_a11y_description.js"] + +["browser_tab_label_during_reload.js"] + +["browser_tab_label_picture_in_picture.js"] + +["browser_tab_manager_close.js"] + +["browser_tab_manager_drag.js"] + +["browser_tab_manager_keyboard_access.js"] + +["browser_tab_manager_visibility.js"] + +["browser_tab_move_to_new_window_reload.js"] + +["browser_tab_play.js"] + +["browser_tab_preview.js"] + +["browser_tab_tooltips.js"] + +["browser_tabswitch_contextmenu.js"] + +["browser_tabswitch_select.js"] +support-files = ["open_window_in_new_tab.html"] + +["browser_tabswitch_updatecommands.js"] + +["browser_tabswitch_window_focus.js"] + +["browser_undo_close_tabs.js"] +skip-if = ["true"] #bug 1642084 + +["browser_undo_close_tabs_at_start.js"] + +["browser_viewsource_of_data_URI_in_file_process.js"] + +["browser_visibleTabs_bookmarkAllTabs.js"] + +["browser_visibleTabs_contextMenu.js"] + +["browser_window_open_modifiers.js"] +support-files = ["file_window_open.html"] diff --git a/browser/base/content/test/tabs/browser_addAdjacentNewTab.js b/browser/base/content/test/tabs/browser_addAdjacentNewTab.js new file mode 100644 index 0000000000..c9b4b45ccc --- /dev/null +++ b/browser/base/content/test/tabs/browser_addAdjacentNewTab.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + // Ensure we can wait for about:newtab to load. + set: [["browser.newtab.preload", false]], + }); + + const tab1 = await addTab(); + const tab2 = await addTab(); + const tab3 = await addTab(); + + const menuItemOpenANewTab = document.getElementById("context_openANewTab"); + + await BrowserTestUtils.switchTab(gBrowser, tab2); + + is(tab1._tPos, 1, "First tab"); + is(tab2._tPos, 2, "Second tab"); + is(tab3._tPos, 3, "Third tab"); + + updateTabContextMenu(tab2); + is(menuItemOpenANewTab.hidden, false, "Open a new Tab is visible"); + + const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + + // Open the tab context menu. + const contextMenu = document.getElementById("tabContextMenu"); + // The TabContextMenu initializes its strings only on a focus or mouseover event. + // Calls focus event on the TabContextMenu early in the test. + gBrowser.selectedTab.focus(); + const popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, { + type: "contextmenu", + button: 2, + }); + await popupShownPromise; + + contextMenu.activateItem(menuItemOpenANewTab); + + let newTab = await newTabPromise; + + is(tab1._tPos, 1, "First tab"); + is(tab2._tPos, 2, "Second tab"); + is(newTab._tPos, 3, "Third tab"); + is(tab3._tPos, 4, "Fourth tab"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(newTab); +}); diff --git a/browser/base/content/test/tabs/browser_addTab_index.js b/browser/base/content/test/tabs/browser_addTab_index.js new file mode 100644 index 0000000000..abfc0c213e --- /dev/null +++ b/browser/base/content/test/tabs/browser_addTab_index.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + let tab = gBrowser.addTrustedTab("about:blank", { index: 10 }); + is(tab._tPos, 1, "added tab index should be 1"); + gBrowser.removeTab(tab); +} diff --git a/browser/base/content/test/tabs/browser_adoptTab_failure.js b/browser/base/content/test/tabs/browser_adoptTab_failure.js new file mode 100644 index 0000000000..f20f4c0c56 --- /dev/null +++ b/browser/base/content/test/tabs/browser_adoptTab_failure.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 'adoptTab' aborts when swapBrowsersAndCloseOther returns false. +// That's usually a bug, but this function forces it to happen in order to check +// that callers will behave as good as possible when it happens accidentally. +function makeAdoptTabFailOnceFor(gBrowser, tab) { + const original = gBrowser.swapBrowsersAndCloseOther; + gBrowser.swapBrowsersAndCloseOther = function (aOurTab, aOtherTab) { + if (tab !== aOtherTab) { + return original.call(gBrowser, aOurTab, aOtherTab); + } + gBrowser.swapBrowsersAndCloseOther = original; + return false; + }; +} + +add_task(async function test_adoptTab() { + const tab = await addTab(); + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + const gBrowser2 = win2.gBrowser; + + makeAdoptTabFailOnceFor(gBrowser2, tab); + is(gBrowser2.adoptTab(tab), null, "adoptTab returns null in case of failure"); + ok(gBrowser2.adoptTab(tab), "adoptTab returns new tab in case of success"); + + await BrowserTestUtils.closeWindow(win2); +}); + +add_task(async function test_replaceTabsWithWindow() { + const nonAdoptableTab = await addTab("data:text/plain,nonAdoptableTab"); + const auxiliaryTab = await addTab("data:text/plain,auxiliaryTab"); + const selectedTab = await addTab("data:text/plain,selectedTab"); + gBrowser.selectedTabs = [selectedTab, nonAdoptableTab, auxiliaryTab]; + + const windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + const win2 = gBrowser.replaceTabsWithWindow(selectedTab); + await BrowserTestUtils.waitForEvent(win2, "DOMContentLoaded"); + const gBrowser2 = win2.gBrowser; + makeAdoptTabFailOnceFor(gBrowser2, nonAdoptableTab); + await windowOpenedPromise; + + // nonAdoptableTab couldn't be adopted, but the new window should have adopted + // the other 2 tabs, and they should be in the proper order. + is(gBrowser2.tabs.length, 2); + is(gBrowser2.tabs[0].label, "data:text/plain,auxiliaryTab"); + is(gBrowser2.tabs[1].label, "data:text/plain,selectedTab"); + + gBrowser.removeTab(nonAdoptableTab); + await BrowserTestUtils.closeWindow(win2); +}); + +add_task(async function test_on_drop() { + const nonAdoptableTab = await addTab("data:text/html,<title>nonAdoptableTab"); + const auxiliaryTab = await addTab("data:text/html,<title>auxiliaryTab"); + const selectedTab = await addTab("data:text/html,<title>selectedTab"); + gBrowser.selectedTabs = [selectedTab, nonAdoptableTab, auxiliaryTab]; + + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + const gBrowser2 = win2.gBrowser; + makeAdoptTabFailOnceFor(gBrowser2, nonAdoptableTab); + const initialTab = gBrowser2.tabs[0]; + await dragAndDrop(selectedTab, initialTab, false, win2, false); + + // nonAdoptableTab couldn't be adopted, but the new window should have adopted + // the other 2 tabs, and they should be in the right position. + is(gBrowser2.tabs.length, 3, "There are 3 tabs"); + is(gBrowser2.tabs[0].label, "auxiliaryTab", "auxiliaryTab became tab 0"); + is(gBrowser2.tabs[1].label, "selectedTab", "selectedTab became tab 1"); + is(gBrowser2.tabs[2], initialTab, "initialTab became tab 2"); + is(gBrowser2.selectedTab, gBrowser2.tabs[1], "Tab 1 is selected"); + is(gBrowser2.multiSelectedTabsCount, 2, "Three multiselected tabs"); + ok(gBrowser2.tabs[0].multiselected, "Tab 0 is multiselected"); + ok(gBrowser2.tabs[1].multiselected, "Tab 1 is multiselected"); + ok(!gBrowser2.tabs[2].multiselected, "Tab 2 is not multiselected"); + + gBrowser.removeTab(nonAdoptableTab); + await BrowserTestUtils.closeWindow(win2); +}); + +add_task(async function test_switchToTabHavingURI() { + const nonAdoptableTab = await addTab("data:text/plain,nonAdoptableTab"); + const uri = nonAdoptableTab.linkedBrowser.currentURI; + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + const gBrowser2 = win2.gBrowser; + + is(nonAdoptableTab.closing, false); + is(nonAdoptableTab.selected, false); + is(gBrowser2.tabs.length, 1); + + makeAdoptTabFailOnceFor(gBrowser2, nonAdoptableTab); + win2.switchToTabHavingURI(uri, false, { adoptIntoActiveWindow: true }); + + is(nonAdoptableTab.closing, false); + is(nonAdoptableTab.selected, true); + is(gBrowser2.tabs.length, 1); + + win2.switchToTabHavingURI(uri, false, { adoptIntoActiveWindow: true }); + + is(nonAdoptableTab.closing, true); + is(nonAdoptableTab.selected, false); + is(gBrowser2.tabs.length, 2); + + await BrowserTestUtils.closeWindow(win2); +}); diff --git a/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js b/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js new file mode 100644 index 0000000000..b782c3aada --- /dev/null +++ b/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js @@ -0,0 +1,40 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +const DUMMY_FILE = "dummy_page.html"; +const DATA_URI = "data:text/html,Hi"; +const DATA_URI_SOURCE = "view-source:" + DATA_URI; + +// Test for bug 1328829. +add_task(async function () { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, DATA_URI); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + }); + + let promiseTab = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI_SOURCE); + BrowserViewSource(tab.linkedBrowser); + let viewSourceTab = await promiseTab; + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(viewSourceTab); + }); + + let dummyPage = getChromeDir(getResolvedURI(gTestPath)); + dummyPage.append(DUMMY_FILE); + dummyPage.normalize(); + const uriString = Services.io.newFileURI(dummyPage).spec; + + let viewSourceBrowser = viewSourceTab.linkedBrowser; + let promiseLoad = BrowserTestUtils.browserLoaded( + viewSourceBrowser, + false, + uriString + ); + BrowserTestUtils.startLoadingURIString(viewSourceBrowser, uriString); + let href = await promiseLoad; + is( + href, + uriString, + "Check file:// URI loads in a browser that was previously for view-source" + ); +}); diff --git a/browser/base/content/test/tabs/browser_audioTabIcon.js b/browser/base/content/test/tabs/browser_audioTabIcon.js new file mode 100644 index 0000000000..53b5140abb --- /dev/null +++ b/browser/base/content/test/tabs/browser_audioTabIcon.js @@ -0,0 +1,684 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +const PAGE = + "https://example.com/browser/browser/base/content/test/tabs/file_mediaPlayback.html"; +const TABATTR_REMOVAL_PREFNAME = "browser.tabs.delayHidingAudioPlayingIconMS"; +const INITIAL_TABATTR_REMOVAL_DELAY_MS = Services.prefs.getIntPref( + TABATTR_REMOVAL_PREFNAME +); + +async function pause(tab, options) { + let extendedDelay = options && options.extendedDelay; + if (extendedDelay) { + // Use 10s to remove possibility of race condition with attr removal. + Services.prefs.setIntPref(TABATTR_REMOVAL_PREFNAME, 10000); + } + + try { + let browser = tab.linkedBrowser; + let awaitDOMAudioPlaybackStopped; + if (!browser.audioMuted) { + awaitDOMAudioPlaybackStopped = BrowserTestUtils.waitForEvent( + browser, + "DOMAudioPlaybackStopped", + "DOMAudioPlaybackStopped event should get fired after pause" + ); + } + await SpecialPowers.spawn(browser, [], async function () { + let audio = content.document.querySelector("audio"); + audio.pause(); + }); + + // If the tab has already be muted, it means the tab won't have soundplaying, + // so we don't need to check this attribute. + if (browser.audioMuted) { + return; + } + + if (extendedDelay) { + ok( + tab.hasAttribute("soundplaying"), + "The tab should still have the soundplaying attribute immediately after pausing" + ); + + await awaitDOMAudioPlaybackStopped; + ok( + tab.hasAttribute("soundplaying"), + "The tab should still have the soundplaying attribute immediately after DOMAudioPlaybackStopped" + ); + } + + await wait_for_tab_playing_event(tab, false); + ok( + !tab.hasAttribute("soundplaying"), + "The tab should not have the soundplaying attribute after the timeout has resolved" + ); + } finally { + // Make sure other tests don't timeout if an exception gets thrown above. + // Need to use setIntPref instead of clearUserPref because + // testing/profiles/common/user.js overrides the default value to help this and + // other tests run faster. + Services.prefs.setIntPref( + TABATTR_REMOVAL_PREFNAME, + INITIAL_TABATTR_REMOVAL_DELAY_MS + ); + } +} + +async function hide_tab(tab) { + let tabHidden = BrowserTestUtils.waitForEvent(tab, "TabHide"); + gBrowser.hideTab(tab); + return tabHidden; +} + +async function show_tab(tab) { + let tabShown = BrowserTestUtils.waitForEvent(tab, "TabShow"); + gBrowser.showTab(tab); + return tabShown; +} + +async function test_tooltip(icon, expectedTooltip, isActiveTab, tab) { + let tooltip = document.getElementById("tabbrowser-tab-tooltip"); + + let tabContent = tab.querySelector(".tab-content"); + await hover_icon(tabContent, tooltip); + + await hover_icon(icon, tooltip); + if (isActiveTab) { + // The active tab should have the keybinding shortcut in the tooltip. + // We check this by ensuring that the strings are not equal but the expected + // message appears in the beginning. + isnot( + tooltip.getAttribute("label"), + expectedTooltip, + "Tooltips should not be equal" + ); + is( + tooltip.getAttribute("label").indexOf(expectedTooltip), + 0, + "Correct tooltip expected" + ); + } else { + is( + tooltip.getAttribute("label"), + expectedTooltip, + "Tooltips should not be equal" + ); + } + leave_icon(icon); +} + +function get_tab_state(tab) { + return JSON.parse(SessionStore.getTabState(tab)); +} + +async function test_muting_using_menu(tab, expectMuted) { + // Show the popup menu + let contextMenu = document.getElementById("tabContextMenu"); + let popupShownPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(tab, { type: "contextmenu", button: 2 }); + await popupShownPromise; + + // Check the menu + let expectedLabel = expectMuted ? "Unmute Tab" : "Mute Tab"; + let expectedAccessKey = expectMuted ? "m" : "M"; + let toggleMute = document.getElementById("context_toggleMuteTab"); + is(toggleMute.label, expectedLabel, "Correct label expected"); + is(toggleMute.accessKey, expectedAccessKey, "Correct accessKey expected"); + + is( + toggleMute.hasAttribute("muted"), + expectMuted, + "Should have the correct state for the muted attribute" + ); + ok( + !toggleMute.hasAttribute("soundplaying"), + "Should not have the soundplaying attribute" + ); + + await play(tab); + + is( + toggleMute.hasAttribute("muted"), + expectMuted, + "Should have the correct state for the muted attribute" + ); + is( + !toggleMute.hasAttribute("soundplaying"), + expectMuted, + "The value of soundplaying attribute is incorrect" + ); + + await pause(tab); + + is( + toggleMute.hasAttribute("muted"), + expectMuted, + "Should have the correct state for the muted attribute" + ); + ok( + !toggleMute.hasAttribute("soundplaying"), + "Should not have the soundplaying attribute" + ); + + // Click on the menu and wait for the tab to be muted. + let mutedPromise = get_wait_for_mute_promise(tab, !expectMuted); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.activateItem(toggleMute); + await popupHiddenPromise; + await mutedPromise; +} + +async function test_playing_icon_on_tab(tab, browser, isPinned) { + let icon = isPinned ? tab.overlayIcon : tab.overlayIcon; + let isActiveTab = tab === gBrowser.selectedTab; + + await play(tab); + + await test_tooltip(icon, "Mute tab", isActiveTab, tab); + + ok( + !("muted" in get_tab_state(tab)), + "No muted attribute should be persisted" + ); + ok( + !("muteReason" in get_tab_state(tab)), + "No muteReason property should be persisted" + ); + + await test_mute_tab(tab, icon, true); + + ok("muted" in get_tab_state(tab), "Muted attribute should be persisted"); + ok( + "muteReason" in get_tab_state(tab), + "muteReason property should be persisted" + ); + + await test_tooltip(icon, "Unmute tab", isActiveTab, tab); + + await test_mute_tab(tab, icon, false); + + ok( + !("muted" in get_tab_state(tab)), + "No muted attribute should be persisted" + ); + ok( + !("muteReason" in get_tab_state(tab)), + "No muteReason property should be persisted" + ); + + await test_tooltip(icon, "Mute tab", isActiveTab, tab); + + await test_mute_tab(tab, icon, true); + + await pause(tab); + + ok(tab.hasAttribute("muted"), "Tab should still be muted (attribute check)"); + ok( + !tab.hasAttribute("soundplaying"), + "Tab should not be playing (attribute check)" + ); + ok(tab.muted, "Tab should still be muted (property check)"); + ok(!tab.soundPlaying, "Tab should not be playing (property check)"); + + await test_tooltip(icon, "Unmute tab", isActiveTab, tab); + + await test_mute_tab(tab, icon, false); + + ok( + !tab.hasAttribute("muted"), + "Tab should not be be muted (attribute check)" + ); + ok( + !tab.hasAttribute("soundplaying"), + "Tab should not be be playing (attribute check)" + ); + ok(!tab.muted, "Tab should not be be muted (property check)"); + ok(!tab.soundPlaying, "Tab should not be be playing (property check)"); + + // Make sure it's possible to mute using the context menu. + await test_muting_using_menu(tab, false); + + // Make sure it's possible to unmute using the context menu. + await test_muting_using_menu(tab, true); +} + +async function test_playing_icon_on_hidden_tab(tab) { + let oldSelectedTab = gBrowser.selectedTab; + let otherTabs = [ + await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true), + await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE, true, true), + ]; + let tabContainer = tab.container; + let alltabsButton = document.getElementById("alltabs-button"); + let alltabsBadge = alltabsButton.badgeLabel; + + function assertIconShowing() { + is( + getComputedStyle(alltabsBadge).backgroundImage, + 'url("chrome://browser/skin/tabbrowser/tab-audio-playing-small.svg")', + "The audio playing icon is shown" + ); + ok( + tabContainer.hasAttribute("hiddensoundplaying"), + "There are hidden audio tabs" + ); + } + + function assertIconHidden() { + is( + getComputedStyle(alltabsBadge).backgroundImage, + "none", + "The audio playing icon is hidden" + ); + ok( + !tabContainer.hasAttribute("hiddensoundplaying"), + "There are no hidden audio tabs" + ); + } + + // Keep the passed in tab selected. + gBrowser.selectedTab = tab; + + // Play sound in the other two (visible) tabs. + await play(otherTabs[0]); + await play(otherTabs[1]); + assertIconHidden(); + + // Hide one of the noisy tabs, we see the icon. + await hide_tab(otherTabs[0]); + assertIconShowing(); + + // Hiding the other tab keeps the icon. + await hide_tab(otherTabs[1]); + assertIconShowing(); + + // Pausing both tabs will hide the icon. + await pause(otherTabs[0]); + assertIconShowing(); + await pause(otherTabs[1]); + assertIconHidden(); + + // The icon returns when audio starts again. + await play(otherTabs[0]); + await play(otherTabs[1]); + assertIconShowing(); + + // There is still an icon after hiding one tab. + await show_tab(otherTabs[0]); + assertIconShowing(); + + // The icon is hidden when both of the tabs are shown. + await show_tab(otherTabs[1]); + assertIconHidden(); + + await BrowserTestUtils.removeTab(otherTabs[0]); + await BrowserTestUtils.removeTab(otherTabs[1]); + + // Make sure we didn't change the selected tab. + gBrowser.selectedTab = oldSelectedTab; +} + +async function test_swapped_browser_while_playing(oldTab, newBrowser) { + // The tab was muted so it won't have soundplaying attribute even it's playing. + ok( + oldTab.hasAttribute("muted"), + "Expected the correct muted attribute on the old tab" + ); + is( + oldTab.muteReason, + null, + "Expected the correct muteReason attribute on the old tab" + ); + ok( + !oldTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the old tab" + ); + + let newTab = gBrowser.getTabForBrowser(newBrowser); + let AttrChangePromise = BrowserTestUtils.waitForEvent( + newTab, + "TabAttrModified", + false, + event => { + return event.detail.changed.includes("muted"); + } + ); + + gBrowser.swapBrowsersAndCloseOther(newTab, oldTab); + await AttrChangePromise; + + ok( + newTab.hasAttribute("muted"), + "Expected the correct muted attribute on the new tab" + ); + is( + newTab.muteReason, + null, + "Expected the correct muteReason property on the new tab" + ); + ok( + !newTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the new tab" + ); + + await test_tooltip(newTab.overlayIcon, "Unmute tab", true, newTab); +} + +async function test_swapped_browser_while_not_playing(oldTab, newBrowser) { + ok( + oldTab.hasAttribute("muted"), + "Expected the correct muted attribute on the old tab" + ); + is( + oldTab.muteReason, + null, + "Expected the correct muteReason property on the old tab" + ); + ok( + !oldTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the old tab" + ); + + let newTab = gBrowser.getTabForBrowser(newBrowser); + let AttrChangePromise = BrowserTestUtils.waitForEvent( + newTab, + "TabAttrModified", + false, + event => { + return event.detail.changed.includes("muted"); + } + ); + + let AudioPlaybackPromise = new Promise(resolve => { + let observer = (subject, topic, data) => { + ok(false, "Should not see an audio-playback notification"); + }; + Services.obs.addObserver(observer, "audio-playback"); + setTimeout(() => { + Services.obs.removeObserver(observer, "audio-playback"); + resolve(); + }, 100); + }); + + gBrowser.swapBrowsersAndCloseOther(newTab, oldTab); + await AttrChangePromise; + + ok( + newTab.hasAttribute("muted"), + "Expected the correct muted attribute on the new tab" + ); + is( + newTab.muteReason, + null, + "Expected the correct muteReason property on the new tab" + ); + ok( + !newTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the new tab" + ); + + // Wait to see if an audio-playback event is dispatched. + await AudioPlaybackPromise; + + ok( + newTab.hasAttribute("muted"), + "Expected the correct muted attribute on the new tab" + ); + is( + newTab.muteReason, + null, + "Expected the correct muteReason property on the new tab" + ); + ok( + !newTab.hasAttribute("soundplaying"), + "Expected the correct soundplaying attribute on the new tab" + ); + + await test_tooltip(newTab.overlayIcon, "Unmute tab", true, newTab); +} + +async function test_browser_swapping(tab, browser) { + // First, test swapping with a playing but muted tab. + await play(tab); + + await test_mute_tab(tab, tab.overlayIcon, true); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + async function (newBrowser) { + await test_swapped_browser_while_playing(tab, newBrowser); + + // Now, test swapping with a muted but not playing tab. + // Note that the tab remains muted, so we only need to pause playback. + tab = gBrowser.getTabForBrowser(newBrowser); + await pause(tab); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + secondAboutBlankBrowser => + test_swapped_browser_while_not_playing(tab, secondAboutBlankBrowser) + ); + } + ); +} + +async function test_click_on_pinned_tab_after_mute() { + async function taskFn(browser) { + let tab = gBrowser.getTabForBrowser(browser); + + gBrowser.selectedTab = originallySelectedTab; + isnot( + tab, + gBrowser.selectedTab, + "Sanity check, the tab should not be selected!" + ); + + // Steps to reproduce the bug: + // Pin the tab. + gBrowser.pinTab(tab); + + // Start playback and wait for it to finish. + await play(tab); + + // Mute the tab. + let icon = tab.overlayIcon; + await test_mute_tab(tab, icon, true); + + // Pause playback and wait for it to finish. + await pause(tab); + + // Unmute tab. + await test_mute_tab(tab, icon, false); + + // Now click on the tab. + EventUtils.synthesizeMouseAtCenter(tab.iconImage, { button: 0 }); + + is(tab, gBrowser.selectedTab, "Tab switch should be successful"); + + // Cleanup. + gBrowser.unpinTab(tab); + gBrowser.selectedTab = originallySelectedTab; + } + + let originallySelectedTab = gBrowser.selectedTab; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + taskFn + ); +} + +// This test only does something useful in e10s! +async function test_cross_process_load() { + async function taskFn(browser) { + let tab = gBrowser.getTabForBrowser(browser); + + // Start playback and wait for it to finish. + await play(tab); + + let soundPlayingStoppedPromise = BrowserTestUtils.waitForEvent( + tab, + "TabAttrModified", + false, + event => event.detail.changed.includes("soundplaying") + ); + + // Go to a different process. + BrowserTestUtils.startLoadingURIString(browser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(browser); + + await soundPlayingStoppedPromise; + + ok( + !tab.hasAttribute("soundplaying"), + "Tab should not be playing sound any more" + ); + ok(!tab.soundPlaying, "Tab should not be playing sound any more"); + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + taskFn + ); +} + +async function test_mute_keybinding() { + async function test_muting_using_keyboard(tab) { + let mutedPromise = get_wait_for_mute_promise(tab, true); + EventUtils.synthesizeKey("m", { ctrlKey: true }); + await mutedPromise; + is( + tab.hasAttribute("indicator-replaces-favicon"), + !tab.pinned, + "Mute indicator should replace the favicon on hover if the tab isn't pinned" + ); + mutedPromise = get_wait_for_mute_promise(tab, false); + EventUtils.synthesizeKey("m", { ctrlKey: true }); + await mutedPromise; + } + async function taskFn(browser) { + let tab = gBrowser.getTabForBrowser(browser); + + // Make sure it's possible to mute before the tab is playing. + await test_muting_using_keyboard(tab); + + // Start playback and wait for it to finish. + await play(tab); + + // Make sure it's possible to mute after the tab is playing. + await test_muting_using_keyboard(tab); + + // Pause playback and wait for it to finish. + await pause(tab); + + // Make sure things work if the tab is pinned. + gBrowser.pinTab(tab); + + // Make sure it's possible to mute before the tab is playing. + await test_muting_using_keyboard(tab); + + // Start playback and wait for it to finish. + await play(tab); + + // Make sure it's possible to mute after the tab is playing. + await test_muting_using_keyboard(tab); + + gBrowser.unpinTab(tab); + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + taskFn + ); +} + +async function test_on_browser(browser) { + let tab = gBrowser.getTabForBrowser(browser); + + // Test the icon in a normal tab. + await test_playing_icon_on_tab(tab, browser, false); + + gBrowser.pinTab(tab); + + // Test the icon in a pinned tab. + await test_playing_icon_on_tab(tab, browser, true); + + gBrowser.unpinTab(tab); + + // Test the sound playing icon for hidden tabs. + await test_playing_icon_on_hidden_tab(tab); + + // Retest with another browser in the foreground tab + if (gBrowser.selectedBrowser.currentURI.spec == PAGE) { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "data:text/html,test", + }, + () => test_on_browser(browser) + ); + } else { + await test_browser_swapping(tab, browser); + } +} + +async function test_delayed_tabattr_removal() { + async function taskFn(browser) { + let tab = gBrowser.getTabForBrowser(browser); + await play(tab); + + // Extend the delay to guarantee the soundplaying attribute + // is not removed from the tab when audio is stopped. Without + // the extended delay the attribute could be removed in the + // same tick and the test wouldn't catch that this broke. + await pause(tab, { extendedDelay: true }); + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + taskFn + ); +} + +requestLongerTimeout(2); +add_task(async function test_page() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: PAGE, + }, + test_on_browser + ); +}); + +add_task(test_click_on_pinned_tab_after_mute); + +add_task(test_cross_process_load); + +add_task(test_mute_keybinding); + +add_task(test_delayed_tabattr_removal); diff --git a/browser/base/content/test/tabs/browser_bfcache_exemption_about_pages.js b/browser/base/content/test/tabs/browser_bfcache_exemption_about_pages.js new file mode 100644 index 0000000000..6eb9298ea6 --- /dev/null +++ b/browser/base/content/test/tabs/browser_bfcache_exemption_about_pages.js @@ -0,0 +1,176 @@ +requestLongerTimeout(2); + +const BASE = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +async function navigateTo(browser, urls, expectedPersist) { + // Navigate to a bunch of urls + for (let url of urls) { + let loaded = BrowserTestUtils.browserLoaded(browser, false, url); + BrowserTestUtils.startLoadingURIString(browser, url); + await loaded; + } + // When we track pageshow event, save the evt.persisted on a doc element, + // so it can be checked from the test directly. + let pageShowCheck = evt => { + evt.target.ownerGlobal.document.documentElement.setAttribute( + "persisted", + evt.persisted + ); + return true; + }; + is( + browser.canGoBack, + true, + `After navigating to urls=${urls}, we can go back from uri=${browser.currentURI.spec}` + ); + if (expectedPersist) { + // If we expect the page to persist, then the uri we are testing is about:blank. + // Currently we are only testing cases when we go forward to about:blank page, + // because it gets removed from history if it is sandwiched between two + // regular history entries. This means we can't test a scenario such as: + // page X, about:blank, page Y, go back -- about:blank page will be removed, and + // going back from page Y will take us to page X. + + // Go back from about:blank (it will be the last uri in 'urls') + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + info(`Navigating back from uri=${browser.currentURI.spec}`); + browser.goBack(); + await pageShowPromise; + info(`Got pageshow event`); + // Now go forward + let forwardPageShow = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow", + false, + pageShowCheck + ); + info(`Navigating forward from uri=${browser.currentURI.spec}`); + browser.goForward(); + await forwardPageShow; + // Check that the page got persisted + let persisted = await SpecialPowers.spawn(browser, [], async function () { + return content.document.documentElement.getAttribute("persisted"); + }); + is( + persisted, + expectedPersist.toString(), + `uri ${browser.currentURI.spec} should have persisted` + ); + } else { + // Go back + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow", + false, + pageShowCheck + ); + info(`Navigating back from uri=${browser.currentURI.spec}`); + browser.goBack(); + await pageShowPromise; + info(`Got pageshow event`); + // Check that the page did not get persisted + let persisted = await SpecialPowers.spawn(browser, [], async function () { + return content.document.documentElement.getAttribute("persisted"); + }); + is( + persisted, + expectedPersist.toString(), + `uri ${browser.currentURI.spec} shouldn't have persisted` + ); + } +} + +add_task(async function testAboutPagesExemptFromBfcache() { + // If Fission is disabled, the pref is no-op. + await SpecialPowers.pushPrefEnv({ set: [["fission.bfcacheInParent", true]] }); + + // Navigate to a bunch of urls, then go back once, check that the penultimate page did not go into BFbache + var browser; + // First page is about:privatebrowsing + const private_test_cases = [ + ["about:blank"], + ["about:blank", "about:privatebrowsing", "about:blank"], + ]; + for (const urls of private_test_cases) { + info(`Private tab - navigate to ${urls} urls`); + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + waitForTabURL: "about:privatebrowsing", + }); + browser = win.gBrowser.selectedTab.linkedBrowser; + await navigateTo(browser, urls, false); + await BrowserTestUtils.closeWindow(win); + } + + // First page is about:blank + const regular_test_cases = [ + ["about:home"], + ["about:home", "about:blank"], + ["about:blank", "about:newtab"], + ]; + for (const urls of regular_test_cases) { + info(`Regular tab - navigate to ${urls} urls`); + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser: win.gBrowser, + opening: "about:newtab", + }); + browser = tab.linkedBrowser; + await navigateTo(browser, urls, false); + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + } +}); + +// Test that about:blank or pages that have about:* subframes get bfcached. +// TODO bug 1705789: add about:reader tests when we make them bfcache compatible. +add_task(async function testAboutPagesBfcacheAllowed() { + // If Fission is disabled, the pref is no-op. + await SpecialPowers.pushPrefEnv({ set: [["fission.bfcacheInParent", true]] }); + + var browser; + // First page is about:privatebrowsing + // about:privatebrowsing -> about:blank, go back, go forward, - about:blank is bfcached + // about:privatebrowsing -> about:home -> about:blank, go back, go forward, - about:blank is bfcached + // about:privatebrowsing -> file_about_srcdoc.html, go back, go forward - file_about_srcdoc.html is bfcached + const private_test_cases = [ + ["about:blank"], + ["about:home", "about:blank"], + [BASE + "file_about_srcdoc.html"], + ]; + for (const urls of private_test_cases) { + info(`Private tab - navigate to ${urls} urls`); + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + waitForTabURL: "about:privatebrowsing", + }); + browser = win.gBrowser.selectedTab.linkedBrowser; + await navigateTo(browser, urls, true); + await BrowserTestUtils.closeWindow(win); + } + + // First page is about:blank + // about:blank -> about:home -> about:blank, go back, go forward, - about:blank is bfcached + // about:blank -> about:home -> file_about_srcdoc.html, go back, go forward - file_about_srcdoc.html is bfcached + const regular_test_cases = [ + ["about:home", "about:blank"], + ["about:home", BASE + "file_about_srcdoc.html"], + ]; + for (const urls of regular_test_cases) { + info(`Regular tab - navigate to ${urls} urls`); + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser: win.gBrowser, + }); + browser = tab.linkedBrowser; + await navigateTo(browser, urls, true); + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + } +}); diff --git a/browser/base/content/test/tabs/browser_bug580956.js b/browser/base/content/test/tabs/browser_bug580956.js new file mode 100644 index 0000000000..511c2ea03e --- /dev/null +++ b/browser/base/content/test/tabs/browser_bug580956.js @@ -0,0 +1,25 @@ +function numClosedTabs() { + return SessionStore.getClosedTabCount(); +} + +function isUndoCloseEnabled() { + updateTabContextMenu(); + return !document.getElementById("context_undoCloseTab").disabled; +} + +add_task(async function test() { + Services.prefs.setIntPref("browser.sessionstore.max_tabs_undo", 0); + Services.prefs.clearUserPref("browser.sessionstore.max_tabs_undo"); + is(numClosedTabs(), 0, "There should be 0 closed tabs."); + ok(!isUndoCloseEnabled(), "Undo Close Tab should be disabled."); + + var tab = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/"); + var browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab); + BrowserTestUtils.removeTab(tab); + await sessionUpdatePromise; + + ok(isUndoCloseEnabled(), "Undo Close Tab should be enabled."); +}); diff --git a/browser/base/content/test/tabs/browser_bug_1387976_restore_lazy_tab_browser_muted_state.js b/browser/base/content/test/tabs/browser_bug_1387976_restore_lazy_tab_browser_muted_state.js new file mode 100644 index 0000000000..a3436fcefb --- /dev/null +++ b/browser/base/content/test/tabs/browser_bug_1387976_restore_lazy_tab_browser_muted_state.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TabState } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabState.sys.mjs" +); + +/** + * Simulate a restart of a tab by removing it, then add a lazy tab + * which is restored with the tabData of the removed tab. + * + * @param tab + * The tab to restart. + * @return {Object} the restored lazy tab + */ +const restartTab = async function (tab) { + let tabData = TabState.clone(tab); + BrowserTestUtils.removeTab(tab); + + let restoredLazyTab = BrowserTestUtils.addTab(gBrowser, "", { + createLazyBrowser: true, + }); + SessionStore.setTabState(restoredLazyTab, JSON.stringify(tabData)); + return restoredLazyTab; +}; + +function get_tab_state(tab) { + return JSON.parse(SessionStore.getTabState(tab)); +} + +add_task(async function () { + const tab = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/"); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Let's make sure the tab is not in a muted state at the beginning + ok(!("muted" in get_tab_state(tab)), "Tab should not be in a muted state"); + + info("toggling Muted audio..."); + tab.toggleMuteAudio(); + + ok("muted" in get_tab_state(tab), "Tab should be in a muted state"); + + info("Restarting tab..."); + let restartedTab = await restartTab(tab); + + ok( + "muted" in get_tab_state(restartedTab), + "Restored tab should still be in a muted state after restart" + ); + ok(!restartedTab.linkedPanel, "Restored tab should not be inserted"); + + BrowserTestUtils.removeTab(restartedTab); +}); diff --git a/browser/base/content/test/tabs/browser_close_during_beforeunload.js b/browser/base/content/test/tabs/browser_close_during_beforeunload.js new file mode 100644 index 0000000000..32bbb65b62 --- /dev/null +++ b/browser/base/content/test/tabs/browser_close_during_beforeunload.js @@ -0,0 +1,46 @@ +"use strict"; + +// Tests that a second attempt to close a window while blocked on a +// beforeunload confirmation ignores the beforeunload listener and +// unblocks the original close call. + +const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref( + "prompts.contentPromptSubDialog", + false +); + +const DIALOG_TOPIC = CONTENT_PROMPT_SUBDIALOG + ? "common-dialog-loaded" + : "tabmodal-dialog-loaded"; + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + + let browser = win.gBrowser.selectedBrowser; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + BrowserTestUtils.startLoadingURIString(browser, "http://example.com/"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/"); + + await SpecialPowers.spawn(browser, [], () => { + // eslint-disable-next-line mozilla/balanced-listeners + content.addEventListener("beforeunload", event => { + event.preventDefault(); + }); + }); + + let confirmationShown = false; + + BrowserUtils.promiseObserved(DIALOG_TOPIC).then(() => { + confirmationShown = true; + win.close(); + }); + + win.close(); + ok(confirmationShown, "Before unload confirmation should have been shown"); + ok(win.closed, "Window should have been closed after second close() call"); +}); diff --git a/browser/base/content/test/tabs/browser_close_tab_by_dblclick.js b/browser/base/content/test/tabs/browser_close_tab_by_dblclick.js new file mode 100644 index 0000000000..9d251f1ea6 --- /dev/null +++ b/browser/base/content/test/tabs/browser_close_tab_by_dblclick.js @@ -0,0 +1,35 @@ +/* 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/. */ + +const PREF_CLOSE_TAB_BY_DBLCLICK = "browser.tabs.closeTabByDblclick"; + +function triggerDblclickOn(target) { + let promise = BrowserTestUtils.waitForEvent(target, "dblclick"); + EventUtils.synthesizeMouseAtCenter(target, { clickCount: 1 }); + EventUtils.synthesizeMouseAtCenter(target, { clickCount: 2 }); + return promise; +} + +add_task(async function dblclick() { + let tab = gBrowser.selectedTab; + await triggerDblclickOn(tab); + ok(!tab.closing, "Double click the selected tab won't close it"); +}); + +add_task(async function dblclickWithPrefSet() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_CLOSE_TAB_BY_DBLCLICK, true]], + }); + + let tab = BrowserTestUtils.addTab(gBrowser, "about:mozilla", { + skipAnimation: true, + }); + isnot(tab, gBrowser.selectedTab, "The new tab is in the background"); + + await triggerDblclickOn(tab); + is(tab, gBrowser.selectedTab, "Double click a background tab will select it"); + + await triggerDblclickOn(tab); + ok(tab.closing, "Double click the selected tab will close it"); +}); diff --git a/browser/base/content/test/tabs/browser_contextmenu_openlink_after_tabnavigated.js b/browser/base/content/test/tabs/browser_contextmenu_openlink_after_tabnavigated.js new file mode 100644 index 0000000000..26e7cd617a --- /dev/null +++ b/browser/base/content/test/tabs/browser_contextmenu_openlink_after_tabnavigated.js @@ -0,0 +1,63 @@ +"use strict"; + +const example_base = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/browser/browser/base/content/test/tabs/"; + +add_task(async function test_contextmenu_openlink_after_tabnavigated() { + let url = example_base + "test_bug1358314.html"; + + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + const contextMenu = document.getElementById("contentAreaContextMenu"); + let awaitPopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouse( + "a", + 0, + 0, + { + type: "contextmenu", + button: 2, + }, + gBrowser.selectedBrowser + ); + await awaitPopupShown; + info("Popup Shown"); + + info("Navigate the tab with the opened context menu"); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "about:blank" + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + let awaitNewTabOpen = BrowserTestUtils.waitForNewTab( + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/", + true + ); + + info("Click the 'open link in new tab' menu item"); + let openLinkMenuItem = contextMenu.querySelector("#context-openlinkintab"); + contextMenu.activateItem(openLinkMenuItem); + + info("Wait for the new tab to be opened"); + const newTab = await awaitNewTabOpen; + + // Close the contextMenu popup if it has not been closed yet. + contextMenu.hidePopup(); + + is( + newTab.linkedBrowser.currentURI.spec, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/", + "Got the expected URL loaded in the new tab" + ); + + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/tabs/browser_dont_process_switch_204.js b/browser/base/content/test/tabs/browser_dont_process_switch_204.js new file mode 100644 index 0000000000..b35170ad1e --- /dev/null +++ b/browser/base/content/test/tabs/browser_dont_process_switch_204.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const TEST_URL = TEST_ROOT + "204.sjs"; +const BLANK_URL = TEST_ROOT + "blank.html"; + +// Test for bug 1626362. +add_task(async function () { + await BrowserTestUtils.withNewTab("about:robots", async function (aBrowser) { + // Get the current pid for browser for comparison later, we expect this + // to be the parent process for about:robots. + let browserPid = await SpecialPowers.spawn(aBrowser, [], () => { + return Services.appinfo.processID; + }); + + is( + Services.appinfo.processID, + browserPid, + "about:robots should have loaded in the parent" + ); + + // Attempt to load a uri that returns a 204 response, and then check that + // we didn't process switch for it. + let stopped = BrowserTestUtils.browserStopped(aBrowser, TEST_URL, true); + BrowserTestUtils.startLoadingURIString(aBrowser, TEST_URL); + await stopped; + + let newPid = await SpecialPowers.spawn(aBrowser, [], () => { + return Services.appinfo.processID; + }); + + is( + browserPid, + newPid, + "Shouldn't change process when we get a 204 response" + ); + + // Load a valid http page and confirm that we did change process + // to confirm that we weren't in a web process to begin with. + let loaded = BrowserTestUtils.browserLoaded(aBrowser, false, BLANK_URL); + BrowserTestUtils.startLoadingURIString(aBrowser, BLANK_URL); + await loaded; + + newPid = await SpecialPowers.spawn(aBrowser, [], () => { + return Services.appinfo.processID; + }); + + isnot(browserPid, newPid, "Should change process for a valid response"); + }); +}); diff --git a/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js b/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js new file mode 100644 index 0000000000..4610551977 --- /dev/null +++ b/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js @@ -0,0 +1,210 @@ +"use strict"; + +const kChildPage = getRootDirectory(gTestPath) + "file_about_child.html"; +const kParentPage = getRootDirectory(gTestPath) + "file_about_parent.html"; + +const kAboutPagesRegistered = Promise.all([ + BrowserTestUtils.registerAboutPage( + registerCleanupFunction, + "test-about-principal-child", + kChildPage, + Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD | + Ci.nsIAboutModule.ALLOW_SCRIPT | + Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT + ), + BrowserTestUtils.registerAboutPage( + registerCleanupFunction, + "test-about-principal-parent", + kParentPage, + Ci.nsIAboutModule.ALLOW_SCRIPT + ), +]); + +add_task(async function test_principal_click() { + await kAboutPagesRegistered; + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.skip_about_page_has_csp_assert", true]], + }); + await BrowserTestUtils.withNewTab( + "about:test-about-principal-parent", + async function (browser) { + let loadPromise = BrowserTestUtils.browserLoaded( + browser, + false, + "about:test-about-principal-child" + ); + let myLink = browser.contentDocument.getElementById( + "aboutchildprincipal" + ); + myLink.click(); + await loadPromise; + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + let channel = content.docShell.currentDocumentChannel; + is( + channel.originalURI.asciiSpec, + "about:test-about-principal-child", + "sanity check - make sure we test the principal for the correct URI" + ); + + let triggeringPrincipal = channel.loadInfo.triggeringPrincipal; + ok( + triggeringPrincipal.isSystemPrincipal, + "loading about: from privileged page must have a triggering of System" + ); + + let contentPolicyType = channel.loadInfo.externalContentPolicyType; + is( + contentPolicyType, + Ci.nsIContentPolicy.TYPE_DOCUMENT, + "sanity check - loading a top level document" + ); + + let loadingPrincipal = channel.loadInfo.loadingPrincipal; + is( + loadingPrincipal, + null, + "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal" + ); + } + ); + } + ); +}); + +add_task(async function test_principal_ctrl_click() { + await kAboutPagesRegistered; + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.sandbox.content.level", 1], + ["dom.security.skip_about_page_has_csp_assert", true], + ], + }); + + await BrowserTestUtils.withNewTab( + "about:test-about-principal-parent", + async function (browser) { + let loadPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:test-about-principal-child", + true + ); + // simulate ctrl+click + BrowserTestUtils.synthesizeMouseAtCenter( + "#aboutchildprincipal", + { ctrlKey: true, metaKey: true }, + gBrowser.selectedBrowser + ); + let tab = await loadPromise; + gBrowser.selectTabAtIndex(2); + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + let channel = content.docShell.currentDocumentChannel; + is( + channel.originalURI.asciiSpec, + "about:test-about-principal-child", + "sanity check - make sure we test the principal for the correct URI" + ); + + let triggeringPrincipal = channel.loadInfo.triggeringPrincipal; + ok( + triggeringPrincipal.isSystemPrincipal, + "loading about: from privileged page must have a triggering of System" + ); + + let contentPolicyType = channel.loadInfo.externalContentPolicyType; + is( + contentPolicyType, + Ci.nsIContentPolicy.TYPE_DOCUMENT, + "sanity check - loading a top level document" + ); + + let loadingPrincipal = channel.loadInfo.loadingPrincipal; + is( + loadingPrincipal, + null, + "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal" + ); + } + ); + BrowserTestUtils.removeTab(tab); + } + ); +}); + +add_task(async function test_principal_right_click_open_link_in_new_tab() { + await kAboutPagesRegistered; + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.sandbox.content.level", 1], + ["dom.security.skip_about_page_has_csp_assert", true], + ], + }); + + await BrowserTestUtils.withNewTab( + "about:test-about-principal-parent", + async function (browser) { + let loadPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:test-about-principal-child", + true + ); + + // simulate right-click open link in tab + BrowserTestUtils.waitForEvent(document, "popupshown", false, event => { + // These are operations that must be executed synchronously with the event. + document.getElementById("context-openlinkintab").doCommand(); + event.target.hidePopup(); + return true; + }); + BrowserTestUtils.synthesizeMouseAtCenter( + "#aboutchildprincipal", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + + let tab = await loadPromise; + gBrowser.selectTabAtIndex(2); + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + let channel = content.docShell.currentDocumentChannel; + is( + channel.originalURI.asciiSpec, + "about:test-about-principal-child", + "sanity check - make sure we test the principal for the correct URI" + ); + + let triggeringPrincipal = channel.loadInfo.triggeringPrincipal; + ok( + triggeringPrincipal.isSystemPrincipal, + "loading about: from privileged page must have a triggering of System" + ); + + let contentPolicyType = channel.loadInfo.externalContentPolicyType; + is( + contentPolicyType, + Ci.nsIContentPolicy.TYPE_DOCUMENT, + "sanity check - loading a top level document" + ); + + let loadingPrincipal = channel.loadInfo.loadingPrincipal; + is( + loadingPrincipal, + null, + "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal" + ); + } + ); + BrowserTestUtils.removeTab(tab); + } + ); +}); diff --git a/browser/base/content/test/tabs/browser_e10s_about_process.js b/browser/base/content/test/tabs/browser_e10s_about_process.js new file mode 100644 index 0000000000..f73e8e659c --- /dev/null +++ b/browser/base/content/test/tabs/browser_e10s_about_process.js @@ -0,0 +1,174 @@ +const CHROME = { + id: "cb34538a-d9da-40f3-b61a-069f0b2cb9fb", + path: "test-chrome", + flags: 0, +}; +const CANREMOTE = { + id: "2480d3e1-9ce4-4b84-8ae3-910b9a95cbb3", + path: "test-allowremote", + flags: Ci.nsIAboutModule.URI_CAN_LOAD_IN_CHILD, +}; +const MUSTREMOTE = { + id: "f849cee5-e13e-44d2-981d-0fb3884aaead", + path: "test-mustremote", + flags: Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD, +}; +const CANPRIVILEGEDREMOTE = { + id: "a04ffafe-6c63-4266-acae-0f4b093165aa", + path: "test-canprivilegedremote", + flags: + Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD | + Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS, +}; +const MUSTEXTENSION = { + id: "f7a1798f-965b-49e9-be83-ec6ee4d7d675", + path: "test-mustextension", + flags: Ci.nsIAboutModule.URI_MUST_LOAD_IN_EXTENSION_PROCESS, +}; + +const TEST_MODULES = [ + CHROME, + CANREMOTE, + MUSTREMOTE, + CANPRIVILEGEDREMOTE, + MUSTEXTENSION, +]; + +function AboutModule() {} + +AboutModule.prototype = { + newChannel(aURI, aLoadInfo) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + getURIFlags(aURI) { + for (let module of TEST_MODULES) { + if (aURI.pathQueryRef.startsWith(module.path)) { + return module.flags; + } + } + + ok(false, "Called getURIFlags for an unknown page " + aURI.spec); + return 0; + }, + + getIndexedDBOriginPostfix(aURI) { + return null; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), +}; + +var AboutModuleFactory = { + createInstance(aIID) { + return new AboutModule().QueryInterface(aIID); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), +}; + +add_setup(async function () { + SpecialPowers.setBoolPref( + "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", + true + ); + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + for (let module of TEST_MODULES) { + registrar.registerFactory( + Components.ID(module.id), + "", + "@mozilla.org/network/protocol/about;1?what=" + module.path, + AboutModuleFactory + ); + } +}); + +registerCleanupFunction(() => { + SpecialPowers.clearUserPref( + "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess" + ); + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + for (let module of TEST_MODULES) { + registrar.unregisterFactory(Components.ID(module.id), AboutModuleFactory); + } +}); + +add_task(async function test_chrome() { + test_url_for_process_types({ + url: "about:" + CHROME.path, + chromeResult: true, + webContentResult: false, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); +}); + +add_task(async function test_any() { + test_url_for_process_types({ + url: "about:" + CANREMOTE.path, + chromeResult: true, + webContentResult: true, + privilegedAboutContentResult: true, + privilegedMozillaContentResult: true, + extensionProcessResult: true, + }); +}); + +add_task(async function test_remote() { + test_url_for_process_types({ + url: "about:" + MUSTREMOTE.path, + chromeResult: false, + webContentResult: true, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); +}); + +add_task(async function test_privileged_remote_true() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.remote.separatePrivilegedContentProcess", true]], + }); + + // This shouldn't be taken literally. We will always use the privleged about + // content type if the URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS flag is enabled and + // the pref is turned on. + test_url_for_process_types({ + url: "about:" + CANPRIVILEGEDREMOTE.path, + chromeResult: false, + webContentResult: false, + privilegedAboutContentResult: true, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); +}); + +add_task(async function test_privileged_remote_false() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.remote.separatePrivilegedContentProcess", false]], + }); + + // This shouldn't be taken literally. We will always use the privleged about + // content type if the URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS flag is enabled and + // the pref is turned on. + test_url_for_process_types({ + url: "about:" + CANPRIVILEGEDREMOTE.path, + chromeResult: false, + webContentResult: true, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); +}); + +add_task(async function test_extension() { + test_url_for_process_types({ + url: "about:" + MUSTEXTENSION.path, + chromeResult: false, + webContentResult: false, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: true, + }); +}); diff --git a/browser/base/content/test/tabs/browser_e10s_chrome_process.js b/browser/base/content/test/tabs/browser_e10s_chrome_process.js new file mode 100644 index 0000000000..c7590a0954 --- /dev/null +++ b/browser/base/content/test/tabs/browser_e10s_chrome_process.js @@ -0,0 +1,136 @@ +// Returns a function suitable for add_task which loads startURL, runs +// transitionTask and waits for endURL to load, checking that the URLs were +// loaded in the correct process. +function makeTest( + name, + startURL, + startProcessIsRemote, + endURL, + endProcessIsRemote, + transitionTask +) { + return async function () { + info("Running test " + name + ", " + transitionTask.name); + let browser = gBrowser.selectedBrowser; + + // In non-e10s nothing should be remote + if (!gMultiProcessBrowser) { + startProcessIsRemote = false; + endProcessIsRemote = false; + } + + // Load the initial URL and make sure we are in the right initial process + info("Loading initial URL"); + BrowserTestUtils.startLoadingURIString(browser, startURL); + await BrowserTestUtils.browserLoaded(browser, false, startURL); + + is(browser.currentURI.spec, startURL, "Shouldn't have been redirected"); + is( + browser.isRemoteBrowser, + startProcessIsRemote, + "Should be displayed in the right process" + ); + + let docLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + endURL + ); + await transitionTask(browser, endURL); + await docLoadedPromise; + + is(browser.currentURI.spec, endURL, "Should have made it to the final URL"); + is( + browser.isRemoteBrowser, + endProcessIsRemote, + "Should be displayed in the right process" + ); + }; +} + +const PATH = ( + getRootDirectory(gTestPath) + "test_process_flags_chrome.html" +).replace("chrome://mochitests", ""); + +const CHROME = "chrome://mochitests" + PATH; +const CANREMOTE = "chrome://mochitests-any" + PATH; +const MUSTREMOTE = "chrome://mochitests-content" + PATH; + +add_setup(async function () { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + forceNotRemote: true, + }); +}); + +registerCleanupFunction(() => { + gBrowser.removeCurrentTab(); +}); + +add_task(async function test_chrome() { + test_url_for_process_types({ + url: CHROME, + chromeResult: true, + webContentResult: false, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); +}); + +add_task(async function test_any() { + test_url_for_process_types({ + url: CANREMOTE, + chromeResult: true, + webContentResult: true, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); +}); + +add_task(async function test_remote() { + test_url_for_process_types({ + url: MUSTREMOTE, + chromeResult: false, + webContentResult: true, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); +}); + +// The set of page transitions +var TESTS = [ + ["chrome -> chrome", CHROME, false, CHROME, false], + ["chrome -> canremote", CHROME, false, CANREMOTE, false], + ["chrome -> mustremote", CHROME, false, MUSTREMOTE, true], + ["remote -> chrome", MUSTREMOTE, true, CHROME, false], + ["remote -> canremote", MUSTREMOTE, true, CANREMOTE, true], + ["remote -> mustremote", MUSTREMOTE, true, MUSTREMOTE, true], +]; + +// The different ways to transition from one page to another +var TRANSITIONS = [ + // Loads the new page by calling browser.loadURI directly + async function loadURI(browser, uri) { + info("Calling browser.loadURI"); + BrowserTestUtils.startLoadingURIString(browser, uri); + }, + + // Loads the new page by finding a link with the right href in the document and + // clicking it + function clickLink(browser, uri) { + info("Clicking link"); + SpecialPowers.spawn(browser, [uri], function frame_script(frameUri) { + let link = content.document.querySelector("a[href='" + frameUri + "']"); + link.click(); + }); + }, +]; + +// Creates a set of test tasks, one for each combination of TESTS and TRANSITIONS. +for (let test of TESTS) { + for (let transition of TRANSITIONS) { + add_task(makeTest(...test, transition)); + } +} diff --git a/browser/base/content/test/tabs/browser_e10s_javascript.js b/browser/base/content/test/tabs/browser_e10s_javascript.js new file mode 100644 index 0000000000..ffb03b4d79 --- /dev/null +++ b/browser/base/content/test/tabs/browser_e10s_javascript.js @@ -0,0 +1,19 @@ +const CHROME_PROCESS = E10SUtils.NOT_REMOTE; +const WEB_CONTENT_PROCESS = E10SUtils.WEB_REMOTE_TYPE; + +add_task(async function () { + let url = "javascript:dosomething()"; + + ok( + E10SUtils.canLoadURIInRemoteType(url, /* fission */ false, CHROME_PROCESS), + "Check URL in chrome process." + ); + ok( + E10SUtils.canLoadURIInRemoteType( + url, + /* fission */ false, + WEB_CONTENT_PROCESS + ), + "Check URL in web content process." + ); +}); diff --git a/browser/base/content/test/tabs/browser_e10s_mozillaweb_process.js b/browser/base/content/test/tabs/browser_e10s_mozillaweb_process.js new file mode 100644 index 0000000000..88542a0b16 --- /dev/null +++ b/browser/base/content/test/tabs/browser_e10s_mozillaweb_process.js @@ -0,0 +1,52 @@ +add_task(async function test_privileged_remote_true() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.separatePrivilegedContentProcess", true], + ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true], + ["browser.tabs.remote.separatedMozillaDomains", "example.org"], + ], + }); + + test_url_for_process_types({ + url: "https://example.com", + chromeResult: false, + webContentResult: true, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); + test_url_for_process_types({ + url: "https://example.org", + chromeResult: false, + webContentResult: false, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: true, + extensionProcessResult: false, + }); +}); + +add_task(async function test_privileged_remote_false() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.separatePrivilegedContentProcess", true], + ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", false], + ], + }); + + test_url_for_process_types({ + url: "https://example.com", + chromeResult: false, + webContentResult: true, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); + test_url_for_process_types({ + url: "https://example.org", + chromeResult: false, + webContentResult: true, + privilegedAboutContentResult: false, + privilegedMozillaContentResult: false, + extensionProcessResult: false, + }); +}); diff --git a/browser/base/content/test/tabs/browser_e10s_switchbrowser.js b/browser/base/content/test/tabs/browser_e10s_switchbrowser.js new file mode 100644 index 0000000000..36d6bfbece --- /dev/null +++ b/browser/base/content/test/tabs/browser_e10s_switchbrowser.js @@ -0,0 +1,493 @@ +requestLongerTimeout(2); + +const DUMMY_PATH = "browser/browser/base/content/test/general/dummy_page.html"; + +const gExpectedHistory = { + index: -1, + entries: [], +}; + +async function get_remote_history(browser) { + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + let sessionHistory = browser.browsingContext?.sessionHistory; + if (!sessionHistory) { + return null; + } + + let result = { + index: sessionHistory.index, + entries: [], + }; + + for (let i = 0; i < sessionHistory.count; i++) { + let entry = sessionHistory.getEntryAtIndex(i); + result.entries.push({ + uri: entry.URI.spec, + title: entry.title, + }); + } + return result; + } + + return SpecialPowers.spawn(browser, [], () => { + let webNav = content.docShell.QueryInterface(Ci.nsIWebNavigation); + let sessionHistory = webNav.sessionHistory; + let result = { + index: sessionHistory.index, + entries: [], + }; + + for (let i = 0; i < sessionHistory.count; i++) { + let entry = sessionHistory.legacySHistory.getEntryAtIndex(i); + result.entries.push({ + uri: entry.URI.spec, + title: entry.title, + }); + } + + return result; + }); +} + +var check_history = async function () { + let sessionHistory = await get_remote_history(gBrowser.selectedBrowser); + + let count = sessionHistory.entries.length; + is( + count, + gExpectedHistory.entries.length, + "Should have the right number of history entries" + ); + is( + sessionHistory.index, + gExpectedHistory.index, + "Should have the right history index" + ); + + for (let i = 0; i < count; i++) { + let entry = sessionHistory.entries[i]; + info("Checking History Entry: " + entry.uri); + is(entry.uri, gExpectedHistory.entries[i].uri, "Should have the right URI"); + is( + entry.title, + gExpectedHistory.entries[i].title, + "Should have the right title" + ); + } +}; + +function clear_history() { + gExpectedHistory.index = -1; + gExpectedHistory.entries = []; +} + +// Waits for a load and updates the known history +var waitForLoad = async function (uriString) { + info("Loading " + uriString); + // Longwinded but this ensures we don't just shortcut to LoadInNewProcess + let loadURIOptions = { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }; + gBrowser.selectedBrowser.webNavigation.loadURI( + Services.io.newURI(uriString), + loadURIOptions + ); + + await BrowserTestUtils.browserStopped(gBrowser, uriString); + + // Some of the documents we're using in this test use Fluent, + // and they may finish localization later. + // To prevent this test from being intermittent, we'll + // wait for the `document.l10n.ready` promise to resolve. + if ( + gBrowser.selectedBrowser.contentWindow && + gBrowser.selectedBrowser.contentWindow.document.l10n + ) { + await gBrowser.selectedBrowser.contentWindow.document.l10n.ready; + } + gExpectedHistory.index++; + gExpectedHistory.entries.push({ + uri: gBrowser.currentURI.spec, + title: gBrowser.contentTitle, + }); +}; + +// Waits for a load and updates the known history +var waitForLoadWithFlags = async function ( + uriString, + flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE +) { + info("Loading " + uriString + " flags = " + flags); + gBrowser.selectedBrowser.loadURI(Services.io.newURI(uriString), { + flags, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + + await BrowserTestUtils.browserStopped(gBrowser, uriString); + if (!(flags & Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY)) { + if (flags & Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY) { + gExpectedHistory.entries.pop(); + } else { + gExpectedHistory.index++; + } + + gExpectedHistory.entries.push({ + uri: gBrowser.currentURI.spec, + title: gBrowser.contentTitle, + }); + } +}; + +var back = async function () { + info("Going back"); + gBrowser.goBack(); + await BrowserTestUtils.browserStopped(gBrowser); + gExpectedHistory.index--; +}; + +var forward = async function () { + info("Going forward"); + gBrowser.goForward(); + await BrowserTestUtils.browserStopped(gBrowser); + gExpectedHistory.index++; +}; + +// Tests that navigating from a page that should be in the remote process and +// a page that should be in the main process works and retains history +add_task(async function test_navigation() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + + let expectedRemote = gMultiProcessBrowser; + + info("1"); + // Create a tab and load a remote page in it + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + skipAnimation: true, + }); + let { permanentKey } = gBrowser.selectedBrowser; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await waitForLoad("http://example.org/" + DUMMY_PATH); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + + info("2"); + // Load another page + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await waitForLoad("http://example.com/" + DUMMY_PATH); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + await check_history(); + + info("3"); + // Load a non-remote page + await waitForLoad("about:robots"); + await TestUtils.waitForCondition( + () => !!gBrowser.selectedBrowser.contentTitle.length, + "Waiting for about:robots title to update" + ); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + false, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + await check_history(); + + info("4"); + // Load a remote page + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await waitForLoad("http://example.org/" + DUMMY_PATH); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + await check_history(); + + info("5"); + await back(); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + false, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + await TestUtils.waitForCondition( + () => !!gBrowser.selectedBrowser.contentTitle.length, + "Waiting for about:robots title to update" + ); + await check_history(); + + info("6"); + await back(); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + await check_history(); + + info("7"); + await forward(); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + false, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + await TestUtils.waitForCondition( + () => !!gBrowser.selectedBrowser.contentTitle.length, + "Waiting for about:robots title to update" + ); + await check_history(); + + info("8"); + await forward(); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + await check_history(); + + info("9"); + await back(); + await TestUtils.waitForCondition( + () => !!gBrowser.selectedBrowser.contentTitle.length, + "Waiting for about:robots title to update" + ); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + false, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + await check_history(); + + info("10"); + // Load a new remote page, this should replace the last history entry + gExpectedHistory.entries.splice(gExpectedHistory.entries.length - 1, 1); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await waitForLoad("http://example.com/" + DUMMY_PATH); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + await check_history(); + + info("11"); + gBrowser.removeCurrentTab(); + clear_history(); +}); + +// Tests that calling gBrowser.loadURI or browser.loadURI to load a page in a +// different process updates the browser synchronously +add_task(async function test_synchronous() { + let expectedRemote = gMultiProcessBrowser; + + info("1"); + // Create a tab and load a remote page in it + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + skipAnimation: true, + }); + let { permanentKey } = gBrowser.selectedBrowser; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await waitForLoad("http://example.org/" + DUMMY_PATH); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + + info("2"); + // Load another page + info("Loading about:robots"); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + "about:robots" + ); + await BrowserTestUtils.browserStopped(gBrowser); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + false, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + + info("3"); + // Load the remote page again + info("Loading http://example.org/" + DUMMY_PATH); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/" + DUMMY_PATH + ); + await BrowserTestUtils.browserStopped(gBrowser); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + is( + gBrowser.selectedBrowser.permanentKey, + permanentKey, + "browser.permanentKey is still the same" + ); + + info("4"); + gBrowser.removeCurrentTab(); + clear_history(); +}); + +// Tests that load flags are correctly passed through to the child process with +// normal loads +add_task(async function test_loadflags() { + let expectedRemote = gMultiProcessBrowser; + + info("1"); + // Create a tab and load a remote page in it + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + skipAnimation: true, + }); + await waitForLoadWithFlags("about:robots"); + await TestUtils.waitForCondition( + () => gBrowser.selectedBrowser.contentTitle != "about:robots", + "Waiting for about:robots title to update" + ); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + false, + "Remote attribute should be correct" + ); + await check_history(); + + info("2"); + // Load a page in the remote process with some custom flags + await waitForLoadWithFlags( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/" + DUMMY_PATH, + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY + ); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + await check_history(); + + info("3"); + // Load a non-remote page + await waitForLoadWithFlags("about:robots"); + await TestUtils.waitForCondition( + () => !!gBrowser.selectedBrowser.contentTitle.length, + "Waiting for about:robots title to update" + ); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + false, + "Remote attribute should be correct" + ); + await check_history(); + + info("4"); + // Load another remote page + await waitForLoadWithFlags( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/" + DUMMY_PATH, + Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY + ); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + await check_history(); + + info("5"); + // Load another remote page from a different origin + await waitForLoadWithFlags( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/" + DUMMY_PATH, + Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY + ); + is( + gBrowser.selectedBrowser.isRemoteBrowser, + expectedRemote, + "Remote attribute should be correct" + ); + await check_history(); + + is( + gExpectedHistory.entries.length, + 2, + "Should end with the right number of history entries" + ); + + info("6"); + gBrowser.removeCurrentTab(); + clear_history(); +}); diff --git a/browser/base/content/test/tabs/browser_file_to_http_named_popup.js b/browser/base/content/test/tabs/browser_file_to_http_named_popup.js new file mode 100644 index 0000000000..57e5ec7ad3 --- /dev/null +++ b/browser/base/content/test/tabs/browser_file_to_http_named_popup.js @@ -0,0 +1,60 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +const TEST_FILE = fileURL("dummy_page.html"); +const TEST_HTTP = httpURL("dummy_page.html"); + +// Test for Bug 1634252 +add_task(async function () { + await BrowserTestUtils.withNewTab(TEST_FILE, async function (fileBrowser) { + info("Tab ready"); + + async function summonPopup(firstRun) { + var winPromise; + if (firstRun) { + winPromise = BrowserTestUtils.waitForNewWindow({ + url: TEST_HTTP, + }); + } + + await SpecialPowers.spawn( + fileBrowser, + [TEST_HTTP, firstRun], + (target, firstRun_) => { + var win = content.open(target, "named", "width=400,height=400"); + win.focus(); + ok(win, "window.open was successful"); + if (firstRun_) { + content.document.firstWindow = win; + } else { + content.document.otherWindow = win; + } + } + ); + + if (firstRun) { + // We should only wait for the window the first time, because only the + // later times no new window should be created. + info("Waiting for new window"); + var win = await winPromise; + ok(win, "Got a window"); + } + } + + info("Opening window"); + await summonPopup(true); + info("Opening window again"); + await summonPopup(false); + + await SpecialPowers.spawn(fileBrowser, [], () => { + ok(content.document.firstWindow, "Window is non-null"); + is( + content.document.otherWindow, + content.document.firstWindow, + "Windows are the same" + ); + + content.document.firstWindow.close(); + }); + }); +}); diff --git a/browser/base/content/test/tabs/browser_file_to_http_script_closable.js b/browser/base/content/test/tabs/browser_file_to_http_script_closable.js new file mode 100644 index 0000000000..00ef3d7322 --- /dev/null +++ b/browser/base/content/test/tabs/browser_file_to_http_script_closable.js @@ -0,0 +1,43 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +const TEST_FILE = fileURL("dummy_page.html"); +const TEST_HTTP = httpURL("tab_that_closes.html"); + +// Test for Bug 1632441 +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.allow_scripts_to_close_windows", false]], + }); + + await BrowserTestUtils.withNewTab(TEST_FILE, async function (fileBrowser) { + info("Tab ready"); + + // The request will open a new tab, capture the new tab and the load in it. + info("Creating promise"); + var newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + url => { + return url.endsWith("tab_that_closes.html"); + }, + true, + false + ); + + // Click the link, which will post to target="_blank" + info("Creating and clicking link"); + await SpecialPowers.spawn(fileBrowser, [TEST_HTTP], target => { + content.open(target); + }); + + // The new tab will load. + info("Waiting for load"); + var newTab = await newTabPromise; + ok(newTab, "Tab is loaded"); + info("waiting for it to close"); + await BrowserTestUtils.waitForTabClosing(newTab); + ok(true, "The test completes without a timeout"); + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/base/content/test/tabs/browser_hiddentab_contextmenu.js b/browser/base/content/test/tabs/browser_hiddentab_contextmenu.js new file mode 100644 index 0000000000..5c54896efb --- /dev/null +++ b/browser/base/content/test/tabs/browser_hiddentab_contextmenu.js @@ -0,0 +1,34 @@ +/* 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/. */ + +// Tests the context menu of hidden tabs which users can open via the All Tabs +// menu's Hidden Tabs view. + +add_task(async function test() { + is(gBrowser.visibleTabs.length, 1, "there is initially one visible tab"); + + let tab = BrowserTestUtils.addTab(gBrowser); + gBrowser.hideTab(tab); + ok(tab.hidden, "new tab is hidden"); + is(gBrowser.visibleTabs.length, 1, "there is still only one visible tabs"); + + updateTabContextMenu(tab); + ok( + document.getElementById("context_moveTabOptions").disabled, + "Move Tab menu is disabled" + ); + ok( + document.getElementById("context_closeTabsToTheStart").disabled, + "Close Tabs to Left is disabled" + ); + ok( + document.getElementById("context_closeTabsToTheEnd").disabled, + "Close Tabs to Right is disabled" + ); + ok( + document.getElementById("context_reopenInContainer").disabled, + "Open in New Container Tab menu is disabled" + ); + gBrowser.removeTab(tab); +}); diff --git a/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js b/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js new file mode 100644 index 0000000000..665bdb7f69 --- /dev/null +++ b/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js @@ -0,0 +1,157 @@ +"use strict"; + +// Helper that watches events that may be triggered when tab browsers are +// swapped during the test. +// +// The primary purpose of this helper is to access tab browser properties +// during tab events, to verify that there are no undesired side effects, as a +// regression test for https://bugzilla.mozilla.org/show_bug.cgi?id=1695346 +class TabEventTracker { + constructor(tab) { + this.tab = tab; + + tab.addEventListener("TabAttrModified", this); + tab.addEventListener("TabShow", this); + tab.addEventListener("TabHide", this); + } + + handleEvent(event) { + let description = `${this._expectations.description} at ${event.type}`; + if (event.type === "TabAttrModified") { + description += `, changed=${event.detail.changed}`; + } + + const browser = this.tab.linkedBrowser; + is( + browser.currentURI.spec, + this._expectations.tabUrl, + `${description} - expected currentURI` + ); + ok(browser._cachedCurrentURI, `${description} - currentURI was cached`); + + if (event.type === "TabAttrModified") { + if (event.detail.changed.includes("muted")) { + if (browser.audioMuted) { + this._counts.muted++; + } else { + this._counts.unmuted++; + } + } + } else if (event.type === "TabShow") { + this._counts.shown++; + } else if (event.type === "TabHide") { + this._counts.hidden++; + } else { + ok(false, `Unexpected event: ${event.type}`); + } + } + + setExpectations(expectations) { + this._expectations = expectations; + + this._counts = { + muted: 0, + unmuted: 0, + shown: 0, + hidden: 0, + }; + } + + checkExpectations() { + const { description, counters, tabUrl } = this._expectations; + Assert.deepEqual( + this._counts, + counters, + `${description} - events observed while swapping tab` + ); + let browser = this.tab.linkedBrowser; + is(browser.currentURI.spec, tabUrl, `${description} - tab's currentURI`); + + // Tabs without titles default to URLs without scheme, according to the + // logic of tabbrowser.js's setTabTitle/_setTabLabel. + // TODO bug 1695512: lazy tabs deviate from that expectation, so the title + // is the full URL instead of the URL with the scheme stripped. + let tabTitle = tabUrl; + is(browser.contentTitle, tabTitle, `${description} - tab's contentTitle`); + } +} + +add_task(async function test_hidden_muted_lazy_tabs_and_swapping() { + const params = { createLazyBrowser: true }; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const URL_HIDDEN = "http://example.com/hide"; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const URL_MUTED = "http://example.com/mute"; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const URL_NORMAL = "http://example.com/back"; + + const lazyTab = BrowserTestUtils.addTab(gBrowser, "", params); + const mutedTab = BrowserTestUtils.addTab(gBrowser, URL_MUTED, params); + const hiddenTab = BrowserTestUtils.addTab(gBrowser, URL_HIDDEN, params); + const normalTab = BrowserTestUtils.addTab(gBrowser, URL_NORMAL, params); + + mutedTab.toggleMuteAudio(); + gBrowser.hideTab(hiddenTab); + + is(lazyTab.linkedPanel, "", "lazyTab is lazy"); + is(hiddenTab.linkedPanel, "", "hiddenTab is lazy"); + is(mutedTab.linkedPanel, "", "mutedTab is lazy"); + is(normalTab.linkedPanel, "", "normalTab is lazy"); + + ok(mutedTab.linkedBrowser.audioMuted, "mutedTab is muted"); + ok(hiddenTab.hidden, "hiddenTab is hidden"); + ok(!lazyTab.linkedBrowser.audioMuted, "lazyTab was not muted"); + ok(!lazyTab.hidden, "lazyTab was not hidden"); + + const tabEventTracker = new TabEventTracker(lazyTab); + + tabEventTracker.setExpectations({ + description: "mutedTab replaced lazyTab (initial)", + counters: { + muted: 1, + unmuted: 0, + shown: 0, + hidden: 0, + }, + tabUrl: URL_MUTED, + }); + gBrowser.swapBrowsersAndCloseOther(lazyTab, mutedTab); + tabEventTracker.checkExpectations(); + is(lazyTab.linkedPanel, "", "muted lazyTab is still lazy"); + ok(lazyTab.linkedBrowser.audioMuted, "muted lazyTab is now muted"); + ok(!lazyTab.hidden, "muted lazyTab is not hidden"); + + tabEventTracker.setExpectations({ + description: "hiddenTab replaced lazyTab/mutedTab", + counters: { + muted: 0, + unmuted: 1, + shown: 0, + hidden: 1, + }, + tabUrl: URL_HIDDEN, + }); + gBrowser.swapBrowsersAndCloseOther(lazyTab, hiddenTab); + tabEventTracker.checkExpectations(); + is(lazyTab.linkedPanel, "", "hidden lazyTab is still lazy"); + ok(!lazyTab.linkedBrowser.audioMuted, "hidden lazyTab is not muted any more"); + ok(lazyTab.hidden, "hidden lazyTab has been hidden"); + + tabEventTracker.setExpectations({ + description: "normalTab replaced lazyTab/hiddenTab", + counters: { + muted: 0, + unmuted: 0, + shown: 1, + hidden: 0, + }, + tabUrl: URL_NORMAL, + }); + gBrowser.swapBrowsersAndCloseOther(lazyTab, normalTab); + tabEventTracker.checkExpectations(); + is(lazyTab.linkedPanel, "", "normal lazyTab is still lazy"); + ok(!lazyTab.linkedBrowser.audioMuted, "normal lazyTab is not muted any more"); + ok(!lazyTab.hidden, "normal lazyTab is not hidden any more"); + + BrowserTestUtils.removeTab(lazyTab); +}); diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_blank_page.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_blank_page.js new file mode 100644 index 0000000000..f01d36fa16 --- /dev/null +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_blank_page.js @@ -0,0 +1,143 @@ +/* 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 the behavior of the tab and the urlbar when opening about:blank by clicking link. + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js", + this +); + +add_task(async function blank_target__foreground() { + await doTestInSameWindow({ + link: "blank-page--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: "", + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: BLANK_TITLE, + urlbar: "", + history: [BLANK_URL], + }, + }); +}); + +add_task(async function blank_target__background() { + await doTestInSameWindow({ + link: "blank-page--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [BLANK_URL], + }, + }); +}); + +add_task(async function other_target__foreground() { + await doTestInSameWindow({ + link: "blank-page--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + history: [BLANK_URL], + }, + }); +}); + +add_task(async function other_target__background() { + await doTestInSameWindow({ + link: "blank-page--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [BLANK_URL], + }, + }); +}); + +add_task(async function by_script() { + await doTestInSameWindow({ + link: "blank-page--by-script", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + history: [BLANK_URL], + }, + }); +}); + +add_task(async function no_target() { + await doTestInSameWindow({ + link: "blank-page--no-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + // Inherit the title and URL until finishing loading a new link when the + // link is opened in same tab. + tab: HOME_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + history: [HOME_URL, BLANK_URL], + }, + }); +}); diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_new_window.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_new_window.js new file mode 100644 index 0000000000..6d18887941 --- /dev/null +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_new_window.js @@ -0,0 +1,54 @@ +/* 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 the behavior of the tab and the urlbar on new window opened by clicking +// link. + +/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js", + this +); + +add_task(async function normal_page__blank_target() { + await doTestWithNewWindow({ + link: "wait-a-bit--blank-target", + expectedSetURICalled: true, + }); +}); + +add_task(async function normal_page__other_target() { + await doTestWithNewWindow({ + link: "wait-a-bit--other-target", + expectedSetURICalled: false, + }); +}); + +add_task(async function normal_page__by_script() { + await doTestWithNewWindow({ + link: "wait-a-bit--by-script", + expectedSetURICalled: false, + }); +}); + +add_task(async function blank_page__blank_target() { + await doTestWithNewWindow({ + link: "blank-page--blank-target", + expectedSetURICalled: false, + }); +}); + +add_task(async function blank_page__other_target() { + await doTestWithNewWindow({ + link: "blank-page--other-target", + expectedSetURICalled: false, + }); +}); + +add_task(async function blank_page__by_script() { + await doTestWithNewWindow({ + link: "blank-page--by-script", + expectedSetURICalled: false, + }); +}); diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js new file mode 100644 index 0000000000..f8773e3720 --- /dev/null +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js @@ -0,0 +1,203 @@ +/* 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 the behavior of the tab and the urlbar when opening normal web page by +// clicking link that the target is "_blank". + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js", + this +); + +add_task(async function normal_page__foreground__click() { + await doTestInSameWindow({ + link: "wait-a-bit--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: WAIT_A_BIT_PAGE_TITLE, + urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), + history: [WAIT_A_BIT_URL], + }, + }); +}); + +add_task(async function normal_page__foreground__contextmenu() { + await doTestInSameWindow({ + link: "wait-a-bit--blank-target", + openBy: OPEN_BY.CONTEXT_MENU, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: WAIT_A_BIT_PAGE_TITLE, + urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), + history: [WAIT_A_BIT_URL], + }, + }); +}); + +add_task(async function normal_page__foreground__abort() { + await doTestInSameWindow({ + link: "wait-a-bit--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Abort loading"); + document.getElementById("stop-button").click(); + }, + finalState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), + history: [WAIT_A_BIT_URL], + }, + }); +}); + +add_task(async function normal_page__foreground__timeout() { + await doTestInSameWindow({ + link: "request-timeout--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: REQUEST_TIMEOUT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(REQUEST_TIMEOUT_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: REQUEST_TIMEOUT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(REQUEST_TIMEOUT_URL), + history: [REQUEST_TIMEOUT_URL], + }, + }); +}); + +add_task(async function normal_page__foreground__session_restore() { + await doSessionRestoreTest({ + link: "wait-a-bit--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + expectedSessionHistory: [WAIT_A_BIT_URL], + expectedSessionRestored: true, + }); +}); + +add_task(async function normal_page__background__click() { + await doTestInSameWindow({ + link: "wait-a-bit--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + loadingState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: WAIT_A_BIT_PAGE_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [WAIT_A_BIT_URL], + }, + }); +}); + +add_task(async function normal_page__background__contextmenu() { + await doTestInSameWindow({ + link: "wait-a-bit--blank-target", + openBy: OPEN_BY.CONTEXT_MENU, + openAs: OPEN_AS.BACKGROUND, + loadingState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: WAIT_A_BIT_PAGE_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [WAIT_A_BIT_URL], + }, + }); +}); + +add_task(async function normal_page__background__abort() { + await doTestInSameWindow({ + link: "wait-a-bit--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + loadingState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Abort loading"); + document.getElementById("stop-button").click(); + }, + finalState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [], + }, + }); +}); + +add_task(async function normal_page__background__timeout() { + await doTestInSameWindow({ + link: "request-timeout--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + loadingState: { + tab: REQUEST_TIMEOUT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: REQUEST_TIMEOUT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [REQUEST_TIMEOUT_URL], + }, + }); +}); + +add_task(async function normal_page__background__session_restore() { + await doSessionRestoreTest({ + link: "wait-a-bit--blank-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + expectedSessionRestored: false, + }); +}); diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js new file mode 100644 index 0000000000..07cf7a8ea2 --- /dev/null +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js @@ -0,0 +1,87 @@ +/* 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 the behavior of the tab and the urlbar when opening normal web page by +// clicking link that opens by script. + +/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js", + this +); +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); + +add_task(async function normal_page__by_script() { + await doTestInSameWindow({ + link: "wait-a-bit--by-script", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: BLANK_URL, + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: WAIT_A_BIT_PAGE_TITLE, + urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), + history: [WAIT_A_BIT_URL], + }, + }); +}); + +add_task(async function normal_page__by_script__abort() { + await doTestInSameWindow({ + link: "wait-a-bit--by-script", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Abort loading"); + document.getElementById("stop-button").click(); + }, + finalState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + history: [], + }, + }); +}); + +add_task(async function normal_page__by_script__timeout() { + await doTestInSameWindow({ + link: "request-timeout--by-script", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: REQUEST_TIMEOUT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(REQUEST_TIMEOUT_URL), + history: [REQUEST_TIMEOUT_URL], + }, + }); +}); + +add_task(async function normal_page__by_script__session_restore() { + await doSessionRestoreTest({ + link: "wait-a-bit--by-script", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + expectedSessionRestored: false, + }); +}); diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js new file mode 100644 index 0000000000..ab18d7c7e0 --- /dev/null +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js @@ -0,0 +1,90 @@ +/* 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 the behavior of the tab and the urlbar when opening normal web page by +// clicking link that has no target. + +/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js", + this +); + +const { UrlbarTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" +); + +add_task(async function normal_page__no_target() { + await doTestInSameWindow({ + link: "wait-a-bit--no-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + // Inherit the title and URL until finishing loading a new link when the + // link is opened in same tab. + tab: HOME_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: WAIT_A_BIT_PAGE_TITLE, + urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), + history: [HOME_URL, WAIT_A_BIT_URL], + }, + }); +}); + +add_task(async function normal_page__no_target__abort() { + await doTestInSameWindow({ + link: "wait-a-bit--no-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: HOME_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Abort loading"); + document.getElementById("stop-button").click(); + }, + finalState: { + tab: HOME_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [HOME_URL], + }, + }); +}); + +add_task(async function normal_page__no_target__timeout() { + await doTestInSameWindow({ + link: "request-timeout--no-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: HOME_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: REQUEST_TIMEOUT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(REQUEST_TIMEOUT_URL), + history: [HOME_URL, REQUEST_TIMEOUT_URL], + }, + }); +}); + +add_task(async function normal_page__no_target__session_restore() { + await doSessionRestoreTest({ + link: "wait-a-bit--no-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + expectedSessionRestored: false, + }); +}); diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js new file mode 100644 index 0000000000..7dc0e8fa45 --- /dev/null +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js @@ -0,0 +1,160 @@ +/* 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 the behavior of the tab and the urlbar when opening normal web page by +// clicking link that the target is "other". + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js", + this +); + +add_task(async function normal_page__other_target__foreground() { + await doTestInSameWindow({ + link: "wait-a-bit--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: WAIT_A_BIT_PAGE_TITLE, + urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), + history: [WAIT_A_BIT_URL], + }, + }); +}); + +add_task(async function normal_page__other_target__foreground__abort() { + await doTestInSameWindow({ + link: "wait-a-bit--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Abort loading"); + document.getElementById("stop-button").click(); + }, + finalState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + history: [], + }, + }); +}); + +add_task(async function normal_page__other_target__foreground__timeout() { + await doTestInSameWindow({ + link: "request-timeout--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + loadingState: { + tab: BLANK_TITLE, + urlbar: UrlbarTestUtils.trimURL(BLANK_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: REQUEST_TIMEOUT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(REQUEST_TIMEOUT_URL), + history: [REQUEST_TIMEOUT_URL], + }, + }); +}); + +add_task(async function normal_page__foreground__session_restore() { + await doSessionRestoreTest({ + link: "wait-a-bit--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.FOREGROUND, + expectedSessionRestored: false, + }); +}); + +add_task(async function normal_page__other_target__background() { + await doTestInSameWindow({ + link: "wait-a-bit--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + loadingState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: WAIT_A_BIT_PAGE_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [WAIT_A_BIT_URL], + }, + }); +}); + +add_task(async function normal_page__other_target__background__abort() { + await doTestInSameWindow({ + link: "wait-a-bit--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + loadingState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Abort loading"); + document.getElementById("stop-button").click(); + }, + finalState: { + tab: WAIT_A_BIT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [], + }, + }); +}); + +add_task(async function normal_page__other_target__background__timeout() { + await doTestInSameWindow({ + link: "request-timeout--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + loadingState: { + tab: REQUEST_TIMEOUT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + }, + async actionWhileLoading(onTabLoaded) { + info("Wait until loading the link target"); + await onTabLoaded; + }, + finalState: { + tab: REQUEST_TIMEOUT_LOADING_TITLE, + urlbar: UrlbarTestUtils.trimURL(HOME_URL), + history: [REQUEST_TIMEOUT_URL], + }, + }); +}); + +add_task(async function normal_page__foreground__session_restore() { + await doSessionRestoreTest({ + link: "wait-a-bit--other-target", + openBy: OPEN_BY.CLICK, + openAs: OPEN_AS.BACKGROUND, + expectedSessionRestored: false, + }); +}); diff --git a/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js b/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js new file mode 100644 index 0000000000..db0571a2c0 --- /dev/null +++ b/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Ensure that tab labels for base64 data: URLs are always truncated + * to ensure that we don't hang trying to paint really long overflown + * text runs. + * This becomes a performance issue with 1mb or so long data URIs; + * this test uses a much shorter one for simplicity's sake. + */ +add_task(async function test_ensure_truncation() { + const MOBY = ` + <!DOCTYPE html> + <meta charset="utf-8"> + Call me Ishmael. Some years ago—never mind how + long precisely—having little or no money in my purse, and nothing particular + to interest me on shore, I thought I would sail about a little and see the + watery part of the world. It is a way I have of driving off the spleen and + regulating the circulation. Whenever I find myself growing grim about the + mouth; whenever it is a damp, drizzly November in my soul; whenever I find + myself involuntarily pausing before coffin warehouses, and bringing up the + rear of every funeral I meet; and especially whenever my hypos get such an + upper hand of me, that it requires a strong moral principle to prevent me + from deliberately stepping into the street, and methodically knocking + people's hats off—then, I account it high time to get to sea as soon as I + can. This is my substitute for pistol and ball. With a philosophical + flourish Cato throws himself upon his sword; I quietly take to the ship. + There is nothing surprising in this. If they but knew it, almost all men in + their degree, some time or other, cherish very nearly the same feelings + towards the ocean with me.`; + + let fileReader = new FileReader(); + const DATA_URL = await new Promise(resolve => { + fileReader.addEventListener("load", e => resolve(fileReader.result)); + fileReader.readAsDataURL(new Blob([MOBY], { type: "text/html" })); + }); + // Substring the full URL to avoid log clutter because Assert will print + // the entire thing. + Assert.stringContains( + DATA_URL.substring(0, 30), + "base64", + "data URL needs to be base64" + ); + + let newTab; + function tabLabelChecker() { + Assert.lessOrEqual( + newTab.label.length, + 501, + "Tab label should not exceed 500 chars + ellipsis." + ); + } + let mutationObserver = new MutationObserver(tabLabelChecker); + registerCleanupFunction(() => mutationObserver.disconnect()); + + gBrowser.tabContainer.addEventListener( + "TabOpen", + event => { + newTab = event.target; + tabLabelChecker(); + mutationObserver.observe(newTab, { + attributeFilter: ["label"], + }); + }, + { once: true } + ); + + await BrowserTestUtils.withNewTab(DATA_URL, async () => { + // Wait another longer-than-tick to ensure more mutation observer things have + // come in. + await new Promise(executeSoon); + + // Check one last time for good measure, for the final label: + tabLabelChecker(); + }); +}); diff --git a/browser/base/content/test/tabs/browser_middle_click_new_tab_button_loads_clipboard.js b/browser/base/content/test/tabs/browser_middle_click_new_tab_button_loads_clipboard.js new file mode 100644 index 0000000000..07d0be0232 --- /dev/null +++ b/browser/base/content/test/tabs/browser_middle_click_new_tab_button_loads_clipboard.js @@ -0,0 +1,255 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const searchclipboardforPref = "browser.tabs.searchclipboardfor.middleclick"; + +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [searchclipboardforPref, true], + // set preloading to false so we can await the new tab being opened. + ["browser.newtab.preload", false], + ], + }); + NewTabPagePreloading.removePreloadedBrowser(window); + // Create an engine to use for the test. + SearchTestUtils.init(this); + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://example.org/", + search_url_get_params: "q={searchTerms}", + }, + { setAsDefaultPrivate: true } + ); + // We overflow tabs, close all the extra ones. + registerCleanupFunction(() => { + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + }); +}); + +add_task(async function middleclick_tabs_newtab_button_with_url_in_clipboard() { + var previousTabsLength = gBrowser.tabs.length; + info("Previous tabs count is " + previousTabsLength); + + let url = "javascript:https://www.example.com/"; + let safeUrl = "https://www.example.com/"; + await new Promise((resolve, reject) => { + SimpleTest.waitForClipboard( + url, + () => { + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(url); + }, + resolve, + () => { + ok(false, "Clipboard copy failed"); + reject(); + } + ); + }); + + info("Middle clicking 'new tab' button"); + let promiseTabLoaded = BrowserTestUtils.waitForNewTab( + gBrowser, + safeUrl, + true + ); + EventUtils.synthesizeMouseAtCenter( + document.getElementById("tabs-newtab-button"), + { button: 1 } + ); + + await promiseTabLoaded; + is(gBrowser.tabs.length, previousTabsLength + 1, "We created a tab"); + is( + gBrowser.currentURI.spec, + safeUrl, + "New Tab URL is the safe content of the clipboard" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task( + async function middleclick_tabs_newtab_button_with_word_in_clipboard() { + var previousTabsLength = gBrowser.tabs.length; + info("Previous tabs count is " + previousTabsLength); + + let word = "word"; + let searchUrl = "https://example.org/?q=word"; + await new Promise((resolve, reject) => { + SimpleTest.waitForClipboard( + word, + () => { + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(word); + }, + resolve, + () => { + ok(false, "Clipboard copy failed"); + reject(); + } + ); + }); + + info("Middle clicking 'new tab' button"); + let promiseTabLoaded = BrowserTestUtils.waitForNewTab( + gBrowser, + searchUrl, + true + ); + EventUtils.synthesizeMouseAtCenter( + document.getElementById("tabs-newtab-button"), + { button: 1 } + ); + + await promiseTabLoaded; + is(gBrowser.tabs.length, previousTabsLength + 1, "We created a tab"); + is( + gBrowser.currentURI.spec, + searchUrl, + "New Tab URL is the search engine with the content of the clipboard" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } +); + +add_task(async function middleclick_new_tab_button_with_url_in_clipboard() { + await BrowserTestUtils.overflowTabs(registerCleanupFunction, window); + await BrowserTestUtils.waitForCondition(() => { + return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen); + }); + + var previousTabsLength = gBrowser.tabs.length; + info("Previous tabs count is " + previousTabsLength); + + let url = "javascript:https://www.example.com/"; + let safeUrl = "https://www.example.com/"; + await new Promise((resolve, reject) => { + SimpleTest.waitForClipboard( + url, + () => { + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(url); + }, + resolve, + () => { + ok(false, "Clipboard copy failed"); + reject(); + } + ); + }); + + info("Middle clicking 'new tab' button"); + let promiseTabLoaded = BrowserTestUtils.waitForNewTab( + gBrowser, + safeUrl, + true + ); + EventUtils.synthesizeMouseAtCenter( + document.getElementById("new-tab-button"), + { button: 1 } + ); + + await promiseTabLoaded; + is(gBrowser.tabs.length, previousTabsLength + 1, "We created a tab"); + is( + gBrowser.currentURI.spec, + safeUrl, + "New Tab URL is the safe content of the clipboard" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function middleclick_new_tab_button_with_word_in_clipboard() { + var previousTabsLength = gBrowser.tabs.length; + info("Previous tabs count is " + previousTabsLength); + + let word = "word"; + let searchUrl = "https://example.org/?q=word"; + await new Promise((resolve, reject) => { + SimpleTest.waitForClipboard( + word, + () => { + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(word); + }, + resolve, + () => { + ok(false, "Clipboard copy failed"); + reject(); + } + ); + }); + + info("Middle clicking 'new tab' button"); + let promiseTabLoaded = BrowserTestUtils.waitForNewTab( + gBrowser, + searchUrl, + true + ); + EventUtils.synthesizeMouseAtCenter( + document.getElementById("new-tab-button"), + { button: 1 } + ); + + await promiseTabLoaded; + is(gBrowser.tabs.length, previousTabsLength + 1, "We created a tab"); + is( + gBrowser.currentURI.spec, + searchUrl, + "New Tab URL is the search engine with the content of the clipboard" + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function middleclick_new_tab_button_with_spaces_in_clipboard() { + let spaces = " \n "; + await new Promise((resolve, reject) => { + SimpleTest.waitForClipboard( + spaces, + () => { + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(spaces); + }, + resolve, + () => { + ok(false, "Clipboard copy failed"); + reject(); + } + ); + }); + + info("Middle clicking 'new tab' button"); + let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser); + EventUtils.synthesizeMouseAtCenter( + document.getElementById("new-tab-button"), + { button: 1 } + ); + + await promiseTabOpened; + is( + gBrowser.currentURI.spec, + "about:newtab", + "New Tab URL is the regular new tab page." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_active_tab_selected_by_default.js b/browser/base/content/test/tabs/browser_multiselect_tabs_active_tab_selected_by_default.js new file mode 100644 index 0000000000..8edf56d3d4 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_active_tab_selected_by_default.js @@ -0,0 +1,52 @@ +add_task(async function multiselectActiveTabByDefault() { + const tab1 = await addTab(); + const tab2 = await addTab(); + const tab3 = await addTab(); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + + info("Try multiselecting Tab1 (active) with click+CtrlKey"); + await triggerClickOn(tab1, { ctrlKey: true }); + + is(gBrowser.selectedTab, tab1, "Tab1 is active"); + ok( + !tab1.multiselected, + "Tab1 is not multi-selected because we are not in multi-select context yet" + ); + ok(!tab2.multiselected, "Tab2 is not multi-selected"); + ok(!tab3.multiselected, "Tab3 is not multi-selected"); + is(gBrowser.multiSelectedTabsCount, 0, "Zero tabs multi-selected"); + + info("We multi-select tab1 and tab2 with ctrl key down"); + await triggerClickOn(tab2, { ctrlKey: true }); + await triggerClickOn(tab3, { ctrlKey: true }); + + is(gBrowser.selectedTab, tab1, "Tab1 is active"); + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(tab2.multiselected, "Tab2 is multi-selected"); + ok(tab3.multiselected, "Tab3 is multi-selected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three tabs multi-selected"); + is( + gBrowser.lastMultiSelectedTab, + tab3, + "Tab3 is the last multi-selected tab" + ); + + info("Unselect tab1 from multi-selection using ctrlKey"); + + await BrowserTestUtils.switchTab( + gBrowser, + triggerClickOn(tab1, { ctrlKey: true }) + ); + + isnot(gBrowser.selectedTab, tab1, "Tab1 is not active anymore"); + is(gBrowser.selectedTab, tab3, "Tab3 is active"); + ok(!tab1.multiselected, "Tab1 is not multi-selected"); + ok(tab2.multiselected, "Tab2 is multi-selected"); + ok(tab3.multiselected, "Tab3 is multi-selected"); + is(gBrowser.multiSelectedTabsCount, 2, "Two tabs multi-selected"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_bookmark.js b/browser/base/content/test/tabs/browser_multiselect_tabs_bookmark.js new file mode 100644 index 0000000000..a24e72c0bb --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_bookmark.js @@ -0,0 +1,81 @@ +/* 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/. */ + +async function addTab_example_com() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const tab = BrowserTestUtils.addTab(gBrowser, "http://example.com/", { + skipAnimation: true, + }); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + return tab; +} + +add_task(async function test() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + + let menuItemBookmarkTab = document.getElementById("context_bookmarkTab"); + let menuItemBookmarkSelectedTabs = document.getElementById( + "context_bookmarkSelectedTabs" + ); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + + // Check the context menu with a non-multiselected tab + updateTabContextMenu(tab3); + is(menuItemBookmarkTab.hidden, false, "Bookmark Tab is visible"); + is( + menuItemBookmarkSelectedTabs.hidden, + true, + "Bookmark Selected Tabs is hidden" + ); + + // Check the context menu with a multiselected tab and one unique page in the selection. + updateTabContextMenu(tab2); + is(menuItemBookmarkTab.hidden, true, "Bookmark Tab is visible"); + is( + menuItemBookmarkSelectedTabs.hidden, + false, + "Bookmark Selected Tabs is hidden" + ); + is( + PlacesCommandHook.uniqueSelectedPages.length, + 1, + "No more than one unique selected page" + ); + + info("Add a different page to selection"); + let tab4 = await addTab_example_com(); + await triggerClickOn(tab4, { ctrlKey: true }); + + ok(tab4.multiselected, "Tab4 is multiselected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + + // Check the context menu with a multiselected tab and two unique pages in the selection. + updateTabContextMenu(tab2); + is(menuItemBookmarkTab.hidden, true, "Bookmark Tab is visible"); + is( + menuItemBookmarkSelectedTabs.hidden, + false, + "Bookmark Selected Tabs is hidden" + ); + is( + PlacesCommandHook.uniqueSelectedPages.length, + 2, + "More than one unique selected page" + ); + + for (let tab of [tab1, tab2, tab3, tab4]) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_clear_selection_when_tab_switch.js b/browser/base/content/test/tabs/browser_multiselect_tabs_clear_selection_when_tab_switch.js new file mode 100644 index 0000000000..6e75e29c9a --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_clear_selection_when_tab_switch.js @@ -0,0 +1,33 @@ +add_task(async function test() { + let initialTab = gBrowser.selectedTab; + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + for (let tab of [tab1, tab2, tab3]) { + await triggerClickOn(tab, { ctrlKey: true }); + } + + is(gBrowser.multiSelectedTabsCount, 4, "Four multiselected tabs"); + is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab"); + + info("Un-select the active tab"); + await BrowserTestUtils.switchTab( + gBrowser, + triggerClickOn(initialTab, { ctrlKey: true }) + ); + + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + is(gBrowser.selectedTab, tab3, "Tab3 is the active tab"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + + is(gBrowser.multiSelectedTabsCount, 0, "Selection cleared after tab-switch"); + is(gBrowser.selectedTab, tab1, "Tab1 is the active tab"); + + for (let tab of [tab1, tab2, tab3]) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close.js new file mode 100644 index 0000000000..2d2295c14a --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close.js @@ -0,0 +1,192 @@ +const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs"; + +async function openTabMenuFor(tab) { + let tabMenu = tab.ownerDocument.getElementById("tabContextMenu"); + + let tabMenuShown = BrowserTestUtils.waitForEvent(tabMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + tab, + { type: "contextmenu" }, + tab.ownerGlobal + ); + await tabMenuShown; + + return tabMenu; +} + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_WARN_ON_CLOSE, false]], + }); +}); + +add_task(async function usingTabCloseButton() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + is(gBrowser.selectedTab, tab1, "Tab1 is active"); + + await triggerClickOn(tab3, { ctrlKey: true }); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + gBrowser.hideTab(tab3); + is( + gBrowser.multiSelectedTabsCount, + 2, + "Two multiselected tabs after hiding one tab" + ); + gBrowser.showTab(tab3); + is( + gBrowser.multiSelectedTabsCount, + 3, + "Three multiselected tabs after re-showing hidden tab" + ); + await triggerClickOn(tab3, { ctrlKey: true }); + is( + gBrowser.multiSelectedTabsCount, + 2, + "Two multiselected tabs after ctrl-clicking multiselected tab" + ); + + // Closing a tab which is not multiselected + let tab4CloseBtn = tab4.closeButton; + let tab4Closing = BrowserTestUtils.waitForTabClosing(tab4); + + tab4.mOverCloseButton = true; + ok(tab4.mOverCloseButton, "Mouse over tab4 close button"); + tab4CloseBtn.click(); + await tab4Closing; + + is(gBrowser.selectedTab, tab1, "Tab1 is active"); + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(!tab1.closing, "Tab1 is not closing"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab2.closing, "Tab2 is not closing"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + ok(!tab3.closing, "Tab3 is not closing"); + ok(tab4.closing, "Tab4 is closing"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + + // Closing a selected tab + let tab2CloseBtn = tab2.closeButton; + tab2.mOverCloseButton = true; + let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1); + let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2); + tab2CloseBtn.click(); + await tab1Closing; + await tab2Closing; + + ok(tab1.closing, "Tab1 is closing"); + ok(tab2.closing, "Tab2 is closing"); + ok(!tab3.closing, "Tab3 is not closing"); + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + BrowserTestUtils.removeTab(tab3); +}); + +add_task(async function usingTabContextMenu() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + + let menuItemCloseTab = document.getElementById("context_closeTab"); + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + + // Check the context menu with a non-multiselected tab + updateTabContextMenu(tab4); + let { args } = document.l10n.getAttributes(menuItemCloseTab); + is(args.tabCount, 1, "Close Tab item lists a single tab"); + + // Check the context menu with a multiselected tab. We have to actually open + // it (not just call `updateTabContextMenu`) in order for + // `TabContextMenu.contextTab` to stay non-null when we click an item. + let menu = await openTabMenuFor(tab2); + ({ args } = document.l10n.getAttributes(menuItemCloseTab)); + is(args.tabCount, 2, "Close Tab item lists more than one tab"); + + let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1); + let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2); + menu.activateItem(menuItemCloseTab); + await tab1Closing; + await tab2Closing; + + ok(tab1.closing, "Tab1 is closing"); + ok(tab2.closing, "Tab2 is closing"); + ok(!tab3.closing, "Tab3 is not closing"); + ok(!tab4.closing, "Tab4 is not closing"); + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); +}); + +add_task(async function closeAllMultiselectedMiddleClick() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + let tab5 = await addTab(); + let tab6 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + // Close currently selected tab1 + await BrowserTestUtils.switchTab(gBrowser, tab1); + let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1); + await triggerMiddleClickOn(tab1); + await tab1Closing; + + // Close a not currently selected tab2 + await BrowserTestUtils.switchTab(gBrowser, tab3); + let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2); + await triggerMiddleClickOn(tab2); + await tab2Closing; + + // Close the not multiselected middle clicked tab6 + await triggerClickOn(tab4, { ctrlKey: true }); + await triggerClickOn(tab5, { ctrlKey: true }); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(tab4.multiselected, "Tab4 is multiselected"); + ok(tab5.multiselected, "Tab5 is multiselected"); + ok(!tab6.multiselected, "Tab6 is not multiselected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + + let tab6Closing = BrowserTestUtils.waitForTabClosing(tab6); + await triggerMiddleClickOn(tab6); + await tab6Closing; + + // Close multiselected tabs(3, 4, 5) + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(tab4.multiselected, "Tab4 is multiselected"); + ok(tab5.multiselected, "Tab5 is multiselected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + + let tab3Closing = BrowserTestUtils.waitForTabClosing(tab3); + let tab4Closing = BrowserTestUtils.waitForTabClosing(tab4); + let tab5Closing = BrowserTestUtils.waitForTabClosing(tab5); + await triggerMiddleClickOn(tab5); + await tab3Closing; + await tab4Closing; + await tab5Closing; +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close_other_tabs.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close_other_tabs.js new file mode 100644 index 0000000000..9214fe00a4 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_other_tabs.js @@ -0,0 +1,122 @@ +const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs"; + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_WARN_ON_CLOSE, false]], + }); +}); + +add_task(async function withAMultiSelectedTab() { + let initialTab = gBrowser.selectedTab; + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await triggerClickOn(tab1, { ctrlKey: true }); + + let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned"); + gBrowser.pinTab(tab4); + await tab4Pinned; + + ok(initialTab.multiselected, "InitialTab is multiselected"); + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(!tab2.multiselected, "Tab2 is not multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + ok(tab4.pinned, "Tab4 is pinned"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab"); + + let closingTabs = [tab2, tab3]; + let tabClosingPromises = []; + for (let tab of closingTabs) { + tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab)); + } + + gBrowser.removeAllTabsBut(tab1); + + for (let promise of tabClosingPromises) { + await promise; + } + + ok(!initialTab.closing, "InitialTab is not closing"); + ok(!tab1.closing, "Tab1 is not closing"); + ok(tab2.closing, "Tab2 is closing"); + ok(tab3.closing, "Tab3 is closing"); + ok(!tab4.closing, "Tab4 is not closing"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + is(gBrowser.selectedTab, initialTab, "InitialTab is still the active tab"); + + gBrowser.clearMultiSelectedTabs(); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab4); +}); + +add_task(async function withNotAMultiSelectedTab() { + let initialTab = gBrowser.selectedTab; + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + let tab5 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + await triggerClickOn(tab5, { ctrlKey: true }); + + let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned"); + gBrowser.pinTab(tab4); + await tab4Pinned; + + let tab5Pinned = BrowserTestUtils.waitForEvent(tab5, "TabPinned"); + gBrowser.pinTab(tab5); + await tab5Pinned; + + ok(!initialTab.multiselected, "InitialTab is not multiselected"); + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + ok(tab4.pinned, "Tab4 is pinned"); + ok(tab5.multiselected, "Tab5 is multiselected"); + ok(tab5.pinned, "Tab5 is pinned"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + is(gBrowser.selectedTab, tab1, "Tab1 is the active tab"); + + let closingTabs = [tab1, tab2, tab3]; + let tabClosingPromises = []; + for (let tab of closingTabs) { + tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab)); + } + + await BrowserTestUtils.switchTab( + gBrowser, + gBrowser.removeAllTabsBut(initialTab) + ); + + for (let promise of tabClosingPromises) { + await promise; + } + + ok(!initialTab.closing, "InitialTab is not closing"); + ok(tab1.closing, "Tab1 is closing"); + ok(tab2.closing, "Tab2 is closing"); + ok(tab3.closing, "Tab3 is closing"); + ok(!tab4.closing, "Tab4 is not closing"); + ok(!tab5.closing, "Tab5 is not closing"); + is( + gBrowser.multiSelectedTabsCount, + 0, + "Zero multiselected tabs, selection is cleared" + ); + is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab now"); + + for (let tab of [tab4, tab5]) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_left.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_left.js new file mode 100644 index 0000000000..874c161bca --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_left.js @@ -0,0 +1,131 @@ +const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs"; + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_WARN_ON_CLOSE, false]], + }); +}); + +add_task(async function withAMultiSelectedTab() { + // don't mess with the original tab + let originalTab = gBrowser.selectedTab; + gBrowser.pinTab(originalTab); + + let tab0 = await addTab(); + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + let tab5 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab2); + await triggerClickOn(tab4, { ctrlKey: true }); + + ok(!tab0.multiselected, "Tab0 is not multiselected"); + ok(!tab1.multiselected, "Tab1 is not multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + ok(tab4.multiselected, "Tab4 is multiselected"); + ok(!tab5.multiselected, "Tab5 is not multiselected"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + + // Tab3 will be closed because tab4 is the contextTab. + let closingTabs = [tab0, tab1, tab3]; + let tabClosingPromises = []; + for (let tab of closingTabs) { + tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab)); + } + + gBrowser.removeTabsToTheStartFrom(tab4); + + for (let promise of tabClosingPromises) { + await promise; + } + + ok(tab0.closing, "Tab0 is closing"); + ok(tab1.closing, "Tab1 is closing"); + ok(!tab2.closing, "Tab2 is not closing"); + ok(tab3.closing, "Tab3 is closing"); + ok(!tab4.closing, "Tab4 is not closing"); + ok(!tab5.closing, "Tab5 is not closing"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + + // cleanup + gBrowser.unpinTab(originalTab); + for (let tab of [tab2, tab4, tab5]) { + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function withNotAMultiSelectedTab() { + // don't mess with the original tab + let originalTab = gBrowser.selectedTab; + gBrowser.pinTab(originalTab); + + let tab0 = await addTab(); + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + let tab5 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab3, { ctrlKey: true }); + await triggerClickOn(tab5, { ctrlKey: true }); + + ok(!tab0.multiselected, "Tab0 is not multiselected"); + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(!tab2.multiselected, "Tab2 is not multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + ok(tab5.multiselected, "Tab5 is multiselected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + + let closingTabs = [tab0, tab1]; + let tabClosingPromises = []; + for (let tab of closingTabs) { + tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab)); + } + + gBrowser.removeTabsToTheStartFrom(tab2); + + for (let promise of tabClosingPromises) { + await promise; + } + + ok(tab0.closing, "Tab0 is closing"); + ok(tab1.closing, "Tab1 is closing"); + ok(!tab2.closing, "Tab2 is not closing"); + ok(!tab3.closing, "Tab3 is not closing"); + ok(!tab4.closing, "Tab4 is not closing"); + ok(!tab5.closing, "Tab5 is not closing"); + is(gBrowser.multiSelectedTabsCount, 2, "Selection is not cleared"); + + closingTabs = [tab2, tab3]; + tabClosingPromises = []; + for (let tab of closingTabs) { + tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab)); + } + + gBrowser.removeTabsToTheStartFrom(tab4); + + for (let promise of tabClosingPromises) { + await promise; + } + + ok(tab2.closing, "Tab2 is closing"); + ok(tab3.closing, "Tab3 is closing"); + ok(!tab4.closing, "Tab4 is not closing"); + ok(!tab5.closing, "Tab5 is not closing"); + is(gBrowser.multiSelectedTabsCount, 0, "Selection is cleared"); + + // cleanup + gBrowser.unpinTab(originalTab); + for (let tab of [tab4, tab5]) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_right.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_right.js new file mode 100644 index 0000000000..f145930364 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_right.js @@ -0,0 +1,113 @@ +const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs"; + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_WARN_ON_CLOSE, false]], + }); +}); + +add_task(async function withAMultiSelectedTab() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + let tab5 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab3, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(!tab2.multiselected, "Tab2 is not multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + ok(!tab5.multiselected, "Tab5 is not multiselected"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + + // Tab2 will be closed because tab1 is the contextTab. + let closingTabs = [tab2, tab4, tab5]; + let tabClosingPromises = []; + for (let tab of closingTabs) { + tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab)); + } + + gBrowser.removeTabsToTheEndFrom(tab1); + + for (let promise of tabClosingPromises) { + await promise; + } + + ok(!tab1.closing, "Tab1 is not closing"); + ok(tab2.closing, "Tab2 is closing"); + ok(!tab3.closing, "Tab3 is not closing"); + ok(tab4.closing, "Tab4 is closing"); + ok(tab5.closing, "Tab5 is closing"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + + for (let tab of [tab1, tab2, tab3]) { + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function withNotAMultiSelectedTab() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + let tab5 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab3, { ctrlKey: true }); + await triggerClickOn(tab5, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(!tab2.multiselected, "Tab2 is not multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + ok(tab5.multiselected, "Tab5 is multiselected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + + let closingTabs = [tab5]; + let tabClosingPromises = []; + for (let tab of closingTabs) { + tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab)); + } + + gBrowser.removeTabsToTheEndFrom(tab4); + + for (let promise of tabClosingPromises) { + await promise; + } + + ok(!tab1.closing, "Tab1 is not closing"); + ok(!tab2.closing, "Tab2 is not closing"); + ok(!tab3.closing, "Tab3 is not closing"); + ok(!tab4.closing, "Tab4 is not closing"); + ok(tab5.closing, "Tab5 is closing"); + is(gBrowser.multiSelectedTabsCount, 2, "Selection is not cleared"); + + closingTabs = [tab3, tab4]; + tabClosingPromises = []; + for (let tab of closingTabs) { + tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab)); + } + + gBrowser.removeTabsToTheEndFrom(tab2); + + for (let promise of tabClosingPromises) { + await promise; + } + + ok(!tab1.closing, "Tab1 is not closing"); + ok(!tab2.closing, "Tab2 is not closing"); + ok(tab3.closing, "Tab3 is closing"); + ok(tab4.closing, "Tab4 is closing"); + is(gBrowser.multiSelectedTabsCount, 0, "Selection is cleared"); + + for (let tab of [tab1, tab2]) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_close_using_shortcuts.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close_using_shortcuts.js new file mode 100644 index 0000000000..da367f6645 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_using_shortcuts.js @@ -0,0 +1,64 @@ +const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs"; + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_WARN_ON_CLOSE, false]], + }); +}); + +add_task(async function using_Ctrl_W() { + for (let key of ["w", "VK_F4"]) { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + + await BrowserTestUtils.switchTab(gBrowser, triggerClickOn(tab1, {})); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await triggerClickOn(tab2, { ctrlKey: true }); + await triggerClickOn(tab3, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + + let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1); + let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2); + let tab3Closing = BrowserTestUtils.waitForTabClosing(tab3); + + EventUtils.synthesizeKey(key, { accelKey: true }); + + // On OSX, Cmd+F4 should not close tabs. + const shouldBeClosing = key == "w" || AppConstants.platform != "macosx"; + + if (shouldBeClosing) { + await tab1Closing; + await tab2Closing; + await tab3Closing; + } + + ok(!tab4.closing, "Tab4 is not closing"); + + if (shouldBeClosing) { + ok(tab1.closing, "Tab1 is closing"); + ok(tab2.closing, "Tab2 is closing"); + ok(tab3.closing, "Tab3 is closing"); + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + } else { + ok(!tab1.closing, "Tab1 is not closing"); + ok(!tab2.closing, "Tab2 is not closing"); + ok(!tab3.closing, "Tab3 is not closing"); + is(gBrowser.multiSelectedTabsCount, 3, "Still Three multiselected tabs"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + } + + BrowserTestUtils.removeTab(tab4); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_copy_through_drag_and_drop.js b/browser/base/content/test/tabs/browser_multiselect_tabs_copy_through_drag_and_drop.js new file mode 100644 index 0000000000..029708560a --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_copy_through_drag_and_drop.js @@ -0,0 +1,51 @@ +add_task(async function test() { + let tab0 = gBrowser.selectedTab; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab1 = await addTab("http://example.com/1"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab2 = await addTab("http://example.com/2"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab3 = await addTab("http://example.com/3"); + let tabs = [tab0, tab1, tab2, tab3]; + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + + is(gBrowser.selectedTab, tab1, "Tab1 is active"); + is(gBrowser.selectedTabs.length, 2, "Two selected tabs"); + is(gBrowser.visibleTabs.length, 4, "Four tabs in window before copy"); + + for (let i of [1, 2]) { + ok(tabs[i].multiselected, "Tab" + i + " is multiselected"); + } + for (let i of [0, 3]) { + ok(!tabs[i].multiselected, "Tab" + i + " is not multiselected"); + } + + await dragAndDrop(tab1, tab3, true); + + is(gBrowser.selectedTab, tab1, "tab1 is still active"); + is(gBrowser.selectedTabs.length, 2, "Two selected tabs"); + is(gBrowser.visibleTabs.length, 6, "Six tabs in window after copy"); + + let tab4 = gBrowser.visibleTabs[4]; + let tab5 = gBrowser.visibleTabs[5]; + tabs.push(tab4); + tabs.push(tab5); + + for (let i of [1, 2]) { + ok(tabs[i].multiselected, "Tab" + i + " is multiselected"); + } + for (let i of [0, 3, 4, 5]) { + ok(!tabs[i].multiselected, "Tab" + i + " is not multiselected"); + } + + await BrowserTestUtils.waitForCondition(() => getUrl(tab4) == getUrl(tab1)); + await BrowserTestUtils.waitForCondition(() => getUrl(tab5) == getUrl(tab2)); + + ok(true, "Tab1 and tab2 are duplicated succesfully"); + + for (let tab of tabs.filter(t => t != tab0)) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_drag_to_bookmarks_toolbar.js b/browser/base/content/test/tabs/browser_multiselect_tabs_drag_to_bookmarks_toolbar.js new file mode 100644 index 0000000000..42342c889c --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_drag_to_bookmarks_toolbar.js @@ -0,0 +1,74 @@ +add_task(async function test() { + // Disable tab animations + gReduceMotionOverride = true; + + // Open Bookmarks Toolbar + let bookmarksToolbar = document.getElementById("PersonalToolbar"); + setToolbarVisibility(bookmarksToolbar, true); + ok(!bookmarksToolbar.collapsed, "bookmarksToolbar should be visible now"); + + let tab1 = await addTab("http://mochi.test:8888/1"); + let tab2 = await addTab("http://mochi.test:8888/2"); + let tab3 = await addTab("http://mochi.test:8888/3"); + let tab4 = await addTab("http://mochi.test:8888/4"); + let tab5 = await addTab("http://mochi.test:8888/5"); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab2); + await triggerClickOn(tab1, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + ok(!tab5.multiselected, "Tab5 is not multiselected"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + + // Use getElementsByClassName so the list is live and will update as items change. + let currentBookmarks = + bookmarksToolbar.getElementsByClassName("bookmark-item"); + let startBookmarksLength = currentBookmarks.length; + + // The destination element should be a non-folder bookmark + let destBookmarkItem = () => + bookmarksToolbar.querySelector( + "#PlacesToolbarItems .bookmark-item:not([container])" + ); + + await EventUtils.synthesizePlainDragAndDrop({ + srcElement: tab1, + destElement: destBookmarkItem(), + }); + await TestUtils.waitForCondition( + () => currentBookmarks.length == startBookmarksLength + 2, + "waiting for 2 bookmarks" + ); + is( + currentBookmarks.length, + startBookmarksLength + 2, + "Bookmark count should have increased by 2" + ); + + // Drag non-selection to the bookmarks toolbar + startBookmarksLength = currentBookmarks.length; + await EventUtils.synthesizePlainDragAndDrop({ + srcElement: tab3, + destElement: destBookmarkItem(), + }); + await TestUtils.waitForCondition( + () => currentBookmarks.length == startBookmarksLength + 1, + "waiting for 1 bookmark" + ); + is( + currentBookmarks.length, + startBookmarksLength + 1, + "Bookmark count should have increased by 1" + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + BrowserTestUtils.removeTab(tab5); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_duplicate.js b/browser/base/content/test/tabs/browser_multiselect_tabs_duplicate.js new file mode 100644 index 0000000000..d9f5e58669 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_duplicate.js @@ -0,0 +1,136 @@ +async function openTabMenuFor(tab) { + let tabMenu = tab.ownerDocument.getElementById("tabContextMenu"); + + let tabMenuShown = BrowserTestUtils.waitForEvent(tabMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + tab, + { type: "contextmenu" }, + tab.ownerGlobal + ); + await tabMenuShown; + + return tabMenu; +} + +add_task(async function test() { + let originalTab = gBrowser.selectedTab; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab1 = await addTab("http://example.com/1"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab2 = await addTab("http://example.com/2"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab3 = await addTab("http://example.com/3"); + + let menuItemDuplicateTab = document.getElementById("context_duplicateTab"); + let menuItemDuplicateTabs = document.getElementById("context_duplicateTabs"); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + + // Check the context menu with a multiselected tabs + updateTabContextMenu(tab2); + is(menuItemDuplicateTab.hidden, true, "Duplicate Tab is hidden"); + is(menuItemDuplicateTabs.hidden, false, "Duplicate Tabs is visible"); + + // Check the context menu with a non-multiselected tab + updateTabContextMenu(tab3); + is(menuItemDuplicateTab.hidden, false, "Duplicate Tab is visible"); + is(menuItemDuplicateTabs.hidden, true, "Duplicate Tabs is hidden"); + + let newTabOpened = BrowserTestUtils.waitForNewTab( + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/3", + true + ); + { + let menu = await openTabMenuFor(tab3); + menu.activateItem(menuItemDuplicateTab); + } + let tab4 = await newTabOpened; + + is( + getUrl(tab4), + getUrl(tab3), + "tab4 should have same URL as tab3, where it was duplicated from" + ); + + // Selection should be cleared after duplication + ok(!tab1.multiselected, "Tab1 is not multiselected"); + ok(!tab2.multiselected, "Tab2 is not multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + + is(gBrowser.selectedTab._tPos, tab4._tPos, "Tab4 should be selected"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab3, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(!tab2.multiselected, "Tab2 is not multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + + // Check the context menu with a non-multiselected tab + updateTabContextMenu(tab3); + is(menuItemDuplicateTab.hidden, true, "Duplicate Tab is hidden"); + is(menuItemDuplicateTabs.hidden, false, "Duplicate Tabs is visible"); + + // 7 tabs because there was already one open when the test starts. + // Can't use BrowserTestUtils.waitForNewTab because waitForNewTab only works + // with one tab at a time. + let newTabsOpened = TestUtils.waitForCondition( + () => gBrowser.visibleTabs.length == 7, + "Wait for two tabs to get created" + ); + { + let menu = await openTabMenuFor(tab3); + menu.activateItem(menuItemDuplicateTabs); + } + await newTabsOpened; + info("Two tabs opened"); + + await TestUtils.waitForCondition(() => { + return ( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + getUrl(gBrowser.visibleTabs[4]) == "http://example.com/1" && + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + getUrl(gBrowser.visibleTabs[5]) == "http://example.com/3" + ); + }); + + is( + originalTab, + gBrowser.visibleTabs[0], + "Original tab should still be first" + ); + is(tab1, gBrowser.visibleTabs[1], "tab1 should still be second"); + is(tab2, gBrowser.visibleTabs[2], "tab2 should still be third"); + is(tab3, gBrowser.visibleTabs[3], "tab3 should still be fourth"); + is( + getUrl(gBrowser.visibleTabs[4]), + getUrl(tab1), + "the first duplicated tab should be placed next to tab3 and have URL of tab1" + ); + is( + getUrl(gBrowser.visibleTabs[5]), + getUrl(tab3), + "the second duplicated tab should have URL of tab3 and maintain same order" + ); + is( + tab4, + gBrowser.visibleTabs[6], + "tab4 should now be the still be the seventh tab" + ); + + let tabsToClose = gBrowser.visibleTabs.filter(t => t != originalTab); + for (let tab of tabsToClose) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_event.js b/browser/base/content/test/tabs/browser_multiselect_tabs_event.js new file mode 100644 index 0000000000..992cf75e5e --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_event.js @@ -0,0 +1,220 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +add_task(async function clickWithPrefSet() { + let detectUnexpected = true; + window.addEventListener("TabMultiSelect", () => { + if (detectUnexpected) { + ok(false, "Shouldn't get unexpected event"); + } + }); + async function expectEvent(callback, expectedTabs) { + let event = new Promise(resolve => { + detectUnexpected = false; + window.addEventListener( + "TabMultiSelect", + () => { + detectUnexpected = true; + resolve(); + }, + { once: true } + ); + }); + await callback(); + await event; + ok(true, "Got TabMultiSelect event"); + expectSelected(expectedTabs); + // Await some time to ensure no additional event is triggered + await new Promise(resolve => setTimeout(resolve, 100)); + } + async function expectNoEvent(callback, expectedTabs) { + await callback(); + expectSelected(expectedTabs); + // Await some time to ensure no event is triggered + await new Promise(resolve => setTimeout(resolve, 100)); + } + function expectSelected(expected) { + let { selectedTabs } = gBrowser; + is(selectedTabs.length, expected.length, "Check number of selected tabs"); + for ( + let i = 0, n = Math.min(expected.length, selectedTabs.length); + i < n; + ++i + ) { + is(selectedTabs[i], expected[i], `Check the selected tab #${i + 1}`); + } + } + + let initialTab = gBrowser.selectedTab; + let tab1, tab2, tab3; + + info("Expect no event when opening tabs"); + await expectNoEvent(async () => { + tab1 = await addTab(); + tab2 = await addTab(); + tab3 = await addTab(); + }, [initialTab]); + + info("Switching tab should trigger event"); + await expectEvent(async () => { + await BrowserTestUtils.switchTab(gBrowser, tab1); + }, [tab1]); + + info("Multiselecting tab with Ctrl+click should trigger event"); + await expectEvent(async () => { + await triggerClickOn(tab2, { ctrlKey: true }); + }, [tab1, tab2]); + + info("Unselecting tab with Ctrl+click should trigger event"); + await expectEvent(async () => { + await triggerClickOn(tab2, { ctrlKey: true }); + }, [tab1]); + + info("Multiselecting tabs with Shift+click should trigger event"); + await expectEvent(async () => { + await triggerClickOn(tab3, { shiftKey: true }); + }, [tab1, tab2, tab3]); + + info("Expect no event if multiselection doesn't change with Shift+click"); + await expectNoEvent(async () => { + await triggerClickOn(tab3, { shiftKey: true }); + }, [tab1, tab2, tab3]); + + info( + "Expect no event if multiselection doesn't change with Ctrl+Shift+click" + ); + await expectNoEvent(async () => { + await triggerClickOn(tab2, { ctrlKey: true, shiftKey: true }); + }, [tab1, tab2, tab3]); + + info( + "Expect no event if selected tab doesn't change with gBrowser.selectedTab" + ); + await expectNoEvent(async () => { + gBrowser.selectedTab = tab1; + }, [tab1, tab2, tab3]); + + info( + "Clearing multiselection by switching tab with gBrowser.selectedTab should trigger event" + ); + await expectEvent(async () => { + await BrowserTestUtils.switchTab(gBrowser, () => { + gBrowser.selectedTab = tab3; + }); + }, [tab3]); + + info( + "Click on the active and the only mutliselected tab should not trigger event" + ); + await expectNoEvent(async () => { + await triggerClickOn(tab3, {}); + }, [tab3]); + + info( + "Expect no event if selected tab doesn't change with gBrowser.selectedTabs" + ); + gBrowser.selectedTabs = [tab3]; + expectSelected([tab3]); + + info("Multiselecting tabs with gBrowser.selectedTabs should trigger event"); + await expectEvent(async () => { + gBrowser.selectedTabs = [tab3, tab2, tab1]; + }, [tab1, tab2, tab3]); + + info( + "Expect no event if multiselection doesn't change with gBrowser.selectedTabs" + ); + await expectNoEvent(async () => { + gBrowser.selectedTabs = [tab3, tab1, tab2]; + }, [tab1, tab2, tab3]); + + info("Switching tab with gBrowser.selectedTabs should trigger event"); + await expectEvent(async () => { + gBrowser.selectedTabs = [tab1, tab2, tab3]; + }, [tab1, tab2, tab3]); + + info( + "Unmultiselection tab with removeFromMultiSelectedTabs should trigger event" + ); + await expectEvent(async () => { + gBrowser.removeFromMultiSelectedTabs(tab3); + }, [tab1, tab2]); + + info("Expect no event if the tab is not multiselected"); + await expectNoEvent(async () => { + gBrowser.removeFromMultiSelectedTabs(tab3); + }, [tab1, tab2]); + + info( + "Clearing multiselection with clearMultiSelectedTabs should trigger event" + ); + await expectEvent(async () => { + gBrowser.clearMultiSelectedTabs(); + }, [tab1]); + + info("Expect no event if there is no multiselection to clear"); + await expectNoEvent(async () => { + gBrowser.clearMultiSelectedTabs(); + }, [tab1]); + + info( + "Expect no event if clearMultiSelectedTabs counteracts addToMultiSelectedTabs" + ); + await expectNoEvent(async () => { + gBrowser.addToMultiSelectedTabs(tab3); + gBrowser.clearMultiSelectedTabs(); + }, [tab1]); + + info( + "Multiselecting tab with gBrowser.addToMultiSelectedTabs should trigger event" + ); + await expectEvent(async () => { + gBrowser.addToMultiSelectedTabs(tab2); + }, [tab1, tab2]); + + info( + "Expect no event if addToMultiSelectedTabs counteracts clearMultiSelectedTabs" + ); + await expectNoEvent(async () => { + gBrowser.clearMultiSelectedTabs(); + gBrowser.addToMultiSelectedTabs(tab1); + gBrowser.addToMultiSelectedTabs(tab2); + }, [tab1, tab2]); + + info( + "Expect no event if removeFromMultiSelectedTabs counteracts addToMultiSelectedTabs" + ); + await expectNoEvent(async () => { + gBrowser.addToMultiSelectedTabs(tab3); + gBrowser.removeFromMultiSelectedTabs(tab3); + }, [tab1, tab2]); + + info( + "Expect no event if addToMultiSelectedTabs counteracts removeFromMultiSelectedTabs" + ); + await expectNoEvent(async () => { + gBrowser.removeFromMultiSelectedTabs(tab2); + gBrowser.addToMultiSelectedTabs(tab2); + }, [tab1, tab2]); + + info("Multiselection with addRangeToMultiSelectedTabs should trigger event"); + await expectEvent(async () => { + gBrowser.addRangeToMultiSelectedTabs(tab1, tab3); + }, [tab1, tab2, tab3]); + + info("Switching to a just multiselected tab should multiselect the old one"); + await expectEvent(async () => { + gBrowser.clearMultiSelectedTabs(); + }, [tab1]); + await expectEvent(async () => { + is(tab1.multiselected, false, "tab1 is not multiselected"); + gBrowser.addToMultiSelectedTabs(tab2); + gBrowser.lockClearMultiSelectionOnce(); + gBrowser.selectedTab = tab2; + }, [tab1, tab2]); + is(tab1.multiselected, true, "tab1 becomes multiselected"); + + detectUnexpected = false; + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_move.js b/browser/base/content/test/tabs/browser_multiselect_tabs_move.js new file mode 100644 index 0000000000..e5de60ea99 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move.js @@ -0,0 +1,192 @@ +add_task(async function testMoveStartEnabledClickedFromNonSelectedTab() { + let tab = gBrowser.selectedTab; + let tab2 = await addTab(); + let tab3 = await addTab(); + + let tabs = [tab2, tab3]; + + let menuItemMoveStartTab = document.getElementById("context_moveToStart"); + + await triggerClickOn(tab, {}); + await triggerClickOn(tab2, { ctrlKey: true }); + + ok(tab.multiselected, "Tab is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + + updateTabContextMenu(tab3); + is(menuItemMoveStartTab.disabled, false, "Move Tab to Start is enabled"); + + for (let tabToRemove of tabs) { + BrowserTestUtils.removeTab(tabToRemove); + } +}); + +add_task(async function testMoveStartDisabledFromFirstUnpinnedTab() { + let tab = gBrowser.selectedTab; + let tab2 = await addTab(); + + let menuItemMoveStartTab = document.getElementById("context_moveToStart"); + + gBrowser.pinTab(tab); + + updateTabContextMenu(tab2); + is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled"); + + BrowserTestUtils.removeTab(tab2); + gBrowser.unpinTab(tab); +}); + +add_task(async function testMoveStartDisabledFromFirstPinnedTab() { + let tab = gBrowser.selectedTab; + let tab2 = await addTab(); + + let menuItemMoveStartTab = document.getElementById("context_moveToStart"); + + gBrowser.pinTab(tab); + + updateTabContextMenu(tab); + is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled"); + + BrowserTestUtils.removeTab(tab2); + gBrowser.unpinTab(tab); +}); + +add_task(async function testMoveStartDisabledFromOnlyTab() { + let tab = gBrowser.selectedTab; + + let menuItemMoveStartTab = document.getElementById("context_moveToStart"); + + updateTabContextMenu(tab); + is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled"); +}); + +add_task(async function testMoveStartDisabledFromOnlyPinnedTab() { + let tab = gBrowser.selectedTab; + + let menuItemMoveStartTab = document.getElementById("context_moveToStart"); + + gBrowser.pinTab(tab); + + updateTabContextMenu(tab); + is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled"); + + gBrowser.unpinTab(tab); +}); + +add_task(async function testMoveStartEnabledFromLastPinnedTab() { + let tab = gBrowser.selectedTab; + let tab2 = await addTab(); + let tab3 = await addTab(); + + let tabs = [tab2, tab3]; + + let menuItemMoveStartTab = document.getElementById("context_moveToStart"); + + gBrowser.pinTab(tab); + gBrowser.pinTab(tab2); + + updateTabContextMenu(tab2); + is(menuItemMoveStartTab.disabled, false, "Move Tab to Start is enabled"); + + for (let tabToRemove of tabs) { + BrowserTestUtils.removeTab(tabToRemove); + } + + gBrowser.unpinTab(tab); +}); + +add_task(async function testMoveStartDisabledFromFirstVisibleTab() { + let tab = gBrowser.selectedTab; + let tab2 = await addTab(); + + let menuItemMoveStartTab = document.getElementById("context_moveToStart"); + + gBrowser.selectTabAtIndex(1); + gBrowser.hideTab(tab); + + updateTabContextMenu(tab2); + is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled"); + + BrowserTestUtils.removeTab(tab2); + gBrowser.showTab(tab); +}); + +add_task(async function testMoveEndEnabledClickedFromNonSelectedTab() { + let tab = gBrowser.selectedTab; + let tab2 = await addTab(); + let tab3 = await addTab(); + + let tabs = [tab2, tab3]; + + let menuItemMoveEndTab = document.getElementById("context_moveToEnd"); + + await triggerClickOn(tab2, {}); + await triggerClickOn(tab3, { ctrlKey: true }); + + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + + updateTabContextMenu(tab); + is(menuItemMoveEndTab.disabled, false, "Move Tab to End is enabled"); + + for (let tabToRemove of tabs) { + BrowserTestUtils.removeTab(tabToRemove); + } +}); + +add_task(async function testMoveEndDisabledFromLastPinnedTab() { + let tab = gBrowser.selectedTab; + let tab2 = await addTab(); + let tab3 = await addTab(); + + let tabs = [tab2, tab3]; + + let menuItemMoveEndTab = document.getElementById("context_moveToEnd"); + + gBrowser.pinTab(tab); + gBrowser.pinTab(tab2); + + updateTabContextMenu(tab2); + is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled"); + + for (let tabToRemove of tabs) { + BrowserTestUtils.removeTab(tabToRemove); + } +}); + +add_task(async function testMoveEndDisabledFromLastVisibleTab() { + let tab = gBrowser.selectedTab; + let tab2 = await addTab(); + + let menuItemMoveEndTab = document.getElementById("context_moveToEnd"); + + gBrowser.hideTab(tab2); + + updateTabContextMenu(tab); + is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled"); + + BrowserTestUtils.removeTab(tab2); + gBrowser.showTab(tab); +}); + +add_task(async function testMoveEndDisabledFromOnlyTab() { + let tab = gBrowser.selectedTab; + + let menuItemMoveEndTab = document.getElementById("context_moveToEnd"); + + updateTabContextMenu(tab); + is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled"); +}); + +add_task(async function testMoveEndDisabledFromOnlyPinnedTab() { + let tab = gBrowser.selectedTab; + + let menuItemMoveEndTab = document.getElementById("context_moveToEnd"); + + gBrowser.pinTab(tab); + + updateTabContextMenu(tab); + is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled"); + + gBrowser.unpinTab(tab); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_another_window_drag.js b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_another_window_drag.js new file mode 100644 index 0000000000..111221c4ec --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_another_window_drag.js @@ -0,0 +1,118 @@ +add_task(async function test() { + // Disable tab animations + gReduceMotionOverride = true; + + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab("http://mochi.test:8888/3"); + let tab4 = await addTab(); + let tab5 = await addTab("http://mochi.test:8888/5"); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab2); + await triggerClickOn(tab1, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + ok(!tab5.multiselected, "Tab5 is not multiselected"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + + let newWindow = gBrowser.replaceTabsWithWindow(tab1); + + // waiting for tab2 to close ensure that the newWindow is created, + // thus newWindow.gBrowser used in the second waitForCondition + // will not be undefined. + await TestUtils.waitForCondition( + () => tab2.closing, + "Wait for tab2 to close" + ); + await TestUtils.waitForCondition( + () => newWindow.gBrowser.visibleTabs.length == 2, + "Wait for all two tabs to get moved to the new window" + ); + + let gBrowser2 = newWindow.gBrowser; + tab1 = gBrowser2.visibleTabs[0]; + tab2 = gBrowser2.visibleTabs[1]; + + if (gBrowser.selectedTab != tab3) { + await BrowserTestUtils.switchTab(gBrowser, tab3); + } + + await triggerClickOn(tab5, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + ok(tab5.multiselected, "Tab5 is multiselected"); + + await dragAndDrop(tab3, tab1, false, newWindow); + + await TestUtils.waitForCondition( + () => gBrowser2.visibleTabs.length == 4, + "Moved tab3 and tab5 to second window" + ); + + tab3 = gBrowser2.visibleTabs[1]; + tab5 = gBrowser2.visibleTabs[2]; + + await BrowserTestUtils.waitForCondition( + () => getUrl(tab3) == "http://mochi.test:8888/3" + ); + await BrowserTestUtils.waitForCondition( + () => getUrl(tab5) == "http://mochi.test:8888/5" + ); + + ok(true, "Tab3 and tab5 are duplicated succesfully"); + + BrowserTestUtils.closeWindow(newWindow); + BrowserTestUtils.removeTab(tab4); +}); + +add_task(async function test_laziness() { + const params = { createLazyBrowser: true }; + const url = "http://mochi.test:8888/?"; + const tab1 = BrowserTestUtils.addTab(gBrowser, url + "1", params); + const tab2 = BrowserTestUtils.addTab(gBrowser, url + "2"); + const tab3 = BrowserTestUtils.addTab(gBrowser, url + "3", params); + + await BrowserTestUtils.switchTab(gBrowser, tab2); + await triggerClickOn(tab1, { ctrlKey: true }); + await triggerClickOn(tab3, { ctrlKey: true }); + + is(gBrowser.selectedTab, tab2, "Tab2 is selected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(!tab1.linkedPanel, "Tab1 is lazy"); + ok(tab2.linkedPanel, "Tab2 is not lazy"); + ok(!tab3.linkedPanel, "Tab3 is lazy"); + + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + const gBrowser2 = win2.gBrowser; + is(gBrowser2.tabs.length, 1, "Second window has 1 tab"); + + await dragAndDrop(tab2, gBrowser2.tabs[0], false, win2); + await TestUtils.waitForCondition( + () => gBrowser2.tabs.length == 4, + "Moved tabs into second window" + ); + is(gBrowser2.tabs[1].linkedBrowser.currentURI.spec, url + "1"); + is(gBrowser2.tabs[2].linkedBrowser.currentURI.spec, url + "2"); + is(gBrowser2.tabs[3].linkedBrowser.currentURI.spec, url + "3"); + is(gBrowser2.selectedTab, gBrowser2.tabs[2], "Tab2 is selected"); + is(gBrowser2.multiSelectedTabsCount, 3, "Three multiselected tabs"); + ok(gBrowser2.tabs[1].multiselected, "Tab1 is multiselected"); + ok(gBrowser2.tabs[2].multiselected, "Tab2 is multiselected"); + ok(gBrowser2.tabs[3].multiselected, "Tab3 is multiselected"); + ok(!gBrowser2.tabs[1].linkedPanel, "Tab1 is lazy"); + ok(gBrowser2.tabs[2].linkedPanel, "Tab2 is not lazy"); + ok(!gBrowser2.tabs[3].linkedPanel, "Tab3 is lazy"); + + await BrowserTestUtils.closeWindow(win2); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js new file mode 100644 index 0000000000..d668d21df8 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js @@ -0,0 +1,129 @@ +add_task(async function test() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab2); + await triggerClickOn(tab1, { ctrlKey: true }); + await triggerClickOn(tab3, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + + let newWindow = gBrowser.replaceTabsWithWindow(tab1); + + // waiting for tab2 to close ensure that the newWindow is created, + // thus newWindow.gBrowser used in the second waitForCondition + // will not be undefined. + await TestUtils.waitForCondition( + () => tab2.closing, + "Wait for tab2 to close" + ); + await TestUtils.waitForCondition( + () => newWindow.gBrowser.visibleTabs.length == 3, + "Wait for all three tabs to get moved to the new window" + ); + + let gBrowser2 = newWindow.gBrowser; + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + is(gBrowser.visibleTabs.length, 2, "Two tabs now in the old window"); + is(gBrowser2.visibleTabs.length, 3, "Three tabs in the new window"); + is( + gBrowser2.visibleTabs.indexOf(gBrowser2.selectedTab), + 1, + "Previously active tab is still the active tab in the new window" + ); + + BrowserTestUtils.closeWindow(newWindow); + BrowserTestUtils.removeTab(tab4); +}); + +add_task(async function testLazyTabs() { + let params = { createLazyBrowser: true }; + let oldTabs = []; + let numTabs = 4; + for (let i = 0; i < numTabs; ++i) { + oldTabs.push( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + BrowserTestUtils.addTab(gBrowser, `http://example.com/?${i}`, params) + ); + } + + await BrowserTestUtils.switchTab(gBrowser, oldTabs[0]); + for (let i = 1; i < numTabs; ++i) { + await triggerClickOn(oldTabs[i], { ctrlKey: true }); + } + + isnot(oldTabs[0].linkedPanel, "", `Old tab 0 shouldn't be lazy`); + for (let i = 1; i < numTabs; ++i) { + is(oldTabs[i].linkedPanel, "", `Old tab ${i} should be lazy`); + } + + is(gBrowser.multiSelectedTabsCount, numTabs, `${numTabs} multiselected tabs`); + for (let i = 0; i < numTabs; ++i) { + ok(oldTabs[i].multiselected, `Old tab ${i} should be multiselected`); + } + + let tabsMoved = new Promise(resolve => { + let numTabsMoved = 0; + window.addEventListener("TabClose", async function listener(event) { + let oldTab = event.target; + let i = oldTabs.indexOf(oldTab); + if (i == 0) { + isnot( + oldTab.linkedPanel, + "", + `Old tab ${i} should continue not being lazy` + ); + } else if (i > 0) { + is(oldTab.linkedPanel, "", `Old tab ${i} should continue being lazy`); + } else { + return; + } + let newTab = event.detail.adoptedBy; + await TestUtils.waitForCondition(() => { + return newTab.linkedBrowser.currentURI.spec != "about:blank"; + }, `Wait for the new tab to finish the adoption of the old tab`); + if (++numTabsMoved == numTabs) { + window.removeEventListener("TabClose", listener); + resolve(); + } + }); + }); + let newWindow = gBrowser.replaceTabsWithWindow(oldTabs[0]); + await tabsMoved; + let newTabs = newWindow.gBrowser.tabs; + + isnot(newTabs[0].linkedPanel, "", `New tab 0 should continue not being lazy`); + for (let i = 1; i < numTabs; ++i) { + is(newTabs[i].linkedPanel, "", `New tab ${i} should continue being lazy`); + } + + is( + newTabs[0].linkedBrowser.currentURI.spec, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + `http://example.com/?0`, + `New tab 0 should have the right URL` + ); + for (let i = 1; i < numTabs; ++i) { + is( + SessionStore.getLazyTabValue(newTabs[i], "url"), + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + `http://example.com/?${i}`, + `New tab ${i} should have the right lazy URL` + ); + } + + for (let i = 0; i < numTabs; ++i) { + ok(newTabs[i].multiselected, `New tab ${i} should be multiselected`); + } + + BrowserTestUtils.closeWindow(newWindow); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js b/browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js new file mode 100644 index 0000000000..83de966e0c --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js @@ -0,0 +1,336 @@ +const PREF_DELAY_AUTOPLAY = "media.block-autoplay-until-in-foreground"; + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_DELAY_AUTOPLAY, true]], + }); +}); + +add_task(async function muteTabs_usingButton() { + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + let tab2 = await addMediaTab(); + let tab3 = await addMediaTab(); + let tab4 = await addMediaTab(); + + let tabs = [tab0, tab1, tab2, tab3, tab4]; + + await BrowserTestUtils.switchTab(gBrowser, tab0); + await play(tab0); + await play(tab1, false); + await play(tab2, false); + + // Multiselecting tab1, tab2 and tab3 + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab3, { shiftKey: true }); + + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + ok(!tab0.multiselected, "Tab0 is not multiselected"); + ok(!tab4.multiselected, "Tab4 is not multiselected"); + + // tab1,tab2 and tab3 should be multiselected. + for (let i = 1; i <= 3; i++) { + ok(tabs[i].multiselected, "Tab" + i + " is multiselected"); + } + + // All five tabs are unmuted + for (let i = 0; i < 5; i++) { + ok(!muted(tabs[i]), "Tab" + i + " is not muted"); + } + + // Mute tab0 which is not multiselected, thus other tabs muted state should not be affected + let tab0MuteAudioBtn = tab0.overlayIcon; + await test_mute_tab(tab0, tab0MuteAudioBtn, true); + + ok(muted(tab0), "Tab0 is muted"); + for (let i = 1; i <= 4; i++) { + ok(!muted(tabs[i]), "Tab" + i + " is not muted"); + } + + // Now we multiselect tab0 + await triggerClickOn(tab0, { ctrlKey: true }); + + // tab0, tab1, tab2, tab3 are multiselected + for (let i = 0; i <= 3; i++) { + ok(tabs[i].multiselected, "tab" + i + " is multiselected"); + } + ok(!tab4.multiselected, "tab4 is not multiselected"); + + // Check mute state + ok(muted(tab0), "Tab0 is still muted"); + ok(!muted(tab1), "Tab1 is not muted"); + ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked"); + ok(activeMediaBlocked(tab2), "Tab2 is media-blocked"); + ok(!muted(tab3), "Tab3 is not muted"); + ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked"); + ok(!muted(tab4), "Tab4 is not muted"); + ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked"); + + // Mute tab1 which is multiselected, thus other multiselected tabs should be affected too + // in the following way: + // a) muted tabs (tab0) will remain muted. + // b) unmuted tabs (tab1, tab3) will become muted. + // b) media-blocked tabs (tab2) will remain media-blocked. + // However tab4 (unmuted) which is not multiselected should not be affected. + let tab1MuteAudioBtn = tab1.overlayIcon; + await test_mute_tab(tab1, tab1MuteAudioBtn, true); + + // Check mute state + ok(muted(tab0), "Tab0 is still muted"); + ok(muted(tab1), "Tab1 is muted"); + ok(activeMediaBlocked(tab2), "Tab2 is still media-blocked"); + ok(muted(tab3), "Tab3 is now muted"); + ok(!muted(tab4), "Tab4 is not muted"); + ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked"); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function unmuteTabs_usingButton() { + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + let tab2 = await addMediaTab(); + let tab3 = await addMediaTab(); + let tab4 = await addMediaTab(); + + let tabs = [tab0, tab1, tab2, tab3, tab4]; + + await BrowserTestUtils.switchTab(gBrowser, tab0); + await play(tab0); + await play(tab1, false); + await play(tab2, false); + + // Mute tab3 and tab4 + await toggleMuteAudio(tab3, true); + await toggleMuteAudio(tab4, true); + + // Multiselecting tab0, tab1, tab2 and tab3 + await triggerClickOn(tab3, { shiftKey: true }); + + // Check multiselection + for (let i = 0; i <= 3; i++) { + ok(tabs[i].multiselected, "tab" + i + " is multiselected"); + } + ok(!tab4.multiselected, "tab4 is not multiselected"); + + // Check tabs mute state + ok(!muted(tab0), "Tab0 is not muted"); + ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked"); + ok(activeMediaBlocked(tab1), "Tab1 is media-blocked"); + ok(activeMediaBlocked(tab2), "Tab2 is media-blocked"); + ok(muted(tab3), "Tab3 is muted"); + ok(muted(tab4), "Tab4 is muted"); + is(gBrowser.selectedTab, tab0, "Tab0 is active"); + + // unmute tab0 which is multiselected, thus other multiselected tabs should be affected too + // in the following way: + // a) muted tabs (tab3) will become unmuted. + // b) unmuted tabs (tab0) will remain unmuted. + // c) media-blocked tabs (tab1, tab2) will remain blocked. + // However tab4 (muted) which is not multiselected should not be affected. + let tab3MuteAudioBtn = tab3.overlayIcon; + await test_mute_tab(tab3, tab3MuteAudioBtn, false); + + ok(!muted(tab0), "Tab0 is not muted"); + ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked"); + ok(!muted(tab1), "Tab1 is not muted"); + ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked"); + ok(!muted(tab2), "Tab2 is not muted"); + ok(activeMediaBlocked(tab2), "Tab2 is activemedia-blocked"); + ok(!muted(tab3), "Tab3 is not muted"); + ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked"); + ok(muted(tab4), "Tab4 is muted"); + is(gBrowser.selectedTab, tab0, "Tab0 is active"); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function muteAndUnmuteTabs_usingKeyboard() { + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + let tab2 = await addMediaTab(); + let tab3 = await addMediaTab(); + let tab4 = await addMediaTab(); + + let tabs = [tab0, tab1, tab2, tab3, tab4]; + + await BrowserTestUtils.switchTab(gBrowser, tab0); + + let mutedPromise = get_wait_for_mute_promise(tab0, true); + EventUtils.synthesizeKey("M", { ctrlKey: true }); + await mutedPromise; + ok(muted(tab0), "Tab0 should be muted"); + ok(!muted(tab1), "Tab1 should not be muted"); + ok(!muted(tab2), "Tab2 should not be muted"); + ok(!muted(tab3), "Tab3 should not be muted"); + ok(!muted(tab4), "Tab4 should not be muted"); + + // Multiselecting tab0, tab1, tab2 and tab3 + await triggerClickOn(tab3, { shiftKey: true }); + + // Check multiselection + for (let i = 0; i <= 3; i++) { + ok(tabs[i].multiselected, "tab" + i + " is multiselected"); + } + ok(!tab4.multiselected, "tab4 is not multiselected"); + + mutedPromise = get_wait_for_mute_promise(tab0, false); + EventUtils.synthesizeKey("M", { ctrlKey: true }); + await mutedPromise; + ok(!muted(tab0), "Tab0 should not be muted"); + ok(!muted(tab1), "Tab1 should not be muted"); + ok(!muted(tab2), "Tab2 should not be muted"); + ok(!muted(tab3), "Tab3 should not be muted"); + ok(!muted(tab4), "Tab4 should not be muted"); + + mutedPromise = get_wait_for_mute_promise(tab0, true); + EventUtils.synthesizeKey("M", { ctrlKey: true }); + await mutedPromise; + ok(muted(tab0), "Tab0 should be muted"); + ok(muted(tab1), "Tab1 should be muted"); + ok(muted(tab2), "Tab2 should be muted"); + ok(muted(tab3), "Tab3 should be muted"); + ok(!muted(tab4), "Tab4 should not be muted"); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function playTabs_usingButton() { + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + let tab2 = await addMediaTab(); + let tab3 = await addMediaTab(); + let tab4 = await addMediaTab(); + + let tabs = [tab0, tab1, tab2, tab3, tab4]; + + await BrowserTestUtils.switchTab(gBrowser, tab0); + await play(tab0); + await play(tab1, false); + await play(tab2, false); + + // Multiselecting tab0, tab1, tab2 and tab3. + await triggerClickOn(tab3, { shiftKey: true }); + + // Mute tab0 and tab4 + await toggleMuteAudio(tab0, true); + await toggleMuteAudio(tab4, true); + + // Check multiselection + for (let i = 0; i <= 3; i++) { + ok(tabs[i].multiselected, "tab" + i + " is multiselected"); + } + ok(!tab4.multiselected, "tab4 is not multiselected"); + + // Check mute state + ok(muted(tab0), "Tab0 is muted"); + ok(activeMediaBlocked(tab1), "Tab1 is media-blocked"); + ok(activeMediaBlocked(tab2), "Tab2 is media-blocked"); + ok(!muted(tab3), "Tab3 is not muted"); + ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked"); + ok(muted(tab4), "Tab4 is muted"); + is(gBrowser.selectedTab, tab0, "Tab0 is active"); + + // play tab2 which is multiselected, thus other multiselected tabs should be affected too + // in the following way: + // a) muted tabs (tab0) will remain muted. + // b) unmuted tabs (tab3) will remain unmuted. + // c) media-blocked tabs (tab1, tab2) will become unblocked. + // However tab4 (muted) which is not multiselected should not be affected. + let tab2MuteAudioBtn = tab2.overlayIcon; + await test_mute_tab(tab2, tab2MuteAudioBtn, false); + + ok(muted(tab0), "Tab0 is muted"); + ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked"); + ok(!muted(tab1), "Tab1 is not muted"); + ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked"); + ok(!muted(tab2), "Tab2 is not muted"); + ok(!activeMediaBlocked(tab2), "Tab2 is not activemedia-blocked"); + ok(!muted(tab3), "Tab3 is not muted"); + ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked"); + ok(muted(tab4), "Tab4 is muted"); + is(gBrowser.selectedTab, tab0, "Tab0 is active"); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function checkTabContextMenu() { + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + let tab2 = await addMediaTab(); + let tab3 = await addMediaTab(); + let tabs = [tab0, tab1, tab2, tab3]; + + let menuItemToggleMuteTab = document.getElementById("context_toggleMuteTab"); + let menuItemToggleMuteSelectedTabs = document.getElementById( + "context_toggleMuteSelectedTabs" + ); + + await play(tab0, false); + await toggleMuteAudio(tab0, true); + await play(tab1, false); + await toggleMuteAudio(tab2, true); + + // multiselect tab0, tab1, tab2. + await triggerClickOn(tab0, { ctrlKey: true }); + await triggerClickOn(tab1, { ctrlKey: true }); + await triggerClickOn(tab2, { ctrlKey: true }); + + // Check multiselected tabs + for (let i = 0; i <= 2; i++) { + ok(tabs[i].multiselected, "Tab" + i + " is multi-selected"); + } + ok(!tab3.multiselected, "Tab3 is not multiselected"); + + // Check mute state for tabs + ok(muted(tab0), "Tab0 is muted"); + ok(activeMediaBlocked(tab0), "Tab0 is activemedia-blocked"); + ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked"); + ok(muted(tab2), "Tab2 is muted"); + ok(!muted(tab3, "Tab3 is not muted")); + + const l10nIds = [ + "tabbrowser-context-unmute-selected-tabs", + "tabbrowser-context-mute-selected-tabs", + "tabbrowser-context-unmute-selected-tabs", + ]; + + for (let i = 0; i <= 2; i++) { + updateTabContextMenu(tabs[i]); + ok( + menuItemToggleMuteTab.hidden, + "toggleMuteAudio menu for one tab is hidden - contextTab" + i + ); + ok( + !menuItemToggleMuteSelectedTabs.hidden, + "toggleMuteAudio menu for selected tab is not hidden - contextTab" + i + ); + is( + menuItemToggleMuteSelectedTabs.dataset.l10nId, + l10nIds[i], + l10nIds[i] + " should be shown" + ); + } + + updateTabContextMenu(tab3); + ok( + !menuItemToggleMuteTab.hidden, + "toggleMuteAudio menu for one tab is not hidden" + ); + ok( + menuItemToggleMuteSelectedTabs.hidden, + "toggleMuteAudio menu for selected tab is hidden" + ); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js b/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js new file mode 100644 index 0000000000..7751c9c420 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js @@ -0,0 +1,143 @@ +add_task(async function test() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab1 = await addTab("http://example.com/1"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab2 = await addTab("http://example.com/2"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab3 = await addTab("http://example.com/3"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(tab2.multiselected, "Tab2 is multi-selected"); + ok(!tab3.multiselected, "Tab3 is not multi-selected"); + + let metaKeyEvent = + AppConstants.platform == "macosx" ? { metaKey: true } : { ctrlKey: true }; + + let newTabButton = gBrowser.tabContainer.newTabButton; + let promiseTabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeMouseAtCenter(newTabButton, metaKeyEvent); + let openEvent = await promiseTabOpened; + let newTab = openEvent.target; + + is( + newTab.previousElementSibling, + tab1, + "New tab should be opened after the selected tab (tab1)" + ); + is( + newTab.nextElementSibling, + tab2, + "New tab should be opened after the selected tab (tab1) and before tab2" + ); + BrowserTestUtils.removeTab(newTab); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + ok(!tab1.multiselected, "Tab1 is not multi-selected"); + ok(!tab2.multiselected, "Tab2 is not multi-selected"); + ok(!tab3.multiselected, "Tab3 is not multi-selected"); + + promiseTabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeMouseAtCenter(newTabButton, metaKeyEvent); + openEvent = await promiseTabOpened; + newTab = openEvent.target; + is( + newTab.previousElementSibling, + tab1, + "New tab should be opened after tab1 when only tab1 is selected" + ); + is( + newTab.nextElementSibling, + tab2, + "New tab should be opened before tab2 when only tab1 is selected" + ); + BrowserTestUtils.removeTab(newTab); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab3, { ctrlKey: true }); + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(!tab2.multiselected, "Tab2 is not multi-selected"); + ok(tab3.multiselected, "Tab3 is multi-selected"); + + promiseTabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeMouseAtCenter(newTabButton, metaKeyEvent); + openEvent = await promiseTabOpened; + newTab = openEvent.target; + let previous = gBrowser.tabContainer.findNextTab(newTab, { direction: -1 }); + is(previous, tab1, "New tab should be opened after the selected tab (tab1)"); + let next = gBrowser.tabContainer.findNextTab(newTab, { direction: 1 }); + is( + next, + tab2, + "New tab should be opened after the selected tab (tab1) and before tab2" + ); + BrowserTestUtils.removeTab(newTab); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + ok(!tab1.multiselected, "Tab1 is not multi-selected"); + ok(!tab2.multiselected, "Tab2 is not multi-selected"); + ok(!tab3.multiselected, "Tab3 is not multi-selected"); + + promiseTabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeMouseAtCenter(newTabButton, {}); + openEvent = await promiseTabOpened; + newTab = openEvent.target; + previous = gBrowser.tabContainer.findNextTab(newTab, { direction: -1 }); + is( + previous, + tab3, + "New tab should be opened after tab3 when ctrlKey is not used without multiselection" + ); + next = gBrowser.tabContainer.findNextTab(newTab, { direction: 1 }); + is( + next, + null, + "New tab should be opened at the end of the tabstrip when ctrlKey is not used without multiselection" + ); + BrowserTestUtils.removeTab(newTab); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(tab2.multiselected, "Tab2 is multi-selected"); + ok(!tab3.multiselected, "Tab3 is not multi-selected"); + + promiseTabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeMouseAtCenter(newTabButton, {}); + openEvent = await promiseTabOpened; + newTab = openEvent.target; + previous = gBrowser.tabContainer.findNextTab(newTab, { direction: -1 }); + is( + previous, + tab3, + "New tab should be opened after tab3 when ctrlKey is not used with multiselection" + ); + next = gBrowser.tabContainer.findNextTab(newTab, { direction: 1 }); + is( + next, + null, + "New tab should be opened at the end of the tabstrip when ctrlKey is not used with multiselection" + ); + BrowserTestUtils.removeTab(newTab); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_pin_unpin.js b/browser/base/content/test/tabs/browser_multiselect_tabs_pin_unpin.js new file mode 100644 index 0000000000..5cd71abbbe --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_pin_unpin.js @@ -0,0 +1,75 @@ +add_task(async function test() { + let tab1 = gBrowser.selectedTab; + let tab2 = await addTab(); + let tab3 = await addTab(); + + let menuItemPinTab = document.getElementById("context_pinTab"); + let menuItemUnpinTab = document.getElementById("context_unpinTab"); + let menuItemPinSelectedTabs = document.getElementById( + "context_pinSelectedTabs" + ); + let menuItemUnpinSelectedTabs = document.getElementById( + "context_unpinSelectedTabs" + ); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await triggerClickOn(tab2, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(!tab3.multiselected, "Tab3 is not multiselected"); + + // Check the context menu with a non-multiselected tab + updateTabContextMenu(tab3); + ok(!tab3.pinned, "Tab3 is unpinned"); + is(menuItemPinTab.hidden, false, "Pin Tab is visible"); + is(menuItemUnpinTab.hidden, true, "Unpin Tab is hidden"); + is(menuItemPinSelectedTabs.hidden, true, "Pin Selected Tabs is hidden"); + is(menuItemUnpinSelectedTabs.hidden, true, "Unpin Selected Tabs is hidden"); + + // Check the context menu with a multiselected and unpinned tab + updateTabContextMenu(tab2); + ok(!tab2.pinned, "Tab2 is unpinned"); + is(menuItemPinTab.hidden, true, "Pin Tab is hidden"); + is(menuItemUnpinTab.hidden, true, "Unpin Tab is hidden"); + is(menuItemPinSelectedTabs.hidden, false, "Pin Selected Tabs is visible"); + is(menuItemUnpinSelectedTabs.hidden, true, "Unpin Selected Tabs is hidden"); + + let tab1Pinned = BrowserTestUtils.waitForEvent(tab1, "TabPinned"); + let tab2Pinned = BrowserTestUtils.waitForEvent(tab2, "TabPinned"); + menuItemPinSelectedTabs.click(); + await tab1Pinned; + await tab2Pinned; + + ok(tab1.pinned, "Tab1 is pinned"); + ok(tab2.pinned, "Tab2 is pinned"); + ok(!tab3.pinned, "Tab3 is unpinned"); + is(tab1._tPos, 0, "Tab1 should still be first after pinning"); + is(tab2._tPos, 1, "Tab2 should still be second after pinning"); + is(tab3._tPos, 2, "Tab3 should still be third after pinning"); + + // Check the context menu with a multiselected and pinned tab + updateTabContextMenu(tab2); + ok(tab2.pinned, "Tab2 is pinned"); + is(menuItemPinTab.hidden, true, "Pin Tab is hidden"); + is(menuItemUnpinTab.hidden, true, "Unpin Tab is hidden"); + is(menuItemPinSelectedTabs.hidden, true, "Pin Selected Tabs is hidden"); + is(menuItemUnpinSelectedTabs.hidden, false, "Unpin Selected Tabs is visible"); + + let tab1Unpinned = BrowserTestUtils.waitForEvent(tab1, "TabUnpinned"); + let tab2Unpinned = BrowserTestUtils.waitForEvent(tab2, "TabUnpinned"); + menuItemUnpinSelectedTabs.click(); + await tab1Unpinned; + await tab2Unpinned; + + ok(!tab1.pinned, "Tab1 is unpinned"); + ok(!tab2.pinned, "Tab2 is unpinned"); + ok(!tab3.pinned, "Tab3 is unpinned"); + is(tab1._tPos, 0, "Tab1 should still be first after unpinning"); + is(tab2._tPos, 1, "Tab2 should still be second after unpinning"); + is(tab3._tPos, 2, "Tab3 should still be third after unpinning"); + + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_play.js b/browser/base/content/test/tabs/browser_multiselect_tabs_play.js new file mode 100644 index 0000000000..281ed50c1b --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_play.js @@ -0,0 +1,254 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Ensure multiselected tabs that are active media blocked act correctly + * when we try to unblock them using the "Play Tabs" icon or by calling + * resumeDelayedMediaOnMultiSelectedTabs() + */ + +"use strict"; + +const PREF_DELAY_AUTOPLAY = "media.block-autoplay-until-in-foreground"; + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_DELAY_AUTOPLAY, true]], + }); +}); + +/* + * Playing blocked media will not mute the selected tabs + */ +add_task(async function testDelayPlayWontAffectUnmuteStatus() { + info("Add media tabs"); + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + + info("Play both tabs"); + await play(tab0, false); + await play(tab1, false); + + info("Multiselect tabs"); + await triggerClickOn(tab1, { shiftKey: true }); + + // Check multiselection + ok(tab0.multiselected, "tab0 is multiselected"); + ok(tab1.multiselected, "tab1 is multiselected"); + + // Check tabs are unmuted + ok(!muted(tab0), "Tab0 is unmuted"); + ok(!muted(tab1), "Tab1 is unmuted"); + + let tab0BlockPromise = wait_for_tab_media_blocked_event(tab0, false); + let tab1BlockPromise = wait_for_tab_media_blocked_event(tab1, false); + + // Play media on selected tabs + gBrowser.resumeDelayedMediaOnMultiSelectedTabs(); + + info("Wait for media to play"); + await tab0BlockPromise; + await tab1BlockPromise; + + // Check tabs are still unmuted + ok(!muted(tab0), "Tab0 is unmuted"); + ok(!muted(tab1), "Tab1 is unmuted"); + + BrowserTestUtils.removeTab(tab0); + BrowserTestUtils.removeTab(tab1); +}); + +/* + * Playing blocked media will not unmute the selected tabs + */ +add_task(async function testDelayPlayWontAffectMuteStatus() { + info("Add media tabs"); + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + + info("Play both tabs"); + await play(tab0, false); + await play(tab1, false); + + // Mute both tabs + toggleMuteAudio(tab0, true); + toggleMuteAudio(tab1, true); + + info("Multiselect tabs"); + await triggerClickOn(tab1, { shiftKey: true }); + + // Check multiselection + ok(tab0.multiselected, "tab0 is multiselected"); + ok(tab1.multiselected, "tab1 is multiselected"); + + // Check tabs are muted + ok(muted(tab0), "Tab0 is muted"); + ok(muted(tab1), "Tab1 is muted"); + + let tab0BlockPromise = wait_for_tab_media_blocked_event(tab0, false); + let tab1BlockPromise = wait_for_tab_media_blocked_event(tab1, false); + + // Play media on selected tabs + gBrowser.resumeDelayedMediaOnMultiSelectedTabs(); + + info("Wait for media to play"); + await tab0BlockPromise; + await tab1BlockPromise; + + // Check tabs are still muted + ok(muted(tab0), "Tab0 is muted"); + ok(muted(tab1), "Tab1 is muted"); + + BrowserTestUtils.removeTab(tab0); + BrowserTestUtils.removeTab(tab1); +}); + +/* + * The "Play Tabs" icon unblocks media + */ +add_task(async function testDelayPlayWhenUsingButton() { + info("Add media tabs"); + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + let tab2 = await addMediaTab(); + let tab3 = await addMediaTab(); + let tab4 = await addMediaTab(); + + let tabs = [tab0, tab1, tab2, tab3, tab4]; + + // All tabs are initially unblocked due to not being played yet + ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked"); + ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked"); + ok(!activeMediaBlocked(tab2), "Tab2 is not activemedia-blocked"); + ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked"); + ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked"); + + // Playing tabs 0, 1, and 2 will block them + info("Play tabs 0, 1, and 2"); + await play(tab0, false); + await play(tab1, false); + await play(tab2, false); + ok(activeMediaBlocked(tab0), "Tab0 is activemedia-blocked"); + ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked"); + ok(activeMediaBlocked(tab2), "Tab2 is activemedia-blocked"); + + // tab3 and tab4 are still unblocked + ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked"); + ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked"); + + // Multiselect tab0, tab1, tab2, and tab3. + info("Multiselect tabs"); + await triggerClickOn(tab3, { shiftKey: true }); + + // Check multiselection + for (let i = 0; i <= 3; i++) { + ok(tabs[i].multiselected, `tab${i} is multiselected`); + } + ok(!tab4.multiselected, "tab4 is not multiselected"); + + let tab0BlockPromise = wait_for_tab_media_blocked_event(tab0, false); + let tab1BlockPromise = wait_for_tab_media_blocked_event(tab1, false); + let tab2BlockPromise = wait_for_tab_media_blocked_event(tab2, false); + + // Use the overlay icon on tab2 to play media on the selected tabs + info("Press play tab2 icon"); + await pressIcon(tab2.overlayIcon); + + // tab0, tab1, and tab2 were played and multiselected + // They will now be unblocked and playing media + info("Wait for tabs to play"); + await tab0BlockPromise; + await tab1BlockPromise; + await tab2BlockPromise; + ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked"); + ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked"); + ok(!activeMediaBlocked(tab2), "Tab2 is not activemedia-blocked"); + // tab3 was also multiselected but never played + // It will be unblocked but not playing media + ok(!activeMediaBlocked(tab3), "Tab3 is not activemedia-blocked"); + // tab4 was not multiselected and was never played + // It remains in its original state + ok(!activeMediaBlocked(tab4), "Tab4 is not activemedia-blocked"); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +/* + * Tab context menus have to show the menu icons "Play Tab" or "Play Tabs" + * depending on the number of tabs selected, and whether blocked media is present + */ +add_task(async function testTabContextMenu() { + info("Add media tabs"); + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + let tab2 = await addMediaTab(); + let tab3 = await addMediaTab(); + let tabs = [tab0, tab1, tab2, tab3]; + + let menuItemPlayTab = document.getElementById("context_playTab"); + let menuItemPlaySelectedTabs = document.getElementById( + "context_playSelectedTabs" + ); + + // Multiselect tab0, tab1, and tab2. + info("Multiselect tabs"); + await triggerClickOn(tab0, { ctrlKey: true }); + await triggerClickOn(tab1, { ctrlKey: true }); + await triggerClickOn(tab2, { ctrlKey: true }); + + // Check multiselected tabs + for (let i = 0; i <= 2; i++) { + ok(tabs[i].multiselected, `tab${i} is multi-selected`); + } + ok(!tab3.multiselected, "tab3 is not multiselected"); + + // No active media yet: + // - "Play Tab" is hidden + // - "Play Tabs" is hidden + for (let i = 0; i <= 2; i++) { + updateTabContextMenu(tabs[i]); + ok(menuItemPlayTab.hidden, `tab${i} "Play Tab" is hidden`); + ok(menuItemPlaySelectedTabs.hidden, `tab${i} "Play Tabs" is hidden`); + ok(!activeMediaBlocked(tabs[i]), `tab${i} is not active media blocked`); + } + + info("Play tabs 0, 1, and 2"); + await play(tab0, false); + await play(tab1, false); + await play(tab2, false); + + // Active media blocked: + // - "Play Tab" is hidden + // - "Play Tabs" is visible + for (let i = 0; i <= 2; i++) { + updateTabContextMenu(tabs[i]); + ok(menuItemPlayTab.hidden, `tab${i} "Play Tab" is hidden`); + ok(!menuItemPlaySelectedTabs.hidden, `tab${i} "Play Tabs" is visible`); + ok(activeMediaBlocked(tabs[i]), `tab${i} is active media blocked`); + } + + info("Play Media on tabs 0, 1, and 2"); + gBrowser.resumeDelayedMediaOnMultiSelectedTabs(); + + // Active media is unblocked: + // - "Play Tab" is hidden + // - "Play Tabs" is hidden + for (let i = 0; i <= 2; i++) { + updateTabContextMenu(tabs[i]); + ok(menuItemPlayTab.hidden, `tab${i} "Play Tab" is hidden`); + ok(menuItemPlaySelectedTabs.hidden, `tab${i} "Play Tabs" is hidden`); + ok(!activeMediaBlocked(tabs[i]), `tab${i} is not active media blocked`); + } + + // tab3 is untouched + updateTabContextMenu(tab3); + ok(menuItemPlayTab.hidden, 'tab3 "Play Tab" is hidden'); + ok(menuItemPlaySelectedTabs.hidden, 'tab3 "Play Tabs" is hidden'); + ok(!activeMediaBlocked(tab3), "tab3 is not active media blocked"); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_reload.js b/browser/base/content/test/tabs/browser_multiselect_tabs_reload.js new file mode 100644 index 0000000000..7a68fd66d5 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_reload.js @@ -0,0 +1,82 @@ +async function tabLoaded(tab) { + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + return true; +} + +add_task(async function test_usingTabContextMenu() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + + let menuItemReloadTab = document.getElementById("context_reloadTab"); + let menuItemReloadSelectedTabs = document.getElementById( + "context_reloadSelectedTabs" + ); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(tab2.multiselected, "Tab2 is multi-selected"); + ok(!tab3.multiselected, "Tab3 is not multi-selected"); + + updateTabContextMenu(tab3); + is(menuItemReloadTab.hidden, false, "Reload Tab is visible"); + is(menuItemReloadSelectedTabs.hidden, true, "Reload Tabs is hidden"); + + updateTabContextMenu(tab2); + is(menuItemReloadTab.hidden, true, "Reload Tab is hidden"); + is(menuItemReloadSelectedTabs.hidden, false, "Reload Tabs is visible"); + + let tab1Loaded = tabLoaded(tab1); + let tab2Loaded = tabLoaded(tab2); + menuItemReloadSelectedTabs.click(); + await tab1Loaded; + await tab2Loaded; + + // We got here because tab1 and tab2 are reloaded. Otherwise the test would have timed out and failed. + ok(true, "Tab1 and Tab2 are reloaded"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); + +add_task(async function test_usingKeyboardShortcuts() { + let keys = [ + ["R", { accelKey: true }], + ["R", { accelKey: true, shift: true }], + ["VK_F5", {}], + ]; + + if (AppConstants.platform != "macosx") { + keys.push(["VK_F5", { accelKey: true }]); + } + + for (let key of keys) { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab2, { ctrlKey: true }); + + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(tab2.multiselected, "Tab2 is multi-selected"); + ok(!tab3.multiselected, "Tab3 is not multi-selected"); + + let tab1Loaded = tabLoaded(tab1); + let tab2Loaded = tabLoaded(tab2); + EventUtils.synthesizeKey(key[0], key[1]); + await tab1Loaded; + await tab2Loaded; + + // We got here because tab1 and tab2 are reloaded. Otherwise the test would have timed out and failed. + ok(true, "Tab1 and Tab2 are reloaded"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_reopen_in_container.js b/browser/base/content/test/tabs/browser_multiselect_tabs_reopen_in_container.js new file mode 100644 index 0000000000..0c9c913844 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_reopen_in_container.js @@ -0,0 +1,133 @@ +"use strict"; + +const PREF_PRIVACY_USER_CONTEXT_ENABLED = "privacy.userContext.enabled"; + +async function openTabMenuFor(tab) { + let tabMenu = tab.ownerDocument.getElementById("tabContextMenu"); + + let tabMenuShown = BrowserTestUtils.waitForEvent(tabMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + tab, + { type: "contextmenu" }, + tab.ownerGlobal + ); + await tabMenuShown; + + return tabMenu; +} + +async function openReopenMenuForTab(tab) { + await openTabMenuFor(tab); + + let reopenItem = tab.ownerDocument.getElementById( + "context_reopenInContainer" + ); + ok(!reopenItem.hidden, "Reopen in Container item should be shown"); + + let reopenMenu = reopenItem.getElementsByTagName("menupopup")[0]; + let reopenMenuShown = BrowserTestUtils.waitForEvent(reopenMenu, "popupshown"); + reopenItem.openMenu(true); + await reopenMenuShown; + + return reopenMenu; +} + +function checkMenuItem(reopenMenu, shown, hidden) { + for (let id of shown) { + ok( + reopenMenu.querySelector(`menuitem[data-usercontextid="${id}"]`), + `User context id ${id} should exist` + ); + } + for (let id of hidden) { + ok( + !reopenMenu.querySelector(`menuitem[data-usercontextid="${id}"]`), + `User context id ${id} shouldn't exist` + ); + } +} + +function openTabInContainer(gBrowser, tab, reopenMenu, id) { + let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, getUrl(tab), true); + let menuitem = reopenMenu.querySelector( + `menuitem[data-usercontextid="${id}"]` + ); + reopenMenu.activateItem(menuitem); + return tabPromise; +} + +add_task(async function testReopen() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_PRIVACY_USER_CONTEXT_ENABLED, true]], + }); + + let tab1 = await addTab("http://mochi.test:8888/1"); + let tab2 = await addTab("http://mochi.test:8888/2"); + let tab3 = await addTab("http://mochi.test:8888/3"); + let tab4 = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/3", { + createLazyBrowser: true, + }); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + + await triggerClickOn(tab2, { ctrlKey: true }); + await triggerClickOn(tab4, { ctrlKey: true }); + + for (let tab of [tab1, tab2, tab3, tab4]) { + ok( + !tab.hasAttribute("usercontextid"), + "Tab with No Container should be opened" + ); + } + + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(tab2.multiselected, "Tab2 is multi-selected"); + ok(!tab3.multiselected, "Tab3 is not multi-selected"); + ok(tab4.multiselected, "Tab4 is multi-selected"); + + is(gBrowser.visibleTabs.length, 5, "We have 5 tabs open"); + + let reopenMenu1 = await openReopenMenuForTab(tab1); + checkMenuItem(reopenMenu1, [1, 2, 3, 4], [0]); + let containerTab1 = await openTabInContainer( + gBrowser, + tab1, + reopenMenu1, + "1" + ); + + let tabs = gBrowser.visibleTabs; + is(tabs.length, 8, "Now we have 8 tabs open"); + + is(containerTab1._tPos, 2, "containerTab1 position is 3"); + is( + containerTab1.getAttribute("usercontextid"), + "1", + "Tab(1) with UCI=1 should be opened" + ); + is(getUrl(containerTab1), getUrl(tab1), "Same page (tab1) should be opened"); + + let containerTab2 = tabs[4]; + is( + containerTab2.getAttribute("usercontextid"), + "1", + "Tab(2) with UCI=1 should be opened" + ); + await TestUtils.waitForCondition(function () { + return getUrl(containerTab2) == getUrl(tab2); + }, "Same page (tab2) should be opened"); + + let containerTab4 = tabs[7]; + is( + containerTab2.getAttribute("usercontextid"), + "1", + "Tab(4) with UCI=1 should be opened" + ); + await TestUtils.waitForCondition(function () { + return getUrl(containerTab4) == getUrl(tab4); + }, "Same page (tab4) should be opened"); + + for (let tab of tabs.filter(t => t != tabs[0])) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js b/browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js new file mode 100644 index 0000000000..c3b3356608 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + // Disable tab animations + gReduceMotionOverride = true; + + let tab0 = gBrowser.selectedTab; + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + let tab5 = await addTab(); + let tabs = [tab0, tab1, tab2, tab3, tab4, tab5]; + + await BrowserTestUtils.switchTab(gBrowser, tab1); + await triggerClickOn(tab3, { ctrlKey: true }); + await triggerClickOn(tab5, { ctrlKey: true }); + + is(gBrowser.selectedTab, tab1, "Tab1 is active"); + is(gBrowser.selectedTabs.length, 3, "Three selected tabs"); + + for (let i of [1, 3, 5]) { + ok(tabs[i].multiselected, "Tab" + i + " is multiselected"); + } + for (let i of [0, 2, 4]) { + ok(!tabs[i].multiselected, "Tab" + i + " is not multiselected"); + } + for (let i of [0, 1, 2, 3, 4, 5]) { + is(tabs[i]._tPos, i, "Tab" + i + " position is :" + i); + } + + await dragAndDrop(tab3, tab4, false); + + is(gBrowser.selectedTab, tab3, "Dragged tab (tab3) is now active"); + is(gBrowser.selectedTabs.length, 3, "Three selected tabs"); + + for (let i of [1, 3, 5]) { + ok(tabs[i].multiselected, "Tab" + i + " is still multiselected"); + } + for (let i of [0, 2, 4]) { + ok(!tabs[i].multiselected, "Tab" + i + " is still not multiselected"); + } + + is(tab0._tPos, 0, "Tab0 position (0) doesn't change"); + + // Multiselected tabs gets grouped at the start of the slide. + is( + tab1._tPos, + tab3._tPos - 1, + "Tab1 is located right at the left of the dragged tab (tab3)" + ); + is( + tab5._tPos, + tab3._tPos + 1, + "Tab5 is located right at the right of the dragged tab (tab3)" + ); + is(tab3._tPos, 4, "Dragged tab (tab3) position is 4"); + + is(tab4._tPos, 2, "Drag target (tab4) has shifted to position 2"); + + for (let tab of tabs.filter(t => t != tab0)) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js new file mode 100644 index 0000000000..93a14a87a7 --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js @@ -0,0 +1,60 @@ +add_task(async function click() { + const initialFocusedTab = await addTab(); + await BrowserTestUtils.switchTab(gBrowser, initialFocusedTab); + const tab = await addTab(); + + await triggerClickOn(tab, { ctrlKey: true }); + ok( + tab.multiselected && gBrowser._multiSelectedTabsSet.has(tab), + "Tab should be (multi) selected after click" + ); + isnot(gBrowser.selectedTab, tab, "Multi-selected tab is not focused"); + is(gBrowser.selectedTab, initialFocusedTab, "Focused tab doesn't change"); + + await triggerClickOn(tab, { ctrlKey: true }); + ok( + !tab.multiselected && !gBrowser._multiSelectedTabsSet.has(tab), + "Tab is not (multi) selected anymore" + ); + is( + gBrowser.selectedTab, + initialFocusedTab, + "Focused tab still doesn't change" + ); + + BrowserTestUtils.removeTab(initialFocusedTab); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function clearSelection() { + const tab1 = await addTab(); + const tab2 = await addTab(); + const tab3 = await addTab(); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + info("We multi-select tab2 with ctrl key down"); + await triggerClickOn(tab2, { ctrlKey: true }); + + ok( + tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1), + "Tab1 is (multi) selected" + ); + ok( + tab2.multiselected && gBrowser._multiSelectedTabsSet.has(tab2), + "Tab2 is (multi) selected" + ); + is(gBrowser.multiSelectedTabsCount, 2, "Two tabs (multi) selected"); + isnot(tab3, gBrowser.selectedTab, "Tab3 doesn't have focus"); + + info("We select tab3 with Ctrl key up"); + await triggerClickOn(tab3, { ctrlKey: false }); + + ok(!tab1.multiselected, "Tab1 is not (multi) selected"); + ok(!tab2.multiselected, "Tab2 is not (multi) selected"); + is(gBrowser.multiSelectedTabsCount, 0, "Multi-selection is cleared"); + is(tab3, gBrowser.selectedTab, "Tab3 has focus"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js new file mode 100644 index 0000000000..ac647bae3c --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js @@ -0,0 +1,159 @@ +add_task(async function noItemsInTheCollectionBeforeShiftClicking() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + let tab5 = await addTab(); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + + is(gBrowser.selectedTab, tab1, "Tab1 has focus now"); + is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected"); + + gBrowser.hideTab(tab3); + ok(tab3.hidden, "Tab3 is hidden"); + + info("Click on tab4 while holding shift key"); + await triggerClickOn(tab4, { shiftKey: true }); + + ok( + tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1), + "Tab1 is multi-selected" + ); + ok( + tab2.multiselected && gBrowser._multiSelectedTabsSet.has(tab2), + "Tab2 is multi-selected" + ); + ok( + !tab3.multiselected && !gBrowser._multiSelectedTabsSet.has(tab3), + "Hidden tab3 is not multi-selected" + ); + ok( + tab4.multiselected && gBrowser._multiSelectedTabsSet.has(tab4), + "Tab4 is multi-selected" + ); + ok( + !tab5.multiselected && !gBrowser._multiSelectedTabsSet.has(tab5), + "Tab5 is not multi-selected" + ); + is(gBrowser.multiSelectedTabsCount, 3, "three multi-selected tabs"); + is(gBrowser.selectedTab, tab1, "Tab1 still has focus"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + BrowserTestUtils.removeTab(tab5); +}); + +add_task(async function itemsInTheCollectionBeforeShiftClicking() { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tab4 = await addTab(); + let tab5 = await addTab(); + + await BrowserTestUtils.switchTab(gBrowser, () => triggerClickOn(tab1, {})); + + is(gBrowser.selectedTab, tab1, "Tab1 has focus now"); + is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected"); + + await triggerClickOn(tab3, { ctrlKey: true }); + is(gBrowser.selectedTab, tab1, "Tab1 still has focus"); + is(gBrowser.multiSelectedTabsCount, 2, "Two tabs are multi-selected"); + ok( + tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1), + "Tab1 is multi-selected" + ); + ok( + tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3), + "Tab3 is multi-selected" + ); + + info("Click on tab5 while holding Shift key"); + await BrowserTestUtils.switchTab( + gBrowser, + triggerClickOn(tab5, { shiftKey: true }) + ); + + is(gBrowser.selectedTab, tab3, "Tab3 has focus"); + ok( + !tab1.multiselected && !gBrowser._multiSelectedTabsSet.has(tab1), + "Tab1 is not multi-selected" + ); + ok( + !tab2.multiselected && !gBrowser._multiSelectedTabsSet.has(tab2), + "Tab2 is not multi-selected " + ); + ok( + tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3), + "Tab3 is multi-selected" + ); + ok( + tab4.multiselected && gBrowser._multiSelectedTabsSet.has(tab4), + "Tab4 is multi-selected" + ); + ok( + tab5.multiselected && gBrowser._multiSelectedTabsSet.has(tab5), + "Tab5 is multi-selected" + ); + is(gBrowser.multiSelectedTabsCount, 3, "Three tabs are multi-selected"); + + info("Click on tab4 while holding Shift key"); + await triggerClickOn(tab4, { shiftKey: true }); + + is(gBrowser.selectedTab, tab3, "Tab3 has focus"); + ok( + !tab1.multiselected && !gBrowser._multiSelectedTabsSet.has(tab1), + "Tab1 is not multi-selected" + ); + ok( + !tab2.multiselected && !gBrowser._multiSelectedTabsSet.has(tab2), + "Tab2 is not multi-selected " + ); + ok( + tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3), + "Tab3 is multi-selected" + ); + ok( + tab4.multiselected && gBrowser._multiSelectedTabsSet.has(tab4), + "Tab4 is multi-selected" + ); + ok( + !tab5.multiselected && !gBrowser._multiSelectedTabsSet.has(tab5), + "Tab5 is not multi-selected" + ); + is(gBrowser.multiSelectedTabsCount, 2, "Two tabs are multi-selected"); + + info("Click on tab1 while holding Shift key"); + await triggerClickOn(tab1, { shiftKey: true }); + + is(gBrowser.selectedTab, tab3, "Tab3 has focus"); + ok( + tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1), + "Tab1 is multi-selected" + ); + ok( + tab2.multiselected && gBrowser._multiSelectedTabsSet.has(tab2), + "Tab2 is multi-selected " + ); + ok( + tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3), + "Tab3 is multi-selected" + ); + ok( + !tab4.multiselected && !gBrowser._multiSelectedTabsSet.has(tab4), + "Tab4 is not multi-selected" + ); + ok( + !tab5.multiselected && !gBrowser._multiSelectedTabsSet.has(tab5), + "Tab5 is not multi-selected" + ); + is(gBrowser.multiSelectedTabsCount, 3, "Three tabs are multi-selected"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + BrowserTestUtils.removeTab(tab5); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift_and_Ctrl.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift_and_Ctrl.js new file mode 100644 index 0000000000..9e26a5562e --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift_and_Ctrl.js @@ -0,0 +1,75 @@ +add_task(async function selectionWithShiftPreviously() { + const tab1 = await addTab(); + const tab2 = await addTab(); + const tab3 = await addTab(); + const tab4 = await addTab(); + const tab5 = await addTab(); + + await BrowserTestUtils.switchTab(gBrowser, tab3); + + is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected"); + + info("Click on tab5 with Shift down"); + await triggerClickOn(tab5, { shiftKey: true }); + + is(gBrowser.selectedTab, tab3, "Tab3 has focus"); + ok(!tab1.multiselected, "Tab1 is not multi-selected"); + ok(!tab2.multiselected, "Tab2 is not multi-selected "); + ok(tab3.multiselected, "Tab3 is multi-selected"); + ok(tab4.multiselected, "Tab4 is multi-selected"); + ok(tab5.multiselected, "Tab5 is multi-selected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three tabs are multi-selected"); + + info("Click on tab1 with both Ctrl/Cmd and Shift down"); + await triggerClickOn(tab1, { ctrlKey: true, shiftKey: true }); + + is(gBrowser.selectedTab, tab3, "Tab3 has focus"); + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(tab2.multiselected, "Tab2 is multi-selected "); + ok(tab3.multiselected, "Tab3 is multi-selected"); + ok(tab4.multiselected, "Tab4 is multi-selected"); + ok(tab5.multiselected, "Tab5 is multi-selected"); + is(gBrowser.multiSelectedTabsCount, 5, "Five tabs are multi-selected"); + + for (let tab of [tab1, tab2, tab3, tab4, tab5]) { + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function selectionWithCtrlPreviously() { + const tab1 = await addTab(); + const tab2 = await addTab(); + const tab3 = await addTab(); + const tab4 = await addTab(); + const tab5 = await addTab(); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + + is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected"); + + info("Click on tab3 with Ctrl key down"); + await triggerClickOn(tab3, { ctrlKey: true }); + + is(gBrowser.selectedTab, tab1, "Tab1 has focus"); + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(!tab2.multiselected, "Tab2 is not multi-selected "); + ok(tab3.multiselected, "Tab3 is multi-selected"); + ok(!tab4.multiselected, "Tab4 is not multi-selected"); + ok(!tab5.multiselected, "Tab5 is not multi-selected"); + is(gBrowser.multiSelectedTabsCount, 2, "Two tabs are multi-selected"); + + info("Click on tab5 with both Ctrl/Cmd and Shift down"); + await triggerClickOn(tab5, { ctrlKey: true, shiftKey: true }); + + is(gBrowser.selectedTab, tab1, "Tab3 has focus"); + ok(tab1.multiselected, "Tab1 is multi-selected"); + ok(!tab2.multiselected, "Tab2 is not multi-selected "); + ok(tab3.multiselected, "Tab3 is multi-selected"); + ok(tab4.multiselected, "Tab4 is multi-selected"); + ok(tab5.multiselected, "Tab5 is multi-selected"); + is(gBrowser.multiSelectedTabsCount, 4, "Four tabs are multi-selected"); + + for (let tab of [tab1, tab2, tab3, tab4, tab5]) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js new file mode 100644 index 0000000000..cdb0b7bf0c --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_keyboard.js @@ -0,0 +1,147 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function synthesizeKeyAndWaitForFocus(element, keyCode, options) { + let focused = BrowserTestUtils.waitForEvent(element, "focus"); + EventUtils.synthesizeKey(keyCode, options); + return focused; +} + +function synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab, keyCode, options) { + let focused = TestUtils.waitForCondition(() => { + return tab.classList.contains("keyboard-focused-tab"); + }, "Waiting for tab to get keyboard focus"); + EventUtils.synthesizeKey(keyCode, options); + return focused; +} + +add_setup(async function () { + // The DevEdition has the DevTools button in the toolbar by default. Remove it + // to prevent branch-specific rules what button should be focused. + CustomizableUI.removeWidgetFromArea("developer-button"); + + let prevActiveElement = document.activeElement; + registerCleanupFunction(() => { + CustomizableUI.reset(); + prevActiveElement.focus(); + }); +}); + +add_task(async function changeSelectionUsingKeyboard() { + const tab1 = await addTab("http://mochi.test:8888/1"); + const tab2 = await addTab("http://mochi.test:8888/2"); + const tab3 = await addTab("http://mochi.test:8888/3"); + const tab4 = await addTab("http://mochi.test:8888/4"); + const tab5 = await addTab("http://mochi.test:8888/5"); + + await BrowserTestUtils.switchTab(gBrowser, tab3); + info("Move focus to location bar using the keyboard"); + await synthesizeKeyAndWaitForFocus(gURLBar, "l", { accelKey: true }); + is(document.activeElement, gURLBar.inputField, "urlbar should be focused"); + + info("Move focus to the selected tab using the keyboard"); + let trackingProtectionIconContainer = document.querySelector( + "#tracking-protection-icon-container" + ); + await synthesizeKeyAndWaitForFocus( + trackingProtectionIconContainer, + "VK_TAB", + { shiftKey: true } + ); + is( + document.activeElement, + trackingProtectionIconContainer, + "tracking protection icon container should be focused" + ); + await synthesizeKeyAndWaitForFocus( + document.getElementById("reload-button"), + "VK_TAB", + { shiftKey: true } + ); + await synthesizeKeyAndWaitForFocus( + document.getElementById("tabs-newtab-button"), + "VK_TAB", + { shiftKey: true } + ); + await synthesizeKeyAndWaitForFocus(tab3, "VK_TAB", { shiftKey: true }); + is(document.activeElement, tab3, "Tab3 should be focused"); + + info("Move focus to tab 1 using the keyboard"); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab2, "KEY_ArrowLeft", { + accelKey: true, + }); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab1, "KEY_ArrowLeft", { + accelKey: true, + }); + is( + gBrowser.tabContainer.ariaFocusedItem, + tab1, + "Tab1 should be the ariaFocusedItem" + ); + + ok(!tab1.multiselected, "Tab1 shouldn't be multiselected"); + info("Select tab1 using keyboard"); + EventUtils.synthesizeKey("VK_SPACE", { accelKey: true }); + ok(tab1.multiselected, "Tab1 should be multiselected"); + + info("Move focus to tab 5 using the keyboard"); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab2, "KEY_ArrowRight", { + accelKey: true, + }); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab3, "KEY_ArrowRight", { + accelKey: true, + }); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab4, "KEY_ArrowRight", { + accelKey: true, + }); + await synthesizeKeyAndWaitForTabToGetKeyboardFocus(tab5, "KEY_ArrowRight", { + accelKey: true, + }); + + ok(!tab5.multiselected, "Tab5 shouldn't be multiselected"); + info("Select tab5 using keyboard"); + EventUtils.synthesizeKey("VK_SPACE", { accelKey: true }); + ok(tab5.multiselected, "Tab5 should be multiselected"); + + ok( + tab1.multiselected && gBrowser._multiSelectedTabsSet.has(tab1), + "Tab1 is (multi) selected" + ); + ok( + tab3.multiselected && gBrowser._multiSelectedTabsSet.has(tab3), + "Tab3 is (multi) selected" + ); + ok( + tab5.multiselected && gBrowser._multiSelectedTabsSet.has(tab5), + "Tab5 is (multi) selected" + ); + is(gBrowser.multiSelectedTabsCount, 3, "Three tabs (multi) selected"); + is(tab3, gBrowser.selectedTab, "Tab3 is still the selected tab"); + + await synthesizeKeyAndWaitForFocus(tab4, "KEY_ArrowLeft", {}); + is( + tab4, + gBrowser.selectedTab, + "Tab4 is now selected tab since tab5 had keyboard focus" + ); + + is(tab4.previousElementSibling, tab3, "tab4 should be after tab3"); + is(tab4.nextElementSibling, tab5, "tab4 should be before tab5"); + + let tabsReordered = BrowserTestUtils.waitForCondition(() => { + return ( + tab4.previousElementSibling == tab2 && tab4.nextElementSibling == tab3 + ); + }, "tab4 should now be after tab2 and before tab3"); + EventUtils.synthesizeKey("KEY_ArrowLeft", { accelKey: true, shiftKey: true }); + await tabsReordered; + + is(tab4.previousElementSibling, tab2, "tab4 should be after tab2"); + is(tab4.nextElementSibling, tab3, "tab4 should be before tab3"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + BrowserTestUtils.removeTab(tab5); +}); diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_using_selectedTabs.js b/browser/base/content/test/tabs/browser_multiselect_tabs_using_selectedTabs.js new file mode 100644 index 0000000000..0db980bf6b --- /dev/null +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_selectedTabs.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function () { + function testSelectedTabs(tabs) { + is( + gBrowser.tabContainer.getAttribute("aria-multiselectable"), + "true", + "tabbrowser should be marked as aria-multiselectable" + ); + gBrowser.selectedTabs = tabs; + let { selectedTab, selectedTabs, _multiSelectedTabsSet } = gBrowser; + is(selectedTab, tabs[0], "The selected tab should be the expected one"); + if (tabs.length == 1) { + ok( + !selectedTab.multiselected, + "Selected tab shouldn't be multi-selected because we are not in multi-select context yet" + ); + ok( + !_multiSelectedTabsSet.has(selectedTab), + "Selected tab shouldn't be in _multiSelectedTabsSet" + ); + is(selectedTabs.length, 1, "selectedTabs should contain a single tab"); + is( + selectedTabs[0], + selectedTab, + "selectedTabs should contain the selected tab" + ); + ok( + !selectedTab.hasAttribute("aria-selected"), + "Selected tab shouldn't be marked as aria-selected when only one tab is selected" + ); + } else { + const uniqueTabs = [...new Set(tabs)]; + is( + selectedTabs.length, + uniqueTabs.length, + "Check number of selected tabs" + ); + for (let tab of uniqueTabs) { + ok(tab.multiselected, "Tab should be multi-selected"); + ok( + _multiSelectedTabsSet.has(tab), + "Tab should be in _multiSelectedTabsSet" + ); + ok(selectedTabs.includes(tab), "Tab should be in selectedTabs"); + is( + tab.getAttribute("aria-selected"), + "true", + "Selected tab should be marked as aria-selected" + ); + } + } + } + + const tab1 = await addTab(); + const tab2 = await addTab(); + const tab3 = await addTab(); + + testSelectedTabs([tab1]); + testSelectedTabs([tab2]); + testSelectedTabs([tab2, tab1]); + testSelectedTabs([tab1, tab2]); + testSelectedTabs([tab3, tab2]); + testSelectedTabs([tab3, tab1]); + testSelectedTabs([tab1, tab2, tab1]); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); +}); diff --git a/browser/base/content/test/tabs/browser_navigatePinnedTab.js b/browser/base/content/test/tabs/browser_navigatePinnedTab.js new file mode 100644 index 0000000000..d9d0728ecb --- /dev/null +++ b/browser/base/content/test/tabs/browser_navigatePinnedTab.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + // Test that changing the URL in a pinned tab works correctly + + let TEST_LINK_INITIAL = "about:mozilla"; + let TEST_LINK_CHANGED = "about:support"; + + let appTab = BrowserTestUtils.addTab(gBrowser, TEST_LINK_INITIAL); + let browser = appTab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + gBrowser.pinTab(appTab); + is(appTab.pinned, true, "Tab was successfully pinned"); + + let initialTabsNo = gBrowser.tabs.length; + + gBrowser.selectedTab = appTab; + gURLBar.focus(); + gURLBar.value = TEST_LINK_CHANGED; + + gURLBar.goButton.click(); + await BrowserTestUtils.browserLoaded(browser); + + is( + appTab.linkedBrowser.currentURI.spec, + TEST_LINK_CHANGED, + "New page loaded in the app tab" + ); + is(gBrowser.tabs.length, initialTabsNo, "No additional tabs were opened"); + + // Now check that opening a link that does create a new tab works, + // and also that it nulls out the opener. + let pageLoadPromise = BrowserTestUtils.browserLoaded( + appTab.linkedBrowser, + false, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/" + ); + BrowserTestUtils.startLoadingURIString( + appTab.linkedBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/" + ); + info("Started loading example.com"); + await pageLoadPromise; + info("Loaded example.com"); + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/" + ); + await SpecialPowers.spawn(browser, [], async function () { + let link = content.document.createElement("a"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + link.href = "http://example.org/"; + content.document.body.appendChild(link); + link.click(); + }); + info("Created & clicked link"); + let extraTab = await newTabPromise; + info("Got a new tab"); + await SpecialPowers.spawn(extraTab.linkedBrowser, [], async function () { + is(content.opener, null, "No opener should be available"); + }); + BrowserTestUtils.removeTab(extraTab); +}); + +registerCleanupFunction(function () { + gBrowser.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/base/content/test/tabs/browser_navigate_home_focuses_addressbar.js b/browser/base/content/test/tabs/browser_navigate_home_focuses_addressbar.js new file mode 100644 index 0000000000..55efdba851 --- /dev/null +++ b/browser/base/content/test/tabs/browser_navigate_home_focuses_addressbar.js @@ -0,0 +1,21 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +const TEST_HTTP = httpURL("dummy_page.html"); + +// Test for Bug 1634272 +add_task(async function () { + await BrowserTestUtils.withNewTab(TEST_HTTP, async function (browser) { + info("Tab ready"); + + CustomizableUI.addWidgetToArea("home-button", "nav-bar"); + registerCleanupFunction(() => + CustomizableUI.removeWidgetFromArea("home-button") + ); + + document.getElementById("home-button").click(); + await BrowserTestUtils.browserLoaded(browser, false, HomePage.get()); + is(gURLBar.value, "", "URL bar should be empty"); + ok(gURLBar.focused, "URL bar should be focused"); + }); +}); diff --git a/browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js b/browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js new file mode 100644 index 0000000000..5b50cc615f --- /dev/null +++ b/browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js @@ -0,0 +1,177 @@ +/* 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/. */ + +/* import-globals-from helper_origin_attrs_testing.js */ +loadTestSubscript("helper_origin_attrs_testing.js"); + +const PATH = "browser/browser/base/content/test/tabs/blank.html"; + +var TEST_CASES = [ + { uri: "https://example.com/" + PATH }, + { uri: "https://example.org/" + PATH }, + { uri: "about:preferences" }, + { uri: "about:config" }, +]; + +// 3 container tabs, 1 regular tab and 1 private tab +const NUM_PAGES_OPEN_FOR_EACH_TEST_CASE = 5; +var remoteTypes; +var xulFrameLoaderCreatedCounter = {}; + +function handleEventLocal(aEvent) { + if (aEvent.type != "XULFrameLoaderCreated") { + return; + } + // Ignore <browser> element in about:preferences and any other special pages + if ("gBrowser" in aEvent.target.ownerGlobal) { + xulFrameLoaderCreatedCounter.numCalledSoFar++; + } +} + +var gPrevRemoteTypeRegularTab; +var gPrevRemoteTypeContainerTab; +var gPrevRemoteTypePrivateTab; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.userContext.enabled", true], + // don't preload tabs so we don't have extra XULFrameLoaderCreated events + // firing + ["browser.newtab.preload", false], + ], + }); + + requestLongerTimeout(4); +}); + +function setupRemoteTypes() { + gPrevRemoteTypeRegularTab = null; + gPrevRemoteTypeContainerTab = {}; + gPrevRemoteTypePrivateTab = null; + + remoteTypes = getExpectedRemoteTypes( + gFissionBrowser, + NUM_PAGES_OPEN_FOR_EACH_TEST_CASE + ); +} + +add_task(async function testNavigate() { + setupRemoteTypes(); + /** + * Open a regular tab, 3 container tabs and a private window, load about:blank or about:privatebrowsing + * For each test case + * load the uri + * verify correct remote type + * close tabs + */ + + let regularPage = await openURIInRegularTab("about:blank", window); + gPrevRemoteTypeRegularTab = regularPage.tab.linkedBrowser.remoteType; + let containerPages = []; + for ( + var user_context_id = 1; + user_context_id <= NUM_USER_CONTEXTS; + user_context_id++ + ) { + let containerPage = await openURIInContainer( + "about:blank", + window, + user_context_id + ); + gPrevRemoteTypeContainerTab[user_context_id] = + containerPage.tab.linkedBrowser.remoteType; + containerPages.push(containerPage); + } + + let privatePage = await openURIInPrivateTab(); + gPrevRemoteTypePrivateTab = privatePage.tab.linkedBrowser.remoteType; + + for (const testCase of TEST_CASES) { + let uri = testCase.uri; + + await loadURIAndCheckRemoteType( + regularPage.tab.linkedBrowser, + uri, + "regular tab", + gPrevRemoteTypeRegularTab + ); + gPrevRemoteTypeRegularTab = regularPage.tab.linkedBrowser.remoteType; + + for (const page of containerPages) { + await loadURIAndCheckRemoteType( + page.tab.linkedBrowser, + uri, + `container tab ${page.user_context_id}`, + gPrevRemoteTypeContainerTab[page.user_context_id] + ); + gPrevRemoteTypeContainerTab[page.user_context_id] = + page.tab.linkedBrowser.remoteType; + } + + await loadURIAndCheckRemoteType( + privatePage.tab.linkedBrowser, + uri, + "private tab", + gPrevRemoteTypePrivateTab + ); + gPrevRemoteTypePrivateTab = privatePage.tab.linkedBrowser.remoteType; + } + // Close tabs + containerPages.forEach(containerPage => { + BrowserTestUtils.removeTab(containerPage.tab); + }); + BrowserTestUtils.removeTab(regularPage.tab); + BrowserTestUtils.removeTab(privatePage.tab); +}); + +async function loadURIAndCheckRemoteType( + aBrowser, + aURI, + aText, + aPrevRemoteType +) { + let expectedCurr = remoteTypes.shift(); + initXulFrameLoaderCreatedCounter(xulFrameLoaderCreatedCounter); + aBrowser.ownerGlobal.gBrowser.addEventListener( + "XULFrameLoaderCreated", + handleEventLocal + ); + let loaded = BrowserTestUtils.browserLoaded(aBrowser, false, aURI); + info(`About to load ${aURI} in ${aText}`); + BrowserTestUtils.startLoadingURIString(aBrowser, aURI); + await loaded; + + // Verify correct remote type + is( + expectedCurr, + aBrowser.remoteType, + `correct remote type for ${aURI} ${aText}` + ); + + // Verify XULFrameLoaderCreated firing correct number of times + info( + `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedCounter.numCalledSoFar} time(s) for ${aURI} ${aText}` + ); + var numExpected = + expectedCurr == aPrevRemoteType && + // With BFCache in the parent we'll get a XULFrameLoaderCreated even if + // expectedCurr == aPrevRemoteType, because we store the old frameloader + // in the BFCache. We have to make an exception for loads in the parent + // process (which have a null aPrevRemoteType/expectedCurr) because + // BFCache in the parent disables caching for those loads. + (!SpecialPowers.Services.appinfo.sessionHistoryInParent || !expectedCurr) + ? 0 + : 1; + is( + xulFrameLoaderCreatedCounter.numCalledSoFar, + numExpected, + `XULFrameLoaderCreated fired correct number of times for ${aURI} ${aText} + prev=${aPrevRemoteType} curr =${aBrowser.remoteType}` + ); + aBrowser.ownerGlobal.gBrowser.removeEventListener( + "XULFrameLoaderCreated", + handleEventLocal + ); +} diff --git a/browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js b/browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js new file mode 100644 index 0000000000..9375f3f164 --- /dev/null +++ b/browser/base/content/test/tabs/browser_new_file_whitelisted_http_tab.js @@ -0,0 +1,37 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const TEST_HTTP = "http://example.org/"; + +// Test for bug 1378377. +add_task(async function () { + // Set prefs to ensure file content process. + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.remote.separateFileUriProcess", true]], + }); + + await BrowserTestUtils.withNewTab(TEST_HTTP, async function (fileBrowser) { + ok( + E10SUtils.isWebRemoteType(fileBrowser.remoteType), + "Check that tab normally has web remote type." + ); + }); + + // Set prefs to whitelist TEST_HTTP for file:// URI use. + await SpecialPowers.pushPrefEnv({ + set: [ + ["capability.policy.policynames", "allowFileURI"], + ["capability.policy.allowFileURI.sites", TEST_HTTP], + ["capability.policy.allowFileURI.checkloaduri.enabled", "allAccess"], + ], + }); + + await BrowserTestUtils.withNewTab(TEST_HTTP, async function (fileBrowser) { + is( + fileBrowser.remoteType, + E10SUtils.FILE_REMOTE_TYPE, + "Check that tab now has file remote type." + ); + }); +}); diff --git a/browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js b/browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js new file mode 100644 index 0000000000..66258659fd --- /dev/null +++ b/browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js @@ -0,0 +1,129 @@ +/* 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/. */ + +"use strict"; + +// Tests that showing the bookmarks toolbar for new tabs only doesn't affect +// the view port height in background tabs. + +let gHeightChanges = 0; +async function expectHeightChanges(tab, expectedNewHeightChanges, msg) { + let contentObservedHeightChanges = await ContentTask.spawn( + tab.linkedBrowser, + null, + async args => { + await new Promise(resolve => content.requestAnimationFrame(resolve)); + return content.document.body.innerText; + } + ); + is( + contentObservedHeightChanges - gHeightChanges, + expectedNewHeightChanges, + msg + ); + gHeightChanges = contentObservedHeightChanges; +} + +async function expectBmToolbarVisibilityChange(triggerFn, visible, msg) { + let collapsedState = BrowserTestUtils.waitForMutationCondition( + BookmarkingUI.toolbar, + { attributes: true, attributeFilter: ["collapsed"] }, + () => BookmarkingUI.toolbar.collapsed != visible + ); + let toolbarItemsVisibilityUpdated = visible + ? BrowserTestUtils.waitForEvent( + BookmarkingUI.toolbar, + "BookmarksToolbarVisibilityUpdated" + ) + : null; + triggerFn(); + await collapsedState; + is( + BookmarkingUI.toolbar.getAttribute("collapsed"), + (!visible).toString(), + `${msg}; collapsed attribute state` + ); + if (visible) { + info(`${msg}; waiting for toolbar items to become visible`); + await toolbarItemsVisibilityUpdated; + isnot( + BookmarkingUI.toolbar.getBoundingClientRect().height, + 0, + `${msg}; should have a height` + ); + } else { + is( + BookmarkingUI.toolbar.getBoundingClientRect().height, + 0, + `${msg}; should have zero height` + ); + } +} + +add_task(async function () { + registerCleanupFunction(() => { + setToolbarVisibility( + BookmarkingUI.toolbar, + gBookmarksToolbarVisibility, + false, + false + ); + }); + + await expectBmToolbarVisibilityChange( + () => setToolbarVisibility(BookmarkingUI.toolbar, false, false, false), + false, + "bookmarks toolbar is hidden initially" + ); + + let pageURL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ); + pageURL = `${pageURL}file_observe_height_changes.html`; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageURL); + + await expectBmToolbarVisibilityChange( + () => setToolbarVisibility(BookmarkingUI.toolbar, true, false, false), + true, + "bookmarks toolbar is visible after explicitly showing it for tab with content loaded" + ); + await expectHeightChanges( + tab, + 1, + "content area height changes when showing the toolbar without the animation" + ); + + await expectBmToolbarVisibilityChange( + () => setToolbarVisibility(BookmarkingUI.toolbar, "newtab", false, false), + false, + "bookmarks toolbar is hidden for non-new tab after setting it to only show for new tabs" + ); + await expectHeightChanges( + tab, + 1, + "content area height changes when hiding the toolbar without the animation" + ); + + info("Opening a new tab, making the previous tab non-selected"); + await expectBmToolbarVisibilityChange( + () => { + BrowserOpenTab(); + ok( + !tab.selected, + "non-new tab is in the background (not the selected tab)" + ); + }, + true, + "bookmarks toolbar is visible for new tab after setting it to only show for new tabs" + ); + await expectHeightChanges( + tab, + 0, + "no additional content area height change in background tab when showing the bookmarks toolbar in new tab" + ); + + gBrowser.removeCurrentTab(); + gBrowser.removeTab(tab); +}); diff --git a/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js b/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js new file mode 100644 index 0000000000..ec11951cb0 --- /dev/null +++ b/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js @@ -0,0 +1,230 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Tests to ensure that Activity Stream loads in the privileged about: + * content process. Normal http web pages should load in the web content + * process. + * Ref: Bug 1469072. + */ + +const ABOUT_BLANK = "about:blank"; +const ABOUT_HOME = "about:home"; +const ABOUT_NEWTAB = "about:newtab"; +const ABOUT_WELCOME = "about:welcome"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const TEST_HTTP = "http://example.org/"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.newtab.preload", false], + ["browser.tabs.remote.separatePrivilegedContentProcess", true], + ["dom.ipc.processCount.privilegedabout", 1], + ["dom.ipc.keepProcessesAlive.privilegedabout", 1], + ], + }); +}); + +/* + * Test to ensure that the Activity Stream tabs open in privileged about: content + * process. We will first open an about:newtab page that acts as a reference to + * the privileged about: content process. With the reference, we can then open + * Activity Stream links in a new tab and ensure that the new tab opens in the same + * privileged about: content process as our reference. + */ +add_task(async function activity_stream_in_privileged_content_process() { + Services.ppmm.releaseCachedProcesses(); + + await BrowserTestUtils.withNewTab(ABOUT_NEWTAB, async function (browser1) { + checkBrowserRemoteType(browser1, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE); + + // Note the processID for about:newtab for comparison later. + let privilegedPid = browser1.frameLoader.remoteTab.osPid; + + for (let url of [ + ABOUT_NEWTAB, + ABOUT_WELCOME, + ABOUT_HOME, + `${ABOUT_NEWTAB}#foo`, + `${ABOUT_WELCOME}#bar`, + `${ABOUT_HOME}#baz`, + `${ABOUT_NEWTAB}?q=foo`, + `${ABOUT_WELCOME}?q=bar`, + `${ABOUT_HOME}?q=baz`, + ]) { + await BrowserTestUtils.withNewTab(url, async function (browser2) { + is( + browser2.frameLoader.remoteTab.osPid, + privilegedPid, + "Check that about:newtab tabs are in the same privileged about: content process." + ); + }); + } + }); + + Services.ppmm.releaseCachedProcesses(); +}); + +/* + * Test to ensure that a process switch occurs when navigating between normal + * web pages and Activity Stream pages in the same tab. + */ +add_task(async function process_switching_through_loading_in_the_same_tab() { + Services.ppmm.releaseCachedProcesses(); + + await BrowserTestUtils.withNewTab(TEST_HTTP, async function (browser) { + checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE); + + for (let [url, remoteType] of [ + [ABOUT_NEWTAB, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE], + [ABOUT_BLANK, E10SUtils.WEB_REMOTE_TYPE], + [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE], + [ABOUT_HOME, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE], + [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE], + [ABOUT_WELCOME, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE], + [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE], + [ABOUT_BLANK, E10SUtils.WEB_REMOTE_TYPE], + [`${ABOUT_NEWTAB}#foo`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE], + [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE], + [`${ABOUT_WELCOME}#bar`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE], + [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE], + [`${ABOUT_HOME}#baz`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE], + [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE], + [`${ABOUT_NEWTAB}?q=foo`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE], + [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE], + [`${ABOUT_WELCOME}?q=bar`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE], + [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE], + [`${ABOUT_HOME}?q=baz`, E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE], + [TEST_HTTP, E10SUtils.WEB_REMOTE_TYPE], + ]) { + BrowserTestUtils.startLoadingURIString(browser, url); + await BrowserTestUtils.browserLoaded(browser, false, url); + checkBrowserRemoteType(browser, remoteType); + } + }); + + Services.ppmm.releaseCachedProcesses(); +}); + +/* + * Test to ensure that a process switch occurs when navigating between normal + * web pages and Activity Stream pages using the browser's navigation features + * such as history and location change. + */ +add_task(async function process_switching_through_navigation_features() { + Services.ppmm.releaseCachedProcesses(); + + await BrowserTestUtils.withNewTab( + ABOUT_NEWTAB, + async function (initialBrowser) { + checkBrowserRemoteType( + initialBrowser, + E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE + ); + + // Note the processID for about:newtab for comparison later. + let privilegedPid = initialBrowser.frameLoader.remoteTab.osPid; + + function assertIsPrivilegedProcess(browser, desc) { + is( + browser.messageManager.remoteType, + E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE, + `Check that ${desc} is loaded in privileged about: content process.` + ); + is( + browser.frameLoader.remoteTab.osPid, + privilegedPid, + `Check that ${desc} is loaded in original privileged process.` + ); + } + + // Check that about:newtab opened from JS in about:newtab page is in the same process. + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + gBrowser, + ABOUT_NEWTAB, + true + ); + await SpecialPowers.spawn(initialBrowser, [ABOUT_NEWTAB], uri => { + content.open(uri, "_blank"); + }); + let newTab = await promiseTabOpened; + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(newTab); + }); + let browser = newTab.linkedBrowser; + assertIsPrivilegedProcess(browser, "new tab opened from about:newtab"); + + // Check that reload does not break the privileged about: content process affinity. + BrowserReload(); + await BrowserTestUtils.browserLoaded(browser, false, ABOUT_NEWTAB); + assertIsPrivilegedProcess(browser, "about:newtab after reload"); + + // Load http webpage + BrowserTestUtils.startLoadingURIString(browser, TEST_HTTP); + await BrowserTestUtils.browserLoaded(browser, false, TEST_HTTP); + checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE); + + // Check that using the history back feature switches back to privileged about: content process. + let promiseLocation = BrowserTestUtils.waitForLocationChange( + gBrowser, + ABOUT_NEWTAB + ); + browser.goBack(); + await promiseLocation; + // We will need to ensure that the process flip has fully completed so that + // the navigation history data will be available when we do browser.goForward(); + await BrowserTestUtils.browserLoaded(browser); + assertIsPrivilegedProcess(browser, "about:newtab after history goBack"); + + // Check that using the history forward feature switches back to the web content process. + promiseLocation = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_HTTP + ); + browser.goForward(); + await promiseLocation; + // We will need to ensure that the process flip has fully completed so that + // the navigation history data will be available when we do browser.gotoIndex(0); + await BrowserTestUtils.browserLoaded(browser); + checkBrowserRemoteType( + browser, + E10SUtils.WEB_REMOTE_TYPE, + "Check that tab runs in the web content process after using history goForward." + ); + + // Check that goto history index does not break the affinity. + promiseLocation = BrowserTestUtils.waitForLocationChange( + gBrowser, + ABOUT_NEWTAB + ); + browser.gotoIndex(0); + await promiseLocation; + is( + browser.frameLoader.remoteTab.osPid, + privilegedPid, + "Check that about:newtab is in privileged about: content process after history gotoIndex." + ); + assertIsPrivilegedProcess( + browser, + "about:newtab after history goToIndex" + ); + + BrowserTestUtils.startLoadingURIString(browser, TEST_HTTP); + await BrowserTestUtils.browserLoaded(browser, false, TEST_HTTP); + checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE); + + // Check that location change causes a change in process type as well. + await SpecialPowers.spawn(browser, [ABOUT_NEWTAB], uri => { + content.location = uri; + }); + await BrowserTestUtils.browserLoaded(browser, false, ABOUT_NEWTAB); + assertIsPrivilegedProcess(browser, "about:newtab after location change"); + } + ); + + Services.ppmm.releaseCachedProcesses(); +}); diff --git a/browser/base/content/test/tabs/browser_new_tab_insert_position.js b/browser/base/content/test/tabs/browser_new_tab_insert_position.js new file mode 100644 index 0000000000..d54aed738b --- /dev/null +++ b/browser/base/content/test/tabs/browser_new_tab_insert_position.js @@ -0,0 +1,288 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", +}); + +const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL; + +function promiseBrowserStateRestored(state) { + if (typeof state != "string") { + state = JSON.stringify(state); + } + // We wait for the notification that restore is done, and for the notification + // that the active tab is loaded and restored. + let promise = Promise.all([ + TestUtils.topicObserved("sessionstore-browser-state-restored"), + BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored"), + ]); + SessionStore.setBrowserState(state); + return promise; +} + +function promiseRemoveThenUndoCloseTab(tab) { + // We wait for the notification that restore is done, and for the notification + // that the active tab is loaded and restored. + let promise = Promise.all([ + TestUtils.topicObserved("sessionstore-closed-objects-changed"), + BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored"), + ]); + BrowserTestUtils.removeTab(tab); + SessionStore.undoCloseTab(window, 0); + return promise; +} + +// Compare the current browser tab order against the session state ordering, they should always match. +function verifyTabState(state) { + let newStateTabs = JSON.parse(state).windows[0].tabs; + for (let i = 0; i < gBrowser.tabs.length; i++) { + is( + gBrowser.tabs[i].linkedBrowser.currentURI.spec, + newStateTabs[i].entries[0].url, + `tab pos ${i} matched ${gBrowser.tabs[i].linkedBrowser.currentURI.spec}` + ); + } +} + +const bulkLoad = [ + "http://mochi.test:8888/#5", + "http://mochi.test:8888/#6", + "http://mochi.test:8888/#7", + "http://mochi.test:8888/#8", +]; + +const sessData = { + windows: [ + { + tabs: [ + { + entries: [ + { url: "http://mochi.test:8888/#0", triggeringPrincipal_base64 }, + ], + }, + { + entries: [ + { url: "http://mochi.test:8888/#1", triggeringPrincipal_base64 }, + ], + }, + { + entries: [ + { url: "http://mochi.test:8888/#3", triggeringPrincipal_base64 }, + ], + }, + { + entries: [ + { url: "http://mochi.test:8888/#4", triggeringPrincipal_base64 }, + ], + }, + ], + }, + ], +}; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const urlbarURL = "http://example.com/#urlbar"; + +async function doTest(aInsertRelatedAfterCurrent, aInsertAfterCurrent) { + const kDescription = + "(aInsertRelatedAfterCurrent=" + + aInsertRelatedAfterCurrent + + ", aInsertAfterCurrent=" + + aInsertAfterCurrent + + "): "; + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.opentabfor.middleclick", true], + ["browser.tabs.loadBookmarksInBackground", false], + ["browser.tabs.insertRelatedAfterCurrent", aInsertRelatedAfterCurrent], + ["browser.tabs.insertAfterCurrent", aInsertAfterCurrent], + ], + }); + + let oldState = SessionStore.getBrowserState(); + + await promiseBrowserStateRestored(sessData); + + // Create a *opener* tab page which has a link to "example.com". + let pageURL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + ); + pageURL = `${pageURL}file_new_tab_page.html`; + let openerTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + pageURL + ); + const openerTabIndex = 1; + gBrowser.moveTabTo(openerTab, openerTabIndex); + + // Open a related tab via Middle click on the cell and test its position. + let openTabIndex = + aInsertRelatedAfterCurrent || aInsertAfterCurrent + ? openerTabIndex + 1 + : gBrowser.tabs.length; + let openTabDescription = + aInsertRelatedAfterCurrent || aInsertAfterCurrent + ? "immediately to the right" + : "at rightmost"; + + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/#linkclick", + true + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#link_to_example_com", + { button: 1 }, + gBrowser.selectedBrowser + ); + let openTab = await newTabPromise; + is( + openTab.linkedBrowser.currentURI.spec, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/#linkclick", + "Middle click should open site to correct url." + ); + is( + openTab._tPos, + openTabIndex, + kDescription + + "Middle click should open site in a new tab " + + openTabDescription + ); + if (aInsertRelatedAfterCurrent || aInsertAfterCurrent) { + is(openTab.owner, openerTab, "tab owner is set correctly"); + } + is(openTab.openerTab, openerTab, "opener tab is set"); + + // Open an unrelated tab from the URL bar and test its position. + openTabIndex = aInsertAfterCurrent + ? openerTabIndex + 1 + : gBrowser.tabs.length; + openTabDescription = aInsertAfterCurrent + ? "immediately to the right" + : "at rightmost"; + + gURLBar.focus(); + gURLBar.select(); + newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, urlbarURL, true); + EventUtils.sendString(urlbarURL); + EventUtils.synthesizeKey("KEY_Alt", { + altKey: true, + code: "AltLeft", + type: "keydown", + }); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true, code: "Enter" }); + EventUtils.synthesizeKey("KEY_Alt", { + altKey: false, + code: "AltLeft", + type: "keyup", + }); + let unrelatedTab = await newTabPromise; + + is( + gBrowser.selectedBrowser.currentURI.spec, + unrelatedTab.linkedBrowser.currentURI.spec, + `${kDescription} ${urlbarURL} should be loaded in the current tab.` + ); + is( + unrelatedTab._tPos, + openTabIndex, + `${kDescription} Alt+Enter in the URL bar should open page in a new tab ${openTabDescription}` + ); + is(unrelatedTab.owner, openerTab, "owner tab is set correctly"); + ok(!unrelatedTab.openerTab, "no opener tab is set"); + + // Closing this should go back to the last selected tab, which just happens to be "openerTab" + // but is not in fact the opener. + BrowserTestUtils.removeTab(unrelatedTab); + is( + gBrowser.selectedTab, + openerTab, + kDescription + `openerTab should be selected after closing unrelated tab` + ); + + // Go back to the opener tab. Closing the child tab should return to the opener. + BrowserTestUtils.removeTab(openTab); + is( + gBrowser.selectedTab, + openerTab, + kDescription + "openerTab should be selected after closing related tab" + ); + + // Flush before messing with browser state. + for (let tab of gBrowser.tabs) { + await TabStateFlusher.flush(tab.linkedBrowser); + } + + // Get the session state, verify SessionStore gives us expected data. + let newState = SessionStore.getBrowserState(); + verifyTabState(newState); + + // Remove the tab at the end, then undo. It should reappear where it was. + await promiseRemoveThenUndoCloseTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + verifyTabState(newState); + + // Remove a tab in the middle, then undo. It should reappear where it was. + await promiseRemoveThenUndoCloseTab(gBrowser.tabs[2]); + verifyTabState(newState); + + // Bug 1442679 - Test bulk opening with loadTabs loads the tabs in order + + let loadPromises = Promise.all( + bulkLoad.map(url => + BrowserTestUtils.waitForNewTab(gBrowser, url, false, true) + ) + ); + // loadTabs will insertAfterCurrent + let nextTab = aInsertAfterCurrent + ? gBrowser.selectedTab._tPos + 1 + : gBrowser.tabs.length; + + gBrowser.loadTabs(bulkLoad, { + inBackground: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + await loadPromises; + for (let i = nextTab, j = 0; j < bulkLoad.length; i++, j++) { + is( + gBrowser.tabs[i].linkedBrowser.currentURI.spec, + bulkLoad[j], + `bulkLoad tab pos ${i} matched` + ); + } + + // Now we want to test that positioning remains correct after a session restore. + + // Restore pre-test state so we can restore and test tab ordering. + await promiseBrowserStateRestored(oldState); + + // Restore test state and verify it is as it was. + await promiseBrowserStateRestored(newState); + verifyTabState(newState); + + // Restore pre-test state for next test. + await promiseBrowserStateRestored(oldState); +} + +add_task(async function test_settings_insertRelatedAfter() { + // Firefox default settings. + await doTest(true, false); +}); + +add_task(async function test_settings_insertAfter() { + await doTest(true, true); +}); + +add_task(async function test_settings_always_insertAfter() { + await doTest(false, true); +}); + +add_task(async function test_settings_always_insertAtEnd() { + await doTest(false, false); +}); diff --git a/browser/base/content/test/tabs/browser_new_tab_url.js b/browser/base/content/test/tabs/browser_new_tab_url.js new file mode 100644 index 0000000000..233cb4e59e --- /dev/null +++ b/browser/base/content/test/tabs/browser_new_tab_url.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +add_task(async function test_browser_open_newtab_default_url() { + BrowserOpenTab(); + const tab = gBrowser.selectedTab; + + if (tab.linkedBrowser.currentURI.spec !== window.BROWSER_NEW_TAB_URL) { + // If about:newtab is not loaded immediately, wait for any location change. + await BrowserTestUtils.waitForLocationChange(gBrowser); + } + + is(tab.linkedBrowser.currentURI.spec, window.BROWSER_NEW_TAB_URL); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_browser_open_newtab_specific_url() { + const url = "https://example.com"; + + BrowserOpenTab({ url }); + const tab = gBrowser.selectedTab; + + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + is(tab.linkedBrowser.currentURI.spec, "https://example.com/"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.js b/browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.js new file mode 100644 index 0000000000..f2577cc8b2 --- /dev/null +++ b/browser/base/content/test/tabs/browser_newwindow_tabstrip_overflow.js @@ -0,0 +1,41 @@ +/* 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/. */ + +const DEFAULT_THEME = "default-theme@mozilla.org"; + +async function selectTheme(id) { + let theme = await AddonManager.getAddonByID(id || DEFAULT_THEME); + await theme.enable(); +} + +registerCleanupFunction(() => { + return selectTheme(null); +}); + +add_task(async function withoutLWT() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + ok( + !win.gBrowser.tabContainer.hasAttribute("overflow"), + "tab container not overflowing" + ); + ok( + !win.gBrowser.tabContainer.arrowScrollbox.hasAttribute("overflowing"), + "arrow scrollbox not overflowing" + ); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function withLWT() { + await selectTheme("firefox-compact-light@mozilla.org"); + let win = await BrowserTestUtils.openNewBrowserWindow(); + ok( + !win.gBrowser.tabContainer.hasAttribute("overflow"), + "tab container not overflowing" + ); + ok( + !win.gBrowser.tabContainer.arrowScrollbox.hasAttribute("overflowing"), + "arrow scrollbox not overflowing" + ); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/base/content/test/tabs/browser_openURI_background.js b/browser/base/content/test/tabs/browser_openURI_background.js new file mode 100644 index 0000000000..53e329dd8f --- /dev/null +++ b/browser/base/content/test/tabs/browser_openURI_background.js @@ -0,0 +1,41 @@ +/* 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/. */ + +"use strict"; + +add_task(async function () { + const tabCount = gBrowser.tabs.length; + const currentTab = gBrowser.selectedTab; + + const tests = [ + ["OPEN_NEWTAB", false], + ["OPEN_NEWTAB_BACKGROUND", true], + ]; + + for (const [flag, isBackground] of tests) { + window.browserDOMWindow.openURI( + makeURI("about:blank"), + null, + Ci.nsIBrowserDOMWindow[flag], + Ci.nsIBrowserDOMWindow.OPEN_NEW, + Services.scriptSecurityManager.getSystemPrincipal() + ); + + is(gBrowser.tabs.length, tabCount + 1, `${flag} opens a new tab`); + + const openedTab = gBrowser.tabs[tabCount]; + + if (isBackground) { + is( + gBrowser.selectedTab, + currentTab, + `${flag} opens a new background tab` + ); + } else { + is(gBrowser.selectedTab, openedTab, `${flag} opens a new foreground tab`); + } + + gBrowser.removeTab(openedTab); + } +}); diff --git a/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js b/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js new file mode 100644 index 0000000000..cb9fc3c6d7 --- /dev/null +++ b/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js @@ -0,0 +1,28 @@ +"use strict"; + +add_task(async function test_browser_open_newtab_start_observer_notification() { + let observerFiredPromise = new Promise(resolve => { + function observe(subject) { + Services.obs.removeObserver(observe, "browser-open-newtab-start"); + resolve(subject.wrappedJSObject); + } + Services.obs.addObserver(observe, "browser-open-newtab-start"); + }); + + // We're calling BrowserOpenTab() (rather the using BrowserTestUtils + // because we want to be sure that it triggers the event to fire, since + // it's very close to where various user-actions are triggered. + BrowserOpenTab(); + const newTabCreatedPromise = await observerFiredPromise; + const browser = await newTabCreatedPromise; + const tab = gBrowser.selectedTab; + + ok(true, "browser-open-newtab-start observer not called"); + Assert.deepEqual( + browser, + tab.linkedBrowser, + "browser-open-newtab-start notified with the created browser" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js b/browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js new file mode 100644 index 0000000000..baf85d0025 --- /dev/null +++ b/browser/base/content/test/tabs/browser_opened_file_tab_navigated_to_web.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +const TEST_FILE = "dummy_page.html"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const WEB_ADDRESS = "http://example.org/"; + +// Test for bug 1321020. +add_task(async function () { + let dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append(TEST_FILE); + + // The file can be a symbolic link on local build. Normalize it to make sure + // the path matches to the actual URI opened in the new tab. + dir.normalize(); + + const uriString = Services.io.newFileURI(dir).spec; + const openedUriString = uriString + "?opened"; + + // Open first file:// page. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + }); + + // Open new file:// tab from JavaScript in first file:// page. + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + gBrowser, + openedUriString, + true + ); + await SpecialPowers.spawn(tab.linkedBrowser, [openedUriString], uri => { + content.open(uri, "_blank"); + }); + + let openedTab = await promiseTabOpened; + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(openedTab); + }); + + let openedBrowser = openedTab.linkedBrowser; + + // Ensure that new file:// tab can be navigated to web content. + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + BrowserTestUtils.startLoadingURIString(openedBrowser, "http://example.org/"); + let href = await BrowserTestUtils.browserLoaded( + openedBrowser, + false, + WEB_ADDRESS + ); + is( + href, + WEB_ADDRESS, + "Check that new file:// page has navigated successfully to web content" + ); +}); diff --git a/browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js b/browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js new file mode 100644 index 0000000000..00bdb83cdd --- /dev/null +++ b/browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js @@ -0,0 +1,106 @@ +/* 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/. */ + +"use strict"; + +/* import-globals-from helper_origin_attrs_testing.js */ +loadTestSubscript("helper_origin_attrs_testing.js"); + +const PATH = "browser/browser/base/content/test/tabs/blank.html"; + +var TEST_CASES = [ + { uri: "https://example.com/" + PATH }, + { uri: "https://example.org/" + PATH }, + { uri: "about:preferences" }, + { uri: "about:config" }, + // file:// uri will be added in setup() +]; + +// 3 container tabs, 1 regular tab and 1 private tab +const NUM_PAGES_OPEN_FOR_EACH_TEST_CASE = 5; +var remoteTypes; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.userContext.enabled", true], + // don't preload tabs so we don't have extra XULFrameLoaderCreated events + // firing + ["browser.newtab.preload", false], + ], + }); + requestLongerTimeout(5); + + // Add a file:// uri + let dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append("blank.html"); + // The file can be a symbolic link on local build. Normalize it to make sure + // the path matches to the actual URI opened in the new tab. + dir.normalize(); + const uriString = Services.io.newFileURI(dir).spec; + TEST_CASES.push({ uri: uriString }); +}); + +function setupRemoteTypes() { + remoteTypes = getExpectedRemoteTypes( + gFissionBrowser, + NUM_PAGES_OPEN_FOR_EACH_TEST_CASE + ); + remoteTypes = remoteTypes.concat( + Array(NUM_PAGES_OPEN_FOR_EACH_TEST_CASE).fill("file") + ); // file uri +} + +add_task(async function test_user_identity_simple() { + setupRemoteTypes(); + var currentRemoteType; + + for (let testData of TEST_CASES) { + info(`Will open ${testData.uri} in different tabs`); + // Open uri without a container + info(`About to open a regular page`); + currentRemoteType = remoteTypes.shift(); + let page_regular = await openURIInRegularTab(testData.uri, window); + is( + page_regular.tab.linkedBrowser.remoteType, + currentRemoteType, + "correct remote type" + ); + + // Open the same uri in different user contexts + info(`About to open container pages`); + let containerPages = []; + for ( + var user_context_id = 1; + user_context_id <= NUM_USER_CONTEXTS; + user_context_id++ + ) { + currentRemoteType = remoteTypes.shift(); + let containerPage = await openURIInContainer( + testData.uri, + window, + user_context_id + ); + is( + containerPage.tab.linkedBrowser.remoteType, + currentRemoteType, + "correct remote type" + ); + containerPages.push(containerPage); + } + + // Open the same uri in a private browser + currentRemoteType = remoteTypes.shift(); + let page_private = await openURIInPrivateTab(testData.uri); + let privateRemoteType = page_private.tab.linkedBrowser.remoteType; + is(privateRemoteType, currentRemoteType, "correct remote type"); + + // Close all the tabs + containerPages.forEach(page => { + BrowserTestUtils.removeTab(page.tab); + }); + BrowserTestUtils.removeTab(page_regular.tab); + BrowserTestUtils.removeTab(page_private.tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_origin_attrs_rel.js b/browser/base/content/test/tabs/browser_origin_attrs_rel.js new file mode 100644 index 0000000000..b4a2a826f4 --- /dev/null +++ b/browser/base/content/test/tabs/browser_origin_attrs_rel.js @@ -0,0 +1,281 @@ +/* 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/. */ + +"use strict"; + +/* import-globals-from helper_origin_attrs_testing.js */ +loadTestSubscript("helper_origin_attrs_testing.js"); + +const PATH = + "browser/browser/base/content/test/tabs/file_rel_opener_noopener.html"; +const URI_EXAMPLECOM = + "https://example.com/browser/browser/base/content/test/tabs/blank.html"; +const URI_EXAMPLEORG = + "https://example.org/browser/browser/base/content/test/tabs/blank.html"; +var TEST_CASES = ["https://example.com/" + PATH, "https://example.org/" + PATH]; +// How many times we navigate (exclude going back) +const NUM_NAVIGATIONS = 5; +// Remote types we expect for all pages that we open, in the order of being opened +// (we don't include remote type for when we navigate back after clicking on a link) +var remoteTypes; +var xulFrameLoaderCreatedCounter = {}; +var LINKS_INFO = [ + { + uri: URI_EXAMPLECOM, + id: "link_noopener_examplecom", + }, + { + uri: URI_EXAMPLECOM, + id: "link_opener_examplecom", + }, + { + uri: URI_EXAMPLEORG, + id: "link_noopener_exampleorg", + }, + { + uri: URI_EXAMPLEORG, + id: "link_opener_exampleorg", + }, +]; + +function handleEventLocal(aEvent) { + if (aEvent.type != "XULFrameLoaderCreated") { + return; + } + // Ignore <browser> element in about:preferences and any other special pages + if ("gBrowser" in aEvent.target.ownerGlobal) { + xulFrameLoaderCreatedCounter.numCalledSoFar++; + } +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.userContext.enabled", true], + // don't preload tabs so we don't have extra XULFrameLoaderCreated events + // firing + ["browser.newtab.preload", false], + ], + }); + requestLongerTimeout(3); +}); + +function setupRemoteTypes() { + if (gFissionBrowser) { + remoteTypes = { + initial: [ + "webIsolated=https://example.com", + "webIsolated=https://example.com^userContextId=1", + "webIsolated=https://example.com^userContextId=2", + "webIsolated=https://example.com^userContextId=3", + "webIsolated=https://example.com^privateBrowsingId=1", + "webIsolated=https://example.org", + "webIsolated=https://example.org^userContextId=1", + "webIsolated=https://example.org^userContextId=2", + "webIsolated=https://example.org^userContextId=3", + "webIsolated=https://example.org^privateBrowsingId=1", + ], + regular: {}, + 1: {}, + 2: {}, + 3: {}, + private: {}, + }; + remoteTypes.regular[URI_EXAMPLECOM] = "webIsolated=https://example.com"; + remoteTypes.regular[URI_EXAMPLEORG] = "webIsolated=https://example.org"; + remoteTypes["1"][URI_EXAMPLECOM] = + "webIsolated=https://example.com^userContextId=1"; + remoteTypes["1"][URI_EXAMPLEORG] = + "webIsolated=https://example.org^userContextId=1"; + remoteTypes["2"][URI_EXAMPLECOM] = + "webIsolated=https://example.com^userContextId=2"; + remoteTypes["2"][URI_EXAMPLEORG] = + "webIsolated=https://example.org^userContextId=2"; + remoteTypes["3"][URI_EXAMPLECOM] = + "webIsolated=https://example.com^userContextId=3"; + remoteTypes["3"][URI_EXAMPLEORG] = + "webIsolated=https://example.org^userContextId=3"; + remoteTypes.private[URI_EXAMPLECOM] = + "webIsolated=https://example.com^privateBrowsingId=1"; + remoteTypes.private[URI_EXAMPLEORG] = + "webIsolated=https://example.org^privateBrowsingId=1"; + } else { + let web = Array(NUM_NAVIGATIONS).fill("web"); + remoteTypes = { + initial: [...web, ...web], + regular: {}, + 1: {}, + 2: {}, + 3: {}, + private: {}, + }; + remoteTypes.regular[URI_EXAMPLECOM] = "web"; + remoteTypes.regular[URI_EXAMPLEORG] = "web"; + remoteTypes["1"][URI_EXAMPLECOM] = "web"; + remoteTypes["1"][URI_EXAMPLEORG] = "web"; + remoteTypes["2"][URI_EXAMPLECOM] = "web"; + remoteTypes["2"][URI_EXAMPLEORG] = "web"; + remoteTypes["3"][URI_EXAMPLECOM] = "web"; + remoteTypes["3"][URI_EXAMPLEORG] = "web"; + remoteTypes.private[URI_EXAMPLECOM] = "web"; + remoteTypes.private[URI_EXAMPLEORG] = "web"; + } +} + +add_task(async function test_user_identity_simple() { + setupRemoteTypes(); + /** + * For each test case + * - open regular, private and container tabs and load uri + * - in all the tabs, click on 4 links, going back each time in between clicks + * and verifying the remote type stays the same throughout + * - close tabs + */ + + for (var idx = 0; idx < TEST_CASES.length; idx++) { + var uri = TEST_CASES[idx]; + info(`Will open ${uri} in different tabs`); + + // Open uri without a container + let page_regular = await openURIInRegularTab(uri); + is( + page_regular.tab.linkedBrowser.remoteType, + remoteTypes.initial.shift(), + "correct remote type" + ); + + let pages_usercontexts = []; + for ( + var user_context_id = 1; + user_context_id <= NUM_USER_CONTEXTS; + user_context_id++ + ) { + let containerPage = await openURIInContainer( + uri, + window, + user_context_id.toString() + ); + is( + containerPage.tab.linkedBrowser.remoteType, + remoteTypes.initial.shift(), + "correct remote type" + ); + pages_usercontexts.push(containerPage); + } + + // Open the same uri in a private browser + let page_private = await openURIInPrivateTab(uri); + is( + page_private.tab.linkedBrowser.remoteType, + remoteTypes.initial.shift(), + "correct remote type" + ); + + info(`Opened initial set of pages`); + + for (const linkInfo of LINKS_INFO) { + info( + `Will make all tabs click on link ${linkInfo.uri} id ${linkInfo.id}` + ); + info(`Will click on link ${linkInfo.uri} in regular tab`); + await clickOnLink( + page_regular.tab.linkedBrowser, + uri, + linkInfo, + "regular" + ); + + info(`Will click on link ${linkInfo.uri} in private tab`); + await clickOnLink( + page_private.tab.linkedBrowser, + uri, + linkInfo, + "private" + ); + + for (const page of pages_usercontexts) { + info( + `Will click on link ${linkInfo.uri} in container ${page.user_context_id}` + ); + await clickOnLink( + page.tab.linkedBrowser, + uri, + linkInfo, + page.user_context_id.toString() + ); + } + } + + // Close all the tabs + pages_usercontexts.forEach(page => { + BrowserTestUtils.removeTab(page.tab); + }); + BrowserTestUtils.removeTab(page_regular.tab); + BrowserTestUtils.removeTab(page_private.tab); + } +}); + +async function clickOnLink(aBrowser, aCurrURI, aLinkInfo, aIdxForRemoteTypes) { + var remoteTypeBeforeNavigation = aBrowser.remoteType; + var currRemoteType; + + // Add a listener + initXulFrameLoaderCreatedCounter(xulFrameLoaderCreatedCounter); + aBrowser.ownerGlobal.gBrowser.addEventListener( + "XULFrameLoaderCreated", + handleEventLocal + ); + + // Retrieve the expected remote type + var expectedRemoteType = remoteTypes[aIdxForRemoteTypes][aLinkInfo.uri]; + + // Click on the link + info(`Clicking on link, expected remote type= ${expectedRemoteType}`); + let newTabLoaded = BrowserTestUtils.waitForNewTab( + aBrowser.ownerGlobal.gBrowser, + aLinkInfo.uri, + true + ); + SpecialPowers.spawn(aBrowser, [aLinkInfo.id], link_id => { + content.document.getElementById(link_id).click(); + }); + + // Wait for the new tab to be opened + info(`About to wait for the clicked link to load in browser`); + let newTab = await newTabLoaded; + + // Check remote type, once we have opened a new tab + info(`Finished waiting for the clicked link to load in browser`); + currRemoteType = newTab.linkedBrowser.remoteType; + is(currRemoteType, expectedRemoteType, "Got correct remote type"); + + // Verify firing of XULFrameLoaderCreated event + info( + `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedCounter.numCalledSoFar} + time(s) for tab ${aIdxForRemoteTypes} when clicking on ${aLinkInfo.id} from page ${aCurrURI}` + ); + var numExpected; + if (!gFissionBrowser && aLinkInfo.id.includes("noopener")) { + numExpected = 1; + } else { + numExpected = currRemoteType == remoteTypeBeforeNavigation ? 1 : 2; + } + info( + `num XULFrameLoaderCreated events expected ${numExpected}, curr ${currRemoteType} prev ${remoteTypeBeforeNavigation}` + ); + is( + xulFrameLoaderCreatedCounter.numCalledSoFar, + numExpected, + `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedCounter.numCalledSoFar} + time(s) for tab ${aIdxForRemoteTypes} when clicking on ${aLinkInfo.id} from page ${aCurrURI}` + ); + + // Remove the event listener + aBrowser.ownerGlobal.gBrowser.removeEventListener( + "XULFrameLoaderCreated", + handleEventLocal + ); + + BrowserTestUtils.removeTab(newTab); +} diff --git a/browser/base/content/test/tabs/browser_originalURI.js b/browser/base/content/test/tabs/browser_originalURI.js new file mode 100644 index 0000000000..8e88be644b --- /dev/null +++ b/browser/base/content/test/tabs/browser_originalURI.js @@ -0,0 +1,181 @@ +/* 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/. */ + +/* + These tests ensure the originalURI property of the <browser> element + has consistent behavior when the URL of a <browser> changes. +*/ + +const EXAMPLE_URL = "https://example.com/some/path"; +const EXAMPLE_URL_2 = "http://mochi.test:8888/"; + +/* + Load a page with no redirect. +*/ +add_task(async function no_redirect() { + await BrowserTestUtils.withNewTab(EXAMPLE_URL, async browser => { + info("Page loaded."); + assertUrlEqualsOriginalURI(EXAMPLE_URL, browser.originalURI); + }); +}); + +/* + Load a page, go to another page, then go back and forth. +*/ +add_task(async function back_and_forth() { + await BrowserTestUtils.withNewTab(EXAMPLE_URL, async browser => { + info("Page loaded."); + + info("Try loading another page."); + let pageLoadPromise = BrowserTestUtils.browserLoaded( + browser, + false, + EXAMPLE_URL_2 + ); + BrowserTestUtils.startLoadingURIString(browser, EXAMPLE_URL_2); + await pageLoadPromise; + info("Other page finished loading."); + assertUrlEqualsOriginalURI(EXAMPLE_URL_2, browser.originalURI); + + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + browser.goBack(); + info("Go back."); + await pageShowPromise; + + info("Loaded previous page."); + assertUrlEqualsOriginalURI(EXAMPLE_URL, browser.originalURI); + + pageShowPromise = BrowserTestUtils.waitForContentEvent(browser, "pageshow"); + browser.goForward(); + info("Go forward."); + await pageShowPromise; + + info("Loaded next page."); + assertUrlEqualsOriginalURI(EXAMPLE_URL_2, browser.originalURI); + }); +}); + +/* + Redirect using the Location interface. +*/ +add_task(async function location_href() { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + let pageLoadPromise = BrowserTestUtils.browserLoaded( + browser, + false, + EXAMPLE_URL + ); + info("Loading page with location.href interface."); + await SpecialPowers.spawn(browser, [EXAMPLE_URL], href => { + content.document.location.href = href; + }); + await pageLoadPromise; + info("Page loaded."); + assertUrlEqualsOriginalURI(EXAMPLE_URL, browser.originalURI); + }); +}); + +/* + Redirect using History API, should not update the originalURI. +*/ +add_task(async function push_state() { + await BrowserTestUtils.withNewTab(EXAMPLE_URL, async browser => { + info("Page loaded."); + + info("Pushing state via History API."); + await SpecialPowers.spawn(browser, [], () => { + let newUrl = content.document.location.href + "/after?page=images"; + content.history.pushState(null, "", newUrl); + }); + Assert.equal( + browser.currentURI.displaySpec, + EXAMPLE_URL + "/after?page=images", + "Current URI should be modified by push state." + ); + assertUrlEqualsOriginalURI(EXAMPLE_URL, browser.originalURI); + }); +}); + +/* + Redirect using the <meta> tag. +*/ +add_task(async function meta_tag() { + let URL = httpURL("redirect_via_meta_tag.html"); + await BrowserTestUtils.withNewTab(URL, async browser => { + info("Page loaded."); + + let pageLoadPromise = BrowserTestUtils.browserLoaded( + browser, + false, + EXAMPLE_URL_2 + ); + await pageLoadPromise; + info("Redirected to ", EXAMPLE_URL_2); + assertUrlEqualsOriginalURI(URL, browser.originalURI); + }); +}); + +/* + Redirect using header from a server. +*/ +add_task(async function server_header() { + let URL = httpURL("redirect_via_header.html"); + await BrowserTestUtils.withNewTab(URL, async browser => { + info("Page loaded."); + + Assert.equal( + browser.currentURI.displaySpec, + EXAMPLE_URL, + `Browser should be re-directed to ${EXAMPLE_URL}` + ); + assertUrlEqualsOriginalURI(URL, browser.originalURI); + }); +}); + +/* + Load a page with an iFrame and then try having the + iFrame load another page. +*/ +add_task(async function page_with_iframe() { + let URL = httpURL("page_with_iframe.html"); + await BrowserTestUtils.withNewTab("about:blank", async browser => { + info("Blank page loaded."); + + info("Load URL."); + BrowserTestUtils.startLoadingURIString(browser, URL); + // Make sure the iFrame is finished loading. + await BrowserTestUtils.browserLoaded( + browser, + true, + "https://example.com/another/site" + ); + info("iFrame finished loading."); + assertUrlEqualsOriginalURI(URL, browser.originalURI); + + info("Change location of the iframe."); + let pageLoadPromise = BrowserTestUtils.browserLoaded( + browser, + true, + EXAMPLE_URL_2 + ); + await SpecialPowers.spawn(browser, [EXAMPLE_URL_2], url => { + content.document.getElementById("hidden-iframe").contentWindow.location = + url; + }); + await pageLoadPromise; + info("iFrame finished loading."); + assertUrlEqualsOriginalURI(URL, browser.originalURI); + }); +}); + +function assertUrlEqualsOriginalURI(url, originalURI) { + let uri = Services.io.newURI(url); + Assert.ok( + uri.equals(gBrowser.selectedBrowser.originalURI), + `URI - ${uri.displaySpec} is not equal to the originalURI - ${originalURI.displaySpec}` + ); +} diff --git a/browser/base/content/test/tabs/browser_overflowScroll.js b/browser/base/content/test/tabs/browser_overflowScroll.js new file mode 100644 index 0000000000..cae033e3bf --- /dev/null +++ b/browser/base/content/test/tabs/browser_overflowScroll.js @@ -0,0 +1,114 @@ +"use strict"; + +requestLongerTimeout(2); + +/** + * Tests that scrolling the tab strip via the scroll buttons scrolls the right + * amount in non-smoothscroll mode. + */ +add_task(async function () { + let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox; + let scrollbox = arrowScrollbox.scrollbox; + + let rect = ele => ele.getBoundingClientRect(); + let width = ele => rect(ele).width; + + let left = ele => rect(ele).left; + let right = ele => rect(ele).right; + let isLeft = (ele, msg) => is(left(ele), left(scrollbox), msg); + let isRight = (ele, msg) => is(right(ele), right(scrollbox), msg); + let elementFromPoint = x => arrowScrollbox._elementFromPoint(x); + let nextLeftElement = () => elementFromPoint(left(scrollbox) - 1); + let nextRightElement = () => elementFromPoint(right(scrollbox) + 1); + let firstScrollable = () => gBrowser.tabs[gBrowser._numPinnedTabs]; + let waitForNextFrame = async function () { + await new Promise(requestAnimationFrame); + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + }; + + await BrowserTestUtils.overflowTabs(registerCleanupFunction, window, { + overflowAtStart: false, + overflowTabFactor: 3, + }); + + gBrowser.pinTab(gBrowser.tabs[0]); + + await BrowserTestUtils.waitForCondition(() => { + return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen); + }); + + ok( + arrowScrollbox.hasAttribute("overflowing"), + "Tab strip should be overflowing" + ); + + let upButton = arrowScrollbox._scrollButtonUp; + let downButton = arrowScrollbox._scrollButtonDown; + let element; + + gBrowser.selectedTab = firstScrollable(); + await TestUtils.waitForTick(); + + Assert.lessOrEqual( + left(scrollbox), + left(firstScrollable()), + "Selecting the first tab scrolls it into view " + + "(" + + left(scrollbox) + + " <= " + + left(firstScrollable()) + + ")" + ); + + element = nextRightElement(); + EventUtils.synthesizeMouseAtCenter(downButton, {}); + await waitForNextFrame(); + isRight(element, "Scrolled one tab to the right with a single click"); + + gBrowser.selectedTab = gBrowser.tabs[gBrowser.tabs.length - 1]; + await waitForNextFrame(); + Assert.lessOrEqual( + right(gBrowser.selectedTab), + right(scrollbox), + "Selecting the last tab scrolls it into view " + + "(" + + right(gBrowser.selectedTab) + + " <= " + + right(scrollbox) + + ")" + ); + + element = nextLeftElement(); + EventUtils.synthesizeMouseAtCenter(upButton, {}); + await waitForNextFrame(); + isLeft(element, "Scrolled one tab to the left with a single click"); + + let elementPoint = left(scrollbox) - width(scrollbox); + element = elementFromPoint(elementPoint); + element = element.nextElementSibling; + + EventUtils.synthesizeMouseAtCenter(upButton, { clickCount: 2 }); + await waitForNextFrame(); + await BrowserTestUtils.waitForCondition( + () => !gBrowser.tabContainer.arrowScrollbox._isScrolling + ); + isLeft(element, "Scrolled one page of tabs with a double click"); + + EventUtils.synthesizeMouseAtCenter(upButton, { clickCount: 3 }); + await waitForNextFrame(); + var firstScrollableLeft = left(firstScrollable()); + Assert.lessOrEqual( + left(scrollbox), + firstScrollableLeft, + "Scrolled to the start with a triple click " + + "(" + + left(scrollbox) + + " <= " + + firstScrollableLeft + + ")" + ); + + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs[0]); + } +}); diff --git a/browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js b/browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js new file mode 100644 index 0000000000..a6b7f96410 --- /dev/null +++ b/browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js @@ -0,0 +1,156 @@ +"use strict"; + +add_task(async function doCheckPasteEventAtMiddleClickOnAnchorElement() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.opentabfor.middleclick", true], + ["middlemouse.paste", true], + ["middlemouse.contentLoadURL", false], + ["general.autoScroll", false], + ], + }); + + await new Promise((resolve, reject) => { + SimpleTest.waitForClipboard( + "Text in the clipboard", + () => { + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString("Text in the clipboard"); + }, + resolve, + () => { + ok(false, "Clipboard copy failed"); + reject(); + } + ); + }); + + is( + gBrowser.tabs.length, + 1, + "Number of tabs should be 1 at starting this test #1" + ); + + let pageURL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ); + pageURL = `${pageURL}file_anchor_elements.html`; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageURL); + + let pasteEventCount = 0; + BrowserTestUtils.addContentEventListener( + gBrowser.selectedBrowser, + "paste", + () => { + ++pasteEventCount; + } + ); + + // Click the usual link. + ok(true, "Clicking on usual link..."); + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "https://example.com/#a_with_href", + true + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#a_with_href", + { button: 1 }, + gBrowser.selectedBrowser + ); + let openTabForUsualLink = await newTabPromise; + is( + openTabForUsualLink.linkedBrowser.currentURI.spec, + "https://example.com/#a_with_href", + "Middle click should open site to correct url at clicking on usual link" + ); + is( + pasteEventCount, + 0, + "paste event should be suppressed when clicking on usual link" + ); + + // Click the link in editing host. + is( + gBrowser.tabs.length, + 3, + "Number of tabs should be 3 at starting this test #2" + ); + ok(true, "Clicking on editable link..."); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#editable_a_with_href", + { button: 1 }, + gBrowser.selectedBrowser + ); + await TestUtils.waitForCondition( + () => pasteEventCount >= 1, + "Waiting for paste event caused by clicking on editable link" + ); + is( + pasteEventCount, + 1, + "paste event should be suppressed when clicking on editable link" + ); + is( + gBrowser.tabs.length, + 3, + "Clicking on editable link shouldn't open new tab" + ); + + // Click the link in non-editable area in editing host. + ok(true, "Clicking on non-editable link in an editing host..."); + newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "https://example.com/#non-editable_a_with_href", + true + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#non-editable_a_with_href", + { button: 1 }, + gBrowser.selectedBrowser + ); + let openTabForNonEditableLink = await newTabPromise; + is( + openTabForNonEditableLink.linkedBrowser.currentURI.spec, + "https://example.com/#non-editable_a_with_href", + "Middle click should open site to correct url at clicking on non-editable link in an editing host." + ); + is( + pasteEventCount, + 1, + "paste event should be suppressed when clicking on non-editable link in an editing host" + ); + + // Click the <a> element without href attribute. + is( + gBrowser.tabs.length, + 4, + "Number of tabs should be 4 at starting this test #3" + ); + ok(true, "Clicking on anchor element without href..."); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#a_with_name", + { button: 1 }, + gBrowser.selectedBrowser + ); + await TestUtils.waitForCondition( + () => pasteEventCount >= 2, + "Waiting for paste event caused by clicking on anchor element without href" + ); + is( + pasteEventCount, + 2, + "paste event should be suppressed when clicking on anchor element without href" + ); + is( + gBrowser.tabs.length, + 4, + "Clicking on anchor element without href shouldn't open new tab" + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(openTabForUsualLink); + BrowserTestUtils.removeTab(openTabForNonEditableLink); +}); diff --git a/browser/base/content/test/tabs/browser_pinnedTabs.js b/browser/base/content/test/tabs/browser_pinnedTabs.js new file mode 100644 index 0000000000..856a08093d --- /dev/null +++ b/browser/base/content/test/tabs/browser_pinnedTabs.js @@ -0,0 +1,97 @@ +var tabs; + +function index(tab) { + return Array.prototype.indexOf.call(gBrowser.tabs, tab); +} + +function indexTest(tab, expectedIndex, msg) { + var diag = "tab " + tab + " should be at index " + expectedIndex; + if (msg) { + msg = msg + " (" + diag + ")"; + } else { + msg = diag; + } + is(index(tabs[tab]), expectedIndex, msg); +} + +function PinUnpinHandler(tab, eventName) { + this.eventCount = 0; + var self = this; + tab.addEventListener( + eventName, + function () { + self.eventCount++; + }, + { capture: true, once: true } + ); + gBrowser.tabContainer.addEventListener( + eventName, + function (e) { + if (e.originalTarget == tab) { + self.eventCount++; + } + }, + { capture: true, once: true } + ); +} + +function test() { + tabs = [ + gBrowser.selectedTab, + BrowserTestUtils.addTab(gBrowser), + BrowserTestUtils.addTab(gBrowser), + BrowserTestUtils.addTab(gBrowser), + ]; + indexTest(0, 0); + indexTest(1, 1); + indexTest(2, 2); + indexTest(3, 3); + + // Discard one of the test tabs to verify that pinning/unpinning + // discarded tabs does not regress (regression test for Bug 1852391). + gBrowser.discardBrowser(tabs[1], true); + + var eh = new PinUnpinHandler(tabs[3], "TabPinned"); + gBrowser.pinTab(tabs[3]); + is(eh.eventCount, 2, "TabPinned event should be fired"); + indexTest(0, 1); + indexTest(1, 2); + indexTest(2, 3); + indexTest(3, 0); + + eh = new PinUnpinHandler(tabs[1], "TabPinned"); + gBrowser.pinTab(tabs[1]); + is(eh.eventCount, 2, "TabPinned event should be fired"); + indexTest(0, 2); + indexTest(1, 1); + indexTest(2, 3); + indexTest(3, 0); + + gBrowser.moveTabTo(tabs[3], 3); + indexTest(3, 1, "shouldn't be able to mix a pinned tab into normal tabs"); + + gBrowser.moveTabTo(tabs[2], 0); + indexTest(2, 2, "shouldn't be able to mix a normal tab into pinned tabs"); + + eh = new PinUnpinHandler(tabs[1], "TabUnpinned"); + gBrowser.unpinTab(tabs[1]); + is(eh.eventCount, 2, "TabUnpinned event should be fired"); + indexTest( + 1, + 1, + "unpinning a tab should move a tab to the start of normal tabs" + ); + + eh = new PinUnpinHandler(tabs[3], "TabUnpinned"); + gBrowser.unpinTab(tabs[3]); + is(eh.eventCount, 2, "TabUnpinned event should be fired"); + indexTest( + 3, + 0, + "unpinning a tab should move a tab to the start of normal tabs" + ); + + gBrowser.removeTab(tabs[1]); + gBrowser.removeTab(tabs[2]); + gBrowser.removeTab(tabs[3]); +} diff --git a/browser/base/content/test/tabs/browser_pinnedTabs_clickOpen.js b/browser/base/content/test/tabs/browser_pinnedTabs_clickOpen.js new file mode 100644 index 0000000000..04420814b0 --- /dev/null +++ b/browser/base/content/test/tabs/browser_pinnedTabs_clickOpen.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function index(tab) { + return Array.prototype.indexOf.call(gBrowser.tabs, tab); +} + +async function testNewTabPosition(expectedPosition, modifiers = {}) { + let opening = BrowserTestUtils.waitForNewTab( + gBrowser, + "http://mochi.test:8888/" + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "#link", + modifiers, + gBrowser.selectedBrowser + ); + let newtab = await opening; + is(index(newtab), expectedPosition, "clicked tab is in correct position"); + return newtab; +} + +// Test that a tab opened from a pinned tab is not in the pinned region. +add_task(async function test_pinned_content_click() { + let testUri = + 'data:text/html;charset=utf-8,<a href="http://mochi.test:8888/" target="_blank" id="link">link</a>'; + let tabs = [ + gBrowser.selectedTab, + await BrowserTestUtils.openNewForegroundTab(gBrowser, testUri), + BrowserTestUtils.addTab(gBrowser), + ]; + gBrowser.pinTab(tabs[1]); + gBrowser.pinTab(tabs[2]); + + // First test new active tabs open at the start of non-pinned tabstrip. + let newtab1 = await testNewTabPosition(2); + // Switch back to our test tab. + await BrowserTestUtils.switchTab(gBrowser, tabs[1]); + let newtab2 = await testNewTabPosition(2); + + gBrowser.removeTab(newtab1); + gBrowser.removeTab(newtab2); + + // Second test new background tabs open in order. + let modifiers = + AppConstants.platform == "macosx" ? { metaKey: true } : { ctrlKey: true }; + await BrowserTestUtils.switchTab(gBrowser, tabs[1]); + + newtab1 = await testNewTabPosition(2, modifiers); + newtab2 = await testNewTabPosition(3, modifiers); + + gBrowser.removeTab(tabs[1]); + gBrowser.removeTab(tabs[2]); + gBrowser.removeTab(newtab1); + gBrowser.removeTab(newtab2); +}); diff --git a/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js b/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js new file mode 100644 index 0000000000..fbcd0bb492 --- /dev/null +++ b/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js @@ -0,0 +1,72 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + function testState(aPinned) { + function elemAttr(id, attr) { + return document.getElementById(id).getAttribute(attr); + } + + is( + elemAttr("key_close", "disabled"), + "", + "key_closed should always be enabled" + ); + is( + elemAttr("menu_close", "key"), + "key_close", + "menu_close should always have key_close set" + ); + } + + let unpinnedTab = gBrowser.selectedTab; + ok(!unpinnedTab.pinned, "We should have started with a regular tab selected"); + + testState(false); + + let pinnedTab = BrowserTestUtils.addTab(gBrowser); + gBrowser.pinTab(pinnedTab); + + // Just pinning the tab shouldn't change the key state. + testState(false); + + // Test key state after selecting a tab. + gBrowser.selectedTab = pinnedTab; + testState(true); + + gBrowser.selectedTab = unpinnedTab; + testState(false); + + gBrowser.selectedTab = pinnedTab; + testState(true); + + // Test the key state after un/pinning the tab. + gBrowser.unpinTab(pinnedTab); + testState(false); + + gBrowser.pinTab(pinnedTab); + testState(true); + + // Test that accel+w in a pinned tab selects the next tab. + let pinnedTab2 = BrowserTestUtils.addTab(gBrowser); + gBrowser.pinTab(pinnedTab2); + gBrowser.selectedTab = pinnedTab; + + EventUtils.synthesizeKey("w", { accelKey: true }); + is(gBrowser.tabs.length, 3, "accel+w in a pinned tab didn't close it"); + is( + gBrowser.selectedTab, + unpinnedTab, + "accel+w in a pinned tab selected the first unpinned tab" + ); + + // Test the key state after removing the tab. + gBrowser.removeTab(pinnedTab); + gBrowser.removeTab(pinnedTab2); + testState(false); + + finish(); +} diff --git a/browser/base/content/test/tabs/browser_positional_attributes.js b/browser/base/content/test/tabs/browser_positional_attributes.js new file mode 100644 index 0000000000..619c5cc517 --- /dev/null +++ b/browser/base/content/test/tabs/browser_positional_attributes.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var tabs = []; + +function addTab(aURL) { + tabs.push( + BrowserTestUtils.addTab(gBrowser, aURL, { + skipAnimation: true, + }) + ); +} + +function switchTab(index) { + return BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[index]); +} + +function testAttrib(tabIndex, attrib, expected) { + is( + gBrowser.tabs[tabIndex].hasAttribute(attrib), + expected, + `tab #${tabIndex} should${ + expected ? "" : "n't" + } have the ${attrib} attribute` + ); +} + +add_setup(async function () { + is(gBrowser.tabs.length, 1, "one tab is open initially"); + + addTab("http://mochi.test:8888/#0"); + addTab("http://mochi.test:8888/#1"); + addTab("http://mochi.test:8888/#2"); + addTab("http://mochi.test:8888/#3"); + + is(gBrowser.tabs.length, 5, "five tabs are open after setup"); +}); + +// Add several new tabs in sequence, hiding some, to ensure that the +// correct attributes get set +add_task(async function test() { + testAttrib(0, "visuallyselected", true); + + await switchTab(2); + + testAttrib(2, "visuallyselected", true); +}); + +add_task(async function test_pinning() { + await switchTab(3); + testAttrib(3, "visuallyselected", true); + // Causes gBrowser.tabs to change indices + gBrowser.pinTab(gBrowser.tabs[3]); + testAttrib(0, "visuallyselected", true); +}); + +add_task(function cleanup() { + tabs.forEach(gBrowser.removeTab, gBrowser); +}); diff --git a/browser/base/content/test/tabs/browser_preloadedBrowser_zoom.js b/browser/base/content/test/tabs/browser_preloadedBrowser_zoom.js new file mode 100644 index 0000000000..698cf82022 --- /dev/null +++ b/browser/base/content/test/tabs/browser_preloadedBrowser_zoom.js @@ -0,0 +1,89 @@ +"use strict"; + +const ZOOM_CHANGE_TOPIC = "browser-fullZoom:location-change"; + +/** + * Helper to check the zoom level of the preloaded browser + */ +async function checkPreloadedZoom(level, message) { + // Clear up any previous preloaded to test a fresh version + NewTabPagePreloading.removePreloadedBrowser(window); + NewTabPagePreloading.maybeCreatePreloadedBrowser(window); + + // Wait for zoom handling of preloaded + const browser = gBrowser.preloadedBrowser; + await new Promise(resolve => + Services.obs.addObserver(function obs(subject) { + if (subject === browser) { + Services.obs.removeObserver(obs, ZOOM_CHANGE_TOPIC); + resolve(); + } + }, ZOOM_CHANGE_TOPIC) + ); + + is(browser.fullZoom.toFixed(2), level, message); + + // Clean up for other tests + NewTabPagePreloading.removePreloadedBrowser(window); +} + +add_task(async function test_default_zoom() { + await checkPreloadedZoom("1.00", "default preloaded zoom is 1"); +}); + +/** + * Helper to open about:newtab and zoom then check matching preloaded zoom + */ +async function zoomNewTab(changeZoom, message) { + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab" + ); + changeZoom(); + const level = tab.linkedBrowser.fullZoom.toFixed(2); + BrowserTestUtils.removeTab(tab); + + // Wait for the the update of the full-zoom content pref value, that happens + // asynchronously after changing the zoom level. + let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + + await BrowserTestUtils.waitForCondition(() => { + return new Promise(resolve => { + cps2.getByDomainAndName( + "about:newtab", + "browser.content.full-zoom", + null, + { + handleResult(pref) { + resolve(level == pref.value); + }, + handleCompletion() { + console.log("handleCompletion"); + }, + } + ); + }); + }); + + await checkPreloadedZoom(level, `${message}: ${level}`); +} + +add_task(async function test_preloaded_zoom_out() { + await zoomNewTab(() => FullZoom.reduce(), "zoomed out applied to preloaded"); +}); + +add_task(async function test_preloaded_zoom_in() { + await zoomNewTab(() => { + FullZoom.enlarge(); + FullZoom.enlarge(); + }, "zoomed in applied to preloaded"); +}); + +add_task(async function test_preloaded_zoom_default() { + await zoomNewTab( + () => FullZoom.reduce(), + "zoomed back to default applied to preloaded" + ); +}); diff --git a/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js b/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js new file mode 100644 index 0000000000..9e1c1ff5cd --- /dev/null +++ b/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js @@ -0,0 +1,212 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Tests to ensure that Mozilla Privileged Webpages load in the privileged + * mozilla web content process. Normal http web pages should load in the web + * content process. + * Ref: Bug 1539595. + */ + +// High and Low Privilege +const TEST_HIGH1 = "https://example.org/"; +const TEST_HIGH2 = "https://test1.example.org/"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const TEST_LOW1 = "http://example.org/"; +const TEST_LOW2 = "https://example.com/"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true], + ["browser.tabs.remote.separatedMozillaDomains", "example.org"], + ["dom.ipc.processCount.privilegedmozilla", 1], + ], + }); +}); + +/* + * Test to ensure that the tabs open in privileged mozilla content process. We + * will first open a page that acts as a reference to the privileged mozilla web + * content process. With the reference, we can then open other links in a new tab + * and ensure that the new tab opens in the same privileged mozilla content process + * as our reference. + */ +add_task(async function webpages_in_privileged_content_process() { + Services.ppmm.releaseCachedProcesses(); + + await BrowserTestUtils.withNewTab(TEST_HIGH1, async function (browser1) { + checkBrowserRemoteType(browser1, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE); + + // Note the processID for about:newtab for comparison later. + let privilegedPid = browser1.frameLoader.remoteTab.osPid; + + for (let url of [ + TEST_HIGH1, + `${TEST_HIGH1}#foo`, + `${TEST_HIGH1}?q=foo`, + TEST_HIGH2, + `${TEST_HIGH2}#foo`, + `${TEST_HIGH2}?q=foo`, + ]) { + await BrowserTestUtils.withNewTab(url, async function (browser2) { + is( + browser2.frameLoader.remoteTab.osPid, + privilegedPid, + "Check that privileged pages are in the same privileged mozilla content process." + ); + }); + } + }); + + Services.ppmm.releaseCachedProcesses(); +}); + +/* + * Test to ensure that a process switch occurs when navigating between normal + * web pages and unprivileged pages in the same tab. + */ +add_task(async function process_switching_through_loading_in_the_same_tab() { + Services.ppmm.releaseCachedProcesses(); + + await BrowserTestUtils.withNewTab(TEST_LOW1, async function (browser) { + checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE); + + for (let [url, remoteType] of [ + [TEST_HIGH1, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE], + [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE], + [TEST_HIGH1, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE], + [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE], + [TEST_HIGH1, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE], + [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE], + [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE], + [`${TEST_HIGH1}#foo`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE], + [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE], + [`${TEST_HIGH1}#bar`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE], + [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE], + [`${TEST_HIGH1}#baz`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE], + [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE], + [`${TEST_HIGH1}?q=foo`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE], + [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE], + [`${TEST_HIGH1}?q=bar`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE], + [TEST_LOW1, E10SUtils.WEB_REMOTE_TYPE], + [`${TEST_HIGH1}?q=baz`, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE], + [TEST_LOW2, E10SUtils.WEB_REMOTE_TYPE], + ]) { + BrowserTestUtils.startLoadingURIString(browser, url); + await BrowserTestUtils.browserLoaded(browser, false, url); + checkBrowserRemoteType(browser, remoteType); + } + }); + + Services.ppmm.releaseCachedProcesses(); +}); + +/* + * Test to ensure that a process switch occurs when navigating between normal + * web pages and privileged pages using the browser's navigation features + * such as history and location change. + */ +add_task(async function process_switching_through_navigation_features() { + Services.ppmm.releaseCachedProcesses(); + + await BrowserTestUtils.withNewTab(TEST_HIGH1, async function (browser) { + checkBrowserRemoteType(browser, E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE); + + // Note the processID for about:newtab for comparison later. + let privilegedPid = browser.frameLoader.remoteTab.osPid; + + // Check that about:newtab opened from JS in about:newtab page is in the same process. + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + gBrowser, + TEST_HIGH1, + true + ); + await SpecialPowers.spawn(browser, [TEST_HIGH1], uri => { + content.open(uri, "_blank"); + }); + let newTab = await promiseTabOpened; + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(newTab); + }); + browser = newTab.linkedBrowser; + is( + browser.frameLoader.remoteTab.osPid, + privilegedPid, + "Check that new tab opened from privileged page is loaded in privileged mozilla content process." + ); + + // Check that reload does not break the privileged mozilla content process affinity. + BrowserReload(); + await BrowserTestUtils.browserLoaded(browser, false, TEST_HIGH1); + is( + browser.frameLoader.remoteTab.osPid, + privilegedPid, + "Check that privileged page is still in privileged mozilla content process after reload." + ); + + // Load http webpage + BrowserTestUtils.startLoadingURIString(browser, TEST_LOW1); + await BrowserTestUtils.browserLoaded(browser, false, TEST_LOW1); + checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE); + + // Check that using the history back feature switches back to privileged mozilla content process. + let promiseLocation = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_HIGH1 + ); + browser.goBack(); + await promiseLocation; + is( + browser.frameLoader.remoteTab.osPid, + privilegedPid, + "Check that privileged page is still in privileged mozilla content process after history goBack." + ); + + // Check that using the history forward feature switches back to the web content process. + promiseLocation = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_LOW1 + ); + browser.goForward(); + await promiseLocation; + checkBrowserRemoteType( + browser, + E10SUtils.WEB_REMOTE_TYPE, + "Check that tab runs in the web content process after using history goForward." + ); + + // Check that goto history index does not break the affinity. + promiseLocation = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_HIGH1 + ); + browser.gotoIndex(0); + await promiseLocation; + is( + browser.frameLoader.remoteTab.osPid, + privilegedPid, + "Check that privileged page is in privileged mozilla content process after history gotoIndex." + ); + + BrowserTestUtils.startLoadingURIString(browser, TEST_LOW2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_LOW2); + checkBrowserRemoteType(browser, E10SUtils.WEB_REMOTE_TYPE); + + // Check that location change causes a change in process type as well. + await SpecialPowers.spawn(browser, [TEST_HIGH1], uri => { + content.location = uri; + }); + await BrowserTestUtils.browserLoaded(browser, false, TEST_HIGH1); + is( + browser.frameLoader.remoteTab.osPid, + privilegedPid, + "Check that privileged page is in privileged mozilla content process after location change." + ); + }); + + Services.ppmm.releaseCachedProcesses(); +}); diff --git a/browser/base/content/test/tabs/browser_progress_keyword_search_handling.js b/browser/base/content/test/tabs/browser_progress_keyword_search_handling.js new file mode 100644 index 0000000000..69aa7f8f8c --- /dev/null +++ b/browser/base/content/test/tabs/browser_progress_keyword_search_handling.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SearchTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" +); + +SearchTestUtils.init(this); + +const kButton = document.getElementById("reload-button"); +const IS_UPGRADING_SCHEMELESS = SpecialPowers.getBoolPref( + "dom.security.https_first_schemeless" +); +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const DEFAULT_URL_SCHEME = IS_UPGRADING_SCHEMELESS ? "https://" : "http://"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.fixup.dns_first_for_single_words", true]], + }); + + // Create an engine to use for the test. + await SearchTestUtils.installSearchExtension( + { + name: "MozSearch", + search_url: "https://example.com/", + search_url_get_params: "q={searchTerms}", + }, + { setAsDefault: true } + ); +}); + +/* + * When loading a keyword search as a result of an unknown host error, + * check that we can stop the load. + * See https://bugzilla.mozilla.org/show_bug.cgi?id=235825 + */ +add_task(async function test_unknown_host() { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + const kNonExistingHost = "idontreallyexistonthisnetwork"; + let searchPromise = BrowserTestUtils.browserStarted( + browser, + Services.uriFixup.keywordToURI(kNonExistingHost).preferredURI.spec + ); + + gURLBar.value = kNonExistingHost; + gURLBar.select(); + EventUtils.synthesizeKey("KEY_Enter"); + + await searchPromise; + // With parent initiated loads, we need to give XULBrowserWindow + // time to process the STATE_START event and set the attribute to true. + await new Promise(resolve => executeSoon(resolve)); + + ok(kButton.hasAttribute("displaystop"), "Should be showing stop"); + + await TestUtils.waitForCondition( + () => !kButton.hasAttribute("displaystop") + ); + ok( + !kButton.hasAttribute("displaystop"), + "Should no longer be showing stop after search" + ); + }); +}); + +/* + * When NOT loading a keyword search as a result of an unknown host error, + * check that the stop button goes back to being a reload button. + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1591183 + */ +add_task(async function test_unknown_host_without_search() { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + const kNonExistingHost = "idontreallyexistonthisnetwork.example.com"; + let searchPromise = BrowserTestUtils.browserLoaded( + browser, + false, + DEFAULT_URL_SCHEME + kNonExistingHost + "/", + true /* want an error page */ + ); + gURLBar.value = kNonExistingHost; + gURLBar.select(); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + await TestUtils.waitForCondition( + () => !kButton.hasAttribute("displaystop") + ); + ok( + !kButton.hasAttribute("displaystop"), + "Should not be showing stop on error page" + ); + }); +}); diff --git a/browser/base/content/test/tabs/browser_relatedTabs_reset.js b/browser/base/content/test/tabs/browser_relatedTabs_reset.js new file mode 100644 index 0000000000..83002a749d --- /dev/null +++ b/browser/base/content/test/tabs/browser_relatedTabs_reset.js @@ -0,0 +1,81 @@ +/* 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/. */ + +add_task(async function () { + is(gBrowser.tabs.length, 1, "one tab is open initially"); + + const TestPage = + "http://mochi.test:8888/browser/browser/base/content/test/general/dummy_page.html"; + + // Add several new tabs in sequence + let tabs = []; + let ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" + ); + + function getPrincipal(url, attrs) { + let uri = Services.io.newURI(url); + if (!attrs) { + attrs = {}; + } + return Services.scriptSecurityManager.createContentPrincipal(uri, attrs); + } + + function addTab(aURL, aReferrer) { + let referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + aReferrer + ); + let triggeringPrincipal = getPrincipal(aURL); + let tab = BrowserTestUtils.addTab(gBrowser, aURL, { + referrerInfo, + triggeringPrincipal, + }); + tabs.push(tab); + return BrowserTestUtils.browserLoaded(tab.linkedBrowser); + } + + function loadTab(tab, url) { + let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + info("Loading page: " + url); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url); + return loaded; + } + + function testPosition(tabNum, expectedPosition, msg) { + is( + Array.prototype.indexOf.call(gBrowser.tabs, tabs[tabNum]), + expectedPosition, + msg + ); + } + + // Initial selected tab + await addTab("http://mochi.test:8888/#0"); + testPosition(0, 1, "Initial tab opened in position 1"); + gBrowser.selectedTab = tabs[0]; + + // Related tabs + await addTab("http://mochi.test:8888/#1", gBrowser.currentURI); + testPosition(1, 2, "Related tab was opened to the far right"); + + await addTab("http://mochi.test:8888/#2", gBrowser.currentURI); + testPosition(2, 3, "Related tab was opened to the far right"); + + // Load a new page + await loadTab(tabs[0], TestPage); + + // Add a new related tab after the page load + await addTab("http://mochi.test:8888/#3", gBrowser.currentURI); + testPosition( + 3, + 2, + "Tab opened to the right of initial tab after system navigation" + ); + + tabs.forEach(gBrowser.removeTab, gBrowser); +}); diff --git a/browser/base/content/test/tabs/browser_reload_deleted_file.js b/browser/base/content/test/tabs/browser_reload_deleted_file.js new file mode 100644 index 0000000000..2051dbfac7 --- /dev/null +++ b/browser/base/content/test/tabs/browser_reload_deleted_file.js @@ -0,0 +1,36 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +const uuidGenerator = Services.uuid; + +const DUMMY_FILE = "dummy_page.html"; + +// Test for bug 1327942. +add_task(async function () { + // Copy dummy page to unique file in TmpD, so that we can safely delete it. + let dummyPage = getChromeDir(getResolvedURI(gTestPath)); + dummyPage.append(DUMMY_FILE); + let disappearingPage = Services.dirsvc.get("TmpD", Ci.nsIFile); + let uniqueName = uuidGenerator.generateUUID().toString(); + dummyPage.copyTo(disappearingPage, uniqueName); + disappearingPage.append(uniqueName); + + // Get file:// URI for new page and load in a new tab. + const uriString = Services.io.newFileURI(disappearingPage).spec; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString); + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(tab); + }); + + // Delete the page, simulate a click of the reload button and check that we + // get a neterror page. + disappearingPage.remove(false); + document.getElementById("reload-button").doCommand(); + await BrowserTestUtils.waitForErrorPage(tab.linkedBrowser); + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + ok( + content.document.documentURI.startsWith("about:neterror"), + "Check that a neterror page was loaded." + ); + }); +}); diff --git a/browser/base/content/test/tabs/browser_removeTabsToTheEnd.js b/browser/base/content/test/tabs/browser_removeTabsToTheEnd.js new file mode 100644 index 0000000000..a9540f708b --- /dev/null +++ b/browser/base/content/test/tabs/browser_removeTabsToTheEnd.js @@ -0,0 +1,30 @@ +/* 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/. */ + +add_task(async function removeTabsToTheEnd() { + // Add three new tabs after the original tab. Pin the second one. + let firstTab = await addTab(); + let pinnedTab = await addTab(); + let lastTab = await addTab(); + gBrowser.pinTab(pinnedTab); + + // Check that there is only one closable tab from firstTab to the end + is( + gBrowser.getTabsToTheEndFrom(firstTab).length, + 1, + "One unpinned tab towards the end" + ); + + // Remove tabs to the end + gBrowser.removeTabsToTheEndFrom(firstTab); + + ok(!firstTab.closing, "First tab is not closing"); + ok(!pinnedTab.closing, "Pinned tab is not closing"); + ok(lastTab.closing, "Last tab is closing"); + + // cleanup + for (let tab of [firstTab, pinnedTab]) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_removeTabsToTheStart.js b/browser/base/content/test/tabs/browser_removeTabsToTheStart.js new file mode 100644 index 0000000000..685da35881 --- /dev/null +++ b/browser/base/content/test/tabs/browser_removeTabsToTheStart.js @@ -0,0 +1,35 @@ +/* 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/. */ + +add_task(async function removeTabsToTheStart() { + // don't mess with the original tab + let originalTab = gBrowser.selectedTab; + gBrowser.pinTab(originalTab); + + // Add three new tabs after the original tab. Pin the second one. + let firstTab = await addTab(); + let pinnedTab = await addTab(); + let lastTab = await addTab(); + gBrowser.pinTab(pinnedTab); + + // Check that there is only one closable tab from lastTab to the start + is( + gBrowser.getTabsToTheStartFrom(lastTab).length, + 1, + "One unpinned tab towards the start" + ); + + // Remove tabs to the start + gBrowser.removeTabsToTheStartFrom(lastTab); + + ok(firstTab.closing, "First tab is closing"); + ok(!pinnedTab.closing, "Pinned tab is not closing"); + ok(!lastTab.closing, "Last tab is not closing"); + + // cleanup + gBrowser.unpinTab(originalTab); + for (let tab of [pinnedTab, lastTab]) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_removeTabs_order.js b/browser/base/content/test/tabs/browser_removeTabs_order.js new file mode 100644 index 0000000000..071cc03716 --- /dev/null +++ b/browser/base/content/test/tabs/browser_removeTabs_order.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +add_task(async function () { + let tab1 = await addTab(); + let tab2 = await addTab(); + let tab3 = await addTab(); + let tabs = [tab1, tab2, tab3]; + + // Add a beforeunload event listener in one of the tabs; it should be called + // before closing any of the tabs. + await ContentTask.spawn(tab2.linkedBrowser, null, async function () { + content.window.addEventListener("beforeunload", function (event) {}, true); + }); + + let permitUnloadSpy = sinon.spy(tab2.linkedBrowser, "asyncPermitUnload"); + let removeTabSpy = sinon.spy(gBrowser, "removeTab"); + + gBrowser.removeTabs(tabs); + + Assert.ok(permitUnloadSpy.calledOnce, "permitUnload was called only once"); + Assert.equal( + removeTabSpy.callCount, + tabs.length, + "removeTab was called for every tab" + ); + Assert.ok( + permitUnloadSpy.lastCall.calledBefore(removeTabSpy.firstCall), + "permitUnload was called before for first removeTab call" + ); + + removeTabSpy.restore(); + permitUnloadSpy.restore(); +}); diff --git a/browser/base/content/test/tabs/browser_removeTabs_skipPermitUnload.js b/browser/base/content/test/tabs/browser_removeTabs_skipPermitUnload.js new file mode 100644 index 0000000000..ecdbe8f5eb --- /dev/null +++ b/browser/base/content/test/tabs/browser_removeTabs_skipPermitUnload.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for waiting for beforeunload before replacing a session. + */ + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +// The first two urls are intentionally different domains to force pages +// to load in different tabs. +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const TEST_URL = "https://example.com/"; + +const BUILDER_URL = "https://example.com/document-builder.sjs?html="; +const PAGE_MARKUP = ` +<html> +<head> + <script> + window.onbeforeunload = function() { + return true; + }; + </script> +</head> +<body>TEST PAGE</body> +</html> +`; +const TEST_URL2 = BUILDER_URL + encodeURI(PAGE_MARKUP); + +let win; +let nonBeforeUnloadTab; +let beforeUnloadTab; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); + + // Run tests in a new window to avoid affecting the main test window. + win = await BrowserTestUtils.openNewBrowserWindow(); + + BrowserTestUtils.startLoadingURIString( + win.gBrowser.selectedBrowser, + TEST_URL + ); + await BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser, + false, + TEST_URL + ); + nonBeforeUnloadTab = win.gBrowser.selectedTab; + beforeUnloadTab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + TEST_URL2 + ); + + registerCleanupFunction(async () => { + await BrowserTestUtils.closeWindow(win); + }); +}); + +add_task(async function test_runBeforeUnloadForTabs() { + let unloadDialogPromise = PromptTestUtils.handleNextPrompt( + win, + { + modalType: Ci.nsIPrompt.MODAL_TYPE_CONTENT, + promptType: "confirmEx", + }, + // Click the cancel button. + { buttonNumClick: 1 } + ); + + let unloadBlocked = await win.gBrowser.runBeforeUnloadForTabs( + win.gBrowser.tabs + ); + + await unloadDialogPromise; + + Assert.ok(unloadBlocked, "Should have reported the unload was blocked"); + Assert.equal(win.gBrowser.tabs.length, 2, "Should have left all tabs open"); + + unloadDialogPromise = PromptTestUtils.handleNextPrompt( + win, + { + modalType: Ci.nsIPrompt.MODAL_TYPE_CONTENT, + promptType: "confirmEx", + }, + // Click the ok button. + { buttonNumClick: 0 } + ); + + unloadBlocked = await win.gBrowser.runBeforeUnloadForTabs(win.gBrowser.tabs); + + await unloadDialogPromise; + + Assert.ok(!unloadBlocked, "Should have reported the unload was not blocked"); + Assert.equal(win.gBrowser.tabs.length, 2, "Should have left all tabs open"); +}); + +add_task(async function test_skipPermitUnload() { + let closePromise = BrowserTestUtils.waitForTabClosing(beforeUnloadTab); + + await win.gBrowser.removeAllTabsBut(nonBeforeUnloadTab, { + animate: false, + skipPermitUnload: true, + }); + + await closePromise; + + Assert.equal(win.gBrowser.tabs.length, 1, "Should have left one tab open"); +}); diff --git a/browser/base/content/test/tabs/browser_replacewithwindow_commands.js b/browser/base/content/test/tabs/browser_replacewithwindow_commands.js new file mode 100644 index 0000000000..1e6f2b8f57 --- /dev/null +++ b/browser/base/content/test/tabs/browser_replacewithwindow_commands.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test verifies that focus is handled correctly when a +// tab is dragged out to a new window, by checking that the +// copy and select all commands are enabled properly. +add_task(async function () { + let tab1 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://www.example.com" + ); + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://www.example.com" + ); + + let delayedStartupPromise = BrowserTestUtils.waitForNewWindow(); + let win = gBrowser.replaceTabWithWindow(tab2); + await delayedStartupPromise; + + let copyCommand = win.document.getElementById("cmd_copy"); + info("Waiting for copy to be enabled"); + await BrowserTestUtils.waitForMutationCondition( + copyCommand, + { attributes: true }, + () => { + return !copyCommand.hasAttribute("disabled"); + } + ); + + ok( + !win.document.getElementById("cmd_copy").hasAttribute("disabled"), + "copy is enabled" + ); + ok( + !win.document.getElementById("cmd_selectAll").hasAttribute("disabled"), + "select all is enabled" + ); + + await BrowserTestUtils.closeWindow(win); + await BrowserTestUtils.removeTab(tab1); +}); diff --git a/browser/base/content/test/tabs/browser_switch_by_scrolling.js b/browser/base/content/test/tabs/browser_switch_by_scrolling.js new file mode 100644 index 0000000000..7d62234d7f --- /dev/null +++ b/browser/base/content/test/tabs/browser_switch_by_scrolling.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function wheel_switches_tabs() { + Services.prefs.setBoolPref("toolkit.tabbox.switchByScrolling", true); + + let newTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + await BrowserTestUtils.switchTab(gBrowser, () => { + EventUtils.synthesizeWheel(newTab, 4, 4, { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: -1.0, + }); + }); + ok(!newTab.selected, "New tab should no longer be selected."); + BrowserTestUtils.removeTab(newTab); +}); + +add_task(async function wheel_switches_tabs_overflow() { + Services.prefs.setBoolPref("toolkit.tabbox.switchByScrolling", true); + + let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox; + let tabs = []; + + while (!arrowScrollbox.hasAttribute("overflowing")) { + tabs.push( + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank") + ); + } + + let newTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + await BrowserTestUtils.switchTab(gBrowser, () => { + EventUtils.synthesizeWheel(newTab, 4, 4, { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: -1.0, + }); + }); + ok(!newTab.selected, "New tab should no longer be selected."); + + BrowserTestUtils.removeTab(newTab); + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/base/content/test/tabs/browser_tabCloseProbes.js b/browser/base/content/test/tabs/browser_tabCloseProbes.js new file mode 100644 index 0000000000..4e5aca8482 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabCloseProbes.js @@ -0,0 +1,112 @@ +"use strict"; + +var gAnimHistogram = Services.telemetry.getHistogramById( + "FX_TAB_CLOSE_TIME_ANIM_MS" +); +var gNoAnimHistogram = Services.telemetry.getHistogramById( + "FX_TAB_CLOSE_TIME_NO_ANIM_MS" +); + +/** + * Takes a Telemetry histogram snapshot and returns the sum of all counts. + * + * @param snapshot (Object) + * The Telemetry histogram snapshot to examine. + * @return (int) + * The sum of all counts in the snapshot. + */ +function snapshotCount(snapshot) { + // Use Array.prototype.reduce to sum up all of the + // snapshot.count entries + return Object.values(snapshot.values).reduce((a, b) => a + b, 0); +} + +/** + * Takes a Telemetry histogram snapshot and makes sure + * that the sum of all counts equals expectedCount. + * + * @param snapshot (Object) + * The Telemetry histogram snapshot to examine. + * @param expectedCount (int) + * What we expect the number of incremented counts to be. For example, + * If we expect this probe to have only had a single recording, this + * would be 1. If we expected it to have not recorded any data at all, + * this would be 0. + */ +function assertCount(snapshot, expectedCount) { + Assert.equal( + snapshotCount(snapshot), + expectedCount, + `Should only be ${expectedCount} collected value.` + ); +} + +/** + * Takes a Telemetry histogram and waits for the sum of all counts becomes + * equal to expectedCount. + * + * @param histogram (Object) + * The Telemetry histogram to examine. + * @param expectedCount (int) + * What we expect the number of incremented counts to become. + * @return (Promise) + * @resolves When the histogram snapshot count becomes the expected count. + */ +function waitForSnapshotCount(histogram, expectedCount) { + return BrowserTestUtils.waitForCondition(() => { + return snapshotCount(histogram.snapshot()) == expectedCount; + }, `Collected value should become ${expectedCount}.`); +} + +add_setup(async function () { + // Force-enable tab animations + gReduceMotionOverride = false; + + // These probes are opt-in, meaning we only capture them if extended + // Telemetry recording is enabled. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + registerCleanupFunction(() => { + Services.telemetry.canRecordExtended = oldCanRecord; + }); +}); + +/** + * Tests the FX_TAB_CLOSE_TIME_ANIM_MS probe by closing a tab with the tab + * close animation. + */ +add_task(async function test_close_time_anim_probe() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.waitForCondition(() => tab._fullyOpen); + + gAnimHistogram.clear(); + gNoAnimHistogram.clear(); + + BrowserTestUtils.removeTab(tab, { animate: true }); + + await waitForSnapshotCount(gAnimHistogram, 1); + assertCount(gNoAnimHistogram.snapshot(), 0); + + gAnimHistogram.clear(); + gNoAnimHistogram.clear(); +}); + +/** + * Tests the FX_TAB_CLOSE_TIME_NO_ANIM_MS probe by closing a tab without the + * tab close animation. + */ +add_task(async function test_close_time_no_anim_probe() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.waitForCondition(() => tab._fullyOpen); + + gAnimHistogram.clear(); + gNoAnimHistogram.clear(); + + BrowserTestUtils.removeTab(tab, { animate: false }); + + await waitForSnapshotCount(gNoAnimHistogram, 1); + assertCount(gAnimHistogram.snapshot(), 0); + + gAnimHistogram.clear(); + gNoAnimHistogram.clear(); +}); diff --git a/browser/base/content/test/tabs/browser_tabCloseSpacer.js b/browser/base/content/test/tabs/browser_tabCloseSpacer.js new file mode 100644 index 0000000000..6996546be2 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabCloseSpacer.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that while clicking to close tabs, the close button remains under the mouse + * even when an underflow happens. + */ +add_task(async function () { + // Disable tab animations + gReduceMotionOverride = true; + + let downButton = gBrowser.tabContainer.arrowScrollbox._scrollButtonDown; + let closingTabsSpacer = gBrowser.tabContainer._closingTabsSpacer; + let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox; + + await BrowserTestUtils.overflowTabs(registerCleanupFunction, window); + + // Make sure scrolling finished. + await new Promise(resolve => { + arrowScrollbox.addEventListener("scrollend", resolve, { once: true }); + }); + + ok( + gBrowser.tabContainer.hasAttribute("overflow"), + "Tab strip should be overflowing" + ); + isnot(downButton.clientWidth, 0, "down button has some width"); + is(closingTabsSpacer.clientWidth, 0, "spacer has no width"); + + let originalCloseButtonLocation = getLastCloseButtonLocation(); + + info( + "Removing half the tabs and making sure the last close button doesn't move" + ); + let numTabs = gBrowser.tabs.length / 2; + while (gBrowser.tabs.length > numTabs) { + let lastCloseButtonLocation = getLastCloseButtonLocation(); + Assert.equal( + lastCloseButtonLocation.top, + originalCloseButtonLocation.top, + "The top of all close buttons should be equal" + ); + Assert.equal( + lastCloseButtonLocation.bottom, + originalCloseButtonLocation.bottom, + "The bottom of all close buttons should be equal" + ); + Assert.equal( + lastCloseButtonLocation.right, + originalCloseButtonLocation.right, + "The right side of the close button should remain consistent" + ); + // Ignore 'left' since non-hovered tabs have their close button + // narrower to display more tab label. + + EventUtils.synthesizeMouseAtCenter(getLastCloseButton(), {}); + await new Promise(r => requestAnimationFrame(r)); + } + + ok(!gBrowser.tabContainer.hasAttribute("overflow"), "not overflowing"); + ok( + gBrowser.tabContainer.hasAttribute("using-closing-tabs-spacer"), + "using spacer" + ); + + is(downButton.clientWidth, 0, "down button has no width"); + isnot(closingTabsSpacer.clientWidth, 0, "spacer has some width"); +}); + +function getLastCloseButton() { + let lastTab = gBrowser.tabs[gBrowser.tabs.length - 1]; + return lastTab.closeButton; +} + +function getLastCloseButtonLocation() { + let rect = getLastCloseButton().getBoundingClientRect(); + return { + left: Math.round(rect.left), + top: Math.round(rect.top), + width: Math.round(rect.width), + height: Math.round(rect.height), + }; +} + +registerCleanupFunction(() => { + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs[0]); + } +}); diff --git a/browser/base/content/test/tabs/browser_tabContextMenu_keyboard.js b/browser/base/content/test/tabs/browser_tabContextMenu_keyboard.js new file mode 100644 index 0000000000..f3a2066653 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabContextMenu_keyboard.js @@ -0,0 +1,64 @@ +/* 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/. */ + +async function openContextMenu() { + let contextMenu = document.getElementById("tabContextMenu"); + let popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(gBrowser.selectedTab, { + type: "contextmenu", + button: 2, + }); + await popupShown; +} + +async function closeContextMenu() { + let contextMenu = document.getElementById("tabContextMenu"); + let popupHidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden"); + EventUtils.synthesizeKey("KEY_Escape"); + await popupHidden; +} + +add_task(async function test() { + if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) { + ok( + true, + "This bug is not possible when native context menus are enabled on macOS." + ); + return; + } + // Ensure tabs are focusable. + await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] }); + + // There should be one tab when we start the test. + let tab1 = gBrowser.selectedTab; + let tab2 = BrowserTestUtils.addTab(gBrowser); + tab1.focus(); + is(document.activeElement, tab1, "tab1 should be focused"); + + // Ensure that DownArrow doesn't switch to tab2 while the context menu is open. + await openContextMenu(); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await closeContextMenu(); + is(gBrowser.selectedTab, tab1, "tab1 should still be active"); + if (AppConstants.platform == "macosx") { + // On Mac, focus doesn't return to the tab after dismissing the context menu. + // Since we're not testing that here, work around it by just focusing again. + tab1.focus(); + } + is(document.activeElement, tab1, "tab1 should be focused"); + + // Switch to tab2 by pressing DownArrow. + await BrowserTestUtils.switchTab(gBrowser, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + is(gBrowser.selectedTab, tab2, "should have switched to tab2"); + is(document.activeElement, tab2, "tab2 should now be focused"); + // Ensure that UpArrow doesn't switch to tab1 while the context menu is open. + await openContextMenu(); + EventUtils.synthesizeKey("KEY_ArrowUp"); + await closeContextMenu(); + is(gBrowser.selectedTab, tab2, "tab2 should still be active"); + + gBrowser.removeTab(tab2); +}); diff --git a/browser/base/content/test/tabs/browser_tabReorder.js b/browser/base/content/test/tabs/browser_tabReorder.js new file mode 100644 index 0000000000..c5ae459065 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabReorder.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + let initialTabsLength = gBrowser.tabs.length; + + let newTab1 = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:robots", + { skipAnimation: true } + )); + let newTab2 = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:about", + { skipAnimation: true } + )); + let newTab3 = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:config", + { skipAnimation: true } + )); + registerCleanupFunction(function () { + while (gBrowser.tabs.length > initialTabsLength) { + gBrowser.removeTab(gBrowser.tabs[initialTabsLength]); + } + }); + + is(gBrowser.tabs.length, initialTabsLength + 3, "new tabs are opened"); + is(gBrowser.tabs[initialTabsLength], newTab1, "newTab1 position is correct"); + is( + gBrowser.tabs[initialTabsLength + 1], + newTab2, + "newTab2 position is correct" + ); + is( + gBrowser.tabs[initialTabsLength + 2], + newTab3, + "newTab3 position is correct" + ); + + await dragAndDrop(newTab1, newTab2, false); + is(gBrowser.tabs.length, initialTabsLength + 3, "tabs are still there"); + is( + gBrowser.tabs[initialTabsLength], + newTab2, + "newTab2 and newTab1 are swapped" + ); + is( + gBrowser.tabs[initialTabsLength + 1], + newTab1, + "newTab1 and newTab2 are swapped" + ); + is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place"); + + await dragAndDrop(newTab2, newTab1, true); + is(gBrowser.tabs.length, initialTabsLength + 4, "a tab is duplicated"); + is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 stays same place"); + is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 stays same place"); + is( + gBrowser.tabs[initialTabsLength + 3], + newTab3, + "a new tab is inserted before newTab3" + ); +}); diff --git a/browser/base/content/test/tabs/browser_tabReorder_overflow.js b/browser/base/content/test/tabs/browser_tabReorder_overflow.js new file mode 100644 index 0000000000..a74677204f --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabReorder_overflow.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +add_task(async function () { + let initialTabsLength = gBrowser.tabs.length; + + let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox; + let tabMinWidth = parseInt( + getComputedStyle(gBrowser.selectedTab, null).minWidth + ); + + let width = ele => ele.getBoundingClientRect().width; + + let tabCountForOverflow = Math.ceil( + (width(arrowScrollbox) / tabMinWidth) * 1.1 + ); + + let newTab1 = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:robots", + { skipAnimation: true } + )); + let newTab2 = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:about", + { skipAnimation: true } + )); + let newTab3 = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:config", + { skipAnimation: true } + )); + + await BrowserTestUtils.overflowTabs(registerCleanupFunction, window, { + overflowAtStart: false, + }); + + registerCleanupFunction(function () { + while (gBrowser.tabs.length > initialTabsLength) { + gBrowser.removeTab( + gBrowser.tabContainer.getItemAtIndex(initialTabsLength) + ); + } + }); + + let tabs = gBrowser.tabs; + is(tabs.length, tabCountForOverflow, "new tabs are opened"); + is(tabs[initialTabsLength], newTab1, "newTab1 position is correct"); + is(tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct"); + is(tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct"); + + await dragAndDrop(newTab1, newTab2, false); + tabs = gBrowser.tabs; + is(tabs.length, tabCountForOverflow, "tabs are still there"); + is(tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped"); + is(tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped"); + is(tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place"); +}); diff --git a/browser/base/content/test/tabs/browser_tabSpinnerProbe.js b/browser/base/content/test/tabs/browser_tabSpinnerProbe.js new file mode 100644 index 0000000000..262d71162e --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabSpinnerProbe.js @@ -0,0 +1,102 @@ +"use strict"; + +/** + * Tests the FX_TAB_SWITCH_SPINNER_VISIBLE_MS and + * FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS telemetry probes + */ +const MIN_HANG_TIME = 500; // ms +const MAX_HANG_TIME = 5 * 1000; // ms + +/** + * Returns the sum of all values in an array. + * @param {Array} aArray An array of integers + * @return {Number} The sum of the integers in the array + */ +function sum(aArray) { + return aArray.reduce(function (previousValue, currentValue) { + return previousValue + currentValue; + }); +} + +/** + * Causes the content process for a remote <xul:browser> to run + * some busy JS for aMs milliseconds. + * + * @param {<xul:browser>} browser + * The browser that's running in the content process that we're + * going to hang. + * @param {int} aMs + * The amount of time, in milliseconds, to hang the content process. + * + * @return {Promise} + * Resolves once the hang is done. + */ +function hangContentProcess(browser, aMs) { + return ContentTask.spawn(browser, aMs, function (ms) { + let then = Date.now(); + while (Date.now() - then < ms) { + // Let's burn some CPU... + } + }); +} + +/** + * A generator intended to be run as a Task. It tests one of the tab spinner + * telemetry probes. + * @param {String} aProbe The probe to test. Should be one of: + * - FX_TAB_SWITCH_SPINNER_VISIBLE_MS + * - FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS + */ +async function testProbe(aProbe) { + info(`Testing probe: ${aProbe}`); + let histogram = Services.telemetry.getHistogramById(aProbe); + let delayTime = MIN_HANG_TIME + 1; // Pick a bucket arbitrarily + + // The tab spinner does not show up instantly. We need to hang for a little + // bit of extra time to account for the tab spinner delay. + delayTime += gBrowser.selectedTab.linkedBrowser + .getTabBrowser() + ._getSwitcher().TAB_SWITCH_TIMEOUT; + + // In order for a spinner to be shown, the tab must have presented before. + let origTab = gBrowser.selectedTab; + let hangTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let hangBrowser = hangTab.linkedBrowser; + ok(hangBrowser.isRemoteBrowser, "New tab should be remote."); + ok(hangBrowser.frameLoader.remoteTab.hasPresented, "New tab has presented."); + + // Now switch back to the original tab and set up our hang. + await BrowserTestUtils.switchTab(gBrowser, origTab); + + let tabHangPromise = hangContentProcess(hangBrowser, delayTime); + histogram.clear(); + let hangTabSwitch = BrowserTestUtils.switchTab(gBrowser, hangTab); + await tabHangPromise; + await hangTabSwitch; + + // Now we should have a hang in our histogram. + let snapshot = histogram.snapshot(); + BrowserTestUtils.removeTab(hangTab); + Assert.greater( + sum(Object.values(snapshot.values)), + 0, + `Spinner probe should now have a value in some bucket` + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + // We can interrupt JS to paint now, which is great for + // users, but bad for testing spinners. We temporarily + // disable that feature for this test so that we can + // easily get ourselves into a predictable tab spinner + // state. + ["browser.tabs.remote.force-paint", false], + ], + }); +}); + +add_task(testProbe.bind(null, "FX_TAB_SWITCH_SPINNER_VISIBLE_MS")); +add_task(testProbe.bind(null, "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS")); diff --git a/browser/base/content/test/tabs/browser_tabSuccessors.js b/browser/base/content/test/tabs/browser_tabSuccessors.js new file mode 100644 index 0000000000..9f577b6200 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabSuccessors.js @@ -0,0 +1,131 @@ +add_task(async function test() { + const tabs = [gBrowser.selectedTab]; + for (let i = 0; i < 6; ++i) { + tabs.push(BrowserTestUtils.addTab(gBrowser)); + } + + // Check that setSuccessor works. + gBrowser.setSuccessor(tabs[0], tabs[2]); + is(tabs[0].successor, tabs[2], "setSuccessor sets successor"); + ok(tabs[2].predecessors.has(tabs[0]), "setSuccessor adds predecessor"); + + BrowserTestUtils.removeTab(tabs[0]); + is( + gBrowser.selectedTab, + tabs[2], + "When closing a selected tab, select its successor" + ); + + // Check that the successor of a hidden tab becomes the successor of the + // tab's predecessors. + gBrowser.setSuccessor(tabs[1], tabs[2]); + gBrowser.setSuccessor(tabs[3], tabs[1]); + ok(!tabs[2].predecessors.has(tabs[3])); + + gBrowser.hideTab(tabs[1]); + is( + tabs[3].successor, + tabs[2], + "A predecessor of a hidden tab should take as its successor the hidden tab's successor" + ); + ok(tabs[2].predecessors.has(tabs[3])); + + gBrowser.showTab(tabs[1]); + + // Check that the successor of a closed tab also becomes the successor of the + // tab's predecessors. + gBrowser.setSuccessor(tabs[1], tabs[2]); + gBrowser.setSuccessor(tabs[3], tabs[1]); + ok(!tabs[2].predecessors.has(tabs[3])); + + BrowserTestUtils.removeTab(tabs[1]); + is( + tabs[3].successor, + tabs[2], + "A predecessor of a closed tab should take as its successor the closed tab's successor" + ); + ok(tabs[2].predecessors.has(tabs[3])); + + // Check that clearing a successor makes the browser fall back to selecting + // the owner or next tab. + await BrowserTestUtils.switchTab(gBrowser, tabs[3]); + gBrowser.setSuccessor(tabs[3], null); + is(tabs[3].successor, null, "setSuccessor(..., null) should clear successor"); + ok( + !tabs[2].predecessors.has(tabs[3]), + "setSuccessor(..., null) should remove the old successor from predecessors" + ); + + BrowserTestUtils.removeTab(tabs[3]); + is( + gBrowser.selectedTab, + tabs[4], + "When the active tab is closed and its successor has been cleared, select the next tab" + ); + + // Like closing or hiding a tab, moving a tab to another window should also + // result in its successor becoming the successor of the moved tab's + // predecessors. + gBrowser.setSuccessor(tabs[4], tabs[2]); + gBrowser.setSuccessor(tabs[2], tabs[5]); + const secondWin = gBrowser.replaceTabsWithWindow(tabs[2]); + await TestUtils.waitForCondition( + () => tabs[2].closing, + "Wait for tab to be transferred" + ); + is( + tabs[4].successor, + tabs[5], + "A predecessor of a tab moved to another window should take as its successor the moved tab's successor" + ); + + // Trying to set a successor across windows should fail. + let threw = false; + try { + gBrowser.setSuccessor(tabs[4], secondWin.gBrowser.selectedTab); + } catch (ex) { + threw = true; + } + ok(threw, "No cross window successors"); + is(tabs[4].successor, tabs[5], "Successor should remain unchanged"); + + threw = false; + try { + secondWin.gBrowser.setSuccessor(tabs[4], null); + } catch (ex) { + threw = true; + } + ok(threw, "No setting successors for another window's tab"); + is(tabs[4].successor, tabs[5], "Successor should remain unchanged"); + + BrowserTestUtils.closeWindow(secondWin); + + // A tab can't be its own successor + gBrowser.setSuccessor(tabs[4], tabs[4]); + is( + tabs[4].successor, + null, + "Successor should be cleared instead of pointing to itself" + ); + + gBrowser.setSuccessor(tabs[4], tabs[5]); + gBrowser.setSuccessor(tabs[5], tabs[4]); + is( + tabs[4].successor, + tabs[5], + "Successors can form cycles of length > 1 [a]" + ); + is( + tabs[5].successor, + tabs[4], + "Successors can form cycles of length > 1 [b]" + ); + BrowserTestUtils.removeTab(tabs[5]); + is( + tabs[4].successor, + null, + "Successor should be cleared instead of pointing to itself" + ); + + gBrowser.removeTab(tabs[4]); +}); diff --git a/browser/base/content/test/tabs/browser_tab_a11y_description.js b/browser/base/content/test/tabs/browser_tab_a11y_description.js new file mode 100644 index 0000000000..04f9a54a1b --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_a11y_description.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +async function waitForFocusAfterKey(ariaFocus, element, key, accel = false) { + let event = ariaFocus ? "AriaFocus" : "focus"; + let friendlyKey = key; + if (accel) { + friendlyKey = "Accel+" + key; + } + key = "KEY_" + key; + let focused = BrowserTestUtils.waitForEvent(element, event); + EventUtils.synthesizeKey(key, { accelKey: accel }); + await focused; + ok(true, element.label + " got " + event + " after " + friendlyKey); +} + +function getA11yDescription(element) { + let descId = element.getAttribute("aria-describedby"); + if (!descId) { + return null; + } + let descElem = document.getElementById(descId); + if (!descElem) { + return null; + } + return descElem.textContent; +} + +add_task(async function testTabA11yDescription() { + const tab1 = await addTab("http://mochi.test:8888/1", { userContextId: 1 }); + tab1.label = "tab1"; + const context1 = ContextualIdentityService.getUserContextLabel(1); + const tab2 = await addTab("http://mochi.test:8888/2", { userContextId: 2 }); + tab2.label = "tab2"; + const context2 = ContextualIdentityService.getUserContextLabel(2); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + let focused = BrowserTestUtils.waitForEvent(tab1, "focus"); + tab1.focus(); + await focused; + ok(true, "tab1 initially focused"); + ok( + getA11yDescription(tab1).endsWith(context1), + "tab1 has correct a11y description" + ); + ok(!getA11yDescription(tab2), "tab2 has no a11y description"); + + info("Moving DOM focus to tab2"); + await waitForFocusAfterKey(false, tab2, "ArrowRight"); + ok( + getA11yDescription(tab2).endsWith(context2), + "tab2 has correct a11y description" + ); + ok(!getA11yDescription(tab1), "tab1 has no a11y description"); + + info("Moving ARIA focus to tab1"); + await waitForFocusAfterKey(true, tab1, "ArrowLeft", true); + ok( + getA11yDescription(tab1).endsWith(context1), + "tab1 has correct a11y description" + ); + ok(!getA11yDescription(tab2), "tab2 has no a11y description"); + + info("Removing ARIA focus (reverting to DOM focus)"); + await waitForFocusAfterKey(true, tab2, "ArrowRight"); + ok( + getA11yDescription(tab2).endsWith(context2), + "tab2 has correct a11y description" + ); + ok(!getA11yDescription(tab1), "tab1 has no a11y description"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/base/content/test/tabs/browser_tab_label_during_reload.js b/browser/base/content/test/tabs/browser_tab_label_during_reload.js new file mode 100644 index 0000000000..ec0728c34a --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_label_during_reload.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function () { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:preferences" + )); + let browser = tab.linkedBrowser; + let labelChanges = 0; + let attrModifiedListener = event => { + if (event.detail.changed.includes("label")) { + labelChanges++; + } + }; + tab.addEventListener("TabAttrModified", attrModifiedListener); + + await BrowserTestUtils.browserLoaded(browser); + is(labelChanges, 1, "number of label changes during initial load"); + isnot(tab.label, "", "about:preferences tab label isn't empty"); + isnot( + tab.label, + "about:preferences", + "about:preferences tab label isn't the URI" + ); + is( + tab.label, + browser.contentTitle, + "about:preferences tab label matches browser.contentTitle" + ); + + labelChanges = 0; + browser.reload(); + await BrowserTestUtils.browserLoaded(browser); + is(labelChanges, 0, "number of label changes during reload"); + + tab.removeEventListener("TabAttrModified", attrModifiedListener); + gBrowser.removeTab(tab); +}); diff --git a/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js b/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js new file mode 100644 index 0000000000..dae4ffc444 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_pip_label_changes_tab() { + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + let pipTab = newWin.document.querySelector(".tabbrowser-tab[selected]"); + pipTab.setAttribute("pictureinpicture", true); + + let pipLabel = pipTab.querySelector(".tab-icon-sound-pip-label"); + + await BrowserTestUtils.withNewTab("about:blank", async browser => { + let selectedTab = newWin.document.querySelector( + ".tabbrowser-tab[selected]" + ); + Assert.ok( + selectedTab != pipTab, + "Picture in picture tab is not selected tab" + ); + + selectedTab = await BrowserTestUtils.switchTab(newWin.gBrowser, () => + pipLabel.click() + ); + Assert.ok(selectedTab == pipTab, "Picture in picture tab is selected tab"); + }); + + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/browser/base/content/test/tabs/browser_tab_manager_close.js b/browser/base/content/test/tabs/browser_tab_manager_close.js new file mode 100644 index 0000000000..6bf15f5648 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_manager_close.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL1 = "data:text/plain,tab1"; +const URL2 = "data:text/plain,tab2"; +const URL3 = "data:text/plain,tab3"; +const URL4 = "data:text/plain,tab4"; +const URL5 = "data:text/plain,tab5"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.tabmanager.enabled", true]], + }); +}); + +/** + * Tests that middle-clicking on a tab in the Tab Manager will close it. + */ +add_task(async function test_tab_manager_close_middle_click() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + win.gTabsPanel.init(); + await addTabTo(win.gBrowser, URL1); + await addTabTo(win.gBrowser, URL2); + await addTabTo(win.gBrowser, URL3); + await addTabTo(win.gBrowser, URL4); + await addTabTo(win.gBrowser, URL5); + + let button = win.document.getElementById("alltabs-button"); + let allTabsView = win.document.getElementById("allTabsMenu-allTabsView"); + let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent( + allTabsView, + "ViewShown" + ); + button.click(); + await allTabsPopupShownPromise; + + let list = win.document.getElementById("allTabsMenu-allTabsView-tabs"); + while (win.gBrowser.tabs.length > 1) { + let row = list.lastElementChild; + let tabClosing = BrowserTestUtils.waitForTabClosing(row.tab); + EventUtils.synthesizeMouseAtCenter(row, { button: 1 }, win); + await tabClosing; + Assert.ok(true, "Closed a tab with middle-click."); + } + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Tests that clicking the close button next to a tab manager item + * will close it. + */ +add_task(async function test_tab_manager_close_button() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + win.gTabsPanel.init(); + await addTabTo(win.gBrowser, URL1); + await addTabTo(win.gBrowser, URL2); + await addTabTo(win.gBrowser, URL3); + await addTabTo(win.gBrowser, URL4); + await addTabTo(win.gBrowser, URL5); + + let button = win.document.getElementById("alltabs-button"); + let allTabsView = win.document.getElementById("allTabsMenu-allTabsView"); + let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent( + allTabsView, + "ViewShown" + ); + button.click(); + await allTabsPopupShownPromise; + + let list = win.document.getElementById("allTabsMenu-allTabsView-tabs"); + while (win.gBrowser.tabs.length > 1) { + let row = list.lastElementChild; + let tabClosing = BrowserTestUtils.waitForTabClosing(row.tab); + let closeButton = row.lastElementChild; + EventUtils.synthesizeMouseAtCenter(closeButton, { button: 1 }, win); + await tabClosing; + Assert.ok(true, "Closed a tab with the close button."); + } + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/base/content/test/tabs/browser_tab_manager_drag.js b/browser/base/content/test/tabs/browser_tab_manager_drag.js new file mode 100644 index 0000000000..b00a09fcdb --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_manager_drag.js @@ -0,0 +1,257 @@ +/** + * Test reordering the tabs in the Tab Manager, moving the tab between the + * Tab Manager and tab bar. + */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const URL1 = "data:text/plain,tab1"; +const URL2 = "data:text/plain,tab2"; +const URL3 = "data:text/plain,tab3"; +const URL4 = "data:text/plain,tab4"; +const URL5 = "data:text/plain,tab5"; + +function assertOrder(order, expected, message) { + is( + JSON.stringify(order), + JSON.stringify(expected), + `The order of the tabs ${message}` + ); +} + +function toIndex(url) { + const m = url.match(/^data:text\/plain,tab(\d)/); + if (m) { + return parseInt(m[1]); + } + return 0; +} + +function getOrderOfList(list) { + return [...list.querySelectorAll("toolbaritem")].map(row => { + const url = row.firstElementChild.tab.linkedBrowser.currentURI.spec; + return toIndex(url); + }); +} + +function getOrderOfTabs(tabs) { + return tabs.map(tab => { + const url = tab.linkedBrowser.currentURI.spec; + return toIndex(url); + }); +} + +async function testWithNewWindow(func) { + Services.prefs.setBoolPref("browser.tabs.tabmanager.enabled", true); + + const newWindow = await BrowserTestUtils.openNewBrowserWindow(); + + await Promise.all([ + addTabTo(newWindow.gBrowser, URL1), + addTabTo(newWindow.gBrowser, URL2), + addTabTo(newWindow.gBrowser, URL3), + addTabTo(newWindow.gBrowser, URL4), + addTabTo(newWindow.gBrowser, URL5), + ]); + + newWindow.gTabsPanel.init(); + + const button = newWindow.document.getElementById("alltabs-button"); + + const allTabsView = newWindow.document.getElementById( + "allTabsMenu-allTabsView" + ); + const allTabsPopupShownPromise = BrowserTestUtils.waitForEvent( + allTabsView, + "ViewShown" + ); + button.click(); + await allTabsPopupShownPromise; + + await func(newWindow); + + await BrowserTestUtils.closeWindow(newWindow); + + Services.prefs.clearUserPref("browser.tabs.tabmanager.enabled"); +} + +add_task(async function test_reorder() { + await testWithNewWindow(async function (newWindow) { + Services.telemetry.clearScalars(); + + const list = newWindow.document.getElementById( + "allTabsMenu-allTabsView-tabs" + ); + + assertOrder(getOrderOfList(list), [0, 1, 2, 3, 4, 5], "before reorder"); + + let rows; + rows = list.querySelectorAll("toolbaritem"); + EventUtils.synthesizeDrop( + rows[3], + rows[1], + null, + "move", + newWindow, + newWindow, + { clientX: 0, clientY: 0 } + ); + + assertOrder(getOrderOfList(list), [0, 3, 1, 2, 4, 5], "after moving up"); + + rows = list.querySelectorAll("toolbaritem"); + EventUtils.synthesizeDrop( + rows[1], + rows[5], + null, + "move", + newWindow, + newWindow, + { clientX: 0, clientY: 0 } + ); + + assertOrder(getOrderOfList(list), [0, 1, 2, 4, 3, 5], "after moving down"); + + rows = list.querySelectorAll("toolbaritem"); + EventUtils.synthesizeDrop( + rows[4], + rows[3], + null, + "move", + newWindow, + newWindow, + { clientX: 0, clientY: 0 } + ); + + assertOrder( + getOrderOfList(list), + [0, 1, 2, 3, 4, 5], + "after moving up again" + ); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar( + scalars, + "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count", + 3 + ); + }); +}); + +function tabOf(row) { + return row.firstElementChild.tab; +} + +add_task(async function test_move_to_tab_bar() { + await testWithNewWindow(async function (newWindow) { + Services.telemetry.clearScalars(); + + const list = newWindow.document.getElementById( + "allTabsMenu-allTabsView-tabs" + ); + + assertOrder(getOrderOfList(list), [0, 1, 2, 3, 4, 5], "before reorder"); + + let rows; + rows = list.querySelectorAll("toolbaritem"); + EventUtils.synthesizeDrop( + rows[3], + tabOf(rows[1]), + null, + "move", + newWindow, + newWindow, + { clientX: 0, clientY: 0 } + ); + + assertOrder( + getOrderOfList(list), + [0, 3, 1, 2, 4, 5], + "after moving up with tab bar" + ); + + rows = list.querySelectorAll("toolbaritem"); + EventUtils.synthesizeDrop( + rows[1], + tabOf(rows[4]), + null, + "move", + newWindow, + newWindow, + { clientX: 0, clientY: 0 } + ); + + assertOrder( + getOrderOfList(list), + [0, 1, 2, 3, 4, 5], + "after moving down with tab bar" + ); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar( + scalars, + "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count", + 2 + ); + }); +}); + +add_task(async function test_move_to_different_tab_bar() { + const newWindow2 = await BrowserTestUtils.openNewBrowserWindow(); + + await testWithNewWindow(async function (newWindow) { + Services.telemetry.clearScalars(); + + const list = newWindow.document.getElementById( + "allTabsMenu-allTabsView-tabs" + ); + + assertOrder( + getOrderOfList(list), + [0, 1, 2, 3, 4, 5], + "before reorder in newWindow" + ); + assertOrder( + getOrderOfTabs(newWindow2.gBrowser.tabs), + [0], + "before reorder in newWindow2" + ); + + let rows; + rows = list.querySelectorAll("toolbaritem"); + EventUtils.synthesizeDrop( + rows[3], + newWindow2.gBrowser.tabs[0], + null, + "move", + newWindow, + newWindow2, + { clientX: 0, clientY: 0 } + ); + + assertOrder( + getOrderOfList(list), + [0, 1, 2, 4, 5], + "after moving to other window in newWindow" + ); + + assertOrder( + getOrderOfTabs(newWindow2.gBrowser.tabs), + [3, 0], + "after moving to other window in newWindow2" + ); + + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true); + TelemetryTestUtils.assertScalar( + scalars, + "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count", + 1 + ); + }); + + await BrowserTestUtils.closeWindow(newWindow2); +}); diff --git a/browser/base/content/test/tabs/browser_tab_manager_keyboard_access.js b/browser/base/content/test/tabs/browser_tab_manager_keyboard_access.js new file mode 100644 index 0000000000..7ce50f20ec --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_manager_keyboard_access.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check we can open the tab manager using the keyboard. + * Note that navigation to buttons in the toolbar is covered + * by other tests. + */ +add_task(async function test_open_tabmanager_keyboard() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.tabmanager.enabled", true]], + }); + let newWindow = await BrowserTestUtils.openNewBrowserWindow(); + let elem = newWindow.document.getElementById("alltabs-button"); + + // Borrowed from forceFocus() in the keyboard directory head.js + elem.setAttribute("tabindex", "-1"); + elem.focus(); + elem.removeAttribute("tabindex"); + + let focused = BrowserTestUtils.waitForEvent(newWindow, "focus", true); + EventUtils.synthesizeKey(" ", {}, newWindow); + let event = await focused; + ok( + event.originalTarget.closest("#allTabsMenu-allTabsView"), + "Focus inside all tabs menu after toolbar button pressed" + ); + let hidden = BrowserTestUtils.waitForEvent( + event.target.closest("panel"), + "popuphidden" + ); + EventUtils.synthesizeKey("KEY_Escape", { shiftKey: false }, newWindow); + await hidden; + await BrowserTestUtils.closeWindow(newWindow); +}); diff --git a/browser/base/content/test/tabs/browser_tab_manager_visibility.js b/browser/base/content/test/tabs/browser_tab_manager_visibility.js new file mode 100644 index 0000000000..b7de777512 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_manager_visibility.js @@ -0,0 +1,53 @@ +/** + * Test the Tab Manager visibility respects browser.tabs.tabmanager.enabled preference + * */ + +"use strict"; + +// The hostname for the test URIs. +const TEST_HOSTNAME = "https://example.com"; +const DUMMY_PAGE_PATH = "/browser/base/content/test/tabs/dummy_page.html"; + +add_task(async function tab_manager_visibility_preference_on() { + Services.prefs.setBoolPref("browser.tabs.tabmanager.enabled", true); + + let newWindow = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.withNewTab( + { + gBrowser: newWindow.gBrowser, + url: TEST_HOSTNAME + DUMMY_PAGE_PATH, + }, + async function (browser) { + await Assert.ok( + BrowserTestUtils.isVisible( + newWindow.document.getElementById("alltabs-button") + ), + "tab manage menu is visible when browser.tabs.tabmanager.enabled preference is set to true" + ); + } + ); + Services.prefs.clearUserPref("browser.tabs.tabmanager.enabled"); + BrowserTestUtils.closeWindow(newWindow); +}); + +add_task(async function tab_manager_visibility_preference_off() { + Services.prefs.setBoolPref("browser.tabs.tabmanager.enabled", false); + + let newWindow = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.withNewTab( + { + gBrowser: newWindow.gBrowser, + url: TEST_HOSTNAME + DUMMY_PAGE_PATH, + }, + async function (browser) { + await Assert.ok( + BrowserTestUtils.isHidden( + newWindow.document.getElementById("alltabs-button") + ), + "tab manage menu is hidden when browser.tabs.tabmanager.enabled preference is set to true" + ); + } + ); + Services.prefs.clearUserPref("browser.tabs.tabmanager.enabled"); + BrowserTestUtils.closeWindow(newWindow); +}); diff --git a/browser/base/content/test/tabs/browser_tab_move_to_new_window_reload.js b/browser/base/content/test/tabs/browser_tab_move_to_new_window_reload.js new file mode 100644 index 0000000000..6af8f440fd --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_move_to_new_window_reload.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { TabStateFlusher } = ChromeUtils.importESModule( + "resource:///modules/sessionstore/TabStateFlusher.sys.mjs" +); + +// Move a tab to a new window the reload it. In Bug 1691135 it would not +// reload. +add_task(async function test() { + let tab1 = await addTab(); + let tab2 = await addTab(); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + let prevBrowser = tab1.linkedBrowser; + + let delayedStartupPromise = BrowserTestUtils.waitForNewWindow(); + let newWindow = gBrowser.replaceTabsWithWindow(tab1); + await delayedStartupPromise; + + ok( + !prevBrowser.frameLoader, + "the swapped-from browser's frameloader has been destroyed" + ); + + let gBrowser2 = newWindow.gBrowser; + + is(gBrowser.visibleTabs.length, 2, "Two tabs now in the old window"); + is(gBrowser2.visibleTabs.length, 1, "One tabs in the new window"); + + tab1 = gBrowser2.visibleTabs[0]; + ok(tab1, "Got a tab1"); + await tab1.focus(); + + await TabStateFlusher.flush(tab1.linkedBrowser); + + info("Reloading"); + let tab1Loaded = BrowserTestUtils.browserLoaded( + gBrowser2.getBrowserForTab(tab1) + ); + + gBrowser2.reload(); + ok(await tab1Loaded, "Tab reloaded"); + + await BrowserTestUtils.closeWindow(newWindow); + await BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/base/content/test/tabs/browser_tab_play.js b/browser/base/content/test/tabs/browser_tab_play.js new file mode 100644 index 0000000000..f98956eeb4 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_play.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Ensure tabs that are active media blocked act correctly + * when we try to unblock them using the "Play Tab" icon or by calling + * resumeDelayedMedia() + */ + +"use strict"; + +const PREF_DELAY_AUTOPLAY = "media.block-autoplay-until-in-foreground"; + +async function playMedia(tab, { expectBlocked }) { + let blockedPromise = wait_for_tab_media_blocked_event(tab, expectBlocked); + tab.resumeDelayedMedia(); + await blockedPromise; + is(activeMediaBlocked(tab), expectBlocked, "tab has wrong media block state"); +} + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_DELAY_AUTOPLAY, true]], + }); +}); + +/* + * Playing blocked media will not mute the selected tabs + */ +add_task(async function testDelayPlayWontAffectUnmuteStatus() { + info("Add media tabs"); + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + + let tabs = [tab0, tab1]; + + info("Play both tabs"); + await play(tab0, false); + await play(tab1, false); + + // Check tabs are unmuted + ok(!muted(tab0), "Tab0 is unmuted"); + ok(!muted(tab1), "Tab1 is unmuted"); + + info("Play media on tab0"); + await playMedia(tab0, { expectBlocked: false }); + + // Check tabs are still unmuted + ok(!muted(tab0), "Tab0 is unmuted"); + ok(!muted(tab1), "Tab1 is unmuted"); + + info("Play media on tab1"); + await playMedia(tab1, { expectBlocked: false }); + + // Check tabs are still unmuted + ok(!muted(tab0), "Tab0 is unmuted"); + ok(!muted(tab1), "Tab1 is unmuted"); + + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +/* + * Playing blocked media will not unmute the selected tabs + */ +add_task(async function testDelayPlayWontAffectMuteStatus() { + info("Add media tabs"); + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + + info("Play both tabs"); + await play(tab0, false); + await play(tab1, false); + + // Mute both tabs + toggleMuteAudio(tab0, true); + toggleMuteAudio(tab1, true); + + // Check tabs are muted + ok(muted(tab0), "Tab0 is muted"); + ok(muted(tab1), "Tab1 is muted"); + + info("Play media on tab0"); + await playMedia(tab0, { expectBlocked: false }); + + // Check tabs are still muted + ok(muted(tab0), "Tab0 is muted"); + ok(muted(tab1), "Tab1 is muted"); + + info("Play media on tab1"); + await playMedia(tab1, { expectBlocked: false }); + + // Check tabs are still muted + ok(muted(tab0), "Tab0 is muted"); + ok(muted(tab1), "Tab1 is muted"); + + BrowserTestUtils.removeTab(tab0); + BrowserTestUtils.removeTab(tab1); +}); + +/* + * Switching tabs will unblock media + */ +add_task(async function testDelayPlayWhenSwitchingTab() { + info("Add media tabs"); + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + + info("Play both tabs"); + await play(tab0, false); + await play(tab1, false); + + // Both tabs are initially active media blocked after being played + ok(activeMediaBlocked(tab0), "Tab0 is activemedia-blocked"); + ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked"); + + info("Switch to tab0"); + await BrowserTestUtils.switchTab(gBrowser, tab0); + is(gBrowser.selectedTab, tab0, "Tab0 is active"); + + // tab0 unblocked, tab1 blocked + ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked"); + ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked"); + + info("Switch to tab1"); + await BrowserTestUtils.switchTab(gBrowser, tab1); + is(gBrowser.selectedTab, tab1, "Tab1 is active"); + + // tab0 unblocked, tab1 unblocked + ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked"); + ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked"); + + BrowserTestUtils.removeTab(tab0); + BrowserTestUtils.removeTab(tab1); +}); + +/* + * The "Play Tab" icon unblocks media + */ +add_task(async function testDelayPlayWhenUsingButton() { + info("Add media tabs"); + let tab0 = await addMediaTab(); + let tab1 = await addMediaTab(); + + info("Play both tabs"); + await play(tab0, false); + await play(tab1, false); + + // Both tabs are initially active media blocked after being played + ok(activeMediaBlocked(tab0), "Tab0 is activemedia-blocked"); + ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked"); + + info("Press the Play Tab icon on tab0"); + await pressIcon(tab0.overlayIcon); + + // tab0 unblocked, tab1 blocked + ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked"); + ok(activeMediaBlocked(tab1), "Tab1 is activemedia-blocked"); + + info("Press the Play Tab icon on tab1"); + await pressIcon(tab1.overlayIcon); + + // tab0 unblocked, tab1 unblocked + ok(!activeMediaBlocked(tab0), "Tab0 is not activemedia-blocked"); + ok(!activeMediaBlocked(tab1), "Tab1 is not activemedia-blocked"); + + BrowserTestUtils.removeTab(tab0); + BrowserTestUtils.removeTab(tab1); +}); + +/* + * Tab context menus have to show the menu icons "Play Tab" or "Play Tabs" + * depending on the number of tabs selected, and whether blocked media is present + */ +add_task(async function testTabContextMenu() { + info("Add media tab"); + let tab0 = await addMediaTab(); + + let menuItemPlayTab = document.getElementById("context_playTab"); + let menuItemPlaySelectedTabs = document.getElementById( + "context_playSelectedTabs" + ); + + // No active media yet: + // - "Play Tab" is hidden + // - "Play Tabs" is hidden + updateTabContextMenu(tab0); + ok(menuItemPlayTab.hidden, 'tab0 "Play Tab" is hidden'); + ok(menuItemPlaySelectedTabs.hidden, 'tab0 "Play Tabs" is hidden'); + ok(!activeMediaBlocked(tab0), "tab0 is not active media blocked"); + + info("Play tab0"); + await play(tab0, false); + + // Active media blocked: + // - "Play Tab" is visible + // - "Play Tabs" is hidden + updateTabContextMenu(tab0); + ok(!menuItemPlayTab.hidden, 'tab0 "Play Tab" is visible'); + ok(menuItemPlaySelectedTabs.hidden, 'tab0 "Play Tabs" is hidden'); + ok(activeMediaBlocked(tab0), "tab0 is active media blocked"); + + info("Play media on tab0"); + await playMedia(tab0, { expectBlocked: false }); + + // Media is playing: + // - "Play Tab" is hidden + // - "Play Tabs" is hidden + updateTabContextMenu(tab0); + ok(menuItemPlayTab.hidden, 'tab0 "Play Tab" is hidden'); + ok(menuItemPlaySelectedTabs.hidden, 'tab0 "Play Tabs" is hidden'); + ok(!activeMediaBlocked(tab0), "tab0 is not active media blocked"); + + BrowserTestUtils.removeTab(tab0); +}); diff --git a/browser/base/content/test/tabs/browser_tab_preview.js b/browser/base/content/test/tabs/browser_tab_preview.js new file mode 100644 index 0000000000..e3dd1b6842 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_preview.js @@ -0,0 +1,154 @@ +/* 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/. */ + +"use strict"; + +async function openPreview(tab) { + const previewShown = BrowserTestUtils.waitForEvent( + document.getElementById("tabbrowser-tab-preview"), + "previewshown", + false, + e => { + return e.detail.tab === tab; + } + ); + EventUtils.synthesizeMouseAtCenter(tab, { type: "mouseover" }); + return previewShown; +} + +async function closePreviews() { + const tabs = document.getElementById("tabbrowser-tabs"); + const previewHidden = BrowserTestUtils.waitForEvent( + document.getElementById("tabbrowser-tab-preview"), + "previewhidden" + ); + EventUtils.synthesizeMouse(tabs, 0, tabs.outerHeight + 1, { + type: "mouseout", + }); + return previewHidden; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.cardPreview.enabled", true], + ["browser.tabs.cardPreview.showThumbnails", false], + ["browser.tabs.cardPreview.delayMs", 0], + ], + }); +}); + +/** + * Verify the following: + * + * 1. Tab preview card appears when the mouse hovers over a tab + * 2. Tab preview card shows the correct preview for the tab being hovered + * 3. Tab preview card is dismissed when the mouse leaves the tab bar + */ +add_task(async () => { + const tabUrl1 = + "data:text/html,<html><head><title>First New Tab</title></head><body>Hello</body></html>"; + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1); + const tabUrl2 = + "data:text/html,<html><head><title>Second New Tab</title></head><body>Hello</body></html>"; + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2); + const previewContainer = document.getElementById("tabbrowser-tab-preview"); + + await openPreview(tab1); + Assert.ok( + ["open", "showing"].includes(previewContainer.panel.state), + "tab1 preview shown" + ); + Assert.equal( + previewContainer.renderRoot.querySelector(".tab-preview-title").innerText, + "First New Tab", + "Preview of tab1 shows correct title" + ); + + await openPreview(tab2); + Assert.ok( + ["open", "showing"].includes(previewContainer.panel.state), + "tab2 preview shown" + ); + Assert.equal( + previewContainer.renderRoot.querySelector(".tab-preview-title").innerText, + "Second New Tab", + "Preview of tab2 shows correct title" + ); + + await closePreviews(); + Assert.ok( + ["closed", "hiding"].includes(previewContainer.panel.state), + "preview container is now hidden" + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +/** + * Verify that non-selected tabs display a thumbnail in their preview + * when browser.tabs.cardPreview.showThumbnails is set to true, + * while the currently selected tab never displays a thumbnail in its preview. + */ +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.cardPreview.showThumbnails", true]], + }); + const tabUrl1 = "about:blank"; + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1); + const tabUrl2 = "about:blank"; + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2); + const previewContainer = document.getElementById("tabbrowser-tab-preview"); + + const thumbnailUpdated = BrowserTestUtils.waitForEvent( + previewContainer, + "previewThumbnailUpdated" + ); + await openPreview(tab1); + await thumbnailUpdated; + Assert.ok( + previewContainer.renderRoot.querySelectorAll("img,canvas").length, + "Tab1 preview contains thumbnail" + ); + + await openPreview(tab2); + Assert.equal( + previewContainer.renderRoot.querySelectorAll("img,canvas").length, + 0, + "Tab2 (selected) does not contain thumbnail" + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + await SpecialPowers.popPrefEnv(); +}); + +/** + * Wheel events at the document-level of the window should hide the preview. + */ +add_task(async () => { + const tabUrl1 = "about:blank"; + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1); + const tabUrl2 = "about:blank"; + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2); + + await openPreview(tab1); + + const tabs = document.getElementById("tabbrowser-tabs"); + const previewHidden = BrowserTestUtils.waitForEvent( + document.getElementById("tabbrowser-tab-preview"), + "previewhidden" + ); + EventUtils.synthesizeMouse(tabs, 0, tabs.outerHeight + 1, { + wheel: true, + deltaY: -1, + deltaMode: WheelEvent.DOM_DELTA_LINE, + }); + await previewHidden; + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/base/content/test/tabs/browser_tab_tooltips.js b/browser/base/content/test/tabs/browser_tab_tooltips.js new file mode 100644 index 0000000000..ee82816bce --- /dev/null +++ b/browser/base/content/test/tabs/browser_tab_tooltips.js @@ -0,0 +1,149 @@ +// Offset within the tab for the mouse event +const MOUSE_OFFSET = 7; + +// Normal tooltips are positioned vertically at least this amount +const MIN_VERTICAL_TOOLTIP_OFFSET = 18; + +function openTooltip(node, tooltip) { + let tooltipShownPromise = BrowserTestUtils.waitForEvent( + tooltip, + "popupshown" + ); + window.windowUtils.disableNonTestMouseEvents(true); + EventUtils.synthesizeMouse(node, 2, 2, { type: "mouseover" }); + EventUtils.synthesizeMouse(node, 4, 4, { type: "mousemove" }); + EventUtils.synthesizeMouse(node, MOUSE_OFFSET, MOUSE_OFFSET, { + type: "mousemove", + }); + EventUtils.synthesizeMouse(node, 2, 2, { type: "mouseout" }); + window.windowUtils.disableNonTestMouseEvents(false); + return tooltipShownPromise; +} + +function closeTooltip(node, tooltip) { + let tooltipHiddenPromise = BrowserTestUtils.waitForEvent( + tooltip, + "popuphidden" + ); + EventUtils.synthesizeMouse(document.documentElement, 2, 2, { + type: "mousemove", + }); + return tooltipHiddenPromise; +} + +// This test verifies that the tab tooltip appears at the correct location, aligned +// with the bottom of the tab, and that the tooltip appears near the close button. +add_task(async function () { + const tabUrl = + "data:text/html,<html><head><title>A Tab</title></head><body>Hello</body></html>"; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl); + + let tooltip = document.getElementById("tabbrowser-tab-tooltip"); + await openTooltip(tab, tooltip); + + let tabRect = tab.getBoundingClientRect(); + let tooltipRect = tooltip.getBoundingClientRect(); + + isfuzzy( + tooltipRect.left, + tabRect.left + MOUSE_OFFSET, + 1, + "tooltip left position for tab" + ); + Assert.greaterOrEqual( + tooltipRect.top, + tabRect.top + MIN_VERTICAL_TOOLTIP_OFFSET + MOUSE_OFFSET, + "tooltip top position for tab" + ); + is( + tooltip.getAttribute("position"), + "", + "tooltip position attribute for tab" + ); + + await closeTooltip(tab, tooltip); + + await openTooltip(tab.closeButton, tooltip); + + let closeButtonRect = tab.closeButton.getBoundingClientRect(); + tooltipRect = tooltip.getBoundingClientRect(); + + isfuzzy( + tooltipRect.left, + closeButtonRect.left + MOUSE_OFFSET, + 1, + "tooltip left position for close button" + ); + Assert.greater( + tooltipRect.top, + closeButtonRect.top + MIN_VERTICAL_TOOLTIP_OFFSET + MOUSE_OFFSET, + "tooltip top position for close button" + ); + ok( + !tooltip.hasAttribute("position"), + "tooltip position attribute for close button" + ); + + await closeTooltip(tab, tooltip); + + BrowserTestUtils.removeTab(tab); +}); + +// This test verifies that a mouse wheel closes the tooltip. +add_task(async function () { + const tabUrl = + "data:text/html,<html><head><title>A Tab</title></head><body>Hello</body></html>"; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl); + + let tooltip = document.getElementById("tabbrowser-tab-tooltip"); + await openTooltip(tab, tooltip); + + EventUtils.synthesizeWheel(tab, 4, 4, { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, + }); + + is(tooltip.state, "closed", "wheel event closed the tooltip"); + + BrowserTestUtils.removeTab(tab); +}); + +// This test verifies that the tooltip in the tab manager panel matches the +// tooltip in the tab strip. +add_task(async function () { + // Open a new tab + const tabUrl = "https://example.com"; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl); + + // Make the popup of allTabs showing up + gTabsPanel.init(); + let allTabsView = document.getElementById("allTabsMenu-allTabsView"); + let allTabsPopupShownPromise = BrowserTestUtils.waitForEvent( + allTabsView, + "ViewShown" + ); + gTabsPanel.showAllTabsPanel(); + await allTabsPopupShownPromise; + + // Get tooltips and compare them + let tabInPanel = Array.from( + gTabsPanel.allTabsViewTabs.querySelectorAll(".all-tabs-button") + ).at(-1).label; + let tabInTabStrip = tab.getAttribute("label"); + + is( + tabInPanel, + tabInTabStrip, + "Tooltip in tab manager panel matches tooltip in tab strip" + ); + + // Close everything + let allTabsPopupHiddenPromise = BrowserTestUtils.waitForEvent( + allTabsView.panelMultiView, + "PanelMultiViewHidden" + ); + gTabsPanel.hideAllTabsPanel(); + await allTabsPopupHiddenPromise; + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/tabs/browser_tabswitch_contextmenu.js b/browser/base/content/test/tabs/browser_tabswitch_contextmenu.js new file mode 100644 index 0000000000..bd671a86c6 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabswitch_contextmenu.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Don't switch tabs via the keyboard while the contextmenu is open. + */ +add_task(async function cant_tabswitch_mid_contextmenu() { + let tab1 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/idontexist" + ); + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org/idontexist" + ); + + const contextMenu = document.getElementById("contentAreaContextMenu"); + let promisePopupShown = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "shown" + ); + await BrowserTestUtils.synthesizeMouse( + "body", + 0, + 0, + { + type: "contextmenu", + button: 2, + }, + tab2.linkedBrowser + ); + await promisePopupShown; + EventUtils.synthesizeKey("VK_TAB", { accelKey: true }); + ok(tab2.selected, "tab2 should stay selected"); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + let promisePopupHidden = BrowserTestUtils.waitForPopupEvent( + contextMenu, + "hidden" + ); + contextMenu.hidePopup(); + await promisePopupHidden; +}); diff --git a/browser/base/content/test/tabs/browser_tabswitch_select.js b/browser/base/content/test/tabs/browser_tabswitch_select.js new file mode 100644 index 0000000000..3868764bed --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabswitch_select.js @@ -0,0 +1,63 @@ +/* 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/. */ + +"use strict"; + +add_task(async function () { + let tab1 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:support" + ); + + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,Goodbye" + ); + + gURLBar.select(); + + await BrowserTestUtils.switchTab(gBrowser, tab1); + + let focusPromise = BrowserTestUtils.waitForEvent( + gURLBar.inputField, + "select", + true + ); + await BrowserTestUtils.switchTab(gBrowser, tab2); + await focusPromise; + + is(gURLBar.selectionStart, 0, "url is selected"); + is(gURLBar.selectionEnd, 22, "url is selected"); + + // Now check that the url bar is focused when a new tab is opened while in fullscreen. + + let fullScreenEntered = TestUtils.waitForCondition( + () => document.documentElement.getAttribute("sizemode") == "fullscreen" + ); + BrowserFullScreen(); + await fullScreenEntered; + + tab2.linkedBrowser.focus(); + + // Open a new tab + focusPromise = BrowserTestUtils.waitForEvent( + gURLBar.inputField, + "focus", + true + ); + EventUtils.synthesizeKey("T", { accelKey: true }); + await focusPromise; + + is(document.activeElement, gURLBar.inputField, "urlbar is focused"); + + let fullScreenExited = TestUtils.waitForCondition( + () => document.documentElement.getAttribute("sizemode") != "fullscreen" + ); + BrowserFullScreen(); + await fullScreenExited; + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js b/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js new file mode 100644 index 0000000000..b5d2762eec --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js @@ -0,0 +1,28 @@ +// This test ensures that only one command update happens when switching tabs. + +"use strict"; + +add_task(async function () { + const uri = "data:text/html,<body><input>"; + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri); + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri); + + let updates = []; + function countUpdates(event) { + updates.push(new Error().stack); + } + let updater = document.getElementById("editMenuCommandSetAll"); + updater.addEventListener("commandupdate", countUpdates, true); + await BrowserTestUtils.switchTab(gBrowser, tab1); + + is(updates.length, 1, "only one command update per tab switch"); + if (updates.length > 1) { + for (let stack of updates) { + info("Update stack:\n" + stack); + } + } + + updater.removeEventListener("commandupdate", countUpdates, true); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/browser/base/content/test/tabs/browser_tabswitch_window_focus.js b/browser/base/content/test/tabs/browser_tabswitch_window_focus.js new file mode 100644 index 0000000000..a808ab4f09 --- /dev/null +++ b/browser/base/content/test/tabs/browser_tabswitch_window_focus.js @@ -0,0 +1,78 @@ +"use strict"; + +// Allow to open popups without any kind of interaction. +SpecialPowers.pushPrefEnv({ set: [["dom.disable_window_flip", false]] }); + +const FILE = getRootDirectory(gTestPath) + "open_window_in_new_tab.html"; + +add_task(async function () { + info("Opening first tab: " + FILE); + let firstTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, FILE); + + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + gBrowser, + FILE + "?open-click", + true + ); + info("Opening second tab using a click"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#open-click", + {}, + firstTab.linkedBrowser + ); + + info("Waiting for the second tab to be opened"); + let secondTab = await promiseTabOpened; + + info("Going back to the first tab"); + await BrowserTestUtils.switchTab(gBrowser, firstTab); + + info("Focusing second tab by clicking on the first tab"); + await BrowserTestUtils.switchTab(gBrowser, async function () { + await SpecialPowers.spawn(firstTab.linkedBrowser, [""], async function () { + content.document.querySelector("#focus").click(); + }); + }); + + is(gBrowser.selectedTab, secondTab, "Should've switched tabs"); + + await BrowserTestUtils.removeTab(firstTab); + await BrowserTestUtils.removeTab(secondTab); +}); + +add_task(async function () { + info("Opening first tab: " + FILE); + let firstTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, FILE); + + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + gBrowser, + FILE + "?open-mousedown", + true + ); + info("Opening second tab using a click"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#open-mousedown", + { type: "mousedown" }, + firstTab.linkedBrowser + ); + + info("Waiting for the second tab to be opened"); + let secondTab = await promiseTabOpened; + + is(gBrowser.selectedTab, secondTab, "Should've switched tabs"); + + info("Ensuring we don't switch back"); + await new Promise(resolve => { + // We need to wait for something _not_ happening, so we need to use an arbitrary setTimeout. + // + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(function () { + is(gBrowser.selectedTab, secondTab, "Should've remained in original tab"); + resolve(); + }, 500); + }); + + info("cleanup"); + await BrowserTestUtils.removeTab(firstTab); + await BrowserTestUtils.removeTab(secondTab); +}); diff --git a/browser/base/content/test/tabs/browser_undo_close_tabs.js b/browser/base/content/test/tabs/browser_undo_close_tabs.js new file mode 100644 index 0000000000..e4e9da5d5d --- /dev/null +++ b/browser/base/content/test/tabs/browser_undo_close_tabs.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs"; + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_WARN_ON_CLOSE, false]], + }); +}); + +add_task(async function withMultiSelectedTabs() { + let initialTab = gBrowser.selectedTab; + let tab1 = await addTab("https://example.com/1"); + let tab2 = await addTab("https://example.com/2"); + let tab3 = await addTab("https://example.com/3"); + let tab4 = await addTab("https://example.com/4"); + + is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs"); + + gBrowser.selectedTab = tab2; + await triggerClickOn(tab4, { shiftKey: true }); + + ok(!initialTab.multiselected, "InitialTab is not multiselected"); + ok(!tab1.multiselected, "Tab1 is not multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + ok(tab4.multiselected, "Tab4 is multiselected"); + is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs"); + + gBrowser.removeMultiSelectedTabs(); + await TestUtils.waitForCondition( + () => SessionStore.getLastClosedTabCount(window) == 3, + "wait for the multi selected tabs to close in SessionStore" + ); + is( + SessionStore.getLastClosedTabCount(window), + 3, + "SessionStore should know how many tabs were just closed" + ); + + undoCloseTab(); + await TestUtils.waitForCondition( + () => gBrowser.tabs.length == 5, + "wait for the tabs to reopen" + ); + + is( + SessionStore.getLastClosedTabCount(window), + SessionStore.getClosedTabCountForWindow(window) ? 1 : 0, + "LastClosedTabCount should be reset" + ); + + info("waiting for the browsers to finish loading"); + // Check that the tabs are restored in the correct order + for (let tabId of [2, 3, 4]) { + let browser = gBrowser.tabs[tabId].linkedBrowser; + await ContentTask.spawn(browser, tabId, async aTabId => { + await ContentTaskUtils.waitForCondition(() => { + return ( + content?.document?.readyState == "complete" && + content?.document?.location.href == "https://example.com/" + aTabId + ); + }, "waiting for tab " + aTabId + " to load"); + }); + } + + gBrowser.removeAllTabsBut(initialTab); +}); + +add_task(async function withBothGroupsAndTab() { + let initialTab = gBrowser.selectedTab; + let tab1 = await addTab("https://example.com/1"); + let tab2 = await addTab("https://example.com/2"); + let tab3 = await addTab("https://example.com/3"); + + gBrowser.selectedTab = tab2; + await triggerClickOn(tab3, { shiftKey: true }); + + ok(!initialTab.multiselected, "InitialTab is not multiselected"); + ok(!tab1.multiselected, "Tab1 is not multiselected"); + ok(tab2.multiselected, "Tab2 is multiselected"); + ok(tab3.multiselected, "Tab3 is multiselected"); + is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs"); + + gBrowser.removeMultiSelectedTabs(); + await TestUtils.waitForCondition( + () => gBrowser.tabs.length == 2, + "wait for the multiselected tabs to close" + ); + + is( + SessionStore.getLastClosedTabCount(window), + 2, + "SessionStore should know how many tabs were just closed" + ); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let tab4 = await addTab("http://example.com/4"); + + is( + SessionStore.getLastClosedTabCount(window), + 2, + "LastClosedTabCount should be the same" + ); + + gBrowser.removeTab(tab4); + + await TestUtils.waitForCondition( + () => SessionStore.getLastClosedTabCount(window) == 1, + "wait for the tab to close in SessionStore" + ); + + let count = 3; + for (let i = 0; i < 3; i++) { + is( + SessionStore.getLastClosedTabCount(window), + 1, + "LastClosedTabCount should be one" + ); + undoCloseTab(); + + await TestUtils.waitForCondition( + () => gBrowser.tabs.length == count, + "wait for the tabs to reopen" + ); + count++; + } + + gBrowser.removeAllTabsBut(initialTab); +}); + +add_task(async function withCloseTabsToTheRight() { + let initialTab = gBrowser.selectedTab; + let tab1 = await addTab("https://example.com/1"); + await addTab("https://example.com/2"); + await addTab("https://example.com/3"); + await addTab("https://example.com/4"); + + gBrowser.removeTabsToTheEndFrom(tab1); + await TestUtils.waitForCondition( + () => gBrowser.tabs.length == 2, + "wait for the multiselected tabs to close" + ); + is( + SessionStore.getLastClosedTabCount(window), + 3, + "SessionStore should know how many tabs were just closed" + ); + + undoCloseTab(); + await TestUtils.waitForCondition( + () => gBrowser.tabs.length == 5, + "wait for the tabs to reopen" + ); + info("waiting for the browsers to finish loading"); + // Check that the tabs are restored in the correct order + for (let tabId of [2, 3, 4]) { + let browser = gBrowser.tabs[tabId].linkedBrowser; + ContentTask.spawn(browser, tabId, async aTabId => { + await ContentTaskUtils.waitForCondition(() => { + return ( + content?.document?.readyState == "complete" && + content?.document?.location.href == "https://example.com/" + aTabId + ); + }, "waiting for tab " + aTabId + " to load"); + }); + } + + gBrowser.removeAllTabsBut(initialTab); +}); diff --git a/browser/base/content/test/tabs/browser_undo_close_tabs_at_start.js b/browser/base/content/test/tabs/browser_undo_close_tabs_at_start.js new file mode 100644 index 0000000000..9ad79ea1c8 --- /dev/null +++ b/browser/base/content/test/tabs/browser_undo_close_tabs_at_start.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs"; + +add_task(async function setPref() { + await SpecialPowers.pushPrefEnv({ + set: [[PREF_WARN_ON_CLOSE, false]], + }); +}); + +add_task(async function replaceEmptyTabs() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + const tabbrowser = win.gBrowser; + ok( + tabbrowser.tabs.length == 1 && tabbrowser.tabs[0].isEmpty, + "One blank tab should be opened." + ); + + let blankTab = tabbrowser.tabs[0]; + await BrowserTestUtils.openNewForegroundTab( + tabbrowser, + "https://example.com/1" + ); + await BrowserTestUtils.openNewForegroundTab( + tabbrowser, + "https://example.com/2" + ); + await BrowserTestUtils.openNewForegroundTab( + tabbrowser, + "https://example.com/3" + ); + + is(tabbrowser.tabs.length, 4, "There should be 4 tabs opened."); + + tabbrowser.removeAllTabsBut(blankTab); + + await TestUtils.waitForCondition( + () => + SessionStore.getLastClosedTabCount(win) == 3 && + tabbrowser.tabs.length == 1, + "wait for the tabs to close in SessionStore" + ); + is( + SessionStore.getLastClosedTabCount(win), + 3, + "SessionStore should know how many tabs were just closed" + ); + + is(tabbrowser.selectedTab, blankTab, "The blank tab should be selected."); + + win.undoCloseTab(); + + await TestUtils.waitForCondition( + () => tabbrowser.tabs.length == 3, + "wait for the tabs to reopen" + ); + + is( + SessionStore.getLastClosedTabCount(win), + SessionStore.getClosedTabCountForWindow(win) ? 1 : 0, + "LastClosedTabCount should be reset" + ); + + ok( + !tabbrowser.tabs.includes(blankTab), + "The blank tab should have been replaced." + ); + + // We can't (at the time of writing) check tab order. + + // Cleanup + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js b/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js new file mode 100644 index 0000000000..b5ae94ce84 --- /dev/null +++ b/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +const DUMMY_FILE = "dummy_page.html"; +const DATA_URI = "data:text/html,Hi"; +const DATA_URI_SOURCE = "view-source:" + DATA_URI; + +// Test for bug 1345807. +add_task(async function () { + // Open file:// page. + let dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append(DUMMY_FILE); + const uriString = Services.io.newFileURI(dir).spec; + + await BrowserTestUtils.withNewTab(uriString, async function (fileBrowser) { + let filePid = await SpecialPowers.spawn(fileBrowser, [], () => { + return Services.appinfo.processID; + }); + + // Navigate to data URI. + let promiseLoad = BrowserTestUtils.browserLoaded( + fileBrowser, + false, + DATA_URI + ); + BrowserTestUtils.startLoadingURIString(fileBrowser, DATA_URI); + let href = await promiseLoad; + is(href, DATA_URI, "Check data URI loaded."); + let dataPid = await SpecialPowers.spawn(fileBrowser, [], () => { + return Services.appinfo.processID; + }); + is(dataPid, filePid, "Check that data URI loaded in file content process."); + + // Make sure we can view-source on the data URI page. + let promiseTab = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI_SOURCE); + BrowserViewSource(fileBrowser); + let viewSourceTab = await promiseTab; + registerCleanupFunction(async function () { + BrowserTestUtils.removeTab(viewSourceTab); + }); + await SpecialPowers.spawn( + viewSourceTab.linkedBrowser, + [DATA_URI_SOURCE], + uri => { + is( + content.document.documentURI, + uri, + "Check that a view-source page was loaded." + ); + } + ); + }); +}); diff --git a/browser/base/content/test/tabs/browser_visibleTabs_bookmarkAllTabs.js b/browser/base/content/test/tabs/browser_visibleTabs_bookmarkAllTabs.js new file mode 100644 index 0000000000..d3b439ce8f --- /dev/null +++ b/browser/base/content/test/tabs/browser_visibleTabs_bookmarkAllTabs.js @@ -0,0 +1,64 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + // There should be one tab when we start the test + let [origTab] = gBrowser.visibleTabs; + is(gBrowser.visibleTabs.length, 1, "1 tab should be open"); + + // Add a tab + let testTab1 = BrowserTestUtils.addTab(gBrowser); + is(gBrowser.visibleTabs.length, 2, "2 tabs should be open"); + + let testTab2 = BrowserTestUtils.addTab(gBrowser, "about:mozilla"); + is(gBrowser.visibleTabs.length, 3, "3 tabs should be open"); + // Wait for tab load, the code checks for currentURI. + testTab2.linkedBrowser.addEventListener( + "load", + function () { + // Hide the original tab + gBrowser.selectedTab = testTab2; + gBrowser.showOnlyTheseTabs([testTab2]); + is(gBrowser.visibleTabs.length, 1, "1 tab should be visible"); + + // Add a tab that will get pinned + let pinned = BrowserTestUtils.addTab(gBrowser); + is(gBrowser.visibleTabs.length, 2, "2 tabs should be visible now"); + gBrowser.pinTab(pinned); + is( + BookmarkTabHidden(), + false, + "Bookmark Tab should be visible on a normal tab" + ); + gBrowser.selectedTab = pinned; + is( + BookmarkTabHidden(), + false, + "Bookmark Tab should be visible on a pinned tab" + ); + + // Show all tabs + let allTabs = Array.from(gBrowser.tabs); + gBrowser.showOnlyTheseTabs(allTabs); + + // reset the environment + gBrowser.removeTab(testTab2); + gBrowser.removeTab(testTab1); + gBrowser.removeTab(pinned); + is(gBrowser.visibleTabs.length, 1, "only orig is left and visible"); + is(gBrowser.tabs.length, 1, "sanity check that it matches"); + is(gBrowser.selectedTab, origTab, "got the orig tab"); + is(origTab.hidden, false, "and it's not hidden -- visible!"); + finish(); + }, + { capture: true, once: true } + ); +} + +function BookmarkTabHidden() { + updateTabContextMenu(); + return document.getElementById("context_bookmarkTab").hidden; +} diff --git a/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js b/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js new file mode 100644 index 0000000000..202c43ce47 --- /dev/null +++ b/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js @@ -0,0 +1,115 @@ +/* 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/. */ + +const remoteClientsFixture = [ + { id: 1, name: "Foo" }, + { id: 2, name: "Bar" }, +]; + +add_task(async function test() { + // There should be one tab when we start the test + let [origTab] = gBrowser.visibleTabs; + is(gBrowser.visibleTabs.length, 1, "there is one visible tab"); + let testTab = BrowserTestUtils.addTab(gBrowser); + is(gBrowser.visibleTabs.length, 2, "there are now two visible tabs"); + + // Check the context menu with two tabs + updateTabContextMenu(origTab); + is( + document.getElementById("context_closeTab").disabled, + false, + "Close Tab is enabled" + ); + + // Hide the original tab. + gBrowser.selectedTab = testTab; + gBrowser.showOnlyTheseTabs([testTab]); + is(gBrowser.visibleTabs.length, 1, "now there is only one visible tab"); + + // Check the context menu with one tab. + updateTabContextMenu(testTab); + is( + document.getElementById("context_closeTab").disabled, + false, + "Close Tab is enabled when more than one tab exists" + ); + + // Add a tab that will get pinned + // So now there's one pinned tab, one visible unpinned tab, and one hidden tab + let pinned = BrowserTestUtils.addTab(gBrowser); + gBrowser.pinTab(pinned); + is(gBrowser.visibleTabs.length, 2, "now there are two visible tabs"); + + // Check the context menu on the pinned tab + updateTabContextMenu(pinned); + ok( + !document.getElementById("context_closeTabOptions").disabled, + "Close Multiple Tabs is enabled on pinned tab" + ); + ok( + !document.getElementById("context_closeOtherTabs").disabled, + "Close Other Tabs is enabled on pinned tab" + ); + ok( + document.getElementById("context_closeTabsToTheStart").disabled, + "Close Tabs To The Start is disabled on pinned tab" + ); + ok( + !document.getElementById("context_closeTabsToTheEnd").disabled, + "Close Tabs To The End is enabled on pinned tab" + ); + + // Check the context menu on the unpinned visible tab + updateTabContextMenu(testTab); + ok( + document.getElementById("context_closeTabOptions").disabled, + "Close Multiple Tabs is disabled on single unpinned tab" + ); + ok( + document.getElementById("context_closeOtherTabs").disabled, + "Close Other Tabs is disabled on single unpinned tab" + ); + ok( + document.getElementById("context_closeTabsToTheStart").disabled, + "Close Tabs To The Start is disabled on single unpinned tab" + ); + ok( + document.getElementById("context_closeTabsToTheEnd").disabled, + "Close Tabs To The End is disabled on single unpinned tab" + ); + + // Show all tabs + let allTabs = Array.from(gBrowser.tabs); + gBrowser.showOnlyTheseTabs(allTabs); + + // Check the context menu now + updateTabContextMenu(testTab); + ok( + !document.getElementById("context_closeTabOptions").disabled, + "Close Multiple Tabs is enabled on unpinned tab when there's another unpinned tab" + ); + ok( + !document.getElementById("context_closeOtherTabs").disabled, + "Close Other Tabs is enabled on unpinned tab when there's another unpinned tab" + ); + ok( + !document.getElementById("context_closeTabsToTheStart").disabled, + "Close Tabs To The Start is enabled on last unpinned tab when there's another unpinned tab" + ); + ok( + document.getElementById("context_closeTabsToTheEnd").disabled, + "Close Tabs To The End is disabled on last unpinned tab" + ); + + // Check the context menu of the original tab + // Close Tabs To The End should now be enabled + updateTabContextMenu(origTab); + ok( + !document.getElementById("context_closeTabsToTheEnd").disabled, + "Close Tabs To The End is enabled on unpinned tab when followed by another" + ); + + gBrowser.removeTab(testTab); + gBrowser.removeTab(pinned); +}); diff --git a/browser/base/content/test/tabs/browser_window_open_modifiers.js b/browser/base/content/test/tabs/browser_window_open_modifiers.js new file mode 100644 index 0000000000..b4376d6824 --- /dev/null +++ b/browser/base/content/test/tabs/browser_window_open_modifiers.js @@ -0,0 +1,175 @@ +/* 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/. */ + +"use strict"; + +// Opening many windows take long time on some configuration. +requestLongerTimeout(4); + +add_task(async function () { + await BrowserTestUtils.withNewTab( + "https://example.com/browser/browser/base/content/test/tabs/file_window_open.html", + async function (browser) { + const metaKey = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey"; + const normalEvent = {}; + const shiftEvent = { shiftKey: true }; + const metaEvent = { [metaKey]: true }; + const metaShiftEvent = { [metaKey]: true, shiftKey: true }; + + const tests = [ + // type, id, options, result + ["mouse", "#instant", normalEvent, "tab"], + ["mouse", "#instant", shiftEvent, "window"], + ["mouse", "#instant", metaEvent, "tab-bg"], + ["mouse", "#instant", metaShiftEvent, "tab"], + + ["mouse", "#instant-popup", normalEvent, "popup"], + ["mouse", "#instant-popup", shiftEvent, "window"], + ["mouse", "#instant-popup", metaEvent, "tab-bg"], + ["mouse", "#instant-popup", metaShiftEvent, "tab"], + + ["mouse", "#delayed", normalEvent, "tab"], + ["mouse", "#delayed", shiftEvent, "window"], + ["mouse", "#delayed", metaEvent, "tab-bg"], + ["mouse", "#delayed", metaShiftEvent, "tab"], + + ["mouse", "#delayed-popup", normalEvent, "popup"], + ["mouse", "#delayed-popup", shiftEvent, "window"], + ["mouse", "#delayed-popup", metaEvent, "tab-bg"], + ["mouse", "#delayed-popup", metaShiftEvent, "tab"], + + // NOTE: meta+keyboard doesn't activate. + + ["VK_SPACE", "#instant", normalEvent, "tab"], + ["VK_SPACE", "#instant", shiftEvent, "window"], + + ["VK_SPACE", "#instant-popup", normalEvent, "popup"], + ["VK_SPACE", "#instant-popup", shiftEvent, "window"], + + ["VK_SPACE", "#delayed", normalEvent, "tab"], + ["VK_SPACE", "#delayed", shiftEvent, "window"], + + ["VK_SPACE", "#delayed-popup", normalEvent, "popup"], + ["VK_SPACE", "#delayed-popup", shiftEvent, "window"], + + ["KEY_Enter", "#link-instant", normalEvent, "tab"], + ["KEY_Enter", "#link-instant", shiftEvent, "window"], + + ["KEY_Enter", "#link-instant-popup", normalEvent, "popup"], + ["KEY_Enter", "#link-instant-popup", shiftEvent, "window"], + + ["KEY_Enter", "#link-delayed", normalEvent, "tab"], + ["KEY_Enter", "#link-delayed", shiftEvent, "window"], + + ["KEY_Enter", "#link-delayed-popup", normalEvent, "popup"], + ["KEY_Enter", "#link-delayed-popup", shiftEvent, "window"], + + // Trigger user-defined shortcut key, where modifiers shouldn't affect. + + ["x", "#instant", normalEvent, "tab"], + ["x", "#instant", shiftEvent, "tab"], + ["x", "#instant", metaEvent, "tab"], + ["x", "#instant", metaShiftEvent, "tab"], + + ["y", "#instant", normalEvent, "popup"], + ["y", "#instant", shiftEvent, "popup"], + ["y", "#instant", metaEvent, "popup"], + ["y", "#instant", metaShiftEvent, "popup"], + ]; + for (const [type, id, event, result] of tests) { + const eventStr = JSON.stringify(event); + + let openPromise; + if (result == "tab" || result == "tab-bg") { + openPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:blank", + true + ); + } else { + openPromise = BrowserTestUtils.waitForNewWindow({ + url: "about:blank", + }); + } + + if (type == "mouse") { + BrowserTestUtils.synthesizeMouseAtCenter(id, { ...event }, browser); + } else { + // Make sure the keyboard activates a simple button on the page. + await ContentTask.spawn(browser, id, elementId => { + content.document.querySelector("#focus-result").value = ""; + content.document.querySelector("#focus-check").focus(); + }); + BrowserTestUtils.synthesizeKey("VK_SPACE", {}, browser); + await ContentTask.spawn(browser, {}, async () => { + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelector("#focus-result").value === "ok" + ); + }); + + // Once confirmed the keyboard event works, send the actual event + // that calls window.open. + await ContentTask.spawn(browser, id, elementId => { + content.document.querySelector(elementId).focus(); + }); + BrowserTestUtils.synthesizeKey(type, { ...event }, browser); + } + + const openedThing = await openPromise; + + if (result == "tab" || result == "tab-bg") { + const newTab = openedThing; + + if (result == "tab") { + Assert.equal( + gBrowser.selectedTab, + newTab, + `${id} with ${type} and ${eventStr} opened a foreground tab` + ); + } else { + Assert.notEqual( + gBrowser.selectedTab, + newTab, + `${id} with ${type} and ${eventStr} opened a background tab` + ); + } + + gBrowser.removeTab(newTab); + } else { + const newWindow = openedThing; + + const tabs = newWindow.document.getElementById("TabsToolbar"); + if (result == "window") { + ok( + !tabs.collapsed, + `${id} with ${type} and ${eventStr} opened a regular window` + ); + } else { + ok( + tabs.collapsed, + `${id} with ${type} and ${eventStr} opened a popup window` + ); + } + + const closedPopupPromise = BrowserTestUtils.windowClosed(newWindow); + newWindow.close(); + await closedPopupPromise; + + // Make sure the focus comes back to this window before proceeding + // to the next test. + if (Services.focus.focusedWindow != window) { + const focusBack = BrowserTestUtils.waitForEvent( + window, + "focus", + true + ); + window.focus(); + await focusBack; + } + } + } + } + ); +}); diff --git a/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js b/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js new file mode 100644 index 0000000000..a06b982615 --- /dev/null +++ b/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js @@ -0,0 +1,255 @@ +/* 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/. */ + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const HOME_URL = `${TEST_ROOT}link_in_tab_title_and_url_prefilled.html`; +const HOME_TITLE = HOME_URL.substring("https://".length); +const WAIT_A_BIT_URL = `${TEST_ROOT}wait-a-bit.sjs`; +const WAIT_A_BIT_LOADING_TITLE = WAIT_A_BIT_URL.substring("https://".length); +const WAIT_A_BIT_PAGE_TITLE = "wait a bit"; +const REQUEST_TIMEOUT_URL = `${TEST_ROOT}request-timeout.sjs`; +const REQUEST_TIMEOUT_LOADING_TITLE = REQUEST_TIMEOUT_URL.substring( + "https://".length +); +const BLANK_URL = "about:blank"; +const BLANK_TITLE = "New Tab"; + +const OPEN_BY = { + CLICK: "click", + CONTEXT_MENU: "context_menu", +}; + +const OPEN_AS = { + FOREGROUND: "foreground", + BACKGROUND: "background", +}; + +async function doTestInSameWindow({ + link, + openBy, + openAs, + loadingState, + actionWhileLoading, + finalState, +}) { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + // NOTE: The behavior after the click <a href="about:blank">link</a> + // (no target) is different when the URL is opened directly with + // BrowserTestUtils.withNewTab() and when it is loaded later. + // Specifically, if we load `about:blank`, expect to see `New Tab` as the + // title of the tab, but the former will continue to display the URL that + // was previously displayed. Therefore, use the latter way. + BrowserTestUtils.startLoadingURIString(browser, HOME_URL); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + HOME_URL + ); + + info(`Open link for ${link} by ${openBy} as ${openAs}`); + const onNewTabCreated = waitForNewTabWithLoadRequest(); + const href = await openLink(browser, link, openBy, openAs); + + info("Wait until starting to load in the target tab"); + const target = await onNewTabCreated; + Assert.equal(target.selected, openAs === OPEN_AS.FOREGROUND); + Assert.equal(gURLBar.value, loadingState.urlbar); + Assert.equal(target.textLabel.textContent, loadingState.tab); + + await actionWhileLoading( + BrowserTestUtils.browserLoaded(target.linkedBrowser, false, href) + ); + + info("Check the final result"); + Assert.equal(gURLBar.value, finalState.urlbar); + Assert.equal(target.textLabel.textContent, finalState.tab); + const sessionHistory = await new Promise(r => + SessionStore.getSessionHistory(target, r) + ); + Assert.deepEqual( + sessionHistory.entries.map(e => e.url), + finalState.history + ); + + BrowserTestUtils.removeTab(target); + }); +} + +async function doTestWithNewWindow({ link, expectedSetURICalled }) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", 2]], + }); + + await BrowserTestUtils.withNewTab(HOME_URL, async browser => { + const onNewWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded(); + + info(`Open link for ${link}`); + const href = await openLink( + browser, + link, + OPEN_BY.CLICK, + OPEN_AS.FOREGROUND + ); + + info("Wait until opening a new window"); + const win = await onNewWindowOpened; + + info("Check whether gURLBar.setURI is called while loading the page"); + const sandbox = sinon.createSandbox(); + registerCleanupFunction(() => { + sandbox.restore(); + }); + let isSetURIWhileLoading = false; + sandbox.stub(win.gURLBar, "setURI").callsFake(uri => { + if ( + !uri && + win.gBrowser.selectedBrowser.browsingContext.nonWebControlledBlankURI + ) { + isSetURIWhileLoading = true; + } + }); + await BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser, + false, + href + ); + sandbox.restore(); + + Assert.equal(isSetURIWhileLoading, expectedSetURICalled); + Assert.equal( + !!win.gBrowser.selectedBrowser.browsingContext.nonWebControlledBlankURI, + expectedSetURICalled + ); + + await BrowserTestUtils.closeWindow(win); + }); + + await SpecialPowers.popPrefEnv(); +} + +async function doSessionRestoreTest({ + link, + openBy, + openAs, + expectedSessionHistory, + expectedSessionRestored, +}) { + await BrowserTestUtils.withNewTab("about:blank", async browser => { + BrowserTestUtils.startLoadingURIString(browser, HOME_URL); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + HOME_URL + ); + + info(`Open link for ${link} by ${openBy} as ${openAs}`); + const onNewTabCreated = waitForNewTabWithLoadRequest(); + const href = await openLink(browser, link, openBy, openAs); + const target = await onNewTabCreated; + await BrowserTestUtils.waitForCondition( + () => + target.linkedBrowser.browsingContext + .mostRecentLoadingSessionHistoryEntry + ); + + info("Close the session"); + const sessionPromise = BrowserTestUtils.waitForSessionStoreUpdate(target); + BrowserTestUtils.removeTab(target); + await sessionPromise; + + info("Restore the session"); + const restoredTab = SessionStore.undoCloseTab(window, 0); + await BrowserTestUtils.browserLoaded(restoredTab.linkedBrowser); + + info("Check the loaded URL of restored tab"); + Assert.equal( + restoredTab.linkedBrowser.currentURI.spec === href, + expectedSessionRestored + ); + + if (expectedSessionRestored) { + info("Check the session history of restored tab"); + const sessionHistory = await new Promise(r => + SessionStore.getSessionHistory(restoredTab, r) + ); + Assert.deepEqual( + sessionHistory.entries.map(e => e.url), + expectedSessionHistory + ); + } + + BrowserTestUtils.removeTab(restoredTab); + }); +} + +async function openLink(browser, link, openBy, openAs) { + let href; + const openAsBackground = openAs === OPEN_AS.BACKGROUND; + if (openBy === OPEN_BY.CLICK) { + href = await synthesizeMouse(browser, link, { + ctrlKey: openAsBackground, + metaKey: openAsBackground, + }); + } else if (openBy === OPEN_BY.CONTEXT_MENU) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.loadInBackground", openAsBackground]], + }); + + const contextMenu = document.getElementById("contentAreaContextMenu"); + const onPopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + + href = await synthesizeMouse(browser, link, { + type: "contextmenu", + button: 2, + }); + + await onPopupShown; + + const openLinkMenuItem = contextMenu.querySelector( + "#context-openlinkintab" + ); + contextMenu.activateItem(openLinkMenuItem); + + await SpecialPowers.popPrefEnv(); + } else { + throw new Error("Invalid openBy"); + } + + return href; +} + +async function synthesizeMouse(browser, link, event) { + return SpecialPowers.spawn( + browser, + [link, event], + (linkInContent, eventInContent) => { + const target = content.document.getElementById(linkInContent); + EventUtils.synthesizeMouseAtCenter(target, eventInContent, content); + return target.href; + } + ); +} + +async function waitForNewTabWithLoadRequest() { + return new Promise(resolve => + gBrowser.addTabsProgressListener({ + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + gBrowser.removeTabsProgressListener(this); + resolve(gBrowser.getTabForBrowser(aBrowser)); + } + }, + }) + ); +} diff --git a/browser/base/content/test/tabs/dummy_page.html b/browser/base/content/test/tabs/dummy_page.html new file mode 100644 index 0000000000..1a87e28408 --- /dev/null +++ b/browser/base/content/test/tabs/dummy_page.html @@ -0,0 +1,9 @@ +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Dummy test page</p> +</body> +</html> diff --git a/browser/base/content/test/tabs/file_about_child.html b/browser/base/content/test/tabs/file_about_child.html new file mode 100644 index 0000000000..41fb745451 --- /dev/null +++ b/browser/base/content/test/tabs/file_about_child.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1329032</title> +</head> +<body> + Just an about page that only loads in the child! +</body> +</html> diff --git a/browser/base/content/test/tabs/file_about_parent.html b/browser/base/content/test/tabs/file_about_parent.html new file mode 100644 index 0000000000..0d910f860b --- /dev/null +++ b/browser/base/content/test/tabs/file_about_parent.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1329032</title> +</head> +<body> + <a href="about:test-about-principal-child" id="aboutchildprincipal">about:test-about-principal-child</a> +</body> +</html> diff --git a/browser/base/content/test/tabs/file_about_srcdoc.html b/browser/base/content/test/tabs/file_about_srcdoc.html new file mode 100644 index 0000000000..0a8d0d74bf --- /dev/null +++ b/browser/base/content/test/tabs/file_about_srcdoc.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + <iframe srcdoc="hello world!"></iframe> + </body> +</html> diff --git a/browser/base/content/test/tabs/file_anchor_elements.html b/browser/base/content/test/tabs/file_anchor_elements.html new file mode 100644 index 0000000000..598a3bd825 --- /dev/null +++ b/browser/base/content/test/tabs/file_anchor_elements.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<head> + <meta charset="utf-8"> + <title>Testing whether paste event is fired at middle click on anchor elements</title> +</head> +<body> + <p>Here is an <a id="a_with_href" href="https://example.com/#a_with_href">anchor element</a></p> + <p contenteditable>Here is an <a id="editable_a_with_href" href="https://example.com/#editable_a_with_href">editable anchor element</a></p> + <p contenteditable>Here is <span contenteditable="false"><a id="non-editable_a_with_href" href="https://example.com/#non-editable_a_with_href">non-editable anchor element</a></span> + <p>Here is an <a id="a_with_name" name="a_with_name">anchor element without href</a></p> +</body> +</html> diff --git a/browser/base/content/test/tabs/file_mediaPlayback.html b/browser/base/content/test/tabs/file_mediaPlayback.html new file mode 100644 index 0000000000..a6979287e2 --- /dev/null +++ b/browser/base/content/test/tabs/file_mediaPlayback.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<audio src="audio.ogg" controls loop> diff --git a/browser/base/content/test/tabs/file_new_tab_page.html b/browser/base/content/test/tabs/file_new_tab_page.html new file mode 100644 index 0000000000..4ef22a8c7c --- /dev/null +++ b/browser/base/content/test/tabs/file_new_tab_page.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + <a href="http://example.com/#linkclick" id="link_to_example_com">go to example.com</a> + </body> +</html> diff --git a/browser/base/content/test/tabs/file_observe_height_changes.html b/browser/base/content/test/tabs/file_observe_height_changes.html new file mode 100644 index 0000000000..18b0fdf251 --- /dev/null +++ b/browser/base/content/test/tabs/file_observe_height_changes.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<style> + /* This lets us measure the height of the viewport + by measuring documentElement.offsetHeight. */ + html { height: 100vh } +</style> +<script> + let mostRecentHeight = 0; + let heightChanges = 0; + function checkDocumentHeight() { + let curHeight = document.documentElement.offsetHeight; + if (curHeight != mostRecentHeight) { + mostRecentHeight = curHeight; + document.body.innerText = heightChanges++; + } + requestAnimationFrame(checkDocumentHeight); + } +</script> +<body onload="checkDocumentHeight();"> +</body> diff --git a/browser/base/content/test/tabs/file_rel_opener_noopener.html b/browser/base/content/test/tabs/file_rel_opener_noopener.html new file mode 100644 index 0000000000..78e872005c --- /dev/null +++ b/browser/base/content/test/tabs/file_rel_opener_noopener.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + <a target="_blank" rel="noopener" href="https://example.com/browser/browser/base/content/test/tabs/blank.html" id="link_noopener_examplecom">Go to example.com</a> + <a target="_blank" rel="opener" href="https://example.com/browser/browser/base/content/test/tabs/blank.html" id="link_opener_examplecom">Go to example.com</a> + <a target="_blank" rel="noopener" href="https://example.org/browser/browser/base/content/test/tabs/blank.html" id="link_noopener_exampleorg">Go to example.org</a> + <a target="_blank" rel="opener" href="https://example.org/browser/browser/base/content/test/tabs/blank.html" id="link_opener_exampleorg">Go to example.org</a> + </body> +</html> diff --git a/browser/base/content/test/tabs/file_window_open.html b/browser/base/content/test/tabs/file_window_open.html new file mode 100644 index 0000000000..831bafe6bd --- /dev/null +++ b/browser/base/content/test/tabs/file_window_open.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<head> + <meta charset="utf-8"> + <title>window.open test</title> +<style> +div { + padding: 20px; +} +</style> +</head> +<body> + <div> + <input id="instant" type="button" value="instant no features" + onclick="window.open('about:blank', '_blank');"> + </div> + <div> + <input id="instant-popup" type="button" value="instant popup" + onclick="window.open('about:blank', '_blank', 'popup=true');"> + </div> + <div> + <input id="delayed" type="button" value="delayed no features" + onclick="setTimeout(() => window.open('about:blank', '_blank'), 100);"> + </div> + <div> + <input id="delayed-popup" type="button" value="delayed popup" + onclick="setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 100);"> + <div> + <div> + <a id="link-instant" href="" + onclick="window.open('about:blank', '_blank'); event.preventDefault()"> + instant no features + </a> + </div> + <div> + <a id="link-instant-popup" href="" + onclick="window.open('about:blank', '_blank', 'popup=true'); event.preventDefault()"> + instant popup + </a> + </div> + <div> + <a id="link-delayed" href="" + onclick="setTimeout(() => window.open('about:blank', '_blank'), 100); event.preventDefault()"> + delayed no features + </a> + </div> + <div> + <a id="link-delayed-popup" href="" + onclick="setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 100); event.preventDefault()"> + delayed popup + </a> + <div> + <div> + <input id="focus-check" type="button" value="check focus" + onclick="document.getElementById('focus-result').value = 'ok';"> + </div> + <div> + <input id="focus-result" type="text" value=""> + +<script type="text/javascript"> +document.addEventListener("keydown", event => { + if (event.key == "x") { + window.open('about:blank', '_blank'); + } + if (event.key == "y") { + window.open('about:blank', '_blank', 'popup=true'); + } +}); +</script> +</body> +</html> diff --git a/browser/base/content/test/tabs/head.js b/browser/base/content/test/tabs/head.js new file mode 100644 index 0000000000..abd6c060f7 --- /dev/null +++ b/browser/base/content/test/tabs/head.js @@ -0,0 +1,564 @@ +function updateTabContextMenu(tab) { + let menu = document.getElementById("tabContextMenu"); + if (!tab) { + tab = gBrowser.selectedTab; + } + var evt = new Event(""); + tab.dispatchEvent(evt); + menu.openPopup(tab, "end_after", 0, 0, true, false, evt); + is( + window.TabContextMenu.contextTab, + tab, + "TabContextMenu context is the expected tab" + ); + menu.hidePopup(); +} + +function triggerClickOn(target, options) { + let promise = BrowserTestUtils.waitForEvent(target, "click"); + if (AppConstants.platform == "macosx") { + options = { + metaKey: options.ctrlKey, + shiftKey: options.shiftKey, + }; + } + EventUtils.synthesizeMouseAtCenter(target, options); + return promise; +} + +function triggerMiddleClickOn(target) { + let promise = BrowserTestUtils.waitForEvent(target, "click"); + EventUtils.synthesizeMouseAtCenter(target, { button: 1 }); + return promise; +} + +async function addTab(url = "http://mochi.test:8888/", params) { + return addTabTo(gBrowser, url, params); +} + +async function addTabTo( + targetBrowser, + url = "http://mochi.test:8888/", + params = {} +) { + params.skipAnimation = true; + const tab = BrowserTestUtils.addTab(targetBrowser, url, params); + const browser = targetBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + return tab; +} + +async function addMediaTab() { + const PAGE = + "https://example.com/browser/browser/base/content/test/tabs/file_mediaPlayback.html"; + const tab = BrowserTestUtils.addTab(gBrowser, PAGE, { skipAnimation: true }); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + return tab; +} + +function muted(tab) { + return tab.linkedBrowser.audioMuted; +} + +function activeMediaBlocked(tab) { + return tab.activeMediaBlocked; +} + +async function toggleMuteAudio(tab, expectMuted) { + let mutedPromise = get_wait_for_mute_promise(tab, expectMuted); + tab.toggleMuteAudio(); + await mutedPromise; +} + +async function pressIcon(icon) { + let tooltip = document.getElementById("tabbrowser-tab-tooltip"); + await hover_icon(icon, tooltip); + EventUtils.synthesizeMouseAtCenter(icon, { button: 0 }); + leave_icon(icon); +} + +async function wait_for_tab_playing_event(tab, expectPlaying) { + if (tab.soundPlaying == expectPlaying) { + ok(true, "The tab should " + (expectPlaying ? "" : "not ") + "be playing"); + return true; + } + return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, event => { + if (event.detail.changed.includes("soundplaying")) { + is( + tab.hasAttribute("soundplaying"), + expectPlaying, + "The tab should " + (expectPlaying ? "" : "not ") + "be playing" + ); + is( + tab.soundPlaying, + expectPlaying, + "The tab should " + (expectPlaying ? "" : "not ") + "be playing" + ); + return true; + } + return false; + }); +} + +async function wait_for_tab_media_blocked_event(tab, expectMediaBlocked) { + if (tab.activeMediaBlocked == expectMediaBlocked) { + ok( + true, + "The tab should " + + (expectMediaBlocked ? "" : "not ") + + "be activemedia-blocked" + ); + return true; + } + return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, event => { + if (event.detail.changed.includes("activemedia-blocked")) { + is( + tab.hasAttribute("activemedia-blocked"), + expectMediaBlocked, + "The tab should " + + (expectMediaBlocked ? "" : "not ") + + "be activemedia-blocked" + ); + is( + tab.activeMediaBlocked, + expectMediaBlocked, + "The tab should " + + (expectMediaBlocked ? "" : "not ") + + "be activemedia-blocked" + ); + return true; + } + return false; + }); +} + +async function is_audio_playing(tab) { + let browser = tab.linkedBrowser; + let isPlaying = await SpecialPowers.spawn(browser, [], async function () { + let audio = content.document.querySelector("audio"); + return !audio.paused; + }); + return isPlaying; +} + +async function play(tab, expectPlaying = true) { + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async function () { + let audio = content.document.querySelector("audio"); + audio.play(); + }); + + // If the tab has already been muted, it means the tab won't get soundplaying, + // so we don't need to check this attribute. + if (browser.audioMuted) { + return; + } + + if (expectPlaying) { + await wait_for_tab_playing_event(tab, true); + } else { + await wait_for_tab_media_blocked_event(tab, true); + } +} + +function disable_non_test_mouse(disable) { + let utils = window.windowUtils; + utils.disableNonTestMouseEvents(disable); +} + +function hover_icon(icon, tooltip) { + disable_non_test_mouse(true); + + let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown"); + EventUtils.synthesizeMouse(icon, 1, 1, { type: "mouseover" }); + EventUtils.synthesizeMouse(icon, 2, 2, { type: "mousemove" }); + EventUtils.synthesizeMouse(icon, 3, 3, { type: "mousemove" }); + EventUtils.synthesizeMouse(icon, 4, 4, { type: "mousemove" }); + return popupShownPromise; +} + +function leave_icon(icon) { + EventUtils.synthesizeMouse(icon, 0, 0, { type: "mouseout" }); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mousemove", + }); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mousemove", + }); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mousemove", + }); + + disable_non_test_mouse(false); +} + +// The set of tabs which have ever had their mute state changed. +// Used to determine whether the tab should have a muteReason value. +let everMutedTabs = new WeakSet(); + +function get_wait_for_mute_promise(tab, expectMuted) { + return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, event => { + if ( + event.detail.changed.includes("muted") || + event.detail.changed.includes("activemedia-blocked") + ) { + is( + tab.hasAttribute("muted"), + expectMuted, + "The tab should " + (expectMuted ? "" : "not ") + "be muted" + ); + is( + tab.muted, + expectMuted, + "The tab muted property " + (expectMuted ? "" : "not ") + "be true" + ); + + if (expectMuted || everMutedTabs.has(tab)) { + everMutedTabs.add(tab); + is(tab.muteReason, null, "The tab should have a null muteReason value"); + } else { + is( + tab.muteReason, + undefined, + "The tab should have an undefined muteReason value" + ); + } + return true; + } + return false; + }); +} + +async function test_mute_tab(tab, icon, expectMuted) { + let mutedPromise = get_wait_for_mute_promise(tab, expectMuted); + + let activeTab = gBrowser.selectedTab; + + let tooltip = document.getElementById("tabbrowser-tab-tooltip"); + + await hover_icon(icon, tooltip); + EventUtils.synthesizeMouseAtCenter(icon, { button: 0 }); + leave_icon(icon); + + is( + gBrowser.selectedTab, + activeTab, + "Clicking on mute should not change the currently selected tab" + ); + + // If the audio is playing, we should check whether clicking on icon affects + // the media element's playing state. + let isAudioPlaying = await is_audio_playing(tab); + if (isAudioPlaying) { + await wait_for_tab_playing_event(tab, !expectMuted); + } + + return mutedPromise; +} + +async function dragAndDrop( + tab1, + tab2, + copy, + destWindow = window, + afterTab = true +) { + let rect = tab2.getBoundingClientRect(); + let event = { + ctrlKey: copy, + altKey: copy, + clientX: rect.left + rect.width / 2 + 10 * (afterTab ? 1 : -1), + clientY: rect.top + rect.height / 2, + }; + + if (destWindow != window) { + // Make sure that both tab1 and tab2 are visible + window.focus(); + window.moveTo(rect.left, rect.top + rect.height * 3); + } + + let originalTPos = tab1._tPos; + EventUtils.synthesizeDrop( + tab1, + tab2, + null, + copy ? "copy" : "move", + window, + destWindow, + event + ); + // Ensure dnd suppression is cleared. + EventUtils.synthesizeMouseAtCenter(tab2, { type: "mouseup" }, destWindow); + if (!copy && destWindow == window) { + await BrowserTestUtils.waitForCondition( + () => tab1._tPos != originalTPos, + "Waiting for tab position to be updated" + ); + } else if (destWindow != window) { + await BrowserTestUtils.waitForCondition( + () => tab1.closing, + "Waiting for tab closing" + ); + } +} + +function getUrl(tab) { + return tab.linkedBrowser.currentURI.spec; +} + +/** + * Takes a xul:browser and makes sure that the remoteTypes for the browser in + * both the parent and the child processes are the same. + * + * @param {xul:browser} browser + * A xul:browser. + * @param {string} expectedRemoteType + * The expected remoteType value for the browser in both the parent + * and child processes. + * @param {optional string} message + * If provided, shows this string as the message when remoteType values + * do not match. If not present, it uses the default message defined + * in the function parameters. + */ +function checkBrowserRemoteType( + browser, + expectedRemoteType, + message = `Ensures that tab runs in the ${expectedRemoteType} content process.` +) { + // Check both parent and child to ensure that they have the correct remoteType. + if (expectedRemoteType == E10SUtils.WEB_REMOTE_TYPE) { + ok(E10SUtils.isWebRemoteType(browser.remoteType), message); + ok( + E10SUtils.isWebRemoteType(browser.messageManager.remoteType), + "Parent and child process should agree on the remote type." + ); + } else { + is(browser.remoteType, expectedRemoteType, message); + is( + browser.messageManager.remoteType, + expectedRemoteType, + "Parent and child process should agree on the remote type." + ); + } +} + +function test_url_for_process_types({ + url, + chromeResult, + webContentResult, + privilegedAboutContentResult, + privilegedMozillaContentResult, + extensionProcessResult, +}) { + const CHROME_PROCESS = E10SUtils.NOT_REMOTE; + const WEB_CONTENT_PROCESS = E10SUtils.WEB_REMOTE_TYPE; + const PRIVILEGEDABOUT_CONTENT_PROCESS = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE; + const PRIVILEGEDMOZILLA_CONTENT_PROCESS = + E10SUtils.PRIVILEGEDMOZILLA_REMOTE_TYPE; + const EXTENSION_PROCESS = E10SUtils.EXTENSION_REMOTE_TYPE; + + is( + E10SUtils.canLoadURIInRemoteType(url, /* fission */ false, CHROME_PROCESS), + chromeResult, + "Check URL in chrome process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url, + /* fission */ false, + WEB_CONTENT_PROCESS + ), + webContentResult, + "Check URL in web content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url, + /* fission */ false, + PRIVILEGEDABOUT_CONTENT_PROCESS + ), + privilegedAboutContentResult, + "Check URL in privileged about content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url, + /* fission */ false, + PRIVILEGEDMOZILLA_CONTENT_PROCESS + ), + privilegedMozillaContentResult, + "Check URL in privileged mozilla content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url, + /* fission */ false, + EXTENSION_PROCESS + ), + extensionProcessResult, + "Check URL in extension process." + ); + + is( + E10SUtils.canLoadURIInRemoteType( + url + "#foo", + /* fission */ false, + CHROME_PROCESS + ), + chromeResult, + "Check URL with ref in chrome process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "#foo", + /* fission */ false, + WEB_CONTENT_PROCESS + ), + webContentResult, + "Check URL with ref in web content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "#foo", + /* fission */ false, + PRIVILEGEDABOUT_CONTENT_PROCESS + ), + privilegedAboutContentResult, + "Check URL with ref in privileged about content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "#foo", + /* fission */ false, + PRIVILEGEDMOZILLA_CONTENT_PROCESS + ), + privilegedMozillaContentResult, + "Check URL with ref in privileged mozilla content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "#foo", + /* fission */ false, + EXTENSION_PROCESS + ), + extensionProcessResult, + "Check URL with ref in extension process." + ); + + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo", + /* fission */ false, + CHROME_PROCESS + ), + chromeResult, + "Check URL with query in chrome process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo", + /* fission */ false, + WEB_CONTENT_PROCESS + ), + webContentResult, + "Check URL with query in web content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo", + /* fission */ false, + PRIVILEGEDABOUT_CONTENT_PROCESS + ), + privilegedAboutContentResult, + "Check URL with query in privileged about content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo", + /* fission */ false, + PRIVILEGEDMOZILLA_CONTENT_PROCESS + ), + privilegedMozillaContentResult, + "Check URL with query in privileged mozilla content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo", + /* fission */ false, + EXTENSION_PROCESS + ), + extensionProcessResult, + "Check URL with query in extension process." + ); + + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo#bar", + /* fission */ false, + CHROME_PROCESS + ), + chromeResult, + "Check URL with query and ref in chrome process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo#bar", + /* fission */ false, + WEB_CONTENT_PROCESS + ), + webContentResult, + "Check URL with query and ref in web content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo#bar", + /* fission */ false, + PRIVILEGEDABOUT_CONTENT_PROCESS + ), + privilegedAboutContentResult, + "Check URL with query and ref in privileged about content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo#bar", + /* fission */ false, + PRIVILEGEDMOZILLA_CONTENT_PROCESS + ), + privilegedMozillaContentResult, + "Check URL with query and ref in privileged mozilla content process." + ); + is( + E10SUtils.canLoadURIInRemoteType( + url + "?foo#bar", + /* fission */ false, + EXTENSION_PROCESS + ), + extensionProcessResult, + "Check URL with query and ref in extension process." + ); +} + +/* + * Get a file URL for the local file name. + */ +function fileURL(filename) { + let ifile = getChromeDir(getResolvedURI(gTestPath)); + ifile.append(filename); + return Services.io.newFileURI(ifile).spec; +} + +/* + * Get a http URL for the local file name. + */ +function httpURL(filename, host = "https://example.com/") { + let root = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + host + ); + return root + filename; +} + +function loadTestSubscript(filePath) { + Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this); +} diff --git a/browser/base/content/test/tabs/helper_origin_attrs_testing.js b/browser/base/content/test/tabs/helper_origin_attrs_testing.js new file mode 100644 index 0000000000..10ef8248c8 --- /dev/null +++ b/browser/base/content/test/tabs/helper_origin_attrs_testing.js @@ -0,0 +1,158 @@ +/* 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/. */ + +"use strict"; + +const NUM_USER_CONTEXTS = 3; + +var xulFrameLoaderCreatedListenerInfo; + +function initXulFrameLoaderListenerInfo() { + xulFrameLoaderCreatedListenerInfo = {}; + xulFrameLoaderCreatedListenerInfo.numCalledSoFar = 0; +} + +function handleEvent(aEvent) { + if (aEvent.type != "XULFrameLoaderCreated") { + return; + } + // Ignore <browser> element in about:preferences and any other special pages + if ("gBrowser" in aEvent.target.ownerGlobal) { + xulFrameLoaderCreatedListenerInfo.numCalledSoFar++; + } +} + +async function openURIInRegularTab(uri, win = window) { + info(`Opening url ${uri} in a regular tab`); + + initXulFrameLoaderListenerInfo(); + win.gBrowser.addEventListener("XULFrameLoaderCreated", handleEvent); + + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, uri); + info( + `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedListenerInfo.numCalledSoFar} time(s) for ${uri} in regular tab` + ); + + is( + xulFrameLoaderCreatedListenerInfo.numCalledSoFar, + 1, + "XULFrameLoaderCreated fired correct number of times" + ); + + win.gBrowser.removeEventListener("XULFrameLoaderCreated", handleEvent); + return { tab, uri }; +} + +async function openURIInContainer(uri, win, userContextId) { + info(`Opening url ${uri} in user context ${userContextId}`); + initXulFrameLoaderListenerInfo(); + win.gBrowser.addEventListener("XULFrameLoaderCreated", handleEvent); + + let tab = BrowserTestUtils.addTab(win.gBrowser, uri, { + userContextId, + }); + is( + tab.getAttribute("usercontextid"), + userContextId.toString(), + "New tab has correct user context id" + ); + + let browser = tab.linkedBrowser; + + await BrowserTestUtils.browserLoaded(browser, false, uri); + info( + `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedListenerInfo.numCalledSoFar} + time(s) for ${uri} in container tab ${userContextId}` + ); + + is( + xulFrameLoaderCreatedListenerInfo.numCalledSoFar, + 1, + "XULFrameLoaderCreated fired correct number of times" + ); + + win.gBrowser.removeEventListener("XULFrameLoaderCreated", handleEvent); + + return { tab, user_context_id: userContextId, uri }; +} + +async function openURIInPrivateTab(uri) { + info( + `Opening url ${ + uri ? uri : "about:privatebrowsing" + } in a private browsing tab` + ); + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + waitForTabURL: "about:privatebrowsing", + }); + if (!uri) { + return { tab: win.gBrowser.selectedTab, uri: "about:privatebrowsing" }; + } + initXulFrameLoaderListenerInfo(); + win.gBrowser.addEventListener("XULFrameLoaderCreated", handleEvent); + + const browser = win.gBrowser.selectedTab.linkedBrowser; + let prevRemoteType = browser.remoteType; + let loaded = BrowserTestUtils.browserLoaded(browser, false, uri); + BrowserTestUtils.startLoadingURIString(browser, uri); + await loaded; + let currRemoteType = browser.remoteType; + + info( + `XULFrameLoaderCreated was fired ${xulFrameLoaderCreatedListenerInfo.numCalledSoFar} time(s) for ${uri} in private tab` + ); + + if ( + SpecialPowers.Services.appinfo.sessionHistoryInParent && + currRemoteType == prevRemoteType && + uri == "about:blank" + ) { + // about:blank page gets flagged for being eligible to go into bfcache + // and thus we create a new XULFrameLoader for these pages + is( + xulFrameLoaderCreatedListenerInfo.numCalledSoFar, + 1, + "XULFrameLoaderCreated fired correct number of times" + ); + } else { + is( + xulFrameLoaderCreatedListenerInfo.numCalledSoFar, + currRemoteType == prevRemoteType ? 0 : 1, + "XULFrameLoaderCreated fired correct number of times" + ); + } + + win.gBrowser.removeEventListener("XULFrameLoaderCreated", handleEvent); + return { tab: win.gBrowser.selectedTab, uri }; +} + +function initXulFrameLoaderCreatedCounter(aXulFrameLoaderCreatedListenerInfo) { + aXulFrameLoaderCreatedListenerInfo.numCalledSoFar = 0; +} + +// Expected remote types for the following tests: +// browser/base/content/test/tabs/browser_navigate_through_urls_origin_attributes.js +// browser/base/content/test/tabs/browser_origin_attrs_in_remote_type.js +function getExpectedRemoteTypes(gFissionBrowser, numPagesOpen) { + var remoteTypes; + if (gFissionBrowser) { + remoteTypes = [ + "webIsolated=https://example.com", + "webIsolated=https://example.com^userContextId=1", + "webIsolated=https://example.com^userContextId=2", + "webIsolated=https://example.com^userContextId=3", + "webIsolated=https://example.com^privateBrowsingId=1", + "webIsolated=https://example.org", + "webIsolated=https://example.org^userContextId=1", + "webIsolated=https://example.org^userContextId=2", + "webIsolated=https://example.org^userContextId=3", + "webIsolated=https://example.org^privateBrowsingId=1", + ]; + } else { + remoteTypes = Array(numPagesOpen * 2).fill("web"); // example.com and example.org + } + remoteTypes = remoteTypes.concat(Array(numPagesOpen * 2).fill(null)); // about: pages + return remoteTypes; +} diff --git a/browser/base/content/test/tabs/link_in_tab_title_and_url_prefilled.html b/browser/base/content/test/tabs/link_in_tab_title_and_url_prefilled.html new file mode 100644 index 0000000000..a7561f4099 --- /dev/null +++ b/browser/base/content/test/tabs/link_in_tab_title_and_url_prefilled.html @@ -0,0 +1,30 @@ +<style> +a { display: block; } +</style> + +<a id="wait-a-bit--blank-target" href="wait-a-bit.sjs" target="_blank">wait-a-bit - _blank target</a> +<a id="wait-a-bit--other-target" href="wait-a-bit.sjs" target="other">wait-a-bit - other target</a> +<a id="wait-a-bit--by-script">wait-a-bit - script</a> +<a id="wait-a-bit--no-target" href="wait-a-bit.sjs">wait-a-bit - no target</a> + +<a id="request-timeout--blank-target" href="request-timeout.sjs" target="_blank">request-timeout - _blank target</a> +<a id="request-timeout--other-target" href="request-timeout.sjs" target="other">request-timeout - other target</a> +<a id="request-timeout--by-script">request-timeout - script</a> +<a id="request-timeout--no-target" href="request-timeout.sjs">request-timeout - no target</a> + +<a id="blank-page--blank-target" href="about:blank" target="_blank">about:blank - _blank target</a> +<a id="blank-page--other-target" href="about:blank" target="other">about:blank - other target</a> +<a id="blank-page--by-script">blank - script</a> +<a id="blank-page--no-target" href="about:blank">about:blank - no target</a> + +<script> +document.getElementById("wait-a-bit--by-script").addEventListener("click", () => { + window.open("wait-a-bit.sjs", "_blank"); +}) +document.getElementById("request-timeout--by-script").addEventListener("click", () => { + window.open("request-timeout.sjs", "_blank"); +}) +document.getElementById("blank-page--by-script").addEventListener("click", () => { + window.open("about:blank", "_blank"); +}) +</script> diff --git a/browser/base/content/test/tabs/open_window_in_new_tab.html b/browser/base/content/test/tabs/open_window_in_new_tab.html new file mode 100644 index 0000000000..2bd7613d26 --- /dev/null +++ b/browser/base/content/test/tabs/open_window_in_new_tab.html @@ -0,0 +1,15 @@ +<!doctype html> +<script> +function openWindow(id) { + window.childWindow = window.open(location.href + "?" + id, "", ""); +} +</script> +<button id="open-click" onclick="openWindow('open-click')">Open window</button> +<button id="focus" onclick="window.childWindow.focus()">Focus window</button> +<button id="open-mousedown">Open window</button> +<script> +document.getElementById("open-mousedown").addEventListener("mousedown", function(e) { + openWindow(this.id); + e.preventDefault(); +}); +</script> diff --git a/browser/base/content/test/tabs/page_with_iframe.html b/browser/base/content/test/tabs/page_with_iframe.html new file mode 100644 index 0000000000..5d821cf980 --- /dev/null +++ b/browser/base/content/test/tabs/page_with_iframe.html @@ -0,0 +1,12 @@ +<!-- 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/. --> + +<html> + <head> + <title>This page has an iFrame</title> + </head> + <body> + <iframe id="hidden-iframe" style="visibility: hidden;" src="https://example.com/another/site"></iframe> + </body> +</html> diff --git a/browser/base/content/test/tabs/redirect_via_header.html b/browser/base/content/test/tabs/redirect_via_header.html new file mode 100644 index 0000000000..5fedca6b4e --- /dev/null +++ b/browser/base/content/test/tabs/redirect_via_header.html @@ -0,0 +1,9 @@ +<!-- 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/. --> + +<html> + <head> + <title>Redirect via the header associated with this file</title> + </head> +</html> diff --git a/browser/base/content/test/tabs/redirect_via_header.html^headers^ b/browser/base/content/test/tabs/redirect_via_header.html^headers^ new file mode 100644 index 0000000000..7543b06689 --- /dev/null +++ b/browser/base/content/test/tabs/redirect_via_header.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Found +Location: https://example.com/some/path diff --git a/browser/base/content/test/tabs/redirect_via_meta_tag.html b/browser/base/content/test/tabs/redirect_via_meta_tag.html new file mode 100644 index 0000000000..42b775055f --- /dev/null +++ b/browser/base/content/test/tabs/redirect_via_meta_tag.html @@ -0,0 +1,13 @@ +<!-- 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/. --> + +<html> + <head> + <title>Page that redirects</title> + <meta http-equiv="refresh" content="1;url=http://mochi.test:8888/" /> + </head> + <body> + <p>This page has moved to http://mochi.test:8888/</p> + </body> +</html> diff --git a/browser/base/content/test/tabs/request-timeout.sjs b/browser/base/content/test/tabs/request-timeout.sjs new file mode 100644 index 0000000000..00e95ca4c0 --- /dev/null +++ b/browser/base/content/test/tabs/request-timeout.sjs @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function handleRequest(request, response) { + response.setStatusLine("1.1", 408, "Request Timeout"); +} diff --git a/browser/base/content/test/tabs/tab_that_closes.html b/browser/base/content/test/tabs/tab_that_closes.html new file mode 100644 index 0000000000..795baec18b --- /dev/null +++ b/browser/base/content/test/tabs/tab_that_closes.html @@ -0,0 +1,15 @@ +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> + <h1>This tab will close</h2> + <script> + // We use half a second timeout because this can race in debug builds. + setTimeout( () => { + window.close(); + }, 500); + </script> +</body> +</html> diff --git a/browser/base/content/test/tabs/test_bug1358314.html b/browser/base/content/test/tabs/test_bug1358314.html new file mode 100644 index 0000000000..9aa2019752 --- /dev/null +++ b/browser/base/content/test/tabs/test_bug1358314.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + <p>Test page</p> + <a href="/">Link</a> + </body> +</html> diff --git a/browser/base/content/test/tabs/test_process_flags_chrome.html b/browser/base/content/test/tabs/test_process_flags_chrome.html new file mode 100644 index 0000000000..c447d7ffb0 --- /dev/null +++ b/browser/base/content/test/tabs/test_process_flags_chrome.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> + +<html> +<body> +<p>chrome: test page</p> +<p><a href="chrome://mochitests/content/browser/browser/base/content/test/tabs/test_process_flags_chrome.html">chrome</a></p> +<p><a href="chrome://mochitests-any/content/browser/browser/base/content/test/tabs/test_process_flags_chrome.html">canremote</a></p> +<p><a href="chrome://mochitests-content/content/browser/browser/base/content/test/tabs/test_process_flags_chrome.html">mustremote</a></p> +</body> +</html> diff --git a/browser/base/content/test/tabs/wait-a-bit.sjs b/browser/base/content/test/tabs/wait-a-bit.sjs new file mode 100644 index 0000000000..e90133d752 --- /dev/null +++ b/browser/base/content/test/tabs/wait-a-bit.sjs @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +async function handleRequest(request, response) { + response.seizePower(); + + await new Promise(r => setTimeout(r, 2000)); + + response.write("HTTP/1.1 200 OK\r\n"); + const body = "<title>wait a bit</title><body>ok</body>"; + response.write("Content-Type: text/html\r\n"); + response.write(`Content-Length: ${body.length}\r\n`); + response.write("\r\n"); + response.write(body); + response.finish(); +} |