Backed out changeset f7e5a0bf9500 (bug 931891) for mochitest-bc orange. a=backout
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 16 Dec 2013 16:45:15 -0500
changeset 175322 9b6627823cf117f1663eb7174006d449ab1a8cf2
parent 175321 2c962cec8c3c1565112e6954a63315aa5d29e050
child 175323 6895de3528c960e0fc6c21f951b4c9b8b49a0bfb
push id445
push userffxbld
push dateMon, 10 Mar 2014 22:05:19 +0000
treeherdermozilla-release@dc38b741b04e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs931891
milestone28.0a2
backs outf7e5a0bf9500ca2e6c39883115e83641da29bc76
Backed out changeset f7e5a0bf9500 (bug 931891) for mochitest-bc orange. a=backout
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/tabbrowser.xml
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_bug887515.js
browser/base/content/test/general/browser_bug896291_closeMaxSessionStoreTabs.js
browser/components/sessionstore/nsISessionStore.idl
browser/components/sessionstore/src/RecentlyClosedTabsAndWindowsMenuUtils.jsm
browser/components/sessionstore/src/SessionStore.jsm
browser/components/sessionstore/test/browser_345898.js
browser/components/tabview/test/browser_tabview_bug608037.js
browser/components/tabview/test/browser_tabview_bug624847.js
browser/components/tabview/test/browser_tabview_bug628270.js
browser/components/tabview/test/browser_tabview_bug706736.js
browser/components/tabview/test/head.js
browser/locales/en-US/chrome/browser/browser.dtd
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6215,26 +6215,43 @@ function convertFromUnicode(charset, str
  * @returns a reference to the reopened tab.
  */
 function undoCloseTab(aIndex) {
   // wallpaper patch to prevent an unnecessary blank tab (bug 343895)
   var blankTabToRemove = null;
   if (gBrowser.tabs.length == 1 && isTabEmpty(gBrowser.selectedTab))
     blankTabToRemove = gBrowser.selectedTab;
 
-  var tab = null;
-  if (SessionStore.getClosedTabCount(window) > (aIndex || 0)) {
+  let numberOfTabsToUndoClose = 0;
+  let index = Number(aIndex);
+
+
+  if (isNaN(index)) {
+    index = 0;
+    numberOfTabsToUndoClose = SessionStore.getNumberOfTabsClosedLast(window);
+  } else {
+    if (0 > index || index >= SessionStore.getClosedTabCount(window))
+      return null;
+    numberOfTabsToUndoClose = 1;
+  }
+
+  let tab = null;
+  while (numberOfTabsToUndoClose > 0 &&
+         numberOfTabsToUndoClose--) {
     TabView.prepareUndoCloseTab(blankTabToRemove);
-    tab = SessionStore.undoCloseTab(window, aIndex || 0);
+    tab = SessionStore.undoCloseTab(window, index);
     TabView.afterUndoCloseTab();
-
-    if (blankTabToRemove)
+    if (blankTabToRemove) {
       gBrowser.removeTab(blankTabToRemove);
+      blankTabToRemove = null;
+    }
   }
 
+  // Reset the number of tabs closed last time to the default.
+  SessionStore.setNumberOfTabsClosedLast(window, 1);
   return tab;
 }
 
 /**
  * Re-open a closed window.
  * @param aIndex
  *        The index of the window (via SessionStore.getClosedWindowData)
  * @returns a reference to the reopened window.
@@ -7054,18 +7071,23 @@ var TabContextMenu = {
       menuItem.disabled = disabled;
 
     disabled = gBrowser.visibleTabs.length == 1;
     menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple-visible");
     for (let menuItem of menuItems)
       menuItem.disabled = disabled;
 
     // Session store
-    document.getElementById("context_undoCloseTab").disabled =
-      SessionStore.getClosedTabCount(window) == 0;
+    let undoCloseTabElement = document.getElementById("context_undoCloseTab");
+    let closedTabCount = SessionStore.getNumberOfTabsClosedLast(window);
+    undoCloseTabElement.disabled = closedTabCount == 0;
+    // Change the label of "Undo Close Tab" to specify if it will undo a batch-close
+    // or a single close.
+    let visibleLabel = closedTabCount <= 1 ? "singletablabel" : "multipletablabel";
+    undoCloseTabElement.setAttribute("label", undoCloseTabElement.getAttribute(visibleLabel));
 
     // Only one of pin/unpin should be visible
     document.getElementById("context_pinTab").hidden = this.contextTab.pinned;
     document.getElementById("context_unpinTab").hidden = !this.contextTab.pinned;
 
     // Disable "Close Tabs to the Right" if there are no tabs
     // following it and hide it when the user rightclicked on a pinned
     // tab.
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -99,17 +99,18 @@
                 accesskey="&bookmarkAllTabs.accesskey;"
                 command="Browser:BookmarkAllTabs"/>
       <menuitem id="context_closeTabsToTheEnd" label="&closeTabsToTheEnd.label;" accesskey="&closeTabsToTheEnd.accesskey;"
                 oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab);"/>
       <menuitem id="context_closeOtherTabs" label="&closeOtherTabs.label;" accesskey="&closeOtherTabs.accesskey;"
                 oncommand="gBrowser.removeAllTabsBut(TabContextMenu.contextTab);"/>
       <menuseparator/>
       <menuitem id="context_undoCloseTab"
-                label="&undoCloseTab.label;"
+                singletablabel="&undoCloseTab.label;"
+                multipletablabel="&undoCloseTabs.label;"
                 accesskey="&undoCloseTab.accesskey;"
                 observes="History:UndoCloseTab"/>
       <menuitem id="context_closeTab" label="&closeTab.label;" accesskey="&closeTab.accesskey;"
                 oncommand="gBrowser.removeTab(TabContextMenu.contextTab, { animate: true });"/>
     </menupopup>
 
     <!-- bug 415444/582485: event.stopPropagation is here for the cloned version
          of this menupopup -->
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1676,23 +1676,36 @@
                 throw new Error("Required argument missing: aTab");
 
               tabsToClose = this.getTabsToTheEndFrom(aTab).length;
               break;
             default:
               throw new Error("Invalid argument: " + aCloseTabs);
           }
 
-          if (tabsToClose <= 1)
-            return true;
-
-          const pref = aCloseTabs == this.closingTabsEnum.ALL ?
-                       "browser.tabs.warnOnClose" : "browser.tabs.warnOnCloseOtherTabs";
-          var shouldPrompt = Services.prefs.getBoolPref(pref);
-          if (!shouldPrompt)
+          let maxUndo =
+            Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+          let warnOnCloseOtherTabs =
+            Services.prefs.getBoolPref("browser.tabs.warnOnCloseOtherTabs");
+          let warnOnCloseWindow =
+            Services.prefs.getBoolPref("browser.tabs.warnOnClose");
+          let isWindowClosing = aCloseTabs == this.closingTabsEnum.ALL;
+
+          let skipWarning =
+            // 1) If there is only one tab to close, we'll never warn the user.
+            tabsToClose <= 1 ||
+            // 2) If the whole window is going to be closed, don't warn the
+            //    user if the user has browser.tabs.warnOnClose set to false.
+            (isWindowClosing && !warnOnCloseWindow) ||
+            // 3) If the number of tabs are less than the undo threshold
+            //    or if the user has specifically opted-in to ignoring
+            //    this warning via the warnOnCloseOtherTabs pref.
+            (!isWindowClosing && (!warnOnCloseOtherTabs ||
+                                            tabsToClose <= maxUndo));
+          if (skipWarning)
             return true;
 
           var ps = Services.prompt;
 
           // default to true: if it were false, we wouldn't get this far
           var warnOnClose = { value: true };
           var bundle = this.mStringBundle;
 
@@ -1706,24 +1719,26 @@
             ps.confirmEx(window,
                          bundle.getString("tabs.closeWarningTitle"),
                          bundle.getFormattedString("tabs.closeWarningMultipleTabs",
                                                    [tabsToClose]),
                          (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0)
                          + (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1),
                          bundle.getString("tabs.closeButtonMultiple"),
                          null, null,
-                         aCloseTabs == this.closingTabsEnum.ALL ?
-                           bundle.getString("tabs.closeWarningPromptMe") : null,
+                         bundle.getString("tabs.closeWarningPromptMe"),
                          warnOnClose);
           var reallyClose = (buttonPressed == 0);
 
           // don't set the pref unless they press OK and it's false
-          if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value)
+          if (reallyClose && !warnOnClose.value) {
+            let pref = isWindowClosing ? "browser.tabs.warnOnClose" :
+                                         "browser.tabs.warnOnCloseOtherTabs";
             Services.prefs.setBoolPref(pref, false);
+          }
 
           return reallyClose;
         ]]>
       </body>
       </method>
 
       <method name="getTabsToTheEndFrom">
         <parameter name="aTab"/>
@@ -1740,39 +1755,45 @@
       </method>
 
       <method name="removeTabsToTheEndFrom">
         <parameter name="aTab"/>
         <body>
           <![CDATA[
             if (this.warnAboutClosingTabs(this.closingTabsEnum.TO_END, aTab)) {
               let tabs = this.getTabsToTheEndFrom(aTab);
-              for (let i = tabs.length - 1; i >= 0; --i) {
+              let numberOfTabsToClose = tabs.length;
+              for (let i = numberOfTabsToClose - 1; i >= 0; --i) {
                 this.removeTab(tabs[i], {animate: true});
               }
+              SessionStore.setNumberOfTabsClosedLast(window, numberOfTabsToClose);
             }
           ]]>
         </body>
       </method>
 
       <method name="removeAllTabsBut">
         <parameter name="aTab"/>
         <body>
           <![CDATA[
             if (aTab.pinned)
               return;
 
             if (this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) {
               let tabs = this.visibleTabs;
               this.selectedTab = aTab;
 
+              let closedTabs = 0;
               for (let i = tabs.length - 1; i >= 0; --i) {
-                if (tabs[i] != aTab && !tabs[i].pinned)
+                if (tabs[i] != aTab && !tabs[i].pinned) {
                   this.removeTab(tabs[i], {animate: true});
+                  closedTabs++;
+                }
               }
+              SessionStore.setNumberOfTabsClosedLast(window, closedTabs);
             }
           ]]>
         </body>
       </method>
 
       <method name="removeCurrentTab">
         <parameter name="aParams"/>
         <body>
@@ -1791,16 +1812,18 @@
         <parameter name="aParams"/>
         <body>
           <![CDATA[
             if (aParams) {
               var animate = aParams.animate;
               var byMouse = aParams.byMouse;
             }
 
+            SessionStore.setNumberOfTabsClosedLast(window, 1);
+
             // Handle requests for synchronously removing an already
             // asynchronously closing tab.
             if (!animate &&
                 aTab.closing) {
               this._endRemoveTab(aTab);
               return;
             }
 
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -230,16 +230,18 @@ run-if = toolkit == "cocoa"
 [browser_bug817947.js]
 [browser_bug818118.js]
 [browser_bug820497.js]
 [browser_bug822367.js]
 [browser_bug832435.js]
 [browser_bug839103.js]
 [browser_bug880101.js]
 [browser_bug882977.js]
+[browser_bug887515.js]
+[browser_bug896291_closeMaxSessionStoreTabs.js]
 [browser_bug902156.js]
 [browser_bug906190.js]
 [browser_canonizeURL.js]
 [browser_clearplugindata.js]
 [browser_contentAreaClick.js]
 [browser_contextSearchTabPosition.js]
 [browser_ctrlTab.js]
 [browser_customize.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug887515.js
@@ -0,0 +1,75 @@
+function numClosedTabs()
+  SessionStore.getNumberOfTabsClosedLast(window);
+
+var originalTab;
+var tab1Loaded = false;
+var tab2Loaded = false;
+
+function verifyUndoMultipleClose() {
+  if (!tab1Loaded || !tab2Loaded)
+    return;
+
+  gBrowser.removeAllTabsBut(originalTab);
+  updateTabContextMenu();
+  let undoCloseTabElement = document.getElementById("context_undoCloseTab");
+  ok(!undoCloseTabElement.disabled, "Undo Close Tabs should be enabled.");
+  is(numClosedTabs(), 2, "There should be 2 closed tabs.");
+  is(gBrowser.tabs.length, 1, "There should only be 1 open tab");
+  updateTabContextMenu();
+  is(undoCloseTabElement.label, undoCloseTabElement.getAttribute("multipletablabel"),
+     "The label should be showing that the command will restore multiple tabs");
+  undoCloseTab();
+
+  is(gBrowser.tabs.length, 3, "There should be 3 open tabs");
+  updateTabContextMenu();
+  is(undoCloseTabElement.label, undoCloseTabElement.getAttribute("singletablabel"),
+     "The label should be showing that the command will restore a single tab");
+
+  gBrowser.removeTabsToTheEndFrom(originalTab);
+  updateTabContextMenu();
+  ok(!undoCloseTabElement.disabled, "Undo Close Tabs should be enabled.");
+  is(numClosedTabs(), 2, "There should be 2 closed tabs.");
+  is(gBrowser.tabs.length, 1, "There should only be 1 open tab");
+  updateTabContextMenu();
+  is(undoCloseTabElement.label, undoCloseTabElement.getAttribute("multipletablabel"),
+     "The label should be showing that the command will restore multiple tabs");
+
+  finish();
+}
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref("browser.tabs.animate", false);
+  registerCleanupFunction(function() {
+    Services.prefs.clearUserPref("browser.tabs.animate");
+    originalTab.linkedBrowser.loadURI("about:blank");
+    originalTab = null;
+  });
+
+  let undoCloseTabElement = document.getElementById("context_undoCloseTab");
+  updateTabContextMenu();
+  is(undoCloseTabElement.label, undoCloseTabElement.getAttribute("singletablabel"),
+     "The label should be showing that the command will restore a single tab");
+
+  originalTab = gBrowser.selectedTab;
+  gBrowser.selectedBrowser.loadURI("http://mochi.test:8888/");
+  var tab1 = gBrowser.addTab("http://mochi.test:8888/");
+  var tab2 = gBrowser.addTab("http://mochi.test:8888/");
+  var browser1 = gBrowser.getBrowserForTab(tab1);
+  browser1.addEventListener("load", function onLoad1() {
+    browser1.removeEventListener("load", onLoad1, true);
+    tab1Loaded = true;
+    tab1 = null;
+
+    verifyUndoMultipleClose();
+  }, true);
+  var browser2 = gBrowser.getBrowserForTab(tab2);
+  browser2.addEventListener("load", function onLoad2() {
+    browser2.removeEventListener("load", onLoad2, true);
+    tab2Loaded = true;
+    tab2 = null;
+
+    verifyUndoMultipleClose();
+  }, true);
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug896291_closeMaxSessionStoreTabs.js
@@ -0,0 +1,108 @@
+function numClosedTabs()
+  Cc["@mozilla.org/browser/sessionstore;1"].
+    getService(Ci.nsISessionStore).
+    getNumberOfTabsClosedLast(window);
+
+let originalTab;
+let maxTabsUndo;
+let maxTabsUndoPlusOne;
+let acceptRemoveAllTabsDialogListener;
+let cancelRemoveAllTabsDialogListener;
+
+function test() {
+  waitForExplicitFinish();
+  Services.prefs.setBoolPref("browser.tabs.animate", false);
+
+  registerCleanupFunction(function() {
+    Services.prefs.clearUserPref("browser.tabs.animate");
+
+    originalTab.linkedBrowser.loadURI("about:blank");
+    originalTab = null;
+  });
+
+  // Creating and throwing away this tab guarantees that the
+  // number of tabs closed in the previous tab-close operation is 1.
+  let throwaway_tab = gBrowser.addTab("http://mochi.test:8888/");
+  gBrowser.removeTab(throwaway_tab);
+
+  let undoCloseTabElement = document.getElementById("context_undoCloseTab");
+  updateTabContextMenu();
+  is(undoCloseTabElement.label, undoCloseTabElement.getAttribute("singletablabel"),
+     "The label should be showing that the command will restore a single tab");
+
+  originalTab = gBrowser.selectedTab;
+  gBrowser.selectedBrowser.loadURI("http://mochi.test:8888/");
+
+  maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+  maxTabsUndoPlusOne = maxTabsUndo + 1;
+  let numberOfTabsLoaded = 0;
+  for (let i = 0; i < maxTabsUndoPlusOne; i++) {
+    let tab = gBrowser.addTab("http://mochi.test:8888/");
+    let browser = gBrowser.getBrowserForTab(tab);
+    browser.addEventListener("load", function onLoad() {
+      browser.removeEventListener("load", onLoad, true);
+
+      if (++numberOfTabsLoaded == maxTabsUndoPlusOne)
+        verifyUndoMultipleClose();
+    }, true);
+  }
+}
+
+function verifyUndoMultipleClose() {
+  info("all tabs opened and loaded");
+  cancelRemoveAllTabsDialogListener = new WindowListener("chrome://global/content/commonDialog.xul", cancelRemoveAllTabsDialog);
+  Services.wm.addListener(cancelRemoveAllTabsDialogListener);
+  gBrowser.removeAllTabsBut(originalTab);
+}
+
+function cancelRemoveAllTabsDialog(domWindow) {
+  ok(true, "dialog appeared in response to multiple tab close action");
+  domWindow.document.documentElement.cancelDialog();
+  Services.wm.removeListener(cancelRemoveAllTabsDialogListener);
+
+  acceptRemoveAllTabsDialogListener = new WindowListener("chrome://global/content/commonDialog.xul", acceptRemoveAllTabsDialog);
+  Services.wm.addListener(acceptRemoveAllTabsDialogListener);
+  waitForCondition(function () gBrowser.tabs.length == 1 + maxTabsUndoPlusOne, function verifyCancel() {
+    is(gBrowser.tabs.length, 1 + maxTabsUndoPlusOne, /* The '1 +' is for the original tab */
+       "All tabs should still be open after the 'Cancel' option on the prompt is chosen");
+    gBrowser.removeAllTabsBut(originalTab);
+  }, "Waited too long to find that no tabs were closed.");
+}
+
+function acceptRemoveAllTabsDialog(domWindow) {
+  ok(true, "dialog appeared in response to multiple tab close action");
+  domWindow.document.documentElement.acceptDialog();
+  Services.wm.removeListener(acceptRemoveAllTabsDialogListener);
+
+  waitForCondition(function () gBrowser.tabs.length == 1, function verifyAccept() {
+    is(gBrowser.tabs.length, 1,
+       "All other tabs should be closed after the 'OK' option on the prompt is chosen");
+    finish();
+  }, "Waited too long for the other tabs to be closed.");
+}
+
+function WindowListener(aURL, aCallback) {
+  this.callback = aCallback;
+  this.url = aURL;
+}
+WindowListener.prototype = {
+  onOpenWindow: function(aXULWindow) {
+    var domWindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIDOMWindow);
+    var self = this;
+    domWindow.addEventListener("load", function() {
+      domWindow.removeEventListener("load", arguments.callee, false);
+
+      info("domWindow.document.location.href: " + domWindow.document.location.href);
+      if (domWindow.document.location.href != self.url)
+        return;
+
+      // Allow other window load listeners to execute before passing to callback
+      executeSoon(function() {
+        self.callback(domWindow);
+      });
+    }, false);
+  },
+  onCloseWindow: function(aXULWindow) {},
+  onWindowTitleChange: function(aXULWindow, aNewTitle) {}
+}
--- a/browser/components/sessionstore/nsISessionStore.idl
+++ b/browser/components/sessionstore/nsISessionStore.idl
@@ -20,17 +20,17 @@ interface nsIDOMNode;
  * global |window| object to the API, though (or |top| from a sidebar).
  * From elsewhere you can get browser windows through the nsIWindowMediator
  * by looking for "navigator:browser" windows.
  *
  * * "Tabbrowser tabs" are all the child nodes of a browser window's
  * |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|.
  */
 
