1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/sessionstore/test/head.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,566 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const TAB_STATE_NEEDS_RESTORE = 1; 1.9 +const TAB_STATE_RESTORING = 2; 1.10 + 1.11 +const ROOT = getRootDirectory(gTestPath); 1.12 +const FRAME_SCRIPTS = [ 1.13 + ROOT + "content.js", 1.14 + ROOT + "content-forms.js" 1.15 +]; 1.16 + 1.17 +let mm = Cc["@mozilla.org/globalmessagemanager;1"] 1.18 + .getService(Ci.nsIMessageListenerManager); 1.19 + 1.20 +for (let script of FRAME_SCRIPTS) { 1.21 + mm.loadFrameScript(script, true); 1.22 +} 1.23 + 1.24 +mm.addMessageListener("SessionStore:setupSyncHandler", onSetupSyncHandler); 1.25 + 1.26 +/** 1.27 + * This keeps track of all SyncHandlers passed to chrome from frame scripts. 1.28 + * We need this to let tests communicate with frame scripts and cause (a)sync 1.29 + * flushes. 1.30 + */ 1.31 +let SyncHandlers = new WeakMap(); 1.32 +function onSetupSyncHandler(msg) { 1.33 + SyncHandlers.set(msg.target, msg.objects.handler); 1.34 +} 1.35 + 1.36 +registerCleanupFunction(() => { 1.37 + for (let script of FRAME_SCRIPTS) { 1.38 + mm.removeDelayedFrameScript(script, true); 1.39 + } 1.40 + mm.removeMessageListener("SessionStore:setupSyncHandler", onSetupSyncHandler); 1.41 +}); 1.42 + 1.43 +let tmp = {}; 1.44 +Cu.import("resource://gre/modules/Promise.jsm", tmp); 1.45 +Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp); 1.46 +Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", tmp); 1.47 +let {Promise, SessionStore, SessionSaver} = tmp; 1.48 + 1.49 +let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); 1.50 + 1.51 +// Some tests here assume that all restored tabs are loaded without waiting for 1.52 +// the user to bring them to the foreground. We ensure this by resetting the 1.53 +// related preference (see the "firefox.js" defaults file for details). 1.54 +Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false); 1.55 +registerCleanupFunction(function () { 1.56 + Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand"); 1.57 +}); 1.58 + 1.59 +// Obtain access to internals 1.60 +Services.prefs.setBoolPref("browser.sessionstore.debug", true); 1.61 +registerCleanupFunction(function () { 1.62 + Services.prefs.clearUserPref("browser.sessionstore.debug"); 1.63 +}); 1.64 + 1.65 + 1.66 +// This kicks off the search service used on about:home and allows the 1.67 +// session restore tests to be run standalone without triggering errors. 1.68 +Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs; 1.69 + 1.70 +function provideWindow(aCallback, aURL, aFeatures) { 1.71 + function callbackSoon(aWindow) { 1.72 + executeSoon(function executeCallbackSoon() { 1.73 + aCallback(aWindow); 1.74 + }); 1.75 + } 1.76 + 1.77 + let win = openDialog(getBrowserURL(), "", aFeatures || "chrome,all,dialog=no", aURL); 1.78 + whenWindowLoaded(win, function onWindowLoaded(aWin) { 1.79 + if (!aURL) { 1.80 + info("Loaded a blank window."); 1.81 + callbackSoon(aWin); 1.82 + return; 1.83 + } 1.84 + 1.85 + aWin.gBrowser.selectedBrowser.addEventListener("load", function selectedBrowserLoadListener() { 1.86 + aWin.gBrowser.selectedBrowser.removeEventListener("load", selectedBrowserLoadListener, true); 1.87 + callbackSoon(aWin); 1.88 + }, true); 1.89 + }); 1.90 +} 1.91 + 1.92 +// This assumes that tests will at least have some state/entries 1.93 +function waitForBrowserState(aState, aSetStateCallback) { 1.94 + let windows = [window]; 1.95 + let tabsRestored = 0; 1.96 + let expectedTabsRestored = 0; 1.97 + let expectedWindows = aState.windows.length; 1.98 + let windowsOpen = 1; 1.99 + let listening = false; 1.100 + let windowObserving = false; 1.101 + let restoreHiddenTabs = Services.prefs.getBoolPref( 1.102 + "browser.sessionstore.restore_hidden_tabs"); 1.103 + 1.104 + aState.windows.forEach(function (winState) { 1.105 + winState.tabs.forEach(function (tabState) { 1.106 + if (restoreHiddenTabs || !tabState.hidden) 1.107 + expectedTabsRestored++; 1.108 + }); 1.109 + }); 1.110 + 1.111 + // There must be only hidden tabs and restoreHiddenTabs = false. We still 1.112 + // expect one of them to be restored because it gets shown automatically. 1.113 + if (!expectedTabsRestored) 1.114 + expectedTabsRestored = 1; 1.115 + 1.116 + function onSSTabRestored(aEvent) { 1.117 + if (++tabsRestored == expectedTabsRestored) { 1.118 + // Remove the event listener from each window 1.119 + windows.forEach(function(win) { 1.120 + win.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true); 1.121 + }); 1.122 + listening = false; 1.123 + info("running " + aSetStateCallback.name); 1.124 + executeSoon(aSetStateCallback); 1.125 + } 1.126 + } 1.127 + 1.128 + // Used to add our listener to further windows so we can catch SSTabRestored 1.129 + // coming from them when creating a multi-window state. 1.130 + function windowObserver(aSubject, aTopic, aData) { 1.131 + if (aTopic == "domwindowopened") { 1.132 + let newWindow = aSubject.QueryInterface(Ci.nsIDOMWindow); 1.133 + newWindow.addEventListener("load", function() { 1.134 + newWindow.removeEventListener("load", arguments.callee, false); 1.135 + 1.136 + if (++windowsOpen == expectedWindows) { 1.137 + Services.ww.unregisterNotification(windowObserver); 1.138 + windowObserving = false; 1.139 + } 1.140 + 1.141 + // Track this window so we can remove the progress listener later 1.142 + windows.push(newWindow); 1.143 + // Add the progress listener 1.144 + newWindow.gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true); 1.145 + }, false); 1.146 + } 1.147 + } 1.148 + 1.149 + // We only want to register the notification if we expect more than 1 window 1.150 + if (expectedWindows > 1) { 1.151 + registerCleanupFunction(function() { 1.152 + if (windowObserving) { 1.153 + Services.ww.unregisterNotification(windowObserver); 1.154 + } 1.155 + }); 1.156 + windowObserving = true; 1.157 + Services.ww.registerNotification(windowObserver); 1.158 + } 1.159 + 1.160 + registerCleanupFunction(function() { 1.161 + if (listening) { 1.162 + windows.forEach(function(win) { 1.163 + win.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true); 1.164 + }); 1.165 + } 1.166 + }); 1.167 + // Add the event listener for this window as well. 1.168 + listening = true; 1.169 + gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true); 1.170 + 1.171 + // Ensure setBrowserState() doesn't remove the initial tab. 1.172 + gBrowser.selectedTab = gBrowser.tabs[0]; 1.173 + 1.174 + // Finally, call setBrowserState 1.175 + ss.setBrowserState(JSON.stringify(aState)); 1.176 +} 1.177 + 1.178 +// Doesn't assume that the tab needs to be closed in a cleanup function. 1.179 +// If that's the case, the test author should handle that in the test. 1.180 +function waitForTabState(aTab, aState, aCallback) { 1.181 + let listening = true; 1.182 + 1.183 + function onSSTabRestored() { 1.184 + aTab.removeEventListener("SSTabRestored", onSSTabRestored, false); 1.185 + listening = false; 1.186 + aCallback(); 1.187 + } 1.188 + 1.189 + aTab.addEventListener("SSTabRestored", onSSTabRestored, false); 1.190 + 1.191 + registerCleanupFunction(function() { 1.192 + if (listening) { 1.193 + aTab.removeEventListener("SSTabRestored", onSSTabRestored, false); 1.194 + } 1.195 + }); 1.196 + ss.setTabState(aTab, JSON.stringify(aState)); 1.197 +} 1.198 + 1.199 +/** 1.200 + * Wait for a content -> chrome message. 1.201 + */ 1.202 +function promiseContentMessage(browser, name) { 1.203 + let deferred = Promise.defer(); 1.204 + let mm = browser.messageManager; 1.205 + 1.206 + function removeListener() { 1.207 + mm.removeMessageListener(name, listener); 1.208 + } 1.209 + 1.210 + function listener(msg) { 1.211 + removeListener(); 1.212 + deferred.resolve(msg.data); 1.213 + } 1.214 + 1.215 + mm.addMessageListener(name, listener); 1.216 + registerCleanupFunction(removeListener); 1.217 + return deferred.promise; 1.218 +} 1.219 + 1.220 +function waitForTopic(aTopic, aTimeout, aCallback) { 1.221 + let observing = false; 1.222 + function removeObserver() { 1.223 + if (!observing) 1.224 + return; 1.225 + Services.obs.removeObserver(observer, aTopic); 1.226 + observing = false; 1.227 + } 1.228 + 1.229 + let timeout = setTimeout(function () { 1.230 + removeObserver(); 1.231 + aCallback(false); 1.232 + }, aTimeout); 1.233 + 1.234 + function observer(aSubject, aTopic, aData) { 1.235 + removeObserver(); 1.236 + timeout = clearTimeout(timeout); 1.237 + executeSoon(() => aCallback(true)); 1.238 + } 1.239 + 1.240 + registerCleanupFunction(function() { 1.241 + removeObserver(); 1.242 + if (timeout) { 1.243 + clearTimeout(timeout); 1.244 + } 1.245 + }); 1.246 + 1.247 + observing = true; 1.248 + Services.obs.addObserver(observer, aTopic, false); 1.249 +} 1.250 + 1.251 +/** 1.252 + * Wait until session restore has finished collecting its data and is 1.253 + * has written that data ("sessionstore-state-write-complete"). 1.254 + * 1.255 + * @param {function} aCallback If sessionstore-state-write is sent 1.256 + * within buffering interval + 100 ms, the callback is passed |true|, 1.257 + * otherwise, it is passed |false|. 1.258 + */ 1.259 +function waitForSaveState(aCallback) { 1.260 + let timeout = 100 + 1.261 + Services.prefs.getIntPref("browser.sessionstore.interval"); 1.262 + return waitForTopic("sessionstore-state-write-complete", timeout, aCallback); 1.263 +} 1.264 +function promiseSaveState() { 1.265 + let deferred = Promise.defer(); 1.266 + waitForSaveState(isSuccessful => { 1.267 + if (isSuccessful) { 1.268 + deferred.resolve(); 1.269 + } else { 1.270 + deferred.reject(new Error("timeout")); 1.271 + }}); 1.272 + return deferred.promise; 1.273 +} 1.274 +function forceSaveState() { 1.275 + return SessionSaver.run(); 1.276 +} 1.277 + 1.278 +function promiseSaveFileContents() { 1.279 + let promise = forceSaveState(); 1.280 + return promise.then(function() { 1.281 + return OS.File.read(OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), { encoding: "utf-8" }); 1.282 + }); 1.283 +} 1.284 + 1.285 +function whenBrowserLoaded(aBrowser, aCallback = next, ignoreSubFrames = true) { 1.286 + aBrowser.addEventListener("load", function onLoad(event) { 1.287 + if (!ignoreSubFrames || event.target == aBrowser.contentDocument) { 1.288 + aBrowser.removeEventListener("load", onLoad, true); 1.289 + executeSoon(aCallback); 1.290 + } 1.291 + }, true); 1.292 +} 1.293 +function promiseBrowserLoaded(aBrowser, ignoreSubFrames = true) { 1.294 + let deferred = Promise.defer(); 1.295 + whenBrowserLoaded(aBrowser, deferred.resolve, ignoreSubFrames); 1.296 + return deferred.promise; 1.297 +} 1.298 +function whenBrowserUnloaded(aBrowser, aContainer, aCallback = next) { 1.299 + aBrowser.addEventListener("unload", function onUnload() { 1.300 + aBrowser.removeEventListener("unload", onUnload, true); 1.301 + executeSoon(aCallback); 1.302 + }, true); 1.303 +} 1.304 +function promiseBrowserUnloaded(aBrowser, aContainer) { 1.305 + let deferred = Promise.defer(); 1.306 + whenBrowserUnloaded(aBrowser, aContainer, deferred.resolve); 1.307 + return deferred.promise; 1.308 +} 1.309 + 1.310 +function whenWindowLoaded(aWindow, aCallback = next) { 1.311 + aWindow.addEventListener("load", function windowLoadListener() { 1.312 + aWindow.removeEventListener("load", windowLoadListener, false); 1.313 + executeSoon(function executeWhenWindowLoaded() { 1.314 + aCallback(aWindow); 1.315 + }); 1.316 + }, false); 1.317 +} 1.318 +function promiseWindowLoaded(aWindow) { 1.319 + let deferred = Promise.defer(); 1.320 + whenWindowLoaded(aWindow, deferred.resolve); 1.321 + return deferred.promise; 1.322 +} 1.323 + 1.324 +function whenTabRestored(aTab, aCallback = next) { 1.325 + aTab.addEventListener("SSTabRestored", function onRestored(aEvent) { 1.326 + aTab.removeEventListener("SSTabRestored", onRestored, true); 1.327 + executeSoon(function executeWhenTabRestored() { 1.328 + aCallback(); 1.329 + }); 1.330 + }, true); 1.331 +} 1.332 + 1.333 +var gUniqueCounter = 0; 1.334 +function r() { 1.335 + return Date.now() + "-" + (++gUniqueCounter); 1.336 +} 1.337 + 1.338 +function BrowserWindowIterator() { 1.339 + let windowsEnum = Services.wm.getEnumerator("navigator:browser"); 1.340 + while (windowsEnum.hasMoreElements()) { 1.341 + let currentWindow = windowsEnum.getNext(); 1.342 + if (!currentWindow.closed) { 1.343 + yield currentWindow; 1.344 + } 1.345 + } 1.346 +} 1.347 + 1.348 +let gWebProgressListener = { 1.349 + _callback: null, 1.350 + 1.351 + setCallback: function (aCallback) { 1.352 + if (!this._callback) { 1.353 + window.gBrowser.addTabsProgressListener(this); 1.354 + } 1.355 + this._callback = aCallback; 1.356 + }, 1.357 + 1.358 + unsetCallback: function () { 1.359 + if (this._callback) { 1.360 + this._callback = null; 1.361 + window.gBrowser.removeTabsProgressListener(this); 1.362 + } 1.363 + }, 1.364 + 1.365 + onStateChange: function (aBrowser, aWebProgress, aRequest, 1.366 + aStateFlags, aStatus) { 1.367 + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && 1.368 + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && 1.369 + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { 1.370 + this._callback(aBrowser); 1.371 + } 1.372 + } 1.373 +}; 1.374 + 1.375 +registerCleanupFunction(function () { 1.376 + gWebProgressListener.unsetCallback(); 1.377 +}); 1.378 + 1.379 +let gProgressListener = { 1.380 + _callback: null, 1.381 + 1.382 + setCallback: function (callback) { 1.383 + Services.obs.addObserver(this, "sessionstore-debug-tab-restored", false); 1.384 + this._callback = callback; 1.385 + }, 1.386 + 1.387 + unsetCallback: function () { 1.388 + if (this._callback) { 1.389 + this._callback = null; 1.390 + Services.obs.removeObserver(this, "sessionstore-debug-tab-restored"); 1.391 + } 1.392 + }, 1.393 + 1.394 + observe: function (browser, topic, data) { 1.395 + gProgressListener.onRestored(browser); 1.396 + }, 1.397 + 1.398 + onRestored: function (browser) { 1.399 + if (browser.__SS_restoreState == TAB_STATE_RESTORING) { 1.400 + let args = [browser].concat(gProgressListener._countTabs()); 1.401 + gProgressListener._callback.apply(gProgressListener, args); 1.402 + } 1.403 + }, 1.404 + 1.405 + _countTabs: function () { 1.406 + let needsRestore = 0, isRestoring = 0, wasRestored = 0; 1.407 + 1.408 + for (let win in BrowserWindowIterator()) { 1.409 + for (let i = 0; i < win.gBrowser.tabs.length; i++) { 1.410 + let browser = win.gBrowser.tabs[i].linkedBrowser; 1.411 + if (!browser.__SS_restoreState) 1.412 + wasRestored++; 1.413 + else if (browser.__SS_restoreState == TAB_STATE_RESTORING) 1.414 + isRestoring++; 1.415 + else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) 1.416 + needsRestore++; 1.417 + } 1.418 + } 1.419 + return [needsRestore, isRestoring, wasRestored]; 1.420 + } 1.421 +}; 1.422 + 1.423 +registerCleanupFunction(function () { 1.424 + gProgressListener.unsetCallback(); 1.425 +}); 1.426 + 1.427 +// Close everything but our primary window. We can't use waitForFocus() 1.428 +// because apparently it's buggy. See bug 599253. 1.429 +function closeAllButPrimaryWindow() { 1.430 + for (let win in BrowserWindowIterator()) { 1.431 + if (win != window) { 1.432 + win.close(); 1.433 + } 1.434 + } 1.435 +} 1.436 + 1.437 +/** 1.438 + * When opening a new window it is not sufficient to wait for its load event. 1.439 + * We need to use whenDelayedStartupFinshed() here as the browser window's 1.440 + * delayedStartup() routine is executed one tick after the window's load event 1.441 + * has been dispatched. browser-delayed-startup-finished might be deferred even 1.442 + * further if parts of the window's initialization process take more time than 1.443 + * expected (e.g. reading a big session state from disk). 1.444 + */ 1.445 +function whenNewWindowLoaded(aOptions, aCallback) { 1.446 + let win = OpenBrowserWindow(aOptions); 1.447 + whenDelayedStartupFinished(win, () => aCallback(win)); 1.448 + return win; 1.449 +} 1.450 +function promiseNewWindowLoaded(aOptions) { 1.451 + let deferred = Promise.defer(); 1.452 + whenNewWindowLoaded(aOptions, deferred.resolve); 1.453 + return deferred.promise; 1.454 +} 1.455 + 1.456 +/** 1.457 + * Chrome windows aren't closed synchronously. Provide a helper method to close 1.458 + * a window and wait until we received the "domwindowclosed" notification for it. 1.459 + */ 1.460 +function promiseWindowClosed(win) { 1.461 + let deferred = Promise.defer(); 1.462 + 1.463 + Services.obs.addObserver(function obs(subject, topic) { 1.464 + if (subject == win) { 1.465 + Services.obs.removeObserver(obs, topic); 1.466 + deferred.resolve(); 1.467 + } 1.468 + }, "domwindowclosed", false); 1.469 + 1.470 + win.close(); 1.471 + return deferred.promise; 1.472 +} 1.473 + 1.474 +/** 1.475 + * This waits for the browser-delayed-startup-finished notification of a given 1.476 + * window. It indicates that the windows has loaded completely and is ready to 1.477 + * be used for testing. 1.478 + */ 1.479 +function whenDelayedStartupFinished(aWindow, aCallback) { 1.480 + Services.obs.addObserver(function observer(aSubject, aTopic) { 1.481 + if (aWindow == aSubject) { 1.482 + Services.obs.removeObserver(observer, aTopic); 1.483 + executeSoon(aCallback); 1.484 + } 1.485 + }, "browser-delayed-startup-finished", false); 1.486 +} 1.487 + 1.488 +/** 1.489 + * The test runner that controls the execution flow of our tests. 1.490 + */ 1.491 +let TestRunner = { 1.492 + _iter: null, 1.493 + 1.494 + /** 1.495 + * Holds the browser state from before we started so 1.496 + * that we can restore it after all tests ran. 1.497 + */ 1.498 + backupState: {}, 1.499 + 1.500 + /** 1.501 + * Starts the test runner. 1.502 + */ 1.503 + run: function () { 1.504 + waitForExplicitFinish(); 1.505 + 1.506 + SessionStore.promiseInitialized.then(() => { 1.507 + this.backupState = JSON.parse(ss.getBrowserState()); 1.508 + this._iter = runTests(); 1.509 + this.next(); 1.510 + }); 1.511 + }, 1.512 + 1.513 + /** 1.514 + * Runs the next available test or finishes if there's no test left. 1.515 + */ 1.516 + next: function () { 1.517 + try { 1.518 + TestRunner._iter.next(); 1.519 + } catch (e if e instanceof StopIteration) { 1.520 + TestRunner.finish(); 1.521 + } 1.522 + }, 1.523 + 1.524 + /** 1.525 + * Finishes all tests and cleans up. 1.526 + */ 1.527 + finish: function () { 1.528 + closeAllButPrimaryWindow(); 1.529 + gBrowser.selectedTab = gBrowser.tabs[0]; 1.530 + waitForBrowserState(this.backupState, finish); 1.531 + } 1.532 +}; 1.533 + 1.534 +function next() { 1.535 + TestRunner.next(); 1.536 +} 1.537 + 1.538 +function promiseTabRestored(tab) { 1.539 + let deferred = Promise.defer(); 1.540 + 1.541 + tab.addEventListener("SSTabRestored", function onRestored() { 1.542 + tab.removeEventListener("SSTabRestored", onRestored); 1.543 + deferred.resolve(); 1.544 + }); 1.545 + 1.546 + return deferred.promise; 1.547 +} 1.548 + 1.549 +function sendMessage(browser, name, data = {}) { 1.550 + browser.messageManager.sendAsyncMessage(name, data); 1.551 + return promiseContentMessage(browser, name); 1.552 +} 1.553 + 1.554 +// This creates list of functions that we will map to their corresponding 1.555 +// ss-test:* messages names. Those will be sent to the frame script and 1.556 +// be used to read and modify form data. 1.557 +const FORM_HELPERS = [ 1.558 + "getTextContent", 1.559 + "getInputValue", "setInputValue", 1.560 + "getInputChecked", "setInputChecked", 1.561 + "getSelectedIndex", "setSelectedIndex", 1.562 + "getMultipleSelected", "setMultipleSelected", 1.563 + "getFileNameArray", "setFileNameArray", 1.564 +]; 1.565 + 1.566 +for (let name of FORM_HELPERS) { 1.567 + let msg = "ss-test:" + name; 1.568 + this[name] = (browser, data) => sendMessage(browser, msg, data); 1.569 +}