Bug 1308061 - Part 1: Update SessionStore.jsm to notify when the list of closed objects changes, r?mikedeboer draft
authorBob Silverberg <bsilverberg@mozilla.com>
Wed, 09 Nov 2016 10:51:32 -0500
changeset 439812 f6f70e6c37d800c80344d0566b967c1ea10b8cde
parent 439707 2598a93e2e1a27f363af9fdd290cf4184cc51d48
child 440451 0c79dc02caf357dea24e79e6403f0b7b74c66379
child 440503 540ae7aba9787dbab5dd06fab75a811581ed54a5
push id36106
push userbmo:bob.silverberg@gmail.com
push dateWed, 16 Nov 2016 17:29:01 +0000
reviewersmikedeboer
bugs1308061
milestone53.0a1
Bug 1308061 - Part 1: Update SessionStore.jsm to notify when the list of closed objects changes, r?mikedeboer MozReview-Commit-ID: 7pBrvAhVQHP
browser/components/sessionstore/SessionStore.jsm
browser/components/sessionstore/test/browser.ini
browser/components/sessionstore/test/browser_closed_objects_changed_notifications_tabs.js
browser/components/sessionstore/test/browser_closed_objects_changed_notifications_windows.js
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -21,16 +21,17 @@ const TAB_STATE_WILL_RESTORE = 3;
 // A new window has just been restored. At this stage, tabs are generally
 // not restored.
 const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
 const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
 const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
 const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
 const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
 const NOTIFY_INITIATING_MANUAL_RESTORE = "sessionstore-initiating-manual-restore";
+const NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
 
 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
 
 // Maximum number of tabs to restore simultaneously. Previously controlled by
 // the browser.sessionstore.max_concurrent_tabs pref.
 const MAX_CONCURRENT_TAB_RESTORES = 3;
 
 // Amount (in CSS px) by which we allow window edges to be off-screen
@@ -1077,17 +1078,19 @@ var SessionStoreInternal = {
           this._restoreCount = aInitialState.windows ? aInitialState.windows.length : 0;
 
           // global data must be restored before restoreWindow is called so that
           // it happens before observers are notified
           this._globalState.setFromState(aInitialState);
 
           let overwrite = this._isCmdLineEmpty(aWindow, aInitialState);
           let options = {firstWindow: true, overwriteTabs: overwrite};
-          this.restoreWindows(aWindow, aInitialState, options);
+          if (this.restoreWindows(aWindow, aInitialState, options)) {
+            this._notifyOfClosedObjectsChange();
+          }
         }
       }
       else {
         // Nothing to restore, notify observers things are complete.
         Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
       }
     }
     // this window was opened by _openWindowWithState
