Bug 1419947 - Extend tabbrowser to track and use tab successors; r=dao
authorRyan Hendrickson <ryan.hendrickson@alum.mit.edu>
Fri, 19 Oct 2018 01:08:06 +0000
changeset 500613 15fd39e497668c53ee7fecd914510c5f4ac35bf7
parent 500612 14e469a4365b820bfb1a890c6993edb118a60d4f
child 500614 929617c0f367c6fe1b8751dbcca043235490494e
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdao
bugs1419947
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1419947 - Extend tabbrowser to track and use tab successors; r=dao The successor of a tab is similar to the owner of a tab, in that if an active tab is closed, the successor will be activated next. The differences are that successor always defaults to being unset (an extension can set it through a not-yet-implemented API), whereas tabbrowser sets owner to be the opener of background tabs; successor is always consulted when an active tab is closed, whereas the use of owner is gated by a pref; and when a tab is closed, hidden, or moved to another window, any tabs whose successor is the affected tab inherit that tab's successor, whereas in the parallel situation with owners, the owner of such tabs is simply set to null. Differential Revision: https://phabricator.services.mozilla.com/D4731
browser/base/content/tabbrowser.js
browser/base/content/test/tabs/browser.ini
browser/base/content/test/tabs/browser_tabSuccessors.js
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -2941,16 +2941,21 @@ window._gBrowser = {
 
     if (newTab)
       this.addTrustedTab(BROWSER_NEW_TAB_URL, {
         skipAnimation: true,
       });
     else
       TabBarVisibility.update();
 
+    // Splice this tab out of any lines of succession before any events are
+    // dispatched.
+    this.replaceInSuccession(aTab, aTab.successor);
+    this.setSuccessor(aTab, null);
+
     // We're committed to closing the tab now.
     // Dispatch a notification.
     // We dispatch it before any teardown so that event listeners can
     // inspect the tab that's about to close.
     let evt = new CustomEvent("TabClose", { bubbles: true, detail: { adoptedBy: adoptedByTab } });
     aTab.dispatchEvent(evt);
 
     if (this.tabs.length == 2) {
@@ -3120,16 +3125,22 @@ window._gBrowser = {
       this._windowIsClosing = closeWindow(true, window.warnAboutClosingWindow);
   },
 
   _findTabToBlurTo(aTab) {
     if (!aTab.selected) {
       return null;
     }
 
+    // If this tab has a successor, it should be selectable, since
+    // hiding or closing a tab removes that tab as a successor.
+    if (aTab.successor) {
+      return aTab.successor;
+    }
+
     if (aTab.owner &&
         !aTab.owner.hidden &&
         !aTab.owner.closing &&
         Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) {
       return aTab.owner;
     }
 
     // Switch to a visible tab unless there aren't any others remaining
@@ -3510,16 +3521,21 @@ window._gBrowser = {
       aTab.setAttribute("hidden", "true");
       this._visibleTabs = null; // invalidate cache
 
       this.tabContainer._updateCloseButtons();
       this.tabContainer._updateHiddenTabsStatus();
 
       this.tabContainer._setPositionalAttributes();
 
+      // Splice this tab out of any lines of succession before any events are
+      // dispatched.
+      this.replaceInSuccession(aTab, aTab.successor);
+      this.setSuccessor(aTab, null);
+
       let event = document.createEvent("Events");
       event.initEvent("TabHide", true, false);
       aTab.dispatchEvent(event);
       if (aSource) {
         SessionStore.setCustomTabValue(aTab, "hiddenBy", aSource);
       }
     }
   },
@@ -4704,16 +4720,50 @@ window._gBrowser = {
       let tab = this.getTabForBrowser(browser);
       if (!tab) {
         return;
       }
 
       Services.obs.notifyObservers(tab, "AudibleAutoplayMediaOccurred");
     });
   },
+
+  setSuccessor(aTab, successorTab) {
+    if (aTab.ownerGlobal != window) {
+      throw new Error("Cannot set the successor of another window's tab");
+    }
+    if (successorTab == aTab) {
+      successorTab = null;
+    }
+    if (successorTab && successorTab.ownerGlobal != window) {
+      throw new Error("Cannot set the successor to another window's tab");
+    }
+    if (aTab.successor) {
+      aTab.successor.predecessors.delete(aTab);
+    }
+    aTab.successor = successorTab;
+    if (successorTab) {
+      if (!successorTab.predecessors) {
+        successorTab.predecessors = new Set();
+      }
+      successorTab.predecessors.add(aTab);
+    }
+  },
+
+  /**
+   * For all tabs with aTab as a successor, set the successor to aOtherTab
+   * instead.
+   */
+  replaceInSuccession(aTab, aOtherTab) {
+    if (aTab.predecessors) {
+      for (const predecessor of Array.from(aTab.predecessors)) {
+        this.setSuccessor(predecessor, aOtherTab);
+      }
+    }
+  },
 };
 
 /**
  * A web progress listener object definition for a given tab.
  */
 class TabProgressListener {
   constructor(aTab, aBrowser, aStartsBlank, aWasPreloadedBrowser, aOrigStateFlags) {
     let stateFlags = aOrigStateFlags || 0;
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -66,14 +66,15 @@ skip-if = (verify && (os == 'win' || os 
 [browser_reload_deleted_file.js]
 skip-if = (debug && os == 'mac') || (debug && os == 'linux' && bits == 64) #Bug 1421183, disabled on Linux/OSX for leaked windows
 [browser_tabCloseProbes.js]
 [browser_tabContextMenu_keyboard.js]
 [browser_tabReorder_overflow.js]
 [browser_tabReorder.js]
 [browser_tabSpinnerProbe.js]
 skip-if = !e10s # Tab spinner is e10s only.
+[browser_tabSuccessors.js]
 [browser_tabSwitchPrintPreview.js]
 skip-if = os == 'mac'
 [browser_tabswitch_updatecommands.js]
 [browser_viewsource_of_data_URI_in_file_process.js]
 [browser_visibleTabs_bookmarkAllTabs.js]
 [browser_visibleTabs_contextMenu.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_tabSuccessors.js
@@ -0,0 +1,96 @@
+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]);
+});