summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/test/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/sessionstore/test/head.js')
-rw-r--r--browser/components/sessionstore/test/head.js782
1 files changed, 782 insertions, 0 deletions
diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js
new file mode 100644
index 0000000000..d73c098eea
--- /dev/null
+++ b/browser/components/sessionstore/test/head.js
@@ -0,0 +1,782 @@
+/* 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 triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+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 HTTPSROOT = ROOT.replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+
+const { SessionSaver } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionSaver.sys.mjs"
+);
+const { SessionFile } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionFile.sys.mjs"
+);
+const { TabState } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabState.sys.mjs"
+);
+const { TabStateFlusher } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
+);
+const ss = SessionStore;
+
+// 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(
+ AppConstants.BROWSER_CHROME_URL,
+ "",
+ 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 () {
+ callbackSoon(aWin);
+ },
+ { capture: true, once: 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"
+ );
+ // This should match the |restoreTabsLazily| value that
+ // SessionStore.restoreWindow() uses.
+ let restoreTabsLazily =
+ Services.prefs.getBoolPref("browser.sessionstore.restore_on_demand") &&
+ Services.prefs.getBoolPref("browser.sessionstore.restore_tabs_lazily");
+
+ aState.windows.forEach(function (winState) {
+ winState.tabs.forEach(function (tabState) {
+ if (!restoreTabsLazily && (restoreHiddenTabs || !tabState.hidden)) {
+ expectedTabsRestored++;
+ }
+ });
+ });
+
+ // If there are only hidden tabs and restoreHiddenTabs = false, we still
+ // expect one of them to be restored because it gets shown automatically.
+ // Otherwise if lazy tab restore there will only be one tab restored per window.
+ if (!expectedTabsRestored) {
+ expectedTabsRestored = 1;
+ } else if (restoreTabsLazily) {
+ expectedTabsRestored = aState.windows.length;
+ }
+
+ 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;
+ newWindow.addEventListener(
+ "load",
+ function () {
+ 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
+ );
+ },
+ { once: true }
+ );
+ }
+ }
+
+ // 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;
+}
+
+function promiseWindowRestoring(win) {
+ return new Promise(resolve =>
+ win.addEventListener("SSWindowRestoring", resolve, { once: true })
+ );
+}
+
+function promiseWindowRestored(win) {
+ return new Promise(resolve =>
+ win.addEventListener("SSWindowRestored", resolve, { once: true })
+ );
+}
+
+async function setBrowserState(state, win = window) {
+ ss.setBrowserState(typeof state != "string" ? JSON.stringify(state) : state);
+ await promiseWindowRestored(win);
+}
+
+async function setWindowState(win, state, overwrite = false) {
+ ss.setWindowState(
+ win,
+ typeof state != "string" ? JSON.stringify(state) : state,
+ overwrite
+ );
+ await promiseWindowRestored(win);
+}
+
+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(subject, topic, data) {
+ removeObserver();
+ timeout = clearTimeout(timeout);
+ executeSoon(() => aCallback(true));
+ }
+
+ registerCleanupFunction(function () {
+ removeObserver();
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ });
+
+ observing = true;
+ Services.obs.addObserver(observer, aTopic);
+}
+
+/**
+ * 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, reject) => {
+ waitForSaveState(isSuccessful => {
+ if (!isSuccessful) {
+ reject(new Error("Save state timeout"));
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+function forceSaveState() {
+ return SessionSaver.run();
+}
+
+function promiseRecoveryFileContents() {
+ let promise = forceSaveState();
+ return promise.then(function () {
+ return IOUtils.readUTF8(SessionFile.Paths.recovery, {
+ decompress: true,
+ });
+ });
+}
+
+var promiseForEachSessionRestoreFile = async function (cb) {
+ for (let key of SessionFile.Paths.loadOrder) {
+ let data = "";
+ try {
+ data = await IOUtils.readUTF8(SessionFile.Paths[key], {
+ decompress: true,
+ });
+ } catch (ex) {
+ // Ignore missing files
+ if (!(DOMException.isInstance(ex) && ex.name == "NotFoundError")) {
+ throw ex;
+ }
+ }
+ cb(data, key);
+ }
+};
+
+function promiseBrowserLoaded(
+ aBrowser,
+ ignoreSubFrames = true,
+ wantLoad = null
+) {
+ return BrowserTestUtils.browserLoaded(aBrowser, !ignoreSubFrames, wantLoad);
+}
+
+function whenWindowLoaded(aWindow, aCallback) {
+ aWindow.addEventListener(
+ "load",
+ function () {
+ executeSoon(function executeWhenWindowLoaded() {
+ aCallback(aWindow);
+ });
+ },
+ { once: true }
+ );
+}
+function promiseWindowLoaded(aWindow) {
+ return new Promise(resolve => whenWindowLoaded(aWindow, resolve));
+}
+
+var gUniqueCounter = 0;
+function r() {
+ return Date.now() + "-" + ++gUniqueCounter;
+}
+
+function* BrowserWindowIterator() {
+ for (let currentWindow of Services.wm.getEnumerator("navigator:browser")) {
+ if (!currentWindow.closed) {
+ yield currentWindow;
+ }
+ }
+}
+
+var gWebProgressListener = {
+ _callback: null,
+
+ setCallback(aCallback) {
+ if (!this._callback) {
+ window.gBrowser.addTabsProgressListener(this);
+ }
+ this._callback = aCallback;
+ },
+
+ unsetCallback() {
+ if (this._callback) {
+ this._callback = null;
+ window.gBrowser.removeTabsProgressListener(this);
+ }
+ },
+
+ onStateChange(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(callback) {
+ Services.obs.addObserver(this, "sessionstore-debug-tab-restored");
+ this._callback = callback;
+ },
+
+ unsetCallback() {
+ if (this._callback) {
+ this._callback = null;
+ Services.obs.removeObserver(this, "sessionstore-debug-tab-restored");
+ }
+ },
+
+ observe(browser, topic, data) {
+ gProgressListener.onRestored(browser);
+ },
+
+ onRestored(browser) {
+ if (ss.getInternalObjectState(browser) == TAB_STATE_RESTORING) {
+ let args = [browser].concat(gProgressListener._countTabs());
+ gProgressListener._callback.apply(gProgressListener, args);
+ }
+ },
+
+ _countTabs() {
+ 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;
+ let state = ss.getInternalObjectState(browser);
+ if (browser.isConnected && !state) {
+ wasRestored++;
+ } else if (state == TAB_STATE_RESTORING) {
+ isRestoring++;
+ } else if (state == TAB_STATE_NEEDS_RESTORE || !browser.isConnected) {
+ 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);
+ }
+}
+
+// Forget all closed tabs for a window
+function forgetClosedTabs(win) {
+ while (ss.getClosedTabCountForWindow(win) > 0) {
+ ss.forgetClosedTab(win, 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(
+ AppConstants.BROWSER_CHROME_URL,
+ "",
+ "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 () {
+ let browser = win.gBrowser.selectedBrowser;
+ promiseBrowserLoaded(browser).then(resolve);
+ },
+ { once: true }
+ );
+ });
+
+ 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");
+}
+function promiseDelayedStartupFinished(aWindow) {
+ return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve));
+}
+
+function promiseTabRestored(tab) {
+ return BrowserTestUtils.waitForEvent(tab, "SSTabRestored");
+}
+
+function promiseTabRestoring(tab) {
+ return BrowserTestUtils.waitForEvent(tab, "SSTabRestoring");
+}
+
+// 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 promiseRemoveTabAndSessionState(tab) {
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ return sessionUpdatePromise;
+}
+
+// Write DOMSessionStorage data to the given browser.
+function modifySessionStorage(browser, storageData, storageOptions = {}) {
+ let browsingContext = browser.browsingContext;
+ if (storageOptions && "frameIndex" in storageOptions) {
+ browsingContext = browsingContext.children[storageOptions.frameIndex];
+ }
+
+ return SpecialPowers.spawn(
+ browsingContext,
+ [[storageData, storageOptions]],
+ async function ([data, options]) {
+ let frame = content;
+ let keys = new Set(Object.keys(data));
+ let isClearing = !keys.size;
+ let storage = frame.sessionStorage;
+
+ return new Promise(resolve => {
+ docShell.chromeEventHandler.addEventListener(
+ "MozSessionStorageChanged",
+ function onStorageChanged(event) {
+ if (event.storageArea == storage) {
+ keys.delete(event.key);
+ }
+
+ if (keys.size == 0) {
+ docShell.chromeEventHandler.removeEventListener(
+ "MozSessionStorageChanged",
+ onStorageChanged,
+ true
+ );
+ resolve();
+ }
+ },
+ true
+ );
+
+ if (isClearing) {
+ storage.clear();
+ } else {
+ for (let key of keys) {
+ frame.sessionStorage[key] = data[key];
+ }
+ }
+ });
+ }
+ );
+}
+
+function pushPrefs(...aPrefs) {
+ return SpecialPowers.pushPrefEnv({ set: aPrefs });
+}
+
+function popPrefs() {
+ return SpecialPowers.popPrefEnv();
+}
+
+function setScrollPosition(bc, x, y) {
+ return SpecialPowers.spawn(bc, [x, y], (childX, childY) => {
+ return new Promise(resolve => {
+ content.addEventListener(
+ "mozvisualscroll",
+ function onScroll(event) {
+ if (content.document.ownerGlobal.visualViewport == event.target) {
+ content.removeEventListener("mozvisualscroll", onScroll, {
+ mozSystemGroup: true,
+ });
+ resolve();
+ }
+ },
+ { mozSystemGroup: true }
+ );
+ content.scrollTo(childX, childY);
+ });
+ });
+}
+
+async function checkScroll(tab, expected, msg) {
+ let browser = tab.linkedBrowser;
+ await TabStateFlusher.flush(browser);
+
+ let scroll = JSON.parse(ss.getTabState(tab)).scroll || null;
+ is(JSON.stringify(scroll), JSON.stringify(expected), msg);
+}
+
+function whenDomWindowClosedHandled(aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ Services.obs.removeObserver(observer, aTopic);
+ aCallback();
+ }, "sessionstore-debug-domwindowclosed-handled");
+}
+
+function getPropertyOfFormField(browserContext, selector, propName) {
+ return SpecialPowers.spawn(
+ browserContext,
+ [selector, propName],
+ (selectorChild, propNameChild) => {
+ return content.document.querySelector(selectorChild)[propNameChild];
+ }
+ );
+}
+
+function setPropertyOfFormField(browserContext, selector, propName, newValue) {
+ return SpecialPowers.spawn(
+ browserContext,
+ [selector, propName, newValue],
+ (selectorChild, propNameChild, newValueChild) => {
+ let node = content.document.querySelector(selectorChild);
+ node[propNameChild] = newValueChild;
+
+ let event = node.ownerDocument.createEvent("UIEvents");
+ event.initUIEvent("input", true, true, node.ownerGlobal, 0);
+ node.dispatchEvent(event);
+ }
+ );
+}
+
+function promiseOnHistoryReplaceEntry(browser) {
+ if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ return new Promise(resolve => {
+ let sessionHistory = browser.browsingContext?.sessionHistory;
+ if (sessionHistory) {
+ var historyListener = {
+ OnHistoryNewEntry() {},
+ OnHistoryGotoIndex() {},
+ OnHistoryPurge() {},
+ OnHistoryReload() {
+ return true;
+ },
+
+ OnHistoryReplaceEntry() {
+ resolve();
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ sessionHistory.addSHistoryListener(historyListener);
+ }
+ });
+ }
+
+ return SpecialPowers.spawn(browser, [], () => {
+ return new Promise(resolve => {
+ var historyListener = {
+ OnHistoryNewEntry() {},
+ OnHistoryGotoIndex() {},
+ OnHistoryPurge() {},
+ OnHistoryReload() {
+ return true;
+ },
+
+ OnHistoryReplaceEntry() {
+ resolve();
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISHistoryListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ var { sessionHistory } = this.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ );
+ if (sessionHistory) {
+ sessionHistory.legacySHistory.addSHistoryListener(historyListener);
+ }
+ });
+ });
+}
+
+function loadTestSubscript(filePath) {
+ Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
+}
+
+function addCoopTask(aFile, aTest, aUrlRoot) {
+ async function taskToBeAdded() {
+ info(`File ${aFile} has COOP headers enabled`);
+ let filePath = `browser/browser/components/sessionstore/test/${aFile}`;
+ let url = aUrlRoot + `coopHeaderCommon.sjs?fileRoot=${filePath}`;
+ await aTest(url);
+ }
+ Object.defineProperty(taskToBeAdded, "name", { value: aTest.name });
+ add_task(taskToBeAdded);
+}
+
+function addNonCoopTask(aFile, aTest, aUrlRoot) {
+ async function taskToBeAdded() {
+ await aTest(aUrlRoot + aFile);
+ }
+ Object.defineProperty(taskToBeAdded, "name", { value: aTest.name });
+ add_task(taskToBeAdded);
+}
+
+async function openAndCloseTab(window, url) {
+ let tab = BrowserTestUtils.addTab(window.gBrowser, url);
+ await promiseBrowserLoaded(tab.linkedBrowser, true, url);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ await promiseRemoveTabAndSessionState(tab);
+}
+
+/**
+ * This is regrettable, but when `promiseBrowserState` resolves, we're still
+ * midway through loading the tabs. To avoid race conditions in URLs for tabs
+ * being available, wait for all the loads to finish:
+ */
+function promiseSessionStoreLoads(numberOfLoads) {
+ let loadsSeen = 0;
+ return new Promise(resolve => {
+ Services.obs.addObserver(function obs(browser) {
+ loadsSeen++;
+ if (loadsSeen == numberOfLoads) {
+ resolve();
+ }
+ // The typeof check is here to avoid one test messing with everything else by
+ // keeping the observer indefinitely.
+ if (typeof info == "undefined" || loadsSeen >= numberOfLoads) {
+ Services.obs.removeObserver(obs, "sessionstore-debug-tab-restored");
+ }
+ info("Saw load for " + browser.currentURI.spec);
+ }, "sessionstore-debug-tab-restored");
+ });
+}