-[scriptable, uuid(6fdf7a49-c6ac-4bfe-a5da-aad6c4116168)]
+[scriptable, uuid(63a4d9f4-373f-11e3-a237-fa91a24410d2)]
 interface nsISessionStore : nsISupports
 {
   /**
    * Is it possible to restore the previous session. Will always be false when
    * in Private Browsing mode.
    */
   attribute boolean canRestoreLastSession;
 
@@ -96,16 +96,30 @@ interface nsISessionStore : nsISupports
    * @param aTab    is the tabbrowser tab to duplicate (can be from a different window).
    * @param aDelta  is the offset to the history entry to load in the duplicated tab.
    * @returns a reference to the newly created tab.
    */
   nsIDOMNode duplicateTab(in nsIDOMWindow aWindow, in nsIDOMNode aTab,
                           [optional] in long aDelta);
 
   /**
+   * Set the number of tabs that was closed during the last close-tabs
+   * operation. This helps us keep track of batch-close operations so
+   * we can restore multiple tabs at once.
+   */
+  void setNumberOfTabsClosedLast(in nsIDOMWindow aWindow, in unsigned long aNumber);
+
+  /**
+   * Get the number of tabs that was closed during the last close-tabs
+   * operation. This helps us keep track of batch-close operations so
+   * we can restore multiple tabs at once.
+   */
+  unsigned long getNumberOfTabsClosedLast(in nsIDOMWindow aWindow);
+
+  /**
    * Get the number of restore-able tabs for a browser window
    */
   unsigned long getClosedTabCount(in nsIDOMWindow aWindow);
 
   /**
    * Get closed tab data
    *
    * @param aWindow is the browser window for which to get closed tab data
--- a/browser/components/sessionstore/src/RecentlyClosedTabsAndWindowsMenuUtils.jsm
+++ b/browser/components/sessionstore/src/RecentlyClosedTabsAndWindowsMenuUtils.jsm
@@ -61,17 +61,17 @@ this.RecentlyClosedTabsAndWindowsMenuUti
           element.setAttribute("key", "key_undoCloseTab");
         fragment.appendChild(element);
       }
 
       fragment.appendChild(doc.createElementNS(kNSXUL, "menuseparator"));
       let restoreAllTabs = fragment.appendChild(doc.createElementNS(kNSXUL, aTagName));
       restoreAllTabs.setAttribute("label", navigatorBundle.GetStringFromName("menuRestoreAllTabs.label"));
       restoreAllTabs.setAttribute("oncommand",
-              "for (var i = 0; i < " + closedTabs.length + "; i++) undoCloseTab();");
+              "for (var i = 0; i < " + closedTabs.length + "; i++) undoCloseTab(0);");
     }
     return fragment;
   },
 
   /**
   * Builds up a document fragment of UI items for the recently closed windows.
   * @param   aWindow
   *          A window that can be used to create the elements and document fragment.
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -185,16 +185,24 @@ this.SessionStore = {
   setTabState: function ss_setTabState(aTab, aState) {
     SessionStoreInternal.setTabState(aTab, aState);
   },
 
   duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0) {
     return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta);
   },
 
+  getNumberOfTabsClosedLast: function ss_getNumberOfTabsClosedLast(aWindow) {
+    return SessionStoreInternal.getNumberOfTabsClosedLast(aWindow);
+  },
+
+  setNumberOfTabsClosedLast: function ss_setNumberOfTabsClosedLast(aWindow, aNumber) {
+    return SessionStoreInternal.setNumberOfTabsClosedLast(aWindow, aNumber);
+  },
+
   getClosedTabCount: function ss_getClosedTabCount(aWindow) {
     return SessionStoreInternal.getClosedTabCount(aWindow);
   },
 
   getClosedTabData: function ss_getClosedTabDataAt(aWindow) {
     return SessionStoreInternal.getClosedTabData(aWindow);
   },
 
@@ -1604,16 +1612,45 @@ let SessionStoreInternal = {
       aWindow.gBrowser.addTab();
 
     this.restoreTabs(aWindow, [newTab], [tabState], 0,
                      true /* Load this tab right away. */);
 
     return newTab;
   },
 
