michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const TAB_STATE_NEEDS_RESTORE = 1; michael@0: const TAB_STATE_RESTORING = 2; michael@0: michael@0: const ROOT = getRootDirectory(gTestPath); michael@0: const FRAME_SCRIPTS = [ michael@0: ROOT + "content.js", michael@0: ROOT + "content-forms.js" michael@0: ]; michael@0: michael@0: let mm = Cc["@mozilla.org/globalmessagemanager;1"] michael@0: .getService(Ci.nsIMessageListenerManager); michael@0: michael@0: for (let script of FRAME_SCRIPTS) { michael@0: mm.loadFrameScript(script, true); michael@0: } michael@0: michael@0: mm.addMessageListener("SessionStore:setupSyncHandler", onSetupSyncHandler); michael@0: michael@0: /** michael@0: * This keeps track of all SyncHandlers passed to chrome from frame scripts. michael@0: * We need this to let tests communicate with frame scripts and cause (a)sync michael@0: * flushes. michael@0: */ michael@0: let SyncHandlers = new WeakMap(); michael@0: function onSetupSyncHandler(msg) { michael@0: SyncHandlers.set(msg.target, msg.objects.handler); michael@0: } michael@0: michael@0: registerCleanupFunction(() => { michael@0: for (let script of FRAME_SCRIPTS) { michael@0: mm.removeDelayedFrameScript(script, true); michael@0: } michael@0: mm.removeMessageListener("SessionStore:setupSyncHandler", onSetupSyncHandler); michael@0: }); michael@0: michael@0: let tmp = {}; michael@0: Cu.import("resource://gre/modules/Promise.jsm", tmp); michael@0: Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp); michael@0: Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", tmp); michael@0: let {Promise, SessionStore, SessionSaver} = tmp; michael@0: michael@0: let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); michael@0: michael@0: // Some tests here assume that all restored tabs are loaded without waiting for michael@0: // the user to bring them to the foreground. We ensure this by resetting the michael@0: // related preference (see the "firefox.js" defaults file for details). michael@0: Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false); michael@0: registerCleanupFunction(function () { michael@0: Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand"); michael@0: }); michael@0: michael@0: // Obtain access to internals michael@0: Services.prefs.setBoolPref("browser.sessionstore.debug", true); michael@0: registerCleanupFunction(function () { michael@0: Services.prefs.clearUserPref("browser.sessionstore.debug"); michael@0: }); michael@0: michael@0: michael@0: // This kicks off the search service used on about:home and allows the michael@0: // session restore tests to be run standalone without triggering errors. michael@0: Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs; michael@0: michael@0: function provideWindow(aCallback, aURL, aFeatures) { michael@0: function callbackSoon(aWindow) { michael@0: executeSoon(function executeCallbackSoon() { michael@0: aCallback(aWindow); michael@0: }); michael@0: } michael@0: michael@0: let win = openDialog(getBrowserURL(), "", aFeatures || "chrome,all,dialog=no", aURL); michael@0: whenWindowLoaded(win, function onWindowLoaded(aWin) { michael@0: if (!aURL) { michael@0: info("Loaded a blank window."); michael@0: callbackSoon(aWin); michael@0: return; michael@0: } michael@0: michael@0: aWin.gBrowser.selectedBrowser.addEventListener("load", function selectedBrowserLoadListener() { michael@0: aWin.gBrowser.selectedBrowser.removeEventListener("load", selectedBrowserLoadListener, true); michael@0: callbackSoon(aWin); michael@0: }, true); michael@0: }); michael@0: } michael@0: michael@0: // This assumes that tests will at least have some state/entries michael@0: function waitForBrowserState(aState, aSetStateCallback) { michael@0: let windows = [window]; michael@0: let tabsRestored = 0; michael@0: let expectedTabsRestored = 0; michael@0: let expectedWindows = aState.windows.length; michael@0: let windowsOpen = 1; michael@0: let listening = false; michael@0: let windowObserving = false; michael@0: let restoreHiddenTabs = Services.prefs.getBoolPref( michael@0: "browser.sessionstore.restore_hidden_tabs"); michael@0: michael@0: aState.windows.forEach(function (winState) { michael@0: winState.tabs.forEach(function (tabState) { michael@0: if (restoreHiddenTabs || !tabState.hidden) michael@0: expectedTabsRestored++; michael@0: }); michael@0: }); michael@0: michael@0: // There must be only hidden tabs and restoreHiddenTabs = false. We still michael@0: // expect one of them to be restored because it gets shown automatically. michael@0: if (!expectedTabsRestored) michael@0: expectedTabsRestored = 1; michael@0: michael@0: function onSSTabRestored(aEvent) { michael@0: if (++tabsRestored == expectedTabsRestored) { michael@0: // Remove the event listener from each window michael@0: windows.forEach(function(win) { michael@0: win.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true); michael@0: }); michael@0: listening = false; michael@0: info("running " + aSetStateCallback.name); michael@0: executeSoon(aSetStateCallback); michael@0: } michael@0: } michael@0: michael@0: // Used to add our listener to further windows so we can catch SSTabRestored michael@0: // coming from them when creating a multi-window state. michael@0: function windowObserver(aSubject, aTopic, aData) { michael@0: if (aTopic == "domwindowopened") { michael@0: let newWindow = aSubject.QueryInterface(Ci.nsIDOMWindow); michael@0: newWindow.addEventListener("load", function() { michael@0: newWindow.removeEventListener("load", arguments.callee, false); michael@0: michael@0: if (++windowsOpen == expectedWindows) { michael@0: Services.ww.unregisterNotification(windowObserver); michael@0: windowObserving = false; michael@0: } michael@0: michael@0: // Track this window so we can remove the progress listener later michael@0: windows.push(newWindow); michael@0: // Add the progress listener michael@0: newWindow.gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true); michael@0: }, false); michael@0: } michael@0: } michael@0: michael@0: // We only want to register the notification if we expect more than 1 window michael@0: if (expectedWindows > 1) { michael@0: registerCleanupFunction(function() { michael@0: if (windowObserving) { michael@0: Services.ww.unregisterNotification(windowObserver); michael@0: } michael@0: }); michael@0: windowObserving = true; michael@0: Services.ww.registerNotification(windowObserver); michael@0: } michael@0: michael@0: registerCleanupFunction(function() { michael@0: if (listening) { michael@0: windows.forEach(function(win) { michael@0: win.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true); michael@0: }); michael@0: } michael@0: }); michael@0: // Add the event listener for this window as well. michael@0: listening = true; michael@0: gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true); michael@0: michael@0: // Ensure setBrowserState() doesn't remove the initial tab. michael@0: gBrowser.selectedTab = gBrowser.tabs[0]; michael@0: michael@0: // Finally, call setBrowserState michael@0: ss.setBrowserState(JSON.stringify(aState)); michael@0: } michael@0: michael@0: // Doesn't assume that the tab needs to be closed in a cleanup function. michael@0: // If that's the case, the test author should handle that in the test. michael@0: function waitForTabState(aTab, aState, aCallback) { michael@0: let listening = true; michael@0: michael@0: function onSSTabRestored() { michael@0: aTab.removeEventListener("SSTabRestored", onSSTabRestored, false); michael@0: listening = false; michael@0: aCallback(); michael@0: } michael@0: michael@0: aTab.addEventListener("SSTabRestored", onSSTabRestored, false); michael@0: michael@0: registerCleanupFunction(function() { michael@0: if (listening) { michael@0: aTab.removeEventListener("SSTabRestored", onSSTabRestored, false); michael@0: } michael@0: }); michael@0: ss.setTabState(aTab, JSON.stringify(aState)); michael@0: } michael@0: michael@0: /** michael@0: * Wait for a content -> chrome message. michael@0: */ michael@0: function promiseContentMessage(browser, name) { michael@0: let deferred = Promise.defer(); michael@0: let mm = browser.messageManager; michael@0: michael@0: function removeListener() { michael@0: mm.removeMessageListener(name, listener); michael@0: } michael@0: michael@0: function listener(msg) { michael@0: removeListener(); michael@0: deferred.resolve(msg.data); michael@0: } michael@0: michael@0: mm.addMessageListener(name, listener); michael@0: registerCleanupFunction(removeListener); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function waitForTopic(aTopic, aTimeout, aCallback) { michael@0: let observing = false; michael@0: function removeObserver() { michael@0: if (!observing) michael@0: return; michael@0: Services.obs.removeObserver(observer, aTopic); michael@0: observing = false; michael@0: } michael@0: michael@0: let timeout = setTimeout(function () { michael@0: removeObserver(); michael@0: aCallback(false); michael@0: }, aTimeout); michael@0: michael@0: function observer(aSubject, aTopic, aData) { michael@0: removeObserver(); michael@0: timeout = clearTimeout(timeout); michael@0: executeSoon(() => aCallback(true)); michael@0: } michael@0: michael@0: registerCleanupFunction(function() { michael@0: removeObserver(); michael@0: if (timeout) { michael@0: clearTimeout(timeout); michael@0: } michael@0: }); michael@0: michael@0: observing = true; michael@0: Services.obs.addObserver(observer, aTopic, false); michael@0: } michael@0: michael@0: /** michael@0: * Wait until session restore has finished collecting its data and is michael@0: * has written that data ("sessionstore-state-write-complete"). michael@0: * michael@0: * @param {function} aCallback If sessionstore-state-write is sent michael@0: * within buffering interval + 100 ms, the callback is passed |true|, michael@0: * otherwise, it is passed |false|. michael@0: */ michael@0: function waitForSaveState(aCallback) { michael@0: let timeout = 100 + michael@0: Services.prefs.getIntPref("browser.sessionstore.interval"); michael@0: return waitForTopic("sessionstore-state-write-complete", timeout, aCallback); michael@0: } michael@0: function promiseSaveState() { michael@0: let deferred = Promise.defer(); michael@0: waitForSaveState(isSuccessful => { michael@0: if (isSuccessful) { michael@0: deferred.resolve(); michael@0: } else { michael@0: deferred.reject(new Error("timeout")); michael@0: }}); michael@0: return deferred.promise; michael@0: } michael@0: function forceSaveState() { michael@0: return SessionSaver.run(); michael@0: } michael@0: michael@0: function promiseSaveFileContents() { michael@0: let promise = forceSaveState(); michael@0: return promise.then(function() { michael@0: return OS.File.read(OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), { encoding: "utf-8" }); michael@0: }); michael@0: } michael@0: michael@0: function whenBrowserLoaded(aBrowser, aCallback = next, ignoreSubFrames = true) { michael@0: aBrowser.addEventListener("load", function onLoad(event) { michael@0: if (!ignoreSubFrames || event.target == aBrowser.contentDocument) { michael@0: aBrowser.removeEventListener("load", onLoad, true); michael@0: executeSoon(aCallback); michael@0: } michael@0: }, true); michael@0: } michael@0: function promiseBrowserLoaded(aBrowser, ignoreSubFrames = true) { michael@0: let deferred = Promise.defer(); michael@0: whenBrowserLoaded(aBrowser, deferred.resolve, ignoreSubFrames); michael@0: return deferred.promise; michael@0: } michael@0: function whenBrowserUnloaded(aBrowser, aContainer, aCallback = next) { michael@0: aBrowser.addEventListener("unload", function onUnload() { michael@0: aBrowser.removeEventListener("unload", onUnload, true); michael@0: executeSoon(aCallback); michael@0: }, true); michael@0: } michael@0: function promiseBrowserUnloaded(aBrowser, aContainer) { michael@0: let deferred = Promise.defer(); michael@0: whenBrowserUnloaded(aBrowser, aContainer, deferred.resolve); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function whenWindowLoaded(aWindow, aCallback = next) { michael@0: aWindow.addEventListener("load", function windowLoadListener() { michael@0: aWindow.removeEventListener("load", windowLoadListener, false); michael@0: executeSoon(function executeWhenWindowLoaded() { michael@0: aCallback(aWindow); michael@0: }); michael@0: }, false); michael@0: } michael@0: function promiseWindowLoaded(aWindow) { michael@0: let deferred = Promise.defer(); michael@0: whenWindowLoaded(aWindow, deferred.resolve); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function whenTabRestored(aTab, aCallback = next) { michael@0: aTab.addEventListener("SSTabRestored", function onRestored(aEvent) { michael@0: aTab.removeEventListener("SSTabRestored", onRestored, true); michael@0: executeSoon(function executeWhenTabRestored() { michael@0: aCallback(); michael@0: }); michael@0: }, true); michael@0: } michael@0: michael@0: var gUniqueCounter = 0; michael@0: function r() { michael@0: return Date.now() + "-" + (++gUniqueCounter); michael@0: } michael@0: michael@0: function BrowserWindowIterator() { michael@0: let windowsEnum = Services.wm.getEnumerator("navigator:browser"); michael@0: while (windowsEnum.hasMoreElements()) { michael@0: let currentWindow = windowsEnum.getNext(); michael@0: if (!currentWindow.closed) { michael@0: yield currentWindow; michael@0: } michael@0: } michael@0: } michael@0: michael@0: let gWebProgressListener = { michael@0: _callback: null, michael@0: michael@0: setCallback: function (aCallback) { michael@0: if (!this._callback) { michael@0: window.gBrowser.addTabsProgressListener(this); michael@0: } michael@0: this._callback = aCallback; michael@0: }, michael@0: michael@0: unsetCallback: function () { michael@0: if (this._callback) { michael@0: this._callback = null; michael@0: window.gBrowser.removeTabsProgressListener(this); michael@0: } michael@0: }, michael@0: michael@0: onStateChange: function (aBrowser, aWebProgress, aRequest, michael@0: aStateFlags, aStatus) { michael@0: if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && michael@0: aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && michael@0: aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { michael@0: this._callback(aBrowser); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: registerCleanupFunction(function () { michael@0: gWebProgressListener.unsetCallback(); michael@0: }); michael@0: michael@0: let gProgressListener = { michael@0: _callback: null, michael@0: michael@0: setCallback: function (callback) { michael@0: Services.obs.addObserver(this, "sessionstore-debug-tab-restored", false); michael@0: this._callback = callback; michael@0: }, michael@0: michael@0: unsetCallback: function () { michael@0: if (this._callback) { michael@0: this._callback = null; michael@0: Services.obs.removeObserver(this, "sessionstore-debug-tab-restored"); michael@0: } michael@0: }, michael@0: michael@0: observe: function (browser, topic, data) { michael@0: gProgressListener.onRestored(browser); michael@0: }, michael@0: michael@0: onRestored: function (browser) { michael@0: if (browser.__SS_restoreState == TAB_STATE_RESTORING) { michael@0: let args = [browser].concat(gProgressListener._countTabs()); michael@0: gProgressListener._callback.apply(gProgressListener, args); michael@0: } michael@0: }, michael@0: michael@0: _countTabs: function () { michael@0: let needsRestore = 0, isRestoring = 0, wasRestored = 0; michael@0: michael@0: for (let win in BrowserWindowIterator()) { michael@0: for (let i = 0; i < win.gBrowser.tabs.length; i++) { michael@0: let browser = win.gBrowser.tabs[i].linkedBrowser; michael@0: if (!browser.__SS_restoreState) michael@0: wasRestored++; michael@0: else if (browser.__SS_restoreState == TAB_STATE_RESTORING) michael@0: isRestoring++; michael@0: else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) michael@0: needsRestore++; michael@0: } michael@0: } michael@0: return [needsRestore, isRestoring, wasRestored]; michael@0: } michael@0: }; michael@0: michael@0: registerCleanupFunction(function () { michael@0: gProgressListener.unsetCallback(); michael@0: }); michael@0: michael@0: // Close everything but our primary window. We can't use waitForFocus() michael@0: // because apparently it's buggy. See bug 599253. michael@0: function closeAllButPrimaryWindow() { michael@0: for (let win in BrowserWindowIterator()) { michael@0: if (win != window) { michael@0: win.close(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * When opening a new window it is not sufficient to wait for its load event. michael@0: * We need to use whenDelayedStartupFinshed() here as the browser window's michael@0: * delayedStartup() routine is executed one tick after the window's load event michael@0: * has been dispatched. browser-delayed-startup-finished might be deferred even michael@0: * further if parts of the window's initialization process take more time than michael@0: * expected (e.g. reading a big session state from disk). michael@0: */ michael@0: function whenNewWindowLoaded(aOptions, aCallback) { michael@0: let win = OpenBrowserWindow(aOptions); michael@0: whenDelayedStartupFinished(win, () => aCallback(win)); michael@0: return win; michael@0: } michael@0: function promiseNewWindowLoaded(aOptions) { michael@0: let deferred = Promise.defer(); michael@0: whenNewWindowLoaded(aOptions, deferred.resolve); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Chrome windows aren't closed synchronously. Provide a helper method to close michael@0: * a window and wait until we received the "domwindowclosed" notification for it. michael@0: */ michael@0: function promiseWindowClosed(win) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: Services.obs.addObserver(function obs(subject, topic) { michael@0: if (subject == win) { michael@0: Services.obs.removeObserver(obs, topic); michael@0: deferred.resolve(); michael@0: } michael@0: }, "domwindowclosed", false); michael@0: michael@0: win.close(); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * This waits for the browser-delayed-startup-finished notification of a given michael@0: * window. It indicates that the windows has loaded completely and is ready to michael@0: * be used for testing. michael@0: */ michael@0: function whenDelayedStartupFinished(aWindow, aCallback) { michael@0: Services.obs.addObserver(function observer(aSubject, aTopic) { michael@0: if (aWindow == aSubject) { michael@0: Services.obs.removeObserver(observer, aTopic); michael@0: executeSoon(aCallback); michael@0: } michael@0: }, "browser-delayed-startup-finished", false); michael@0: } michael@0: michael@0: /** michael@0: * The test runner that controls the execution flow of our tests. michael@0: */ michael@0: let TestRunner = { michael@0: _iter: null, michael@0: michael@0: /** michael@0: * Holds the browser state from before we started so michael@0: * that we can restore it after all tests ran. michael@0: */ michael@0: backupState: {}, michael@0: michael@0: /** michael@0: * Starts the test runner. michael@0: */ michael@0: run: function () { michael@0: waitForExplicitFinish(); michael@0: michael@0: SessionStore.promiseInitialized.then(() => { michael@0: this.backupState = JSON.parse(ss.getBrowserState()); michael@0: this._iter = runTests(); michael@0: this.next(); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Runs the next available test or finishes if there's no test left. michael@0: */ michael@0: next: function () { michael@0: try { michael@0: TestRunner._iter.next(); michael@0: } catch (e if e instanceof StopIteration) { michael@0: TestRunner.finish(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Finishes all tests and cleans up. michael@0: */ michael@0: finish: function () { michael@0: closeAllButPrimaryWindow(); michael@0: gBrowser.selectedTab = gBrowser.tabs[0]; michael@0: waitForBrowserState(this.backupState, finish); michael@0: } michael@0: }; michael@0: michael@0: function next() { michael@0: TestRunner.next(); michael@0: } michael@0: michael@0: function promiseTabRestored(tab) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: tab.addEventListener("SSTabRestored", function onRestored() { michael@0: tab.removeEventListener("SSTabRestored", onRestored); michael@0: deferred.resolve(); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function sendMessage(browser, name, data = {}) { michael@0: browser.messageManager.sendAsyncMessage(name, data); michael@0: return promiseContentMessage(browser, name); michael@0: } michael@0: michael@0: // This creates list of functions that we will map to their corresponding michael@0: // ss-test:* messages names. Those will be sent to the frame script and michael@0: // be used to read and modify form data. michael@0: const FORM_HELPERS = [ michael@0: "getTextContent", michael@0: "getInputValue", "setInputValue", michael@0: "getInputChecked", "setInputChecked", michael@0: "getSelectedIndex", "setSelectedIndex", michael@0: "getMultipleSelected", "setMultipleSelected", michael@0: "getFileNameArray", "setFileNameArray", michael@0: ]; michael@0: michael@0: for (let name of FORM_HELPERS) { michael@0: let msg = "ss-test:" + name; michael@0: this[name] = (browser, data) => sendMessage(browser, msg, data); michael@0: }