@@ -1103,17 +1106,19 @@ var SessionStoreInternal = {
              aWindow.toolbar.visible) {
 
       // global data must be restored before restoreWindow is called so that
       // it happens before observers are notified
       this._globalState.setFromState(this._deferredInitialState);
 
       this._restoreCount = this._deferredInitialState.windows ?
         this._deferredInitialState.windows.length : 0;
-      this.restoreWindows(aWindow, this._deferredInitialState, {firstWindow: true});
+      if (this.restoreWindows(aWindow, this._deferredInitialState, {firstWindow: true})) {
+        this._notifyOfClosedObjectsChange();
+      }
       this._deferredInitialState = null;
     }
     else if (this._restoreLastWindow && aWindow.toolbar.visible &&
              this._closedWindows.length && !isPrivateWindow) {
 
       // default to the most-recently closed window
       // don't use popup windows
       let closedWindowState = null;
@@ -1141,28 +1146,28 @@ var SessionStoreInternal = {
           // These are our pinned tabs, which we should restore
           if (appTabsState.windows.length) {
             newWindowState = appTabsState.windows[0];
             delete newWindowState.__lastSessionWindowID;
           }
 
           // In case there were no unpinned tabs, remove the window from _closedWindows
           if (!normalTabsState.windows.length) {
-            this._closedWindows.splice(closedWindowIndex, 1);
+            this._removeClosedWindow(closedWindowIndex);
           }
           // Or update _closedWindows with the modified state
           else {
             delete normalTabsState.windows[0].__lastSessionWindowID;
             this._closedWindows[closedWindowIndex] = normalTabsState.windows[0];
           }
         }
         else {
           // If we're just restoring the window, make sure it gets removed from
           // _closedWindows.
-          this._closedWindows.splice(closedWindowIndex, 1);
+          this._removeClosedWindow(closedWindowIndex);
           newWindowState = closedWindowState;
           delete newWindowState.hidden;
         }
 
         if (newWindowState) {
           // Ensure that the window state isn't hidden
           this._restoreCount = 1;
           let state = { windows: [newWindowState] };
@@ -1470,18 +1475,19 @@ var SessionStoreInternal = {
         }
 
         // About to save the closed window, add a unique ID.
         winData.closedId = this._nextClosedId++;
 
         // Insert tabData at the right position.
         this._closedWindows.splice(index, 0, winData);
         this._capClosedWindows();
+        this._notifyOfClosedObjectsChange();
       } else if (!shouldStore && alreadyStored) {
-        this._closedWindows.splice(winIndex, 1);
+        this._removeClosedWindow(winIndex);
       }
     }
   },
 
   /**
    * On quit application granted
    */
   onQuitApplicationGranted: function ssi_onQuitApplicationGranted(syncShutdown=false) {
@@ -1633,48 +1639,58 @@ var SessionStoreInternal = {
 
     this._uninit();
   },
 
   /**
    * On purge of session history
    */
   onPurgeSessionHistory: function ssi_onPurgeSessionHistory() {
+    let notifyOfClosedObjectsChange = false;
     SessionFile.wipe();
     // If the browser is shutting down, simply return after clearing the
     // session data on disk as this notification fires after the
     // quit-application notification so the browser is about to exit.
     if (RunState.isQuitting)
       return;
     LastSession.clear();
 
     let openWindows = {};
     // Collect open windows.
     this._forEachBrowserWindow(({__SSi: id}) => openWindows[id] = true);
 
     // also clear all data about closed tabs and windows
     for (let ix in this._windows) {
       if (ix in openWindows) {
-        this._windows[ix]._closedTabs = [];
+        if (this._windows[ix]._closedTabs.length) {
+          this._windows[ix]._closedTabs = [];
+          notifyOfClosedObjectsChange = true;
+        }
       } else {
         delete this._windows[ix];
       }
     }
     // also clear all data about closed windows
-    this._closedWindows = [];
+    if (this._closedWindows.length) {
+      this._closedWindows = [];
+      notifyOfClosedObjectsChange = true;
+    }
     // give the tabbrowsers a chance to clear their histories first
     var win = this._getMostRecentBrowserWindow();
     if (win) {
       win.setTimeout(() => SessionSaver.run(), 0);
     } else if (RunState.isRunning) {
       SessionSaver.run();
     }
 
     this._clearRestoringWindows();
     this._saveableClosedWindowData = new WeakSet();
+    if (notifyOfClosedObjectsChange) {
+      this._notifyOfClosedObjectsChange();
+    }
   },
 
   /**
    * On purge of domain data
    * @param aData
    *        String domain data
    */
   onPurgeDomainData: function ssi_onPurgeDomainData(aData) {
@@ -1684,18 +1700,20 @@ var SessionStoreInternal = {
         return true;
       }
       return aEntry.children && aEntry.children.some(containsDomain, this);
     }
     // remove all closed tabs containing a reference to the given domain
     for (let ix in this._windows) {
       let closedTabs = this._windows[ix]._closedTabs;
       for (let i = closedTabs.length - 1; i >= 0; i--) {
-        if (closedTabs[i].state.entries.some(containsDomain, this))
+        if (closedTabs[i].state.entries.some(containsDomain, this)) {
           closedTabs.splice(i, 1);
+          this._notifyOfClosedObjectsChange();
+        }
       }
     }
     // remove all open & closed tabs containing a reference to the given
     // domain in closed windows
     for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) {
       let closedTabs = this._closedWindows[ix]._closedTabs;
       let openTabs = this._closedWindows[ix].tabs;
       let openTabCount = openTabs.length;
@@ -1735,24 +1753,33 @@ var SessionStoreInternal = {
    * @param aData
    *        String preference changed
    */
   onPrefChange: function ssi_onPrefChange(aData) {
     switch (aData) {
       // if the user decreases the max number of closed tabs they want
       // preserved update our internal states to match that max
       case "sessionstore.max_tabs_undo":
+        let notifyOfClosedObjectsChange = false;
         this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
         for (let ix in this._windows) {
-          this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length);
+          if (this._windows[ix]._closedTabs.length > this._max_tabs_undo) {
+            this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length);
+            notifyOfClosedObjectsChange = true;
+          }
+        }
+        if (notifyOfClosedObjectsChange) {
+          this._notifyOfClosedObjectsChange();
         }
         break;
       case "sessionstore.max_windows_undo":
         this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
-        this._capClosedWindows();
+        if (this._capClosedWindows()) {
+          this._notifyOfClosedObjectsChange();
+        }
         break;
     }
   },
 
   /**
    * save state when new tab is added
    * @param aWindow
    *        Window reference
@@ -1876,16 +1903,17 @@ var SessionStoreInternal = {
    * maximum of |this._max_tabs_undo| entries.
    *
    * @param closedTabs (array)
    *        The list of closed tabs for a window.
    * @param tabData (object)
    *        The tabData to be inserted.
    */
   saveClosedTabData(closedTabs, tabData) {
+    let notifyOfClosedObjectsChange = false;
     // Find the index of the first tab in the list
     // of closed tabs that was closed before our tab.
     let index = closedTabs.findIndex(tab => {
       return tab.closedAt < tabData.closedAt;
     });
 
     // If we found no tab closed before our
     // tab then just append it to the list.
@@ -1893,36 +1921,42 @@ var SessionStoreInternal = {
       index = closedTabs.length;
     }
 
     // About to save the closed tab, add a unique ID.
     tabData.closedId = this._nextClosedId++;
 
     // Insert tabData at the right position.
     closedTabs.splice(index, 0, tabData);
+    notifyOfClosedObjectsChange = true;
 
     // Truncate the list of closed tabs, if needed.
     if (closedTabs.length > this._max_tabs_undo) {
       closedTabs.splice(this._max_tabs_undo, closedTabs.length);
+    notifyOfClosedObjectsChange = true;
+    }
+    if (notifyOfClosedObjectsChange) {
+      this._notifyOfClosedObjectsChange();
     }
   },
 
   /**
    * Remove the closed tab data at |index| from the list of |closedTabs|. If
    * the tab's final message is still pending we will simply discard it when
    * it arrives so that the tab doesn't reappear in the list.
    *
    * @param closedTabs (array)
    *        The list of closed tabs for a window.
    * @param index (uint)
    *        The index of the tab to remove.
    */
   removeClosedTabData(closedTabs, index) {
     // Remove the given index from the list.
     let [closedTab] = closedTabs.splice(index, 1);
+    this._notifyOfClosedObjectsChange();
 
     // If the closed tab's state still has a .permanentKey property then we
     // haven't seen its final update message yet. Remove it from the map of
     // closed tabs so that we will simply discard its last messages and will
     // not add it back to the list of closed tabs again.
     if (closedTab.permanentKey) {
       this._closedTabs.delete(closedTab.permanentKey);
       this._closedWindowTabs.delete(closedTab.permanentKey);
@@ -2035,42 +2069,51 @@ var SessionStoreInternal = {
     }
   },
 
   // Clean up data that has been closed a long time ago.
   // Do not reschedule a save. This will wait for the next regular
   // save.
   onIdleDaily: function() {
     // Remove old closed windows
-    this._cleanupOldData([this._closedWindows]);
+    let somethingChanged = this._cleanupOldData([this._closedWindows]);
 
     // Remove closed tabs of closed windows
     this._cleanupOldData(this._closedWindows.map((winData) => winData._closedTabs));
 
     // Remove closed tabs of open windows
-    this._cleanupOldData(Object.keys(this._windows).map((key) => this._windows[key]._closedTabs));
+    somethingChanged = this._cleanupOldData(
+      Object.keys(this._windows).map((key) => this._windows[key]._closedTabs)) || somethingChanged;
+
+    // Notify if closed window or tab data changed.
+    if (somethingChanged) {
+      this._notifyOfClosedObjectsChange();
+    }
   },
 
   // Remove "old" data from an array
   _cleanupOldData: function(targets) {
     const TIME_TO_LIVE = this._prefBranch.getIntPref("sessionstore.cleanup.forget_closed_after");
     const now = Date.now();
+    let somethingChanged = false;
 
     for (let array of targets) {
       for (let i = array.length - 1; i >= 0; --i)  {
         let data = array[i];
         // Make sure that we have a timestamp to tell us when the target
         // has been closed. If we don't have a timestamp, default to a
         // safe timestamp: just now.
         data.closedAt = data.closedAt || now;
         if (now - data.closedAt > TIME_TO_LIVE) {
           array.splice(i, 1);
+          somethingChanged = true;
         }
       }
     }
+    return somethingChanged;
   },
 
   /* ........ nsISessionStore API .............. */
 
   getBrowserState: function ssi_getBrowserState() {
     let state = this.getCurrentState();
 
     // Don't include the last session state in getBrowserState().
@@ -2078,16 +2121,17 @@ var SessionStoreInternal = {
 
     // Don't include any deferred initial state.
     delete state.deferredInitialState;
 
     return JSON.stringify(state);
   },
 
   setBrowserState: function ssi_setBrowserState(aState) {
+    let notifyOfClosedObjectsChange = false;
     this._handleClosedWindows();
 
     try {
       var state = JSON.parse(aState);
     }
     catch (ex) { /* invalid state object - don't restore anything */ }
     if (!state) {
       throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG);
@@ -2112,27 +2156,33 @@ var SessionStoreInternal = {
     this._forEachBrowserWindow(function(aWindow) {
       if (aWindow != window) {
         aWindow.close();
         this.onClose(aWindow);
       }
     });
 
     // make sure closed window data isn't kept
-    this._closedWindows = [];
+    if (this._closedWindows.length) {
+      this._closedWindows = [];
+      notifyOfClosedObjectsChange = true;
+    }
 
     // determine how many windows are meant to be restored
     this._restoreCount = state.windows ? state.windows.length : 0;
 
     // global data must be restored before restoreWindow is called so that
     // it happens before observers are notified
     this._globalState.setFromState(state);
 
     // restore to the given state
-    this.restoreWindows(window, state, {overwriteTabs: true});
+    if (this.restoreWindows(window, state, {overwriteTabs: true}) ||
+      notifyOfClosedObjectsChange) {
+      this._notifyOfClosedObjectsChange();
+    }
   },
 
   getWindowState: function ssi_getWindowState(aWindow) {
     if ("__SSi" in aWindow) {
       return JSON.stringify(this._getWindowState(aWindow));
     }
 
     if (DyingWindowCache.has(aWindow)) {
@@ -2143,17 +2193,19 @@ var SessionStoreInternal = {
     throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
   },
 
   setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) {
     if (!aWindow.__SSi) {
       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
 
-    this.restoreWindows(aWindow, aState, {overwriteTabs: aOverwrite});
+    if (this.restoreWindows(aWindow, aState, {overwriteTabs: aOverwrite})) {
+      this._notifyOfClosedObjectsChange();
+    }
   },
 
   getTabState: function ssi_getTabState(aTab) {
     if (!aTab.ownerGlobal.__SSi) {
       throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
 
     let tabState = TabState.collect(aTab);
@@ -2330,34 +2382,34 @@ var SessionStoreInternal = {
   },
 
   undoCloseWindow: function ssi_undoCloseWindow(aIndex) {
     if (!(aIndex in this._closedWindows)) {
       throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
     }
 
     // reopen the window
-    let state = { windows: this._closedWindows.splice(aIndex, 1) };
+    let state = { windows: this._removeClosedWindow(aIndex) };
     delete state.windows[0].closedAt; // Window is now open.
 
     let window = this._openWindowWithState(state);
     this.windowToFocus = window;
     return window;
   },
 
   forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) {
     // default to the most-recently closed window
     aIndex = aIndex || 0;
     if (!(aIndex in this._closedWindows)) {
       throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
     }
 
     // remove closed window from the array
     let winData = this._closedWindows[aIndex];
-    this._closedWindows.splice(aIndex, 1);
+    this._removeClosedWindow(aIndex);
     this._saveableClosedWindowData.delete(winData);
   },
 
   getWindowValue: function ssi_getWindowValue(aWindow, aKey) {
     if ("__SSi" in aWindow) {
       var data = this._windows[aWindow.__SSi].extData || {};
       return data[aKey] || "";
     }
@@ -2563,16 +2615,17 @@ var SessionStoreInternal = {
         this._openWindowWithState({ windows: [winState] });
       }
     }
 
     // Merge closed windows from this session with ones from last session
     if (lastSessionState._closedWindows) {
       this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
       this._capClosedWindows();
+      this._notifyOfClosedObjectsChange();
     }
 
     if (lastSessionState.scratchpads) {
       ScratchpadManager.restoreSession(lastSessionState.scratchpads);
     }
 
     // Set data that persists between sessions
     this._recentCrashes = lastSessionState.session &&
@@ -3233,18 +3286,21 @@ var SessionStoreInternal = {
    * @param aState
    *        JS object or JSON string
    * @param aOptions
    *        {overwriteTabs: true} to overwrite existing tabs w/ new ones
    *        {isFollowUp: true} if this is not the restoration of the 1st window
    *        {firstWindow: true} if this is the first non-private window we're
    *                            restoring in this session, that might open an
    *                            external link as well
+   *
+   * @returns A boolean indicating whether to notify of closed objects changing.
    */
   restoreWindows: function ssi_restoreWindows(aWindow, aState, aOptions = {}) {
+    let notifyOfClosedObjectsChange = false;
     let isFollowUp = aOptions && aOptions.isFollowUp;
 
     if (isFollowUp) {
       this.windowToFocus = aWindow;
     }
 
     // initialize window if necessary
     if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
@@ -3258,16 +3314,17 @@ var SessionStoreInternal = {
       debug(ex);
       this._sendRestoreCompletedNotifications();
       return;
     }
 
     // Restore closed windows if any.
     if (root._closedWindows) {
       this._closedWindows = root._closedWindows;
+      notifyOfClosedObjectsChange = true;
     }
 
     // We're done here if there are no windows.
     if (!root.windows || !root.windows.length) {
       this._sendRestoreCompletedNotifications();
       return;
     }
 
@@ -3288,16 +3345,18 @@ var SessionStoreInternal = {
       }
     }
 
     this.restoreWindow(aWindow, root.windows[0], aOptions);
 
     if (aState.scratchpads) {
       ScratchpadManager.restoreSession(aState.scratchpads);
     }
+
+    return notifyOfClosedObjectsChange;
   },
 
   /**
    * Manage history restoration for a window
    * @param aWindow
    *        Window to restore the tabs into
    * @param aTabs
    *        Array of tab references
@@ -3766,16 +3825,41 @@ var SessionStoreInternal = {
     }
 
     SessionSaver.runDelayed();
   },
 
   /* ........ Auxiliary Functions .............. */
 
   /**
+   * Remove a closed window from the list of closed
+   * windows and notify of the change.
+   *
+   * @param index
+   *        The index of the window in this._closedWindows.
+   *
+   * @returns Array of closed windows.
+   */
+  _removeClosedWindow(index) {
+    let windows = this._closedWindows.splice(index, 1);
+    this._notifyOfClosedObjectsChange();
+    return windows;
+  },
+
+  /**
+   * Notifies observers that the list of closed tabs and/or windows has changed.
+   * Waits a tick to allow SessionStorage a chance to register the change.
+   */
+  _notifyOfClosedObjectsChange() {
+    setTimeout(() => {
+      Services.obs.notifyObservers(null, NOTIFY_CLOSED_OBJECTS_CHANGED, null);
+    }, 0);
+  },
+
+  /**
    * Determines whether or not a tab that is being restored needs
    * to have its remoteness flipped first.
    *
    * @param (object) with the following properties:
    *
    *        tabbrowser (<xul:tabbrowser>):
    *          The tabbrowser that the browser belongs to.
    *
@@ -4313,31 +4397,38 @@ var SessionStoreInternal = {
     }
     return aString;
   },
 
   /**
    * Resize this._closedWindows to the value of the pref, except in the case
    * where we don't have any non-popup windows on Windows and Linux. Then we must
    * resize such that we have at least one non-popup window.
+   *
+   * @returns A boolean indicating whether to notify of closed objects changing.
    */
   _capClosedWindows : function ssi_capClosedWindows() {
+    let notifyOfClosedObjectsChange = false;
     if (this._closedWindows.length <= this._max_windows_undo)
       return;
     let spliceTo = this._max_windows_undo;
     if (AppConstants.platform != "macosx") {
       let normalWindowIndex = 0;
       // try to find a non-popup window in this._closedWindows
       while (normalWindowIndex < this._closedWindows.length &&
              !!this._closedWindows[normalWindowIndex].isPopup)
         normalWindowIndex++;
       if (normalWindowIndex >= this._max_windows_undo)
         spliceTo = normalWindowIndex + 1;
     }
-    this._closedWindows.splice(spliceTo, this._closedWindows.length);
+    if (spliceTo < this._closedWindows.length) {
+      this._closedWindows.splice(spliceTo, this._closedWindows.length);
+      notifyOfClosedObjectsChange = true;
+    }
+    return notifyOfClosedObjectsChange;
   },
 
   /**
    * Clears the set of windows that are "resurrected" before writing to disk to
    * make closing windows one after the other until shutdown work as expected.
    *
    * This function should only be called when we are sure that there has been
    * a user action that indicates the browser is actively being used and all
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -232,8 +232,11 @@ run-if = e10s
 [browser_remoteness_flip_on_restore.js]
 run-if = e10s
 [browser_background_tab_crash.js]
 run-if = e10s && crashreporter
 
 # Disabled on debug for frequent intermittent failures:
 [browser_undoCloseById.js]
 skip-if = debug
+
+[browser_closed_objects_changed_notifications_tabs.js]
+[browser_closed_objects_changed_notifications_windows.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_closed_objects_changed_notifications_tabs.js
@@ -0,0 +1,130 @@
+"use strict";
+
+/**
+ * This test is for the sessionstore-closed-objects-changed notifications.
+ */
+
+Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
+
+let notificationsCount = 0;
+let topic = "sessionstore-closed-objects-changed";
+let maxTabsUndoPrefName = "browser.sessionstore.max_tabs_undo";
+
+function* openWindow(url) {
+  let win = yield promiseNewWindowLoaded();
+  let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+  win.gBrowser.selectedBrowser.loadURIWithFlags(url, flags);
+  yield promiseBrowserLoaded(win.gBrowser.selectedBrowser, true, url);
+  return win;
+}
+
+function closeWindow(win) {
+  yield BrowserTestUtils.closeWindow(win);
+  // Wait 20 ms to allow SessionStorage a chance to register the closed window.
+  yield new Promise(resolve => setTimeout(resolve, 20));
+}
+
+function* openTab(window, url) {
+  let tab = window.gBrowser.addTab(url);
+  yield promiseBrowserLoaded(tab.linkedBrowser, true, url);
+  yield TabStateFlusher.flush(tab.linkedBrowser);
+  return tab;
+}
+
+function openAndCloseTab(window, url) {
+  let tab = yield openTab(window, url);
+  yield promiseRemoveTab(tab);
+}
+
+function countingObserver() {
+  notificationsCount++;
+}
+
+function assertNotificationCount(count) {
+  is(notificationsCount, count, "The expected number of notifications was received.");
+}
+
+function* awaitNotification(callback) {
+  let awaitNotification = TestUtils.topicObserved(topic);
+  executeSoon(callback);
+  yield awaitNotification;
+}
+
+add_task(function* test_closedObjectsChangedNotifications() {
+  // Forget any previous closed windows or tabs from other tests that may have
+  // run in the same session.
+  let awaitPurge = TestUtils.topicObserved("browser:purge-session-history");
+  Services.obs.notifyObservers(null, "browser:purge-session-history", 0);
+  yield awaitPurge;
+  // The purge needs time to settle before starting to wait for other notifications.
+  yield new Promise(resolve => setTimeout(resolve, 20));
+
+  // Add an observer to count the number of notifications.
+  Services.obs.addObserver(countingObserver, topic, false);
+
+  // Open a new window.
+  let win = yield openWindow("about:robots");
+
+  info("Opening and closing a tab.");
+  yield openAndCloseTab(win, "about:mozilla");
+  assertNotificationCount(1);
+
+  info("Opening and closing a second tab.");
+  yield openAndCloseTab(win, "about:mozilla");
+  assertNotificationCount(2);
+
+  info(`Changing the ${maxTabsUndoPrefName} pref.`);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(maxTabsUndoPrefName);
+  });
+  yield awaitNotification(() => {Services.prefs.setIntPref(maxTabsUndoPrefName, 1);});
+  assertNotificationCount(3);
+
+  info("Undoing close of remaining closed tab.");
+  let tab = SessionStore.undoCloseTab(win, 0);
+  yield promiseTabRestored(tab);
+  assertNotificationCount(4);
+
+  info("Closing tab again.");
+  yield promiseRemoveTab(tab);
+  assertNotificationCount(5);
+
+  info("Purging session history.");
+  yield awaitNotification(() => {Services.obs.notifyObservers(null, "browser:purge-session-history", 0);});
+  assertNotificationCount(6);
+
+  info("Opening and closing another tab.");
+  yield openAndCloseTab(win, "http://example.com/");
+  assertNotificationCount(7);
+
+  info("Purging domain data with no matches.")
+  Services.obs.notifyObservers(null, "browser:purge-domain-data", "mozilla.com");
+  assertNotificationCount(7);
+
+  info("Purging domain data with matches.")
+  yield awaitNotification(() => {Services.obs.notifyObservers(null, "browser:purge-domain-data", "example.com");});
+  assertNotificationCount(8);
+
+  info("Opening and closing another tab.");
+  yield openAndCloseTab(win, "http://example.com/");
+  assertNotificationCount(9);
+
+  /* TODO: I cannot seem to get this to work.
+     `yield promiseBrowserState(state)` never yields, and I'm not sure why.
+
+  info("Setting browser state to trigger change onIdleDaily.")
+  let state = Cu.cloneInto(JSON.parse(ss.getBrowserState()), {});
+  state.windows[1]._closedTabs[0].closedAt = 1;
+  yield promiseBrowserState(state);
+  info("State change done!")
+
+  info("Sending idle-daily");
+  yield awaitNotification(() => {Services.obs.notifyObservers(null, "idle-daily", "");});
+  assertNotificationCount(10);
+   */
+
+  yield closeWindow(win);
+  assertNotificationCount(10);
+
+  Services.obs.removeObserver(countingObserver, topic);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_closed_objects_changed_notifications_windows.js
@@ -0,0 +1,119 @@
+"use strict";
+
+/**
+ * This test is for the sessionstore-closed-objects-changed notifications.
+ */
+
+Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
+
+let notificationsCount = 0;
+let topic = "sessionstore-closed-objects-changed";
+let maxWindowsUndoPrefName = "browser.sessionstore.max_windows_undo";
+
+function* openWindow(url) {
+  let win = yield promiseNewWindowLoaded();
+  let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+  win.gBrowser.selectedBrowser.loadURIWithFlags(url, flags);
+  yield promiseBrowserLoaded(win.gBrowser.selectedBrowser, true, url);
+  return win;
+}
+
+function closeWindow(win) {
+  yield BrowserTestUtils.closeWindow(win);
+  // Wait 20 ms to allow SessionStorage a chance to register the closed window.
+  yield new Promise(resolve => setTimeout(resolve, 20));
+}
+
+function* openAndCloseWindow(url) {
+  let win = yield openWindow(url);
+  yield closeWindow(win);
+}
+
+function countingObserver() {
+  notificationsCount++;
+}
+
+function assertNotificationCount(count) {
+  is(notificationsCount, count, "The expected number of notifications was received.");
+}
+
+function* awaitNotification(callback) {
+  let awaitNotification = TestUtils.topicObserved(topic);
+  executeSoon(callback);
+  yield awaitNotification;
+}
+
+add_task(function* test_closedObjectsChangedNotifications() {
+  // Forget any previous closed windows or tabs from other tests that may have
+  // run in the same session.
+  let awaitPurge = TestUtils.topicObserved("browser:purge-session-history");
+  Services.obs.notifyObservers(null, "browser:purge-session-history", 0);
+  yield awaitPurge;
+  // The purge needs time to settle before starting to wait for other notifications.
+  yield new Promise(resolve => setTimeout(resolve, 20));
+
+  // Add an observer to count the number of notifications.
+  Services.obs.addObserver(countingObserver, topic, false);
+
+  info("Opening and closing initial window.");
+  yield openAndCloseWindow("about:robots");
+  assertNotificationCount(1);
+
+  // Store state with a single closed window for use in later tests.
+  let closedState = Cu.cloneInto(JSON.parse(ss.getBrowserState()), {});
+
+  info("Undoing close of initial window.");
+  let win = SessionStore.undoCloseWindow(0);
+  yield promiseDelayedStartupFinished(win);
+  assertNotificationCount(2);
+
+  // Open a second window.
+  let win2 = yield openWindow("about:mozilla");
+
+  info("Closing both windows.");
+  yield closeWindow(win);
+  assertNotificationCount(3);
+  yield closeWindow(win2);
+  assertNotificationCount(4);
+
+  info(`Changing the ${maxWindowsUndoPrefName} pref.`);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(maxWindowsUndoPrefName);
+  });
+  yield awaitNotification(() => {Services.prefs.setIntPref(maxWindowsUndoPrefName, 1);});
+  assertNotificationCount(5);
+
+  info("Forgetting a closed window.");
+  yield awaitNotification(() => {SessionStore.forgetClosedWindow();});
+  assertNotificationCount(6);
+
+  info("Opening and closing another window.");
+  yield openAndCloseWindow("about:robots");
+  assertNotificationCount(7);
+
+  info("Setting browser state to trigger change onIdleDaily.")
+  let state = Cu.cloneInto(JSON.parse(ss.getBrowserState()), {});
+  state._closedWindows[0].closedAt = 1;
+  yield promiseBrowserState(state);
+  assertNotificationCount(8);
+
+  info("Sending idle-daily");
+  yield awaitNotification(() => {Services.obs.notifyObservers(null, "idle-daily", "");});
+  assertNotificationCount(9);
+
+  info("Opening and closing another window.");
+  yield openAndCloseWindow("about:robots");
+  assertNotificationCount(10);
+
+  info("Purging session history.");
+  yield awaitNotification(() => {Services.obs.notifyObservers(null, "browser:purge-session-history", 0);});
+  assertNotificationCount(11);
+
+  info("Setting window state.")
+  win = yield openWindow("about:mozilla");
+  yield awaitNotification(() => {SessionStore.setWindowState(win, closedState);});
+  assertNotificationCount(12);
+
+  Services.obs.removeObserver(countingObserver, topic);
+  yield BrowserTestUtils.closeWindow(win);
+});