+  setNumberOfTabsClosedLast: function ssi_setNumberOfTabsClosedLast(aWindow, aNumber) {
+    if (this._disabledForMultiProcess) {
+      return;
+    }
+
+    if (!("__SSi" in aWindow)) {
+      throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    return NumberOfTabsClosedLastPerWindow.set(aWindow, aNumber);
+  },
+
+  /* Used to undo batch tab-close operations. Defaults to 1. */
+  getNumberOfTabsClosedLast: function ssi_getNumberOfTabsClosedLast(aWindow) {
+    if (this._disabledForMultiProcess) {
+      return 0;
+    }
+
+    if (!("__SSi" in aWindow)) {
+      throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
+    }
+    // Blank tabs cannot be undo-closed, so the number returned by
+    // the NumberOfTabsClosedLastPerWindow can be greater than the
+    // return value of getClosedTabCount. We won't restore blank
+    // tabs, so we return the minimum of these two values.
+    return Math.min(NumberOfTabsClosedLastPerWindow.get(aWindow) || 1,
+                    this.getClosedTabCount(aWindow));
+  },
+
   getClosedTabCount: function ssi_getClosedTabCount(aWindow) {
     if ("__SSi" in aWindow) {
       return this._windows[aWindow.__SSi]._closedTabs.length;
     }
 
     if (!DyingWindowCache.has(aWindow)) {
       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
@@ -3997,16 +4034,21 @@ let DirtyWindows = {
     this._data.delete(window);
   },
 
   clear: function (window) {
     this._data.clear();
   }
 };
 
+// A map storing the number of tabs last closed per windoow. This only
+// stores the most recent tab-close operation, and is used to undo
+// batch tab-closing operations.
+let NumberOfTabsClosedLastPerWindow = new WeakMap();
+
 // This is used to help meter the number of restoring tabs. This is the control
 // point for telling the next tab to restore. It gets attached to each gBrowser
 // via gBrowser.addTabsProgressListener
 let gRestoreTabsProgressListener = {
   onStateChange: function(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
     // Ignore state changes on browsers that we've already restored and state
     // changes that aren't applicable.
     if (aBrowser.__SS_restoreState &&
--- a/browser/components/sessionstore/test/browser_345898.js
+++ b/browser/components/sessionstore/test/browser_345898.js
@@ -35,9 +35,13 @@ function test() {
   ok(test(function() ss.undoCloseTab({}, 0)),
      "Invalid window for undoCloseTab throws");
   ok(test(function() ss.undoCloseTab(window, -1)),
      "Invalid index for undoCloseTab throws");
   ok(test(function() ss.getWindowValue({}, "")),
      "Invalid window for getWindowValue throws");
   ok(test(function() ss.setWindowValue({}, "", "")),
      "Invalid window for setWindowValue throws");
+  ok(test(function() ss.getNumberOfTabsClosedLast({})),
+     "Invalid window for getNumberOfTabsClosedLast  throws");
+  ok(test(function() ss.setNumberOfTabsClosedLast({}, 1)),
+     "Invalid window for setNumberOfTabsClosedLast throws");
 }
--- a/browser/components/tabview/test/browser_tabview_bug608037.js
+++ b/browser/components/tabview/test/browser_tabview_bug608037.js
@@ -33,10 +33,10 @@ function onTabViewWindowLoaded() {
     is(groupItems[0].getChildren().length, 3, "The group still has three tab items");
 
     // clean up and finish
     hideTabView(function () {
       gBrowser.removeTab(tabOne);
       gBrowser.removeTab(tabTwo);
       finish();
     });
-  });
+  }, 0);
 }
--- a/browser/components/tabview/test/browser_tabview_bug624847.js
+++ b/browser/components/tabview/test/browser_tabview_bug624847.js
@@ -59,17 +59,17 @@ function test() {
 
       restoreTab(function () {
         prefix = 'unpinned-restored';
         assertValidPrerequisites();
         assertGroupItemPreserved();
 
         createBlankTab();
         afterAllTabsLoaded(testUndoCloseWithSelectedBlankPinnedTab);
-      });
+      }, 0);
     });
   }
 
   let testUndoCloseWithSelectedBlankPinnedTab = function () {
     prefix = 'pinned';
     assertNumberOfTabs(2);
 
     afterAllTabsLoaded(function () {
@@ -89,17 +89,17 @@ function test() {
         prefix = 'pinned-restored';
         assertValidPrerequisites();
         assertGroupItemPreserved();
 
         createBlankTab();
         gBrowser.removeTab(gBrowser.tabs[0]);
 
         afterAllTabsLoaded(finishTest);
-      });
+      }, 0);
     });
   }
 
   waitForExplicitFinish();
   registerCleanupFunction(function () TabView.hide());
 
   showTabView(function () {
     hideTabView(function () {
--- a/browser/components/tabview/test/browser_tabview_bug628270.js
+++ b/browser/components/tabview/test/browser_tabview_bug628270.js
@@ -76,17 +76,17 @@ function test() {
 
     restoreTab(function () {
       assertNumberOfTabsInGroup(groupItem, 2);
 
       activateFirstGroupItem();
       gBrowser.removeTab(gBrowser.tabs[1]);
       gBrowser.removeTab(gBrowser.tabs[1]);
       hideTabView(finishTest);
-    });
+    }, 0);
   }
 
   waitForExplicitFinish();
   assertTabViewIsHidden();
   registerCleanupFunction(function () TabView.hide());
 
   showTabView(function () {
     cw = TabView.getContentWindow();
--- a/browser/components/tabview/test/browser_tabview_bug706736.js
+++ b/browser/components/tabview/test/browser_tabview_bug706736.js
@@ -15,17 +15,17 @@ function test() {
     is(groupItemOne.getChildren().length, 1, "Group one has 1 tab item");
 
     let groupItemTwo = createGroupItemWithBlankTabs(win, 300, 300, 40, 1);
     is(groupItemTwo.getChildren().length, 1, "Group two has 1 tab items");
 
     whenTabViewIsHidden(function() {
       win.gBrowser.removeTab(win.gBrowser.selectedTab);
       executeSoon(function() {
-        win.undoCloseTab();
+        win.undoCloseTab(0);
 
         groupItemTwo.addSubscriber("childAdded", function onChildAdded(data) {
           groupItemTwo.removeSubscriber("childAdded", onChildAdded);
 
           is(groupItemOne.getChildren().length, 1, "Group one still has 1 tab item");
           is(groupItemTwo.getChildren().length, 1, "Group two still has 1 tab item");
         });
 
--- a/browser/components/tabview/test/head.js
+++ b/browser/components/tabview/test/head.js
@@ -357,17 +357,17 @@ function newWindowWithState(state, callb
     });
   });
 }
 
 // ----------
 function restoreTab(callback, index, win) {
   win = win || window;
 
-  let tab = win.undoCloseTab(index || 0);
+  let tab = win.undoCloseTab(index);
   let tabItem = tab._tabViewTabItem;
 
   let finalize = function () {
     afterAllTabsLoaded(function () callback(tab), win);
   };
 
   if (tabItem._reconnected) {
     finalize();
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -41,16 +41,21 @@ can reach it easily. -->
 <!ENTITY  moveToGroup.accesskey              "M">
 <!ENTITY  moveToNewGroup.label               "New Group">
 <!ENTITY  moveToNewWindow.label              "Move to New Window">
 <!ENTITY  moveToNewWindow.accesskey          "W">
 <!ENTITY  bookmarkAllTabs.label              "Bookmark All Tabs…">
 <!ENTITY  bookmarkAllTabs.accesskey          "T">
 <!ENTITY  undoCloseTab.label                 "Undo Close Tab">
 <!ENTITY  undoCloseTab.accesskey             "U">
+<!-- LOCALIZATION NOTE (undoCloseTabs.label) : This label is used
+when the previous tab-closing operation closed more than one tab. It
+replaces the undoCloseTab.label and will use the same accesskey as the
+undoCloseTab.label so users will not need to learn new keyboard controls. -->
+<!ENTITY  undoCloseTabs.label                "Undo Close Tabs">
 <!ENTITY  closeTab.label                     "Close Tab">
 <!ENTITY  closeTab.accesskey                 "c">
 
 <!ENTITY  listAllTabs.label      "List all tabs">
 
 <!ENTITY tabCmd.label "New Tab">
 <!ENTITY tabCmd.accesskey "T">
 <!ENTITY tabCmd.commandkey "t">