--- 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
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);
+});