diff options
Diffstat (limited to 'browser/components/sessionstore/test/head.js')
-rw-r--r-- | browser/components/sessionstore/test/head.js | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js new file mode 100644 index 000000000..5a8c5dbfc --- /dev/null +++ b/browser/components/sessionstore/test/head.js @@ -0,0 +1,564 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TAB_STATE_NEEDS_RESTORE = 1; +const TAB_STATE_RESTORING = 2; + +const ROOT = getRootDirectory(gTestPath); +const HTTPROOT = ROOT.replace("chrome://mochitests/content/", "http://example.com/"); +const FRAME_SCRIPTS = [ + ROOT + "content.js", + ROOT + "content-forms.js" +]; + +var mm = Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); + +for (let script of FRAME_SCRIPTS) { + mm.loadFrameScript(script, true); +} + +registerCleanupFunction(() => { + for (let script of FRAME_SCRIPTS) { + mm.removeDelayedFrameScript(script, true); + } +}); + +const {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +const {SessionStore} = Cu.import("resource:///modules/sessionstore/SessionStore.jsm", {}); +const {SessionSaver} = Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", {}); +const {SessionFile} = Cu.import("resource:///modules/sessionstore/SessionFile.jsm", {}); +const {TabState} = Cu.import("resource:///modules/sessionstore/TabState.jsm", {}); +const {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {}); + +const ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + +// Some tests here assume that all restored tabs are loaded without waiting for +// the user to bring them to the foreground. We ensure this by resetting the +// related preference (see the "firefox.js" defaults file for details). +Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false); +registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand"); +}); + +// Obtain access to internals +Services.prefs.setBoolPref("browser.sessionstore.debug", true); +registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.sessionstore.debug"); +}); + + +// This kicks off the search service used on about:home and allows the +// session restore tests to be run standalone without triggering errors. +Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs; + +function provideWindow(aCallback, aURL, aFeatures) { + function callbackSoon(aWindow) { + executeSoon(function executeCallbackSoon() { + aCallback(aWindow); + }); + } + + let win = openDialog(getBrowserURL(), "", aFeatures || "chrome,all,dialog=no", aURL || "about:blank"); + whenWindowLoaded(win, function onWindowLoaded(aWin) { + if (!aURL) { + info("Loaded a blank window."); + callbackSoon(aWin); + return; + } + + aWin.gBrowser.selectedBrowser.addEventListener("load", function selectedBrowserLoadListener() { + aWin.gBrowser.selectedBrowser.removeEventListener("load", selectedBrowserLoadListener, true); + callbackSoon(aWin); + }, true); + }); +} + +// This assumes that tests will at least have some state/entries +function waitForBrowserState(aState, aSetStateCallback) { + if (typeof aState == "string") { + aState = JSON.parse(aState); + } + if (typeof aState != "object") { + throw new TypeError("Argument must be an object or a JSON representation of an object"); + } + let windows = [window]; + let tabsRestored = 0; + let expectedTabsRestored = 0; + let expectedWindows = aState.windows.length; + let windowsOpen = 1; + let listening = false; + let windowObserving = false; + let restoreHiddenTabs = Services.prefs.getBoolPref( + "browser.sessionstore.restore_hidden_tabs"); + + aState.windows.forEach(function (winState) { + winState.tabs.forEach(function (tabState) { + if (restoreHiddenTabs || !tabState.hidden) + expectedTabsRestored++; + }); + }); + + // There must be only hidden tabs and restoreHiddenTabs = false. We still + // expect one of them to be restored because it gets shown automatically. + if (!expectedTabsRestored) + expectedTabsRestored = 1; + + function onSSTabRestored(aEvent) { + if (++tabsRestored == expectedTabsRestored) { + // Remove the event listener from each window + windows.forEach(function(win) { + win.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true); + }); + listening = false; + info("running " + aSetStateCallback.name); + executeSoon(aSetStateCallback); + } + } + + // Used to add our listener to further windows so we can catch SSTabRestored + // coming from them when creating a multi-window state. + function windowObserver(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + let newWindow = aSubject.QueryInterface(Ci.nsIDOMWindow); + newWindow.addEventListener("load", function() { + newWindow.removeEventListener("load", arguments.callee, false); + + if (++windowsOpen == expectedWindows) { + Services.ww.unregisterNotification(windowObserver); + windowObserving = false; + } + + // Track this window so we can remove the progress listener later + windows.push(newWindow); + // Add the progress listener + newWindow.gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true); + }, false); + } + } + + // We only want to register the notification if we expect more than 1 window + if (expectedWindows > 1) { + registerCleanupFunction(function() { + if (windowObserving) { + Services.ww.unregisterNotification(windowObserver); + } + }); + windowObserving = true; + Services.ww.registerNotification(windowObserver); + } + + registerCleanupFunction(function() { + if (listening) { + windows.forEach(function(win) { + win.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true); + }); + } + }); + // Add the event listener for this window as well. + listening = true; + gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true); + + // Ensure setBrowserState() doesn't remove the initial tab. + gBrowser.selectedTab = gBrowser.tabs[0]; + + // Finally, call setBrowserState + ss.setBrowserState(JSON.stringify(aState)); +} + +function promiseBrowserState(aState) { + return new Promise(resolve => waitForBrowserState(aState, resolve)); +} + +function promiseTabState(tab, state) { + if (typeof(state) != "string") { + state = JSON.stringify(state); + } + + let promise = promiseTabRestored(tab); + ss.setTabState(tab, state); + return promise; +} + +/** + * Wait for a content -> chrome message. + */ +function promiseContentMessage(browser, name) { + let mm = browser.messageManager; + + return new Promise(resolve => { + function removeListener() { + mm.removeMessageListener(name, listener); + } + + function listener(msg) { + removeListener(); + resolve(msg.data); + } + + mm.addMessageListener(name, listener); + registerCleanupFunction(removeListener); + }); +} + +function waitForTopic(aTopic, aTimeout, aCallback) { + let observing = false; + function removeObserver() { + if (!observing) + return; + Services.obs.removeObserver(observer, aTopic); + observing = false; + } + + let timeout = setTimeout(function () { + removeObserver(); + aCallback(false); + }, aTimeout); + + function observer(aSubject, aTopic, aData) { + removeObserver(); + timeout = clearTimeout(timeout); + executeSoon(() => aCallback(true)); + } + + registerCleanupFunction(function() { + removeObserver(); + if (timeout) { + clearTimeout(timeout); + } + }); + + observing = true; + Services.obs.addObserver(observer, aTopic, false); +} + +/** + * Wait until session restore has finished collecting its data and is + * has written that data ("sessionstore-state-write-complete"). + * + * @param {function} aCallback If sessionstore-state-write-complete is sent + * within buffering interval + 100 ms, the callback is passed |true|, + * otherwise, it is passed |false|. + */ +function waitForSaveState(aCallback) { + let timeout = 100 + + Services.prefs.getIntPref("browser.sessionstore.interval"); + return waitForTopic("sessionstore-state-write-complete", timeout, aCallback); +} +function promiseSaveState() { + return new Promise(resolve => { + waitForSaveState(isSuccessful => { + if (!isSuccessful) { + throw new Error("timeout"); + } + + resolve(); + }); + }); +} +function forceSaveState() { + return SessionSaver.run(); +} + +function promiseRecoveryFileContents() { + let promise = forceSaveState(); + return promise.then(function() { + return OS.File.read(SessionFile.Paths.recovery, { encoding: "utf-8" }); + }); +} + +var promiseForEachSessionRestoreFile = Task.async(function*(cb) { + for (let key of SessionFile.Paths.loadOrder) { + let data = ""; + try { + data = yield OS.File.read(SessionFile.Paths[key], { encoding: "utf-8" }); + } catch (ex) { + // Ignore missing files + if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) { + throw ex; + } + } + cb(data, key); + } +}); + +function promiseBrowserLoaded(aBrowser, ignoreSubFrames = true, wantLoad = null) { + return BrowserTestUtils.browserLoaded(aBrowser, !ignoreSubFrames, wantLoad); +} + +function whenWindowLoaded(aWindow, aCallback = next) { + aWindow.addEventListener("load", function windowLoadListener() { + aWindow.removeEventListener("load", windowLoadListener, false); + executeSoon(function executeWhenWindowLoaded() { + aCallback(aWindow); + }); + }, false); +} +function promiseWindowLoaded(aWindow) { + return new Promise(resolve => whenWindowLoaded(aWindow, resolve)); +} + +var gUniqueCounter = 0; +function r() { + return Date.now() + "-" + (++gUniqueCounter); +} + +function* BrowserWindowIterator() { + let windowsEnum = Services.wm.getEnumerator("navigator:browser"); + while (windowsEnum.hasMoreElements()) { + let currentWindow = windowsEnum.getNext(); + if (!currentWindow.closed) { + yield currentWindow; + } + } +} + +var gWebProgressListener = { + _callback: null, + + setCallback: function (aCallback) { + if (!this._callback) { + window.gBrowser.addTabsProgressListener(this); + } + this._callback = aCallback; + }, + + unsetCallback: function () { + if (this._callback) { + this._callback = null; + window.gBrowser.removeTabsProgressListener(this); + } + }, + + onStateChange: function (aBrowser, aWebProgress, aRequest, + aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { + this._callback(aBrowser); + } + } +}; + +registerCleanupFunction(function () { + gWebProgressListener.unsetCallback(); +}); + +var gProgressListener = { + _callback: null, + + setCallback: function (callback) { + Services.obs.addObserver(this, "sessionstore-debug-tab-restored", false); + this._callback = callback; + }, + + unsetCallback: function () { + if (this._callback) { + this._callback = null; + Services.obs.removeObserver(this, "sessionstore-debug-tab-restored"); + } + }, + + observe: function (browser, topic, data) { + gProgressListener.onRestored(browser); + }, + + onRestored: function (browser) { + if (browser.__SS_restoreState == TAB_STATE_RESTORING) { + let args = [browser].concat(gProgressListener._countTabs()); + gProgressListener._callback.apply(gProgressListener, args); + } + }, + + _countTabs: function () { + let needsRestore = 0, isRestoring = 0, wasRestored = 0; + + for (let win of BrowserWindowIterator()) { + for (let i = 0; i < win.gBrowser.tabs.length; i++) { + let browser = win.gBrowser.tabs[i].linkedBrowser; + if (!browser.__SS_restoreState) + wasRestored++; + else if (browser.__SS_restoreState == TAB_STATE_RESTORING) + isRestoring++; + else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) + needsRestore++; + } + } + return [needsRestore, isRestoring, wasRestored]; + } +}; + +registerCleanupFunction(function () { + gProgressListener.unsetCallback(); +}); + +// Close all but our primary window. +function promiseAllButPrimaryWindowClosed() { + let windows = []; + for (let win of BrowserWindowIterator()) { + if (win != window) { + windows.push(win); + } + } + + return Promise.all(windows.map(BrowserTestUtils.closeWindow)); +} + +// Forget all closed windows. +function forgetClosedWindows() { + while (ss.getClosedWindowCount() > 0) { + ss.forgetClosedWindow(0); + } +} + +/** + * When opening a new window it is not sufficient to wait for its load event. + * We need to use whenDelayedStartupFinshed() here as the browser window's + * delayedStartup() routine is executed one tick after the window's load event + * has been dispatched. browser-delayed-startup-finished might be deferred even + * further if parts of the window's initialization process take more time than + * expected (e.g. reading a big session state from disk). + */ +function whenNewWindowLoaded(aOptions, aCallback) { + let features = ""; + let url = "about:blank"; + + if (aOptions && aOptions.private || false) { + features = ",private"; + url = "about:privatebrowsing"; + } + + let win = openDialog(getBrowserURL(), "", "chrome,all,dialog=no" + features, url); + let delayedStartup = promiseDelayedStartupFinished(win); + + let browserLoaded = new Promise(resolve => { + if (url == "about:blank") { + resolve(); + return; + } + + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + let browser = win.gBrowser.selectedBrowser; + promiseBrowserLoaded(browser).then(resolve); + }); + }); + + Promise.all([delayedStartup, browserLoaded]).then(() => aCallback(win)); +} +function promiseNewWindowLoaded(aOptions) { + return new Promise(resolve => whenNewWindowLoaded(aOptions, resolve)); +} + +/** + * This waits for the browser-delayed-startup-finished notification of a given + * window. It indicates that the windows has loaded completely and is ready to + * be used for testing. + */ +function whenDelayedStartupFinished(aWindow, aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + executeSoon(aCallback); + } + }, "browser-delayed-startup-finished", false); +} +function promiseDelayedStartupFinished(aWindow) { + return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve)); +} + +function promiseEvent(element, eventType, isCapturing = false) { + return new Promise(resolve => { + element.addEventListener(eventType, function listener(event) { + element.removeEventListener(eventType, listener, isCapturing); + resolve(event); + }, isCapturing); + }); +} + +function promiseTabRestored(tab) { + return promiseEvent(tab, "SSTabRestored"); +} + +function promiseTabRestoring(tab) { + return promiseEvent(tab, "SSTabRestoring"); +} + +function sendMessage(browser, name, data = {}) { + browser.messageManager.sendAsyncMessage(name, data); + return promiseContentMessage(browser, name); +} + +// This creates list of functions that we will map to their corresponding +// ss-test:* messages names. Those will be sent to the frame script and +// be used to read and modify form data. +const FORM_HELPERS = [ + "getTextContent", + "getInputValue", "setInputValue", + "getInputChecked", "setInputChecked", + "getSelectedIndex", "setSelectedIndex", + "getMultipleSelected", "setMultipleSelected", + "getFileNameArray", "setFileNameArray", +]; + +for (let name of FORM_HELPERS) { + let msg = "ss-test:" + name; + this[name] = (browser, data) => sendMessage(browser, msg, data); +} + +// Removes the given tab immediately and returns a promise that resolves when +// all pending status updates (messages) of the closing tab have been received. +function promiseRemoveTab(tab) { + return BrowserTestUtils.removeTab(tab); +} + +// Write DOMSessionStorage data to the given browser. +function modifySessionStorage(browser, data, options = {}) { + return ContentTask.spawn(browser, [data, options], function* ([data, options]) { + let frame = content; + if (options && "frameIndex" in options) { + frame = content.frames[options.frameIndex]; + } + + let keys = new Set(Object.keys(data)); + let storage = frame.sessionStorage; + + return new Promise(resolve => { + addEventListener("MozSessionStorageChanged", function onStorageChanged(event) { + if (event.storageArea == storage) { + keys.delete(event.key); + } + + if (keys.size == 0) { + removeEventListener("MozSessionStorageChanged", onStorageChanged, true); + resolve(); + } + }, true); + + for (let key of keys) { + frame.sessionStorage[key] = data[key]; + } + }); + }); +} + +function pushPrefs(...aPrefs) { + return new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve); + }); +} + +function popPrefs() { + return new Promise(resolve => { + SpecialPowers.popPrefEnv(resolve); + }); +} + +function* checkScroll(tab, expected, msg) { + let browser = tab.linkedBrowser; + yield TabStateFlusher.flush(browser); + + let scroll = JSON.parse(ss.getTabState(tab)).scroll || null; + is(JSON.stringify(scroll), JSON.stringify(expected), msg); +} |