Merge last PGO-green changeset of mozilla-inbound to mozilla-central
authorPhil Ringnalda <philringnalda@gmail.com>
Fri, 28 Sep 2012 22:06:29 -0700
changeset 108606 c09a0c022b2eda4a4f5f48658d19290100d4837b
parent 108558 551152b8c271020905260a9478248e7011d562af (current diff)
parent 108605 dcc6a2d0bd16fc5ded65e9acbe4f17ef15c178d6 (diff)
child 108607 b56f7cb51b1f3d108ae6726482ee0b827a9cdd43
child 108629 1f7dbc3b3bafca41cafaddc27955eeab69bc7b6b
child 111062 ba6826b6b77d208b73a31c755f6ead50a9306750
push id15595
push userphilringnalda@gmail.com
push dateSat, 29 Sep 2012 07:05:36 +0000
treeherdermozilla-inbound@b56f7cb51b1f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone18.0a1
first release with
nightly linux32
c09a0c022b2e / 18.0a1 / 20120929030624 / files
nightly linux64
c09a0c022b2e / 18.0a1 / 20120929030624 / files
nightly mac
c09a0c022b2e / 18.0a1 / 20120929030624 / files
nightly win32
c09a0c022b2e / 18.0a1 / 20120929030624 / files
nightly win64
c09a0c022b2e / 18.0a1 / 20120929030624 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge last PGO-green changeset of mozilla-inbound to mozilla-central
browser/components/sessionstore/test/browser_586068-cascaded_restore.js
content/base/src/nsXMLHttpRequest.cpp
dom/apps/src/Webapps.jsm
--- a/browser/components/sessionstore/test/Makefile.in
+++ b/browser/components/sessionstore/test/Makefile.in
@@ -83,17 +83,25 @@ MOCHITEST_BROWSER_FILES = \
 	browser_522545.js \
 	browser_524745.js \
 	browser_528776.js \
 	browser_579868.js \
 	browser_579879.js \
 	browser_581593.js \
 	browser_581937.js \
 	browser_586147.js \
-	browser_586068-cascaded_restore.js \
+	browser_586068-apptabs.js \
+	browser_586068-apptabs_ondemand.js \
+	browser_586068-browser_state_interrupted.js \
+	browser_586068-cascade.js \
+	browser_586068-multi_window.js \
+	browser_586068-reload.js \
+	browser_586068-select.js \
+	browser_586068-window_state.js \
+	browser_586068-window_state_override.js \
 	browser_588426.js \
 	browser_590268.js \
 	browser_590563.js \
 	browser_595601-restore_hidden.js \
 	browser_597315.js \
 	browser_597315_index.html \
 	browser_597315_a.html \
 	browser_597315_b.html \
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-apptabs.js
@@ -0,0 +1,55 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+let stateBackup = ss.getBrowserState();
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+  });
+
+  let state = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
+    { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
+    { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
+    { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
+  ], selected: 5 }] };
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    loadCount++;
+
+    // We'll make sure that the loads we get come from pinned tabs or the
+    // the selected tab.
+
+    // get the tab
+    let tab;
+    for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+      if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+        tab = window.gBrowser.tabs[i];
+    }
+
+    ok(tab.pinned || gBrowser.selectedTab == tab,
+       "load came from pinned or selected tab");
+
+    // We should get 4 loads: 3 app tabs + 1 normal selected tab
+    if (loadCount < 4)
+      return;
+
+    gProgressListener.unsetCallback();
+    executeSoon(function () {
+      waitForBrowserState(JSON.parse(stateBackup), finish);
+    });
+  });
+
+  ss.setBrowserState(JSON.stringify(state));
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-apptabs_ondemand.js
@@ -0,0 +1,52 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+const PREF_RESTORE_PINNED_TABS_ON_DEMAND = "browser.sessionstore.restore_pinned_tabs_on_demand";
+
+let stateBackup = ss.getBrowserState();
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+  Services.prefs.setBoolPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND, true);
+
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+    Services.prefs.clearUserPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND);
+  });
+
+  let state = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
+    { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
+    { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
+    { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
+  ], selected: 5 }] };
+
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    // get the tab
+    let tab;
+    for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+      if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+        tab = window.gBrowser.tabs[i];
+    }
+
+    // Check that the load only comes from the selected tab.
+    ok(gBrowser.selectedTab == tab, "load came from selected tab");
+    is(aNeedRestore, 6, "six tabs left to restore");
+    is(aRestoring, 1, "one tab is restoring");
+    is(aRestored, 0, "no tabs have been restored, yet");
+
+    gProgressListener.unsetCallback();
+    executeSoon(function () {
+      waitForBrowserState(JSON.parse(stateBackup), finish);
+    });
+  });
+
+  ss.setBrowserState(JSON.stringify(state));
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js
@@ -0,0 +1,111 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+let stateBackup = ss.getBrowserState();
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+  });
+
+  // The first state will be loaded using setBrowserState, followed by the 2nd
+  // state also being loaded using setBrowserState, interrupting the first restore.
+  let state1 = { windows: [
+    {
+      tabs: [
+        { entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }
+      ],
+      selected: 1
+    },
+    {
+      tabs: [
+        { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
+      ],
+      selected: 3
+    }
+  ] };
+  let state2 = { windows: [
+    {
+      tabs: [
+        { entries: [{ url: "http://example.org#5" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.org#6" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.org#7" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.org#8" }], extData: { "uniq": r() } }
+      ],
+      selected: 3
+    },
+    {
+      tabs: [
+        { entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#7" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#8" }], extData: { "uniq": r() } },
+      ],
+      selected: 1
+    }
+  ] };
+
+  // interruptedAfter will be set after the selected tab from each window have loaded.
+  let interruptedAfter = 0;
+  let loadedWindow1 = false;
+  let loadedWindow2 = false;
+  let numTabs = state2.windows[0].tabs.length + state2.windows[1].tabs.length;
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    loadCount++;
+
+    if (aBrowser.currentURI.spec == state1.windows[0].tabs[2].entries[0].url)
+      loadedWindow1 = true;
+    if (aBrowser.currentURI.spec == state1.windows[1].tabs[0].entries[0].url)
+      loadedWindow2 = true;
+
+    if (!interruptedAfter && loadedWindow1 && loadedWindow2) {
+      interruptedAfter = loadCount;
+      ss.setBrowserState(JSON.stringify(state2));
+      return;
+    }
+
+    if (loadCount < numTabs + interruptedAfter)
+      return;
+
+    // We don't actually care about load order in this test, just that they all
+    // do load.
+    is(loadCount, numTabs + interruptedAfter, "all tabs were restored");
+    is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+    // Remove the progress listener from this window, it will be removed from
+    // theWin when that window is closed (in setBrowserState).
+    gProgressListener.unsetCallback();
+    executeSoon(function () {
+      closeAllButPrimaryWindow();
+      waitForBrowserState(JSON.parse(stateBackup), finish);
+    });
+  });
+
+  // We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened
+  Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
+    if (aTopic == "domwindowopened") {
+      let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+      win.addEventListener("load", function onLoad() {
+        win.removeEventListener("load", onLoad);
+        Services.ww.unregisterNotification(observer);
+        win.gBrowser.addTabsProgressListener(gProgressListener);
+      });
+    }
+  });
+
+  ss.setBrowserState(JSON.stringify(state1));
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-cascade.js
@@ -0,0 +1,53 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+let stateBackup = ss.getBrowserState();
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+  });
+
+  let state = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } }
+  ] }] };
+
+  let expectedCounts = [
+    [3, 3, 0],
+    [2, 3, 1],
+    [1, 3, 2],
+    [0, 3, 3],
+    [0, 2, 4],
+    [0, 1, 5]
+  ];
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    loadCount++;
+    let expected = expectedCounts[loadCount - 1];
+
+    is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
+    is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
+    is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
+
+    if (loadCount == state.windows[0].tabs.length) {
+      gProgressListener.unsetCallback();
+      executeSoon(function () {
+        waitForBrowserState(JSON.parse(stateBackup), finish);
+      });
+    }
+  });
+
+  ss.setBrowserState(JSON.stringify(state));
+}
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_586068-cascaded_restore.js
+++ /dev/null
@@ -1,848 +0,0 @@
-/* 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/. */
-
-let stateBackup = ss.getBrowserState();
-
-const TAB_STATE_NEEDS_RESTORE = 1;
-const TAB_STATE_RESTORING = 2;
-
-function test() {
-  /** Test for Bug 586068 - Cascade page loads when restoring **/
-  waitForExplicitFinish();
-  // This test does a lot of window opening / closing and waiting for loads.
-  // In order to prevent timeouts, we'll extend the default that mochitest uses.
-  requestLongerTimeout(4);
-  runNextTest();
-}
-
-// test_reloadCascade, test_reloadReload are generated tests that are run out
-// of cycle (since they depend on current state). They're listed in [tests] here
-// so that it is obvious when they run in respect to the other tests.
-let tests = [test_cascade, test_select, test_multiWindowState,
-             test_setWindowStateNoOverwrite, test_setWindowStateOverwrite,
-             test_setBrowserStateInterrupted, test_reload,
-             /* test_reloadReload, */ test_reloadCascadeSetup,
-             /* test_reloadCascade, */ test_apptabs_only,
-             test_restore_apptabs_ondemand];
-function runNextTest() {
-  // Reset the pref
-  try {
-    Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
-    Services.prefs.clearUserPref("browser.sessionstore.restore_pinned_tabs_on_demand");
-  } catch (e) {}
-
-  // set an empty state & run the next test, or finish
-  if (tests.length) {
-    // Enumerate windows and close everything but our primary window. We can't
-    // use waitForFocus() because apparently it's buggy. See bug 599253.
-    var windowsEnum = Services.wm.getEnumerator("navigator:browser");
-    while (windowsEnum.hasMoreElements()) {
-      var currentWindow = windowsEnum.getNext();
-      if (currentWindow != window) {
-        currentWindow.close();
-      }
-    }
-
-    ss.setBrowserState(JSON.stringify({ windows: [{ tabs: [{ url: 'about:blank' }] }] }));
-    let currentTest = tests.shift();
-    info("running " + currentTest.name);
-    executeSoon(currentTest);
-  }
-  else {
-    ss.setBrowserState(stateBackup);
-    executeSoon(finish);
-  }
-}
-
-
-function test_cascade() {
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      dump("\n\nload: " + aBrowser.currentURI.spec + "\n" + JSON.stringify(countTabs()) + "\n\n");
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_cascade_progressCallback();
-    }
-  }
-
-  let state = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com" }], extData: { "uniq": r() } }
-  ] }] };
-
-  let loadCount = 0;
-  // Since our progress listener is fired before the one in sessionstore, our
-  // expected counts look a little weird. This is because we inspect the state
-  // before sessionstore has marked the tab as finished restoring and before it
-  // starts restoring the next tab
-  let expectedCounts = [
-    [3, 3, 0],
-    [2, 3, 1],
-    [1, 3, 2],
-    [0, 3, 3],
-    [0, 2, 4],
-    [0, 1, 5]
-  ];
-
-  function test_cascade_progressCallback() {
-    loadCount++;
-    let counts = countTabs();
-    let expected = expectedCounts[loadCount - 1];
-
-    is(counts[0], expected[0], "test_cascade: load " + loadCount + " - # tabs that need to be restored");
-    is(counts[1], expected[1], "test_cascade: load " + loadCount + " - # tabs that are restoring");
-    is(counts[2], expected[2], "test_cascade: load " + loadCount + " - # tabs that has been restored");
-
-    if (loadCount < state.windows[0].tabs.length)
-      return;
-
-    window.gBrowser.removeTabsProgressListener(progressListener);
-    runNextTest();
-  }
-
-  // This progress listener will get attached before the listener in session store.
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setBrowserState(JSON.stringify(state));
-}
-
-
-function test_select() {
-  // Set the pref to true so we know exactly how many tabs should be restoring at
-  // any given time. This guarantees that a finishing load won't start another.
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_select_progressCallback(aBrowser);
-    }
-  }
-
-  let state = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } }
-  ], selected: 1 }] };
-
-  let loadCount = 0;
-  // expectedCounts looks a little wierd for the test case, but it works. See
-  // comment in test_cascade for an explanation
-  let expectedCounts = [
-    [5, 1, 0],
-    [4, 1, 1],
-    [3, 1, 2],
-    [2, 1, 3],
-    [1, 1, 4],
-    [0, 1, 5]
-  ];
-  let tabOrder = [0, 5, 1, 4, 3, 2];
-
-  function test_select_progressCallback(aBrowser) {
-    loadCount++;
-
-    let counts = countTabs();
-    let expected = expectedCounts[loadCount - 1];
-
-    is(counts[0], expected[0], "test_select: load " + loadCount + " - # tabs that need to be restored");
-    is(counts[1], expected[1], "test_select: load " + loadCount + " - # tabs that are restoring");
-    is(counts[2], expected[2], "test_select: load " + loadCount + " - # tabs that has been restored");
-
-    if (loadCount < state.windows[0].tabs.length) {
-      // double check that this tab was the right one
-      let expectedData = state.windows[0].tabs[tabOrder[loadCount - 1]].extData.uniq;
-      let tab;
-      for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-        if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
-          tab = window.gBrowser.tabs[i];
-      }
-      is(ss.getTabValue(tab, "uniq"), expectedData, "test_select: load " + loadCount + " - correct tab was restored");
-
-      // select the next tab
-      window.gBrowser.selectTabAtIndex(tabOrder[loadCount]);
-      return;
-    }
-
-    window.gBrowser.removeTabsProgressListener(progressListener);
-    runNextTest();
-  }
-
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setBrowserState(JSON.stringify(state));
-}
-
-
-function test_multiWindowState() {
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      // We only care about load events when the tab still has
-      // __SS_restoreState == TAB_STATE_RESTORING on it.
-      // Since our listener is attached before the sessionstore one, this works out.
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_multiWindowState_progressCallback(aBrowser);
-    }
-  }
-
-  // The first window will be put into the already open window and the second
-  // window will be opened with _openWindowWithState, which is the source of the problem.
-  let state = { windows: [
-    {
-      tabs: [
-        { entries: [{ url: "http://example.org#0" }], extData: { "uniq": r() } }
-      ],
-      selected: 1
-    },
-    {
-      tabs: [
-        { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } }
-      ],
-      selected: 4
-    }
-  ] };
-  let numTabs = state.windows[0].tabs.length + state.windows[1].tabs.length;
-
-  let loadCount = 0;
-  function test_multiWindowState_progressCallback(aBrowser) {
-    loadCount++;
-
-    if (loadCount < numTabs)
-      return;
-
-    // We don't actually care about load order in this test, just that they all
-    // do load.
-    is(loadCount, numTabs, "test_multiWindowState: all tabs were restored");
-    let count = countTabs();
-    is(count[0], 0,
-       "test_multiWindowState: there are no tabs left needing restore");
-
-    // Remove the progress listener from this window, it will be removed from
-    // theWin when that window is closed (in setBrowserState).
-    window.gBrowser.removeTabsProgressListener(progressListener);
-    runNextTest();
-  }
-
-  // We also want to catch the 2nd window, so we need to observe domwindowopened
-  function windowObserver(aSubject, aTopic, aData) {
-    let theWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
-    if (aTopic == "domwindowopened") {
-      theWin.addEventListener("load", function() {
-        theWin.removeEventListener("load", arguments.callee, false);
-
-        Services.ww.unregisterNotification(windowObserver);
-        theWin.gBrowser.addTabsProgressListener(progressListener);
-      }, false);
-    }
-  }
-  Services.ww.registerNotification(windowObserver);
-
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setBrowserState(JSON.stringify(state));
-}
-
-
-function test_setWindowStateNoOverwrite() {
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      // We only care about load events when the tab still has
-      // __SS_restoreState == TAB_STATE_RESTORING on it.
-      // Since our listener is attached before the sessionstore one, this works out.
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_setWindowStateNoOverwrite_progressCallback(aBrowser);
-    }
-  }
-
-  // We'll use 2 states so that we can make sure calling setWindowState doesn't
-  // wipe out currently restoring data.
-  let state1 = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.com#1" }] },
-    { entries: [{ url: "http://example.com#2" }] },
-    { entries: [{ url: "http://example.com#3" }] },
-    { entries: [{ url: "http://example.com#4" }] },
-    { entries: [{ url: "http://example.com#5" }] },
-  ] }] };
-  let state2 = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.org#1" }] },
-    { entries: [{ url: "http://example.org#2" }] },
-    { entries: [{ url: "http://example.org#3" }] },
-    { entries: [{ url: "http://example.org#4" }] },
-    { entries: [{ url: "http://example.org#5" }] }
-  ] }] };
-
-  let numTabs = state1.windows[0].tabs.length + state2.windows[0].tabs.length;
-
-  let loadCount = 0;
-  function test_setWindowStateNoOverwrite_progressCallback(aBrowser) {
-    loadCount++;
-
-    // When loadCount == 2, we'll also restore state2 into the window
-    if (loadCount == 2)
-      ss.setWindowState(window, JSON.stringify(state2), false);
-
-    if (loadCount < numTabs)
-      return;
-
-    // We don't actually care about load order in this test, just that they all
-    // do load.
-    is(loadCount, numTabs, "test_setWindowStateNoOverwrite: all tabs were restored");
-    // window.__SS_tabsToRestore isn't decremented until after the progress
-    // listener is called. Since we get in here before that, we still expect
-    // the count to be 1.
-    is(window.__SS_tabsToRestore, 1,
-       "test_setWindowStateNoOverwrite: window doesn't think there are more tabs to restore");
-    let count = countTabs();
-    is(count[0], 0,
-       "test_setWindowStateNoOverwrite: there are no tabs left needing restore");
-
-    // Remove the progress listener from this window, it will be removed from
-    // theWin when that window is closed (in setBrowserState).
-    window.gBrowser.removeTabsProgressListener(progressListener);
-
-    runNextTest();
-  }
-
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setWindowState(window, JSON.stringify(state1), true);
-}
-
-
-function test_setWindowStateOverwrite() {
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      // We only care about load events when the tab still has
-      // __SS_restoreState == TAB_STATE_RESTORING on it.
-      // Since our listener is attached before the sessionstore one, this works out.
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_setWindowStateOverwrite_progressCallback(aBrowser);
-    }
-  }
-
-  // We'll use 2 states so that we can make sure calling setWindowState doesn't
-  // wipe out currently restoring data.
-  let state1 = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.com#1" }] },
-    { entries: [{ url: "http://example.com#2" }] },
-    { entries: [{ url: "http://example.com#3" }] },
-    { entries: [{ url: "http://example.com#4" }] },
-    { entries: [{ url: "http://example.com#5" }] },
-  ] }] };
-  let state2 = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.org#1" }] },
-    { entries: [{ url: "http://example.org#2" }] },
-    { entries: [{ url: "http://example.org#3" }] },
-    { entries: [{ url: "http://example.org#4" }] },
-    { entries: [{ url: "http://example.org#5" }] }
-  ] }] };
-
-  let numTabs = 2 + state2.windows[0].tabs.length;
-
-  let loadCount = 0;
-  function test_setWindowStateOverwrite_progressCallback(aBrowser) {
-    loadCount++;
-
-    // When loadCount == 2, we'll also restore state2 into the window
-    if (loadCount == 2)
-      ss.setWindowState(window, JSON.stringify(state2), true);
-
-    if (loadCount < numTabs)
-      return;
-
-    // We don't actually care about load order in this test, just that they all
-    // do load.
-    is(loadCount, numTabs, "test_setWindowStateOverwrite: all tabs were restored");
-    // window.__SS_tabsToRestore isn't decremented until after the progress
-    // listener is called. Since we get in here before that, we still expect
-    // the count to be 1.
-    is(window.__SS_tabsToRestore, 1,
-       "test_setWindowStateOverwrite: window doesn't think there are more tabs to restore");
-    let count = countTabs();
-    is(count[0], 0,
-       "test_setWindowStateOverwrite: there are no tabs left needing restore");
-
-    // Remove the progress listener from this window, it will be removed from
-    // theWin when that window is closed (in setBrowserState).
-    window.gBrowser.removeTabsProgressListener(progressListener);
-
-    runNextTest();
-  }
-
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setWindowState(window, JSON.stringify(state1), true);
-}
-
-
-function test_setBrowserStateInterrupted() {
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      // We only care about load events when the tab still has
-      // __SS_restoreState == TAB_STATE_RESTORING on it.
-      // Since our listener is attached before the sessionstore one, this works out.
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_setBrowserStateInterrupted_progressCallback(aBrowser);
-    }
-  }
-
-  // The first state will be loaded using setBrowserState, followed by the 2nd
-  // state also being loaded using setBrowserState, interrupting the first restore.
-  let state1 = { windows: [
-    {
-      tabs: [
-        { entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }
-      ],
-      selected: 1
-    },
-    {
-      tabs: [
-        { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
-      ],
-      selected: 3
-    }
-  ] };
-  let state2 = { windows: [
-    {
-      tabs: [
-        { entries: [{ url: "http://example.org#5" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.org#6" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.org#7" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.org#8" }], extData: { "uniq": r() } }
-      ],
-      selected: 3
-    },
-    {
-      tabs: [
-        { entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#7" }], extData: { "uniq": r() } },
-        { entries: [{ url: "http://example.com#8" }], extData: { "uniq": r() } },
-      ],
-      selected: 1
-    }
-  ] };
-
-  // interruptedAfter will be set after the selected tab from each window have loaded.
-  let interruptedAfter = 0;
-  let loadedWindow1 = false;
-  let loadedWindow2 = false;
-  let numTabs = state2.windows[0].tabs.length + state2.windows[1].tabs.length;
-
-  let loadCount = 0;
-  function test_setBrowserStateInterrupted_progressCallback(aBrowser) {
-    loadCount++;
-
-    if (aBrowser.currentURI.spec == state1.windows[0].tabs[2].entries[0].url)
-      loadedWindow1 = true;
-    if (aBrowser.currentURI.spec == state1.windows[1].tabs[0].entries[0].url)
-      loadedWindow2 = true;
-
-    if (!interruptedAfter && loadedWindow1 && loadedWindow2) {
-      interruptedAfter = loadCount;
-      ss.setBrowserState(JSON.stringify(state2));
-      return;
-    }
-
-    if (loadCount < numTabs + interruptedAfter)
-      return;
-
-    // We don't actually care about load order in this test, just that they all
-    // do load.
-    is(loadCount, numTabs + interruptedAfter,
-       "test_setBrowserStateInterrupted: all tabs were restored");
-    let count = countTabs();
-    is(count[0], 0,
-       "test_setBrowserStateInterrupted: there are no tabs left needing restore");
-
-    // Remove the progress listener from this window, it will be removed from
-    // theWin when that window is closed (in setBrowserState).
-    window.gBrowser.removeTabsProgressListener(progressListener);
-    Services.ww.unregisterNotification(windowObserver);
-    runNextTest();
-  }
-
-  // We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened
-  function windowObserver(aSubject, aTopic, aData) {
-    let theWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
-    if (aTopic == "domwindowopened") {
-      theWin.addEventListener("load", function() {
-        theWin.removeEventListener("load", arguments.callee, false);
-
-        Services.ww.unregisterNotification(windowObserver);
-        theWin.gBrowser.addTabsProgressListener(progressListener);
-      }, false);
-    }
-  }
-  Services.ww.registerNotification(windowObserver);
-
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setBrowserState(JSON.stringify(state1));
-}
-
-
-function test_reload() {
-  // Set the pref to true so we know exactly how many tabs should be restoring at
-  // any given time. This guarantees that a finishing load won't start another.
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_reload_progressCallback(aBrowser);
-    }
-  }
-
-  let state = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#8" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#9" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#10" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#11" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#12" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#13" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#14" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#15" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#16" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#17" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#18" }], extData: { "uniq": r() } }
-  ], selected: 1 }] };
-
-  let loadCount = 0;
-  function test_reload_progressCallback(aBrowser) {
-    loadCount++;
-
-    is(aBrowser.currentURI.spec, state.windows[0].tabs[loadCount - 1].entries[0].url,
-       "test_reload: load " + loadCount + " - browser loaded correct url");
-
-    if (loadCount <= state.windows[0].tabs.length) {
-      // double check that this tab was the right one
-      let expectedData = state.windows[0].tabs[loadCount - 1].extData.uniq;
-      let tab;
-      for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-        if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
-          tab = window.gBrowser.tabs[i];
-      }
-      is(ss.getTabValue(tab, "uniq"), expectedData,
-         "test_reload: load " + loadCount + " - correct tab was restored");
-
-      if (loadCount == state.windows[0].tabs.length) {
-        window.gBrowser.removeTabsProgressListener(progressListener);
-        executeSoon(function() {
-          _test_reloadAfter("test_reloadReload", state, runNextTest);
-        });
-      }
-      else {
-        // reload the next tab
-        window.gBrowser.reloadTab(window.gBrowser.tabs[loadCount]);
-      }
-    }
-
-  }
-
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setBrowserState(JSON.stringify(state));
-}
-
-
-// This doesn't actually test anything, just does a cascaded restore with default
-// settings. This really just sets up to test that reloads work.
-function test_reloadCascadeSetup() {
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_cascadeReloadSetup_progressCallback();
-    }
-  }
-
-  let state = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.com/#1" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com/#2" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com/#3" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com/#4" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com/#5" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.com/#6" }], extData: { "uniq": r() } }
-  ] }] };
-
-  let loadCount = 0;
-  function test_cascadeReloadSetup_progressCallback() {
-    loadCount++;
-    if (loadCount < state.windows[0].tabs.length)
-      return;
-
-    window.gBrowser.removeTabsProgressListener(progressListener);
-    executeSoon(function() {
-      _test_reloadAfter("test_reloadCascade", state, runNextTest);
-    });
-  }
-
-  // This progress listener will get attached before the listener in session store.
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setBrowserState(JSON.stringify(state));
-}
-
-
-// This is a generic function that will attempt to reload each test. We do this
-// a couple times, so make it utilitarian.
-// This test expects that aState contains a single window and that each tab has
-// a unique extData value eg. { "uniq": value }.
-function _test_reloadAfter(aTestName, aState, aCallback) {
-  info("starting " + aTestName);
-  let progressListener = {
-    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)
-        test_reloadAfter_progressCallback(aBrowser);
-    }
-  }
-
-  // Simulate a left mouse button click with no modifiers, which is what
-  // Command-R, or clicking reload does.
-  let fakeEvent = {
-    button: 0,
-    metaKey: false,
-    altKey: false,
-    ctrlKey: false,
-    shiftKey: false,
-  }
-
-  let loadCount = 0;
-  function test_reloadAfter_progressCallback(aBrowser) {
-    loadCount++;
-
-    if (loadCount <= aState.windows[0].tabs.length) {
-      // double check that this tab was the right one
-      let expectedData = aState.windows[0].tabs[loadCount - 1].extData.uniq;
-      let tab;
-      for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-        if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
-          tab = window.gBrowser.tabs[i];
-      }
-      is(ss.getTabValue(tab, "uniq"), expectedData,
-         aTestName + ": load " + loadCount + " - correct tab was reloaded");
-
-      if (loadCount == aState.windows[0].tabs.length) {
-        window.gBrowser.removeTabsProgressListener(progressListener);
-        aCallback();
-      }
-      else {
-        // reload the next tab
-        window.gBrowser.selectTabAtIndex(loadCount);
-        BrowserReloadOrDuplicate(fakeEvent);
-      }
-    }
-  }
-
-  window.gBrowser.addTabsProgressListener(progressListener);
-  BrowserReloadOrDuplicate(fakeEvent);
-}
-
-
-// This test ensures that app tabs are restored regardless of restore_on_demand
-function test_apptabs_only() {
-  // Set the pref to true so we know exactly how many tabs should be restoring at
-  // any given time. This guarantees that a finishing load won't start another.
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_apptabs_only_progressCallback(aBrowser);
-    }
-  }
-
-  let state = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
-    { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
-    { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
-    { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
-  ], selected: 5 }] };
-
-  let loadCount = 0;
-  function test_apptabs_only_progressCallback(aBrowser) {
-    loadCount++;
-
-    // We'll make sure that the loads we get come from pinned tabs or the
-    // the selected tab.
-
-    // get the tab
-    let tab;
-    for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-      if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
-        tab = window.gBrowser.tabs[i];
-    }
-
-    ok(tab.pinned || gBrowser.selectedTab == tab,
-       "test_apptabs_only: load came from pinned or selected tab");
-
-    // We should get 4 loads: 3 app tabs + 1 normal selected tab
-    if (loadCount < 4)
-      return;
-
-    window.gBrowser.removeTabsProgressListener(progressListener);
-    runNextTest();
-  }
-
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setBrowserState(JSON.stringify(state));
-}
-
-
-// This test ensures that app tabs are not restored when restore_pinned_tabs_on_demand is set
-function test_restore_apptabs_ondemand() {
-  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
-  Services.prefs.setBoolPref("browser.sessionstore.restore_pinned_tabs_on_demand", true);
-
-  // We have our own progress listener for this test, which we'll attach before our state is set
-  let progressListener = {
-    onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
-      if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
-          aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
-        test_restore_apptabs_ondemand_progressCallback(aBrowser);
-    }
-  }
-
-  let state = { windows: [{ tabs: [
-    { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
-    { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
-    { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
-    { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
-    { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
-  ], selected: 5 }] };
-
-  let loadCount = 0;
-  let nextTestTimer;
-  function test_restore_apptabs_ondemand_progressCallback(aBrowser) {
-    loadCount++;
-
-    // get the tab
-    let tab;
-    for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-      if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
-        tab = window.gBrowser.tabs[i];
-    }
-
-    // Check that the load only comes from the selected tab.
-    ok(gBrowser.selectedTab == tab,
-       "test_restore_apptabs_ondemand: load came from selected tab");
-
-    // We should get only 1 load: the selected tab
-    if (loadCount == 1) {
-      nextTestTimer = setTimeout(nextTest, 1000);
-      return;
-    }
-    else if (loadCount > 1) {
-      clearTimeout(nextTestTimer);
-    }
-
-    function nextTest() {
-      window.gBrowser.removeTabsProgressListener(progressListener);
-      runNextTest();
-    }
-    nextTest();
-  }
-
-  window.gBrowser.addTabsProgressListener(progressListener);
-  ss.setBrowserState(JSON.stringify(state));
-}
-
-
-function countTabs() {
-  let needsRestore = 0,
-      isRestoring = 0,
-      wasRestored = 0;
-
-  let windowsEnum = Services.wm.getEnumerator("navigator:browser");
-
-  while (windowsEnum.hasMoreElements()) {
-    let window = windowsEnum.getNext();
-    if (window.closed)
-      continue;
-
-    for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-      let browser = window.gBrowser.tabs[i].linkedBrowser;
-      if (browser.__SS_restoreState == TAB_STATE_RESTORING)
-        isRestoring++;
-      else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
-        needsRestore++;
-      else
-        wasRestored++;
-    }
-  }
-  return [needsRestore, isRestoring, wasRestored];
-}
-
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-multi_window.js
@@ -0,0 +1,69 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+let stateBackup = ss.getBrowserState();
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+  });
+
+  // The first window will be put into the already open window and the second
+  // window will be opened with _openWindowWithState, which is the source of the problem.
+  let state = { windows: [
+    {
+      tabs: [
+        { entries: [{ url: "http://example.org#0" }], extData: { "uniq": r() } }
+      ],
+      selected: 1
+    },
+    {
+      tabs: [
+        { entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
+        { entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } }
+      ],
+      selected: 4
+    }
+  ] };
+  let numTabs = state.windows[0].tabs.length + state.windows[1].tabs.length;
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    if (++loadCount == numTabs) {
+      // We don't actually care about load order in this test, just that they all
+      // do load.
+      is(loadCount, numTabs, "all tabs were restored");
+      is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+      gProgressListener.unsetCallback();
+      executeSoon(function () {
+        closeAllButPrimaryWindow();
+        waitForBrowserState(JSON.parse(stateBackup), finish);
+      });
+    }
+  });
+
+  // We also want to catch the 2nd window, so we need to observe domwindowopened
+  Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
+    if (aTopic == "domwindowopened") {
+      let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+      win.addEventListener("load", function onLoad() {
+        win.removeEventListener("load", onLoad);
+        Services.ww.unregisterNotification(observer);
+        win.gBrowser.addTabsProgressListener(gProgressListener);
+      });
+    }
+  });
+
+  ss.setBrowserState(JSON.stringify(state));
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-reload.js
@@ -0,0 +1,137 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+let stateBackup = ss.getBrowserState();
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+  });
+
+  let state = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#8" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#9" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#10" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#11" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#12" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#13" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#14" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#15" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#16" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#17" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org/#18" }], extData: { "uniq": r() } }
+  ], selected: 1 }] };
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    loadCount++;
+    is(aBrowser.currentURI.spec, state.windows[0].tabs[loadCount - 1].entries[0].url,
+       "load " + loadCount + " - browser loaded correct url");
+
+    if (loadCount <= state.windows[0].tabs.length) {
+      // double check that this tab was the right one
+      let expectedData = state.windows[0].tabs[loadCount - 1].extData.uniq;
+      let tab;
+      for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+        if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+          tab = window.gBrowser.tabs[i];
+      }
+      is(ss.getTabValue(tab, "uniq"), expectedData,
+         "load " + loadCount + " - correct tab was restored");
+
+      if (loadCount == state.windows[0].tabs.length) {
+        gProgressListener.unsetCallback();
+        executeSoon(function () {
+          reloadAllTabs(state, function () {
+            waitForBrowserState(JSON.parse(stateBackup), testCascade);
+          });
+        });
+      } else {
+        // reload the next tab
+        window.gBrowser.reloadTab(window.gBrowser.tabs[loadCount]);
+      }
+    }
+  });
+
+  ss.setBrowserState(JSON.stringify(state));
+}
+
+function testCascade() {
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+
+  let state = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.com/#1" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com/#2" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com/#3" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com/#4" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com/#5" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.com/#6" }], extData: { "uniq": r() } }
+  ] }] };
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    if (++loadCount < state.windows[0].tabs.length) {
+      return;
+    }
+
+    gProgressListener.unsetCallback();
+    executeSoon(function () {
+      reloadAllTabs(state, function () {
+        waitForBrowserState(JSON.parse(stateBackup), finish);
+      });
+    });
+  });
+
+  ss.setBrowserState(JSON.stringify(state));
+}
+
+function reloadAllTabs(aState, aCallback) {
+  // Simulate a left mouse button click with no modifiers, which is what
+  // Command-R, or clicking reload does.
+  let fakeEvent = {
+    button: 0,
+    metaKey: false,
+    altKey: false,
+    ctrlKey: false,
+    shiftKey: false
+  };
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    if (++loadCount <= aState.windows[0].tabs.length) {
+      // double check that this tab was the right one
+      let expectedData = aState.windows[0].tabs[loadCount - 1].extData.uniq;
+      let tab;
+      for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+        if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+          tab = window.gBrowser.tabs[i];
+      }
+      is(ss.getTabValue(tab, "uniq"), expectedData,
+         "load " + loadCount + " - correct tab was reloaded");
+
+      if (loadCount == aState.windows[0].tabs.length) {
+        gProgressListener.unsetCallback();
+        executeSoon(aCallback);
+      } else {
+        // reload the next tab
+        window.gBrowser.selectTabAtIndex(loadCount);
+        BrowserReloadOrDuplicate(fakeEvent);
+      }
+    }
+  }, false);
+
+  BrowserReloadOrDuplicate(fakeEvent);
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-select.js
@@ -0,0 +1,68 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+let stateBackup = ss.getBrowserState();
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+  });
+
+  let state = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
+    { entries: [{ url: "http://example.org" }], extData: { "uniq": r() } }
+  ], selected: 1 }] };
+
+  let expectedCounts = [
+    [5, 1, 0],
+    [4, 1, 1],
+    [3, 1, 2],
+    [2, 1, 3],
+    [1, 1, 4],
+    [0, 1, 5]
+  ];
+  let tabOrder = [0, 5, 1, 4, 3, 2];
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    loadCount++;
+    let expected = expectedCounts[loadCount - 1];
+
+    is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
+    is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
+    is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
+
+    if (loadCount < state.windows[0].tabs.length) {
+      // double check that this tab was the right one
+      let expectedData = state.windows[0].tabs[tabOrder[loadCount - 1]].extData.uniq;
+      let tab;
+      for (let i = 0; i < window.gBrowser.tabs.length; i++) {
+        if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
+          tab = window.gBrowser.tabs[i];
+      }
+
+      is(ss.getTabValue(tab, "uniq"), expectedData,
+        "load " + loadCount + " - correct tab was restored");
+
+      // select the next tab
+      window.gBrowser.selectTabAtIndex(tabOrder[loadCount]);
+    } else {
+      gProgressListener.unsetCallback();
+      executeSoon(function () {
+        waitForBrowserState(JSON.parse(stateBackup), finish);
+      });
+    }
+  });
+
+  ss.setBrowserState(JSON.stringify(state));
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-window_state.js
@@ -0,0 +1,62 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+let stateBackup = ss.getBrowserState();
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+  });
+
+  // We'll use 2 states so that we can make sure calling setWindowState doesn't
+  // wipe out currently restoring data.
+  let state1 = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.com#1" }] },
+    { entries: [{ url: "http://example.com#2" }] },
+    { entries: [{ url: "http://example.com#3" }] },
+    { entries: [{ url: "http://example.com#4" }] },
+    { entries: [{ url: "http://example.com#5" }] },
+  ] }] };
+  let state2 = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.org#1" }] },
+    { entries: [{ url: "http://example.org#2" }] },
+    { entries: [{ url: "http://example.org#3" }] },
+    { entries: [{ url: "http://example.org#4" }] },
+    { entries: [{ url: "http://example.org#5" }] }
+  ] }] };
+  let numTabs = state1.windows[0].tabs.length + state2.windows[0].tabs.length;
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    // When loadCount == 2, we'll also restore state2 into the window
+    if (++loadCount == 2) {
+      ss.setWindowState(window, JSON.stringify(state2), false);
+    }
+
+    if (loadCount < numTabs) {
+      return;
+    }
+
+    // We don't actually care about load order in this test, just that they all
+    // do load.
+    is(loadCount, numTabs, "test_setWindowStateNoOverwrite: all tabs were restored");
+    // window.__SS_tabsToRestore isn't decremented until after the progress
+    // listener is called. Since we get in here before that, we still expect
+    // the count to be 1.
+    is(window.__SS_tabsToRestore, 1, "window doesn't think there are more tabs to restore");
+    is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+    gProgressListener.unsetCallback();
+    executeSoon(function () {
+      waitForBrowserState(JSON.parse(stateBackup), finish);
+    });
+  });
+
+  ss.setWindowState(window, JSON.stringify(state1), true);
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_586068-window_state_override.js
@@ -0,0 +1,62 @@
+/* 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 PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
+
+let stateBackup = ss.getBrowserState();
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
+  });
+
+  // We'll use 2 states so that we can make sure calling setWindowState doesn't
+  // wipe out currently restoring data.
+  let state1 = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.com#1" }] },
+    { entries: [{ url: "http://example.com#2" }] },
+    { entries: [{ url: "http://example.com#3" }] },
+    { entries: [{ url: "http://example.com#4" }] },
+    { entries: [{ url: "http://example.com#5" }] },
+  ] }] };
+  let state2 = { windows: [{ tabs: [
+    { entries: [{ url: "http://example.org#1" }] },
+    { entries: [{ url: "http://example.org#2" }] },
+    { entries: [{ url: "http://example.org#3" }] },
+    { entries: [{ url: "http://example.org#4" }] },
+    { entries: [{ url: "http://example.org#5" }] }
+  ] }] };
+  let numTabs = 2 + state2.windows[0].tabs.length;
+
+  let loadCount = 0;
+  gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
+    // When loadCount == 2, we'll also restore state2 into the window
+    if (++loadCount == 2) {
+      ss.setWindowState(window, JSON.stringify(state2), true);
+    }
+
+    if (loadCount < numTabs) {
+      return;
+    }
+
+    // We don't actually care about load order in this test, just that they all
+    // do load.
+    is(loadCount, numTabs, "all tabs were restored");
+    // window.__SS_tabsToRestore isn't decremented until after the progress
+    // listener is called. Since we get in here before that, we still expect
+    // the count to be 1.
+    is(window.__SS_tabsToRestore, 1, "window doesn't think there are more tabs to restore");
+    is(aNeedRestore, 0, "there are no tabs left needing restore");
+
+    gProgressListener.unsetCallback();
+    executeSoon(function () {
+      waitForBrowserState(JSON.parse(stateBackup), finish);
+    });
+  });
+
+  ss.setWindowState(window, JSON.stringify(state1), true);
+}
--- a/browser/components/sessionstore/test/browser_595601-restore_hidden.js
+++ b/browser/components/sessionstore/test/browser_595601-restore_hidden.js
@@ -1,14 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TAB_STATE_NEEDS_RESTORE = 1;
-const TAB_STATE_RESTORING = 2;
-
 let state = {windows:[{tabs:[
   {entries:[{url:"http://example.com#1"}]},
   {entries:[{url:"http://example.com#2"}]},
   {entries:[{url:"http://example.com#3"}]},
   {entries:[{url:"http://example.com#4"}]},
   {entries:[{url:"http://example.com#5"}], hidden: true},
   {entries:[{url:"http://example.com#6"}], hidden: true},
   {entries:[{url:"http://example.com#7"}], hidden: true},
--- a/browser/components/sessionstore/test/browser_599909.js
+++ b/browser/components/sessionstore/test/browser_599909.js
@@ -1,15 +1,12 @@
 /* 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;
-
 let stateBackup = ss.getBrowserState();
 
 function cleanup() {
   // Reset the pref
   try {
     Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
   } catch (e) {}
   ss.setBrowserState(stateBackup);
--- a/browser/components/sessionstore/test/browser_607016.js
+++ b/browser/components/sessionstore/test/browser_607016.js
@@ -1,15 +1,12 @@
 /* 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;
-
 let stateBackup = ss.getBrowserState();
 
 function cleanup() {
   // Reset the pref
   try {
     Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
   } catch (e) {}
   ss.setBrowserState(stateBackup);
--- a/browser/components/sessionstore/test/browser_636279.js
+++ b/browser/components/sessionstore/test/browser_636279.js
@@ -1,14 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TAB_STATE_NEEDS_RESTORE = 1;
-const TAB_STATE_RESTORING = 2;
-
 let stateBackup = ss.getBrowserState();
 
 let statePinned = {windows:[{tabs:[
   {entries:[{url:"http://example.com#1"}], pinned: true}
 ]}]};
 
 let state = {windows:[{tabs:[
   {entries:[{url:"http://example.com#1"}]},
--- a/browser/components/sessionstore/test/browser_739805.js
+++ b/browser/components/sessionstore/test/browser_739805.js
@@ -1,13 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TAB_STATE_NEEDS_RESTORE = 1;
-
 let tabState = {
   entries: [{url: "data:text/html,<input%20id='foo'>", formdata: { id: { "foo": "bar" } } }]
 };
 
 function test() {
   waitForExplicitFinish();
   Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
 
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -1,12 +1,15 @@
 /* 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;
+
 let 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");
@@ -195,8 +198,82 @@ function whenWindowLoaded(aWindow, aCall
     });
   }, false);
 }
 
 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;
+    }
+  }
+}
+
+let gProgressListener = {
+  _callback: null,
+  _checkRestoreState: true,
+
+  setCallback: function gProgressListener_setCallback(aCallback, aCheckRestoreState = true) {
+    if (!this._callback) {
+      window.gBrowser.addTabsProgressListener(this);
+    }
+    this._callback = aCallback;
+    this._checkRestoreState = aCheckRestoreState;
+  },
+
+  unsetCallback: function gProgressListener_unsetCallback() {
+    if (this._callback) {
+      this._callback = null;
+      window.gBrowser.removeTabsProgressListener(this);
+    }
+  },
+
+  onStateChange:
+  function gProgressListener_onStateChange(aBrowser, aWebProgress, aRequest,
+                                           aStateFlags, aStatus) {
+    if ((!this._checkRestoreState ||
+         aBrowser.__SS_restoreState == TAB_STATE_RESTORING) &&
+        aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+        aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+        aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+      let args = [aBrowser].concat(this._countTabs());
+      this._callback.apply(this, args);
+    }
+  },
+
+  _countTabs: function gProgressListener_countTabs() {
+    let needsRestore = 0, isRestoring = 0, wasRestored = 0;
+
+    for (let win in BrowserWindowIterator()) {
+      for (let i = 0; i < win.gBrowser.tabs.length; i++) {
+        let browser = win.gBrowser.tabs[i].linkedBrowser;
+        if (browser.__SS_restoreState == TAB_STATE_RESTORING)
+          isRestoring++;
+        else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+          needsRestore++;
+        else
+          wasRestored++;
+      }
+    }
+    return [needsRestore, isRestoring, wasRestored];
+  }
+};
+
+registerCleanupFunction(function () {
+  gProgressListener.unsetCallback();
+});
+
+// Close everything but our primary window. We can't use waitForFocus()
+// because apparently it's buggy. See bug 599253.
+function closeAllButPrimaryWindow() {
+  for (let win in BrowserWindowIterator()) {
+    if (win != window) {
+      win.close();
+    }
+  }
+}
--- a/browser/components/thumbnails/PageThumbs.jsm
+++ b/browser/components/thumbnails/PageThumbs.jsm
@@ -440,37 +440,23 @@ let PageThumbsExpiration = {
     for (let filter of this._filters) {
       if (typeof filter == "function")
         filter(filterCallback)
       else
         filter.filterForThumbnailExpiration(filterCallback);
     }
   },
 
-  expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
-    let keep = {};
-
-    // Transform all these URLs into file names.
-    for (let url of aURLsToKeep) {
-      keep[PageThumbsStorage.getLeafNameForURL(url)] = true;
-    }
-
-    let numFilesRemoved = 0;
-    let dir = PageThumbsStorage.getDirectory().path;
-    let msg = {type: "getFilesInDirectory", path: dir};
-
-    PageThumbsWorker.postMessage(msg, function (aData) {
-      let files = [file for (file of aData.result) if (!(file in keep))];
-      let maxFilesToRemove = Math.max(EXPIRATION_MIN_CHUNK_SIZE,
-                                      Math.round(files.length / 2));
-
-      let fileNames = files.slice(0, maxFilesToRemove);
-      let filePaths = [dir + "/" + fileName for (fileName of fileNames)];
-      PageThumbsWorker.postMessage({type: "removeFiles", paths: filePaths});
-    });
+  expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep, aCallback) {
+    PageThumbsWorker.postMessage({
+      type: "expireFilesInDirectory",
+      minChunkSize: EXPIRATION_MIN_CHUNK_SIZE,
+      path: PageThumbsStorage.getDirectory().path,
+      filesToKeep: [PageThumbsStorage.getLeafNameForURL(url) for (url of aURLsToKeep)]
+    }, aCallback);
   }
 };
 
 /**
  * Interface to a dedicated thread handling I/O
  */
 let PageThumbsWorker = {
   /**
--- a/browser/components/thumbnails/PageThumbsWorker.js
+++ b/browser/components/thumbnails/PageThumbsWorker.js
@@ -17,60 +17,57 @@ let PageThumbsWorker = {
   handleMessage: function Worker_handleMessage(aEvent) {
     let msg = aEvent.data;
     let data = {result: null, data: null};
 
     switch (msg.type) {
       case "removeFile":
         data.result = this.removeFile(msg);
         break;
-      case "removeFiles":
-        data.result = this.removeFiles(msg);
-        break;
-      case "getFilesInDirectory":
-        data.result = this.getFilesInDirectory(msg);
+      case "expireFilesInDirectory":
+        data.result = this.expireFilesInDirectory(msg);
         break;
       default:
         data.result = false;
         data.detail = "message not understood";
         break;
     }
 
     self.postMessage(data);
   },
 
-  getFilesInDirectory: function Worker_getFilesInDirectory(msg) {
-    let iter = new OS.File.DirectoryIterator(msg.path);
-    let entries = [];
-
-    for (let entry in iter) {
-      if (!entry.isDir && !entry.isSymLink) {
-        entries.push(entry.name);
-      }
-    }
-
-    iter.close();
-    return entries;
-  },
-
   removeFile: function Worker_removeFile(msg) {
     try {
       OS.File.remove(msg.path);
       return true;
     } catch (e) {
       return false;
     }
   },
 
-  removeFiles: function Worker_removeFiles(msg) {
-    for (let file of msg.paths) {
-      try {
-        OS.File.remove(file);
-      } catch (e) {
-        // We couldn't remove the file for some reason.
-        // Let's just continue with the next one.
+  expireFilesInDirectory: function Worker_expireFilesInDirectory(msg) {
+    let entries = this.getFileEntriesInDirectory(msg.path, msg.filesToKeep);
+    let limit = Math.max(msg.minChunkSize, Math.round(entries.length / 2));
+
+    for (let entry of entries) {
+      this.removeFile(entry);
+
+      // Check if we reached the limit of files to remove.
+      if (--limit <= 0) {
+        break;
       }
     }
+
     return true;
+  },
+
+  getFileEntriesInDirectory:
+  function Worker_getFileEntriesInDirectory(aPath, aSkipFiles) {
+    let skip = new Set(aSkipFiles);
+    let iter = new OS.File.DirectoryIterator(aPath);
+
+    return [entry
+            for (entry in iter)
+            if (!entry.isDir && !entry.isSymLink && !skip.has(entry.name))];
   }
 };
 
 self.onmessage = PageThumbsWorker.handleMessage.bind(PageThumbsWorker);
--- a/browser/components/thumbnails/test/Makefile.in
+++ b/browser/components/thumbnails/test/Makefile.in
@@ -8,16 +8,17 @@ srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir  = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_FILES = \
 	browser_thumbnails_capture.js \
+	browser_thumbnails_expiration.js \
 	browser_thumbnails_privacy.js \
 	browser_thumbnails_redirect.js \
 	browser_thumbnails_storage.js \
 	browser_thumbnails_bug726727.js \
 	head.js \
 	background_red.html \
 	background_red_redirect.sjs \
 	privacy_cache_control.sjs \
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/test/browser_thumbnails_expiration.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URL = "http://mochi.test:8888/?t=" + Date.now();
+const URL1 = URL + "#1";
+const URL2 = URL + "#2";
+const URL3 = URL + "#3";
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+  "resource://gre/modules/FileUtils.jsm");
+
+let tmp = {};
+Cc["@mozilla.org/moz/jssubscript-loader;1"]
+  .getService(Ci.mozIJSSubScriptLoader)
+  .loadSubScript("resource:///modules/PageThumbs.jsm", tmp);
+
+const {EXPIRATION_MIN_CHUNK_SIZE, PageThumbsExpiration} = tmp;
+
+function runTests() {
+  // Create three thumbnails.
+  createDummyThumbnail(URL1);
+  ok(thumbnailExists(URL1), "first thumbnail created");
+
+  createDummyThumbnail(URL2);
+  ok(thumbnailExists(URL2), "second thumbnail created");
+
+  createDummyThumbnail(URL3);
+  ok(thumbnailExists(URL3), "third thumbnail created");
+
+  // Remove the third thumbnail.
+  yield expireThumbnails([URL1, URL2]);
+  ok(thumbnailExists(URL1), "first thumbnail still exists");
+  ok(thumbnailExists(URL2), "second thumbnail still exists");
+  ok(!thumbnailExists(URL3), "third thumbnail has been removed");
+
+  // Remove the second thumbnail.
+  yield expireThumbnails([URL1]);
+  ok(thumbnailExists(URL1), "first thumbnail still exists");
+  ok(!thumbnailExists(URL2), "second thumbnail has been removed");
+
+  // Remove all thumbnails.
+  yield expireThumbnails([]);
+  ok(!thumbnailExists(URL1), "all thumbnails have been removed");
+
+  // Create some more files than the min chunk size.
+  let urls = [];
+  for (let i = 0; i < EXPIRATION_MIN_CHUNK_SIZE + 10; i++) {
+    urls.push(URL + "#dummy" + i);
+  }
+
+  urls.forEach(createDummyThumbnail);
+  ok(urls.every(thumbnailExists), "all dummy thumbnails created");
+
+  // Expire thumbnails and expect 10 remaining.
+  yield expireThumbnails([]);
+  let remainingURLs = [u for (u of urls) if (thumbnailExists(u))];
+  is(remainingURLs.length, 10, "10 dummy thumbnails remaining");
+
+  // Expire thumbnails again. All should be gone by now.
+  yield expireThumbnails([]);
+  remainingURLs = [u for (u of remainingURLs) if (thumbnailExists(u))];
+  is(remainingURLs.length, 0, "no dummy thumbnails remaining");
+}
+
+function createDummyThumbnail(aURL) {
+  let file = PageThumbsStorage.getFileForURL(aURL);
+  let fos = FileUtils.openSafeFileOutputStream(file);
+
+  let data = "dummy";
+  fos.write(data, data.length);
+  FileUtils.closeSafeFileOutputStream(fos);
+}
+
+function expireThumbnails(aKeep) {
+  PageThumbsExpiration.expireThumbnails(aKeep, function () {
+    executeSoon(next);
+  });
+}
--- a/browser/components/thumbnails/test/browser_thumbnails_storage.js
+++ b/browser/components/thumbnails/test/browser_thumbnails_storage.js
@@ -91,17 +91,16 @@ function createThumbnail() {
       gBrowser.removeTab(gBrowser.selectedTab);
       next();
     });
   });
 }
 
 function whenFileExists(aCallback) {
   let callback;
-  let file = PageThumbsStorage.getFileForURL(URL);
-  if (file.exists() && file.fileSize) {
+  if (thumbnailExists(URL)) {
     callback = aCallback;
   } else {
     callback = function () whenFileExists(aCallback);
   }
 
   executeSoon(callback);
 }
--- a/browser/components/thumbnails/test/head.js
+++ b/browser/components/thumbnails/test/head.js
@@ -141,8 +141,17 @@ function checkThumbnailColor(aURL, aRed,
  * @param aGreen The green component's intensity.
  * @param aBlue The blue component's intensity.
  * @param aMessage The info message to print when comparing the pixel color.
  */
 function checkCanvasColor(aContext, aRed, aGreen, aBlue, aMessage) {
   let [r, g, b] = aContext.getImageData(0, 0, 1, 1).data;
   ok(r == aRed && g == aGreen && b == aBlue, aMessage);
 }
+
+/**
+ * Checks if a thumbnail for the given URL exists.
+ * @param aURL The url associated to the thumbnail.
+ */
+function thumbnailExists(aURL) {
+  let file = PageThumbsStorage.getFileForURL(aURL);
+  return file.exists() && file.fileSize;
+}
--- a/build/valgrind/cross-architecture.sup
+++ b/build/valgrind/cross-architecture.sup
@@ -246,8 +246,17 @@
 {
    Bug 794374
    Memcheck:Leak
    fun:malloc
    fun:moz_xmalloc
    fun:_ZN22nsComponentManagerImpl17ManifestComponentERNS_25ManifestProcessingContextEiPKPc
    ...
 }
+{
+   Bug 795395
+   Memcheck:Addr4
+   fun:PR_UnloadLibrary
+   fun:_ZN18nsGSettingsServiceD1Ev
+   fun:_ZN18nsGSettingsService7ReleaseEv
+   fun:_ZL29nsGSettingsServiceConstructorP11nsISupportsRK4nsIDPPv
+   ...
+}
--- a/build/valgrind/i386-redhat-linux-gnu.sup
+++ b/build/valgrind/i386-redhat-linux-gnu.sup
@@ -6,24 +6,16 @@
 
 {
    Bug 793537
    Memcheck:Leak
    ...
    obj:/usr/lib/libpango-1.0.so.0.2800.1
    ...
 }
-#{
-#   Bug 713802 now seems fixed, commenting out suppression to verify that bug 793539 is also gone
-#   Bug 793539 which may be fixed or change when bug 713802 is fixed
-#   Memcheck:Leak
-#   ...
-#   obj:/usr/lib/libgnomevfs-2.so.0.2400.2
-#   ...
-#}
 {
    Bug 793598
    Memcheck:Leak
    ...
    obj:/lib/libdbus-1.so.3.4.0
    ...
 }
 {
--- a/build/valgrind/x86_64-redhat-linux-gnu.sup
+++ b/build/valgrind/x86_64-redhat-linux-gnu.sup
@@ -6,24 +6,16 @@
 
 {
    Bug 793537
    Memcheck:Leak
    ...
    obj:/usr/lib64/libpango-1.0.so.0.2800.1
    ...
 }
-#{
-#   Bug 713802 now seems fixed, commenting out suppression to verify that bug 793539 is also gone
-#   Bug 793539 which may be fixed or change when bug 713802 is fixed
-#   Memcheck:Leak
-#   ...
-#   obj:/usr/lib64/libgnomevfs-2.so.0.2400.2
-#   ...
-#}
 {
    Bug 793598
    Memcheck:Leak
    ...
    obj:/lib64/libdbus-1.so.3.4.0
    ...
 }
 {
--- a/content/base/src/nsXMLHttpRequest.cpp
+++ b/content/base/src/nsXMLHttpRequest.cpp
@@ -524,17 +524,18 @@ NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(
     if (!isBlack && tmp->PreservingWrapper()) {
       xpc_UnmarkGrayObject(tmp->GetWrapperPreserveColor());
     }
     return true;
   }
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END
 
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(nsXMLHttpRequest)
-  return tmp->IsBlack();
+  return tmp->
+    IsBlackAndDoesNotNeedTracing(static_cast<nsDOMEventTargetHelper*>(tmp));
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
 
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsXMLHttpRequest)
   return tmp->IsBlack();
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsXMLHttpRequest,
                                                   nsXHREventTarget)
--- a/content/events/src/nsDOMEventTargetHelper.cpp
+++ b/content/events/src/nsDOMEventTargetHelper.cpp
@@ -48,17 +48,17 @@ NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(
     if (tmp->mListenerManager) {
       tmp->mListenerManager->UnmarkGrayJSListeners();
     }
     return true;
   }
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END
 
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(nsDOMEventTargetHelper)
-  return tmp->IsBlack();
+  return tmp->IsBlackAndDoesNotNeedTracing(tmp);
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
 
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsDOMEventTargetHelper)
   return tmp->IsBlack();
 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDOMEventTargetHelper)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -537,18 +537,17 @@ let DOMApplicationRegistry = {
       case "Webapps:Install":
         // always ask for UI to install
         Services.obs.notifyObservers(mm, "webapps-ask-install", JSON.stringify(msg));
         break;
       case "Webapps:GetSelf":
         this.getSelf(msg, mm);
         break;
       case "Webapps:Uninstall":
-        Services.obs.notifyObservers(mm, "webapps-uninstall", JSON.stringify(msg));
-        this.uninstall(msg);
+        this.uninstall(msg, mm);
         debug("Webapps:Uninstall");
         break;
       case "Webapps:Launch":
         this.launchApp(msg, mm);
         break;
       case "Webapps:IsInstalled":
         this.isInstalled(msg, mm);
         break;
@@ -560,18 +559,17 @@ let DOMApplicationRegistry = {
         break;
       case "Webapps:GetAll":
         if (msg.hasPrivileges)
           this.getAll(msg, mm);
         else
           mm.sendAsyncMessage("Webapps:GetAll:Return:KO", msg);
         break;
       case "Webapps:InstallPackage":
-        // always ask for UI to install
-        Services.obs.notifyObservers(mm, "webapps-ask-install", JSON.stringify(msg));
+        this.installPackage(msg, mm);
         break;
       case "Webapps:GetBasePath":
         return this.webapps[msg.id].basePath;
         break;
       case "Webapps:RegisterForMessages":
         this.addMessageListener(msg, mm);
         break;
       case "Webapps:UnregisterForMessages":
@@ -1250,28 +1248,32 @@ let DOMApplicationRegistry = {
           cleanup(e);
         } finally {
           zipReader.close();
         }
       });
     });
   },
 
-  uninstall: function(aData) {
-    let found = false;
+  uninstall: function(aData, aMm) {
     for (let id in this.webapps) {
       let app = this.webapps[id];
       if (app.origin != aData.origin) {
         continue;
       }
 
       if (!this.webapps[id].removable)
         return;
 
-      found = true;
+      // Clear private data first.
+      this._clearPrivateData(app.localId, false);
+
+      // Then notify observers.
+      Services.obs.notifyObservers(aMm, "webapps-uninstall", JSON.stringify(aData));
+
       let appNote = JSON.stringify(AppsUtils.cloneAppObject(app));
       appNote.id = id;
 
 #ifdef MOZ_SYS_MSG
       this._readManifests([{ id: id }], (function unregisterManifest(aResult) {
         this._unregisterActivities(aResult[0].manifest, app);
       }).bind(this));
 #endif
@@ -1283,21 +1285,21 @@ let DOMApplicationRegistry = {
 
       delete this.webapps[id];
 
       this._saveApps((function() {
         this.broadcastMessage("Webapps:Uninstall:Return:OK", aData);
         Services.obs.notifyObservers(this, "webapps-sync-uninstall", appNote);
         this.broadcastMessage("Webapps:RemoveApp", { id: id });
       }).bind(this));
+
+      return;
     }
 
-    if (!found) {
-      aData.mm.sendAsyncMessage("Webapps:Uninstall:Return:KO", aData);
-    }
+    aMm.sendAsyncMessage("Webapps:Uninstall:Return:KO", aData);
   },
 
   getSelf: function(aData, aMm) {
     aData.apps = [];
 
     if (aData.appId == Ci.nsIScriptSecurityManager.NO_APP_ID ||
         aData.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) {
       aMm.sendAsyncMessage("Webapps:GetSelf:Return:OK", aData);
@@ -1598,20 +1600,22 @@ let DOMApplicationRegistry = {
           observers.push(observer);
         }
       } catch(e) { }
     }
 
     // Next enumerate the registered observers.
     enumerator = Services.obs.enumerateObservers(topic);
     while (enumerator.hasMoreElements()) {
-      let observer = enumerator.getNext();
-      if (observers.indexOf(observer) == -1) {
-        observers.push(observer);
-      }
+      try {
+        let observer = enumerator.getNext().QueryInterface(Ci.nsIObserver);
+        if (observers.indexOf(observer) == -1) {
+          observers.push(observer);
+        }
+      } catch (e) { }
     }
 
     observers.forEach(function (observer) {
       try {
         observer.observe(subject, topic, data);
       } catch(e) { }
     });
   },
@@ -1625,24 +1629,28 @@ let DOMApplicationRegistry = {
     this.frameMessages.forEach(function(msgName) {
       mm.addMessageListener(msgName, listener);
     });
   },
 
   receiveAppMessage: function(appId, message) {
     switch (message.name) {
       case "Webapps:ClearBrowserData":
-        let subject = {
-          appId: appId,
-          browserOnly: true,
-          QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams])
-        };
-        this._notifyCategoryAndObservers(subject, "webapps-clear-data", null);
+        this._clearPrivateData(appId, true);
         break;
     }
+  },
+
+  _clearPrivateData: function(appId, browserOnly) {
+    let subject = {
+      appId: appId,
+      browserOnly: browserOnly,
+      QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams])
+    };
+    this._notifyCategoryAndObservers(subject, "webapps-clear-data", null);
   }
 };
 
 /**
  * Appcache download observer
  */
 let AppcacheObserver = function(aApp) {
   this.app = aApp;
--- a/dom/base/nsWrapperCache.h
+++ b/dom/base/nsWrapperCache.h
@@ -154,16 +154,22 @@ public:
     return nullptr;
   }
 
   /**
    * Returns true if the object has a non-gray wrapper.
    */
   bool IsBlack();
 
+  /**
+   * Returns true if the object has a black wrapper,
+   * and all the GC things it is keeping alive are black too.
+   */
+  bool IsBlackAndDoesNotNeedTracing(nsISupports* aThis);
+
   // Only meant to be called by code that preserves a wrapper.
   void SetPreservingWrapper(bool aPreserve)
   {
     if(aPreserve) {
       mWrapperPtrBits |= WRAPPER_BIT_PRESERVED;
     }
     else {
       mWrapperPtrBits &= ~WRAPPER_BIT_PRESERVED;
--- a/dom/base/nsWrapperCacheInlines.h
+++ b/dom/base/nsWrapperCacheInlines.h
@@ -19,9 +19,31 @@ nsWrapperCache::GetWrapper() const
 
 inline bool
 nsWrapperCache::IsBlack()
 {
   JSObject* o = GetWrapperPreserveColor();
   return o && !xpc_IsGrayGCThing(o);
 }
 
+static void
+SearchGray(void* aGCThing, const char* aName, void* aClosure)
+{
+  bool* hasGrayObjects = static_cast<bool*>(aClosure);
+  if (!*hasGrayObjects && aGCThing && xpc_IsGrayGCThing(aGCThing)) {
+    *hasGrayObjects = true;
+  }
+}
+
+inline bool
+nsWrapperCache::IsBlackAndDoesNotNeedTracing(nsISupports* aThis)
+{
+  if (IsBlack()) {
+    nsXPCOMCycleCollectionParticipant* participant = nullptr;
+    CallQueryInterface(aThis, &participant);
+    bool hasGrayObjects = false;
+    participant->Trace(aThis, SearchGray, &hasGrayObjects);
+    return !hasGrayObjects;
+  }
+  return false;
+}
+
 #endif /* nsWrapperCache_h___ */
--- a/python/mozbuild/mozbuild/base.py
+++ b/python/mozbuild/mozbuild/base.py
@@ -4,16 +4,17 @@
 
 from __future__ import unicode_literals
 
 import logging
 import multiprocessing
 import os
 import pymake.parser
 import shlex
+import sys
 import subprocess
 import which
 
 from mozprocess.processhandler import ProcessHandlerMixin
 from pymake.data import Makefile
 from tempfile import TemporaryFile
 
 from mozbuild.config import ConfigProvider
@@ -299,16 +300,20 @@ class MozbuildObject(object):
         within a UNIX environment. Basically, if we are on Windows, it will
         execute the command via an appropriate UNIX-like shell.
         """
         args = self._normalize_command(args, require_unix_environment)
 
         self.log(logging.INFO, 'process', {'args': args}, ' '.join(args))
 
         def handleLine(line):
+            # Converts str to unicode on Python 2 and bytes to str on Python 3.
+            if isinstance(line, bytes):
+                line = line.decode(sys.stdout.encoding)
+
             if line_handler:
                 line_handler(line)
 
             if not log_name:
                 return
 
             self.log(log_level, log_name, {'line': line.strip()}, '{line}')
 
--- a/python/mozbuild/mozbuild/logger.py
+++ b/python/mozbuild/mozbuild/logger.py
@@ -92,17 +92,17 @@ class StructuredTerminalFormatter(Struct
 
         result = s
 
         if s.startswith('TEST-PASS'):
             result = self.terminal.green(s[0:9]) + s[9:]
         elif s.startswith('TEST-UNEXPECTED'):
             result = self.terminal.red(s[0:20]) + s[21:]
 
-        return result.decode('UTF-8', 'ignore')
+        return result
 
 
 class LoggingManager(object):
     """Holds and controls global logging state.
 
     A mozbuild application should instantiate one of these and configure it
     as needed.
 
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/test_logger.py
@@ -0,0 +1,44 @@
+# 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/.
+
+from __future__ import unicode_literals
+
+import logging
+import time
+import unittest
+
+from mozbuild.logger import StructuredHumanFormatter
+
+
+class DummyLogger(logging.Logger):
+    def __init__(self, cb):
+        logging.Logger.__init__(self, 'test')
+
+        self._cb = cb
+
+    def handle(self, record):
+        self._cb(record)
+
+
+class TestStructuredHumanFormatter(unittest.TestCase):
+    def test_non_ascii_logging(self):
+        # Ensures the formatter doesn't choke when non-ASCII characters are
+        # present in printed parameters.
+        formatter = StructuredHumanFormatter(time.time())
+
+        def on_record(record):
+            result = formatter.format(record)
+            relevant = result[5:]
+
+            self.assertEqual(relevant, 'Test: s\xe9curit\xe9')
+
+        logger = DummyLogger(on_record)
+
+        value = 's\xe9curit\xe9'
+
+        logger.log(logging.INFO, 'Test: {utf}',
+            extra={'action': 'action', 'params': {'utf': value}})
+
+
+
--- a/testing/marionette/marionette-actors.js
+++ b/testing/marionette/marionette-actors.js
@@ -3,16 +3,18 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /**
  * Gecko-specific actors.
  */
 
+const FRAME_SCRIPT = "chrome://marionette/content/marionette-listener.js";
+
 let {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
                .getService(Ci.mozIJSSubScriptLoader);
 loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js");
 loader.loadSubScript("chrome://marionette/content/marionette-log-obj.js");
 loader.loadSubScript("chrome://marionette/content/marionette-perf.js");
 Cu.import("chrome://marionette/content/marionette-elements.js");
@@ -89,85 +91,152 @@ MarionetteRootActor.prototype = {
    *
    * @return object
    *         Returns the ID the client can use to communicate with the
    *         MarionetteDriverActor
    */
   getMarionetteID: function MRA_getMarionette() {
     return { "from": "root",
              "id": this._marionetteActor.actorID } ;
-  },
-}
+  }
+};
 
 // register the calls
 MarionetteRootActor.prototype.requestTypes = {
   "getMarionetteID": MarionetteRootActor.prototype.getMarionetteID,
   "sayHello": MarionetteRootActor.prototype.sayHello
 };
 
 /**
+ * An object representing a frame that Marionette has loaded a
+ * frame script in.
+ */
+function MarionetteRemoteFrame(windowId, frameId) {
+  this.windowId = windowId;
+  this.frameId = frameId;
+  this.targetFrameId = null;
+  this.messageManager = null;
+}
+// persistent list of remote frames that Marionette has loaded a frame script in
+let remoteFrames = [];
+
+/**
  * This actor is responsible for all marionette API calls. It gets created
- * for each connection and manages all chrome and browser based calls. It 
+ * for each connection and manages all chrome and browser based calls. It
  * mediates content calls by issuing appropriate messages to the content process.
  */
 function MarionetteDriverActor(aConnection)
 {
   this.uuidGen = Cc["@mozilla.org/uuid-generator;1"]
                    .getService(Ci.nsIUUIDGenerator);
 
   this.conn = aConnection;
-  this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
-                          .getService(Ci.nsIMessageBroadcaster);
+  this.globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
+                             .getService(Ci.nsIMessageBroadcaster);
+  this.messageManager = this.globalMessageManager;
   this.browsers = {}; //holds list of BrowserObjs
   this.curBrowser = null; // points to current browser
   this.context = "content";
   this.scriptTimeout = null;
   this.timer = null;
   this.marionetteLog = new MarionetteLogObj();
   this.marionettePerf = new MarionettePerfData();
   this.command_id = null;
   this.mainFrame = null; //topmost chrome frame
   this.curFrame = null; //subframe that currently has focus
   this.importedScripts = FileUtils.getFile('TmpD', ['marionettescriptchrome']);
-  
+  this.currentRemoteFrame = null; // a member of remoteFrames
+
   //register all message listeners
-  this.messageManager.addMessageListener("Marionette:ok", this);
-  this.messageManager.addMessageListener("Marionette:done", this);
-  this.messageManager.addMessageListener("Marionette:error", this);
-  this.messageManager.addMessageListener("Marionette:log", this);
-  this.messageManager.addMessageListener("Marionette:shareData", this);
-  this.messageManager.addMessageListener("Marionette:register", this);
-  this.messageManager.addMessageListener("Marionette:goUrl", this);
-  this.messageManager.addMessageListener("Marionette:runEmulatorCmd", this);
+  this.addMessageManagerListeners(this.messageManager);
 }
 
 MarionetteDriverActor.prototype = {
 
   //name of the actor
   actorPrefix: "marionette",
 
   /**
+   * Helper methods:
+   */
+
+  /**
+   * Switches to the global ChromeMessageBroadcaster, potentially replacing a frame-specific
+   * ChromeMessageSender.  Has no effect if the global ChromeMessageBroadcaster is already
+   * in use.  If this replaces a frame-specific ChromeMessageSender, it removes the message
+   * listeners from that sender, and then puts the corresponding frame script "to sleep",
+   * which removes most of the message listeners from it as well.
+   */
+  switchToGlobalMessageManager: function MDA_switchToGlobalMM() {
+    if (this.currentRemoteFrame !== null) {
+      this.removeMessageManagerListeners(this.messageManager);
+      this.sendAsync("sleepSession");
+    }
+    this.messageManager = this.globalMessageManager;
+    this.currentRemoteFrame = null;
+  },
+
+  /**
    * Helper method to send async messages to the content listener
    *
    * @param string name
    *        Suffix of the targetted message listener (Marionette:<suffix>)
    * @param object values
    *        Object to send to the listener
    */
   sendAsync: function MDA_sendAsync(name, values) {
-    this.messageManager.broadcastAsyncMessage("Marionette:" + name + this.curBrowser.curFrameId, values);
+    if (this.currentRemoteFrame !== null) {
+      this.messageManager.sendAsyncMessage(
+        "Marionette:" + name + this.currentRemoteFrame.targetFrameId, values);
+    }
+    else {
+      this.messageManager.broadcastAsyncMessage(
+        "Marionette:" + name + this.curBrowser.curFrameId, values);
+    }
   },
 
   /**
-   * Helper methods:
+   * Adds listeners for messages from content frame scripts.
+   *
+   * @param object messageManager
+   *        The messageManager object (ChromeMessageBroadcaster or ChromeMessageSender)
+   *        to which the listeners should be added.
    */
+  addMessageManagerListeners: function MDA_addMessageManagerListeners(messageManager) {
+    messageManager.addMessageListener("Marionette:ok", this);
+    messageManager.addMessageListener("Marionette:done", this);
+    messageManager.addMessageListener("Marionette:error", this);
+    messageManager.addMessageListener("Marionette:log", this);
+    messageManager.addMessageListener("Marionette:shareData", this);
+    messageManager.addMessageListener("Marionette:register", this);
+    messageManager.addMessageListener("Marionette:runEmulatorCmd", this);
+    messageManager.addMessageListener("Marionette:switchToFrame", this);
+  },
+
+  /**
+   * Removes listeners for messages from content frame scripts.
+   *
+   * @param object messageManager
+   *        The messageManager object (ChromeMessageBroadcaster or ChromeMessageSender)
+   *        from which the listeners should be removed.
+   */
+  removeMessageManagerListeners: function MDA_removeMessageManagerListeners(messageManager) {
+    messageManager.removeMessageListener("Marionette:ok", this);
+    messageManager.removeMessageListener("Marionette:done", this);
+    messageManager.removeMessageListener("Marionette:error", this);
+    messageManager.removeMessageListener("Marionette:log", this);
+    messageManager.removeMessageListener("Marionette:shareData", this);
+    messageManager.removeMessageListener("Marionette:register", this);
+    messageManager.removeMessageListener("Marionette:runEmulatorCmd", this);
+    messageManager.removeMessageListener("Marionette:switchToFrame", this);
+  },
 
   /**
    * Generic method to pass a response to the client
-   * 
+   *
    * @param object msg
    *        Response to send back to client
    * @param string command_id
    *        Unique identifier assigned to the client's request.
    *        Used to distinguish the asynchronous responses.
    */
   sendToClient: function MDA_sendToClient(msg, command_id) {
     logger.info("sendToClient: " + JSON.stringify(msg) + ", " + command_id + ", " + this.command_id);
@@ -311,17 +380,17 @@ MarionetteDriverActor.prototype = {
    * @param nsIDOMWindow win
    *        Window whose browser we need to access
    * @param boolean newSession
    *        True if this is the first time we're talking to this browser
    */
   whenBrowserStarted: function MDA_whenBrowserStarted(win, newSession) {
     try {
       if (!Services.prefs.getBoolPref("marionette.contentListener") || !newSession) {
-        this.curBrowser.loadFrameScript("chrome://marionette/content/marionette-listener.js", win);
+        this.curBrowser.loadFrameScript(FRAME_SCRIPT, win);
       }
     }
     catch (e) {
       //there may not always be a content process
       logger.info("could not load listener into content for page: " + win.location.href);
     }
     utils.window = win;
   },
@@ -372,16 +441,18 @@ MarionetteDriverActor.prototype = {
       if (!win || (appName == "Firefox" && !win.gBrowser) || (appName == "Fennec" && !win.BrowserApp)) { 
         checkTimer.initWithCallback(waitForWindow.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
       }
       else {
         this.startBrowser(win, true);
       }
     }
 
+    this.switchToGlobalMessageManager();
+
     if (!Services.prefs.getBoolPref("marionette.contentListener")) {
       waitForWindow.call(this);
     }
     else if ((appName != "Firefox") && (this.curBrowser == null)) {
       //if there is a content listener, then we just wake it up
       this.addBrowser(this.getCurrentWindow());
       this.curBrowser.startSession(false, this.getCurrentWindow(), this.whenBrowserStarted);
       this.messageManager.broadcastAsyncMessage("Marionette:restart", {});
@@ -963,16 +1034,24 @@ MarionetteDriverActor.prototype = {
         this.curFrame = curWindow;
         this.curFrame.focus();
         this.sendOk();
       } else {
         this.sendError("Unable to locate frame: " + aRequest.value, 8, null);
       }
     }
     else {
+      if ((aRequest.value == null) && (aRequest.element == null) &&
+          (this.currentRemoteFrame !== null)) {
+        // We're currently using a ChromeMessageSender for a remote frame, so this
+        // request indicates we need to switch back to the top-level (parent) frame.
+        // We'll first switch to the parent's (global) ChromeMessageBroadcaster, so
+        // we send the message to the right listener.
+        this.switchToGlobalMessageManager();
+      }
       this.sendAsync("switchToFrame", aRequest);
     }
   },
 
   /**
    * Set timeout for searching for elements
    *
    * @param object aRequest
@@ -1297,17 +1376,17 @@ MarionetteDriverActor.prototype = {
 
       // if there is only 1 window left, delete the session
       if (numOpenWindows === 1){
         this.deleteSession();
         return;
       }
 
       try{
-        this.messageManager.removeDelayedFrameScript("chrome://marionette/content/marionette-listener.js"); 
+        this.messageManager.removeDelayedFrameScript(FRAME_SCRIPT); 
         this.getCurrentWindow().close();
         this.sendOk();
       }
       catch (e) {
         this.sendError("Could not close window: " + e.message, 13, e.stack);
       }
     }
   }, 
@@ -1319,44 +1398,38 @@ MarionetteDriverActor.prototype = {
    *
    * If it is a B2G environment, it will make the main content listener sleep, and close
    * all other listeners. The main content listener persists after disconnect (it's the homescreen),
    * and can safely be reused.
    */
   deleteSession: function MDA_deleteSession() {
     if (this.curBrowser != null) {
       if (appName == "B2G") {
-        this.messageManager.broadcastAsyncMessage("Marionette:sleepSession" + this.curBrowser.mainContentId, {});
+        this.globalMessageManager.broadcastAsyncMessage("Marionette:sleepSession" + this.curBrowser.mainContentId, {});
         this.curBrowser.knownFrames.splice(this.curBrowser.knownFrames.indexOf(this.curBrowser.mainContentId), 1);
       }
       else {
         //don't set this pref for B2G since the framescript can be safely reused
         Services.prefs.setBoolPref("marionette.contentListener", false);
       }
       this.curBrowser.closeTab();
       //delete session in each frame in each browser
       for (let win in this.browsers) {
         for (let i in this.browsers[win].knownFrames) {
-          this.messageManager.broadcastAsyncMessage("Marionette:deleteSession" + this.browsers[win].knownFrames[i], {});
+          this.globalMessageManager.broadcastAsyncMessage("Marionette:deleteSession" + this.browsers[win].knownFrames[i], {});
         }
       }
       let winEnum = this.getWinEnumerator();
       while (winEnum.hasMoreElements()) {
-        winEnum.getNext().messageManager.removeDelayedFrameScript("chrome://marionette/content/marionette-listener.js"); 
+        winEnum.getNext().messageManager.removeDelayedFrameScript(FRAME_SCRIPT); 
       }
     }
     this.sendOk();
-    this.messageManager.removeMessageListener("Marionette:ok", this);
-    this.messageManager.removeMessageListener("Marionette:done", this);
-    this.messageManager.removeMessageListener("Marionette:error", this);
-    this.messageManager.removeMessageListener("Marionette:log", this);
-    this.messageManager.removeMessageListener("Marionette:shareData", this);
-    this.messageManager.removeMessageListener("Marionette:register", this);
-    this.messageManager.removeMessageListener("Marionette:goUrl", this);
-    this.messageManager.removeMessageListener("Marionette:runEmulatorCmd", this);
+    this.removeMessageManagerListeners(this.globalMessageManager);
+    this.switchToGlobalMessageManager();
     this.curBrowser = null;
     try {
       this.importedScripts.remove(false);
     }
     catch (e) {
     }
   },
 
@@ -1411,16 +1484,25 @@ MarionetteDriverActor.prototype = {
       this.sendOk();
     }
     else {
       this.sendAsync("importScript", {script: aRequest.script});
     }
   },
 
   /**
+   * Helper function to convert an outerWindowID into a UID that Marionette
+   * tracks.
+   */
+  generateFrameId: function MDA_generateFrameId(id) {
+    let uid = id + (appName == "B2G" ? "-b2g" : "");
+    return uid;
+  },
+
+  /**
    * Receives all messages from content messageManager
    */
   receiveMessage: function MDA_receiveMessage(message) {
     switch (message.name) {
       case "DOMContentLoaded":
         this.sendOk();
         this.messageManager.removeMessageListener("DOMContentLoaded", this, true);
         break;
@@ -1444,33 +1526,80 @@ MarionetteDriverActor.prototype = {
         }
         if (message.json.perf) {
           this.marionettePerf.appendPerfData(message.json.perf);
         }
         break;
       case "Marionette:runEmulatorCmd":
         this.sendToClient(message.json);
         break;
+      case "Marionette:switchToFrame":
+        // Switch to a remote frame.
+
+        for (let i = 0; i < remoteFrames.length; i++) {
+          let frame = remoteFrames[i];
+          if ((frame.windowId == message.json.win) && (frame.frameId == message.json.frame)) {
+            // The frame script has already been loaded in this frame, so just wake it up.
+            this.currentRemoteFrame = frame;
+            this.messageManager = frame.messageManager;
+            this.addMessageManagerListeners(this.messageManager);
+            this.messageManager.sendAsyncMessage("Marionette:restart", {});
+            return;
+          }
+        }
+
+        // Load the frame script in this frame, and set the frame's ChromeMessageSender
+        // as the active message manager.
+        let thisWin = this.getCurrentWindow();
+        let frameWindow = thisWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                                 .getInterface(Ci.nsIDOMWindowUtils)
+                                 .getOuterWindowWithId(message.json.win);
+        let thisFrame = frameWindow.document.getElementsByTagName("iframe")[message.json.frame];
+        let mm = thisFrame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager
+        this.addMessageManagerListeners(mm);
+        mm.loadFrameScript(FRAME_SCRIPT, true);
+        this.messageManager = mm;
+        let aFrame = new MarionetteRemoteFrame(message.json.win, message.json.frame);
+        aFrame.messageManager = this.messageManager;
+        remoteFrames.push(aFrame);
+        this.currentRemoteFrame = aFrame;
+        break;
       case "Marionette:register":
         // This code processes the content listener's registration information
         // and either accepts the listener, or ignores it
         let nullPrevious = (this.curBrowser.curFrameId == null);
         let curWin = this.getCurrentWindow();
-        let frameObject = curWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).getOuterWindowWithId(message.json.value);
+        let listenerWindow = curWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                                   .getInterface(Ci.nsIDOMWindowUtils)
+                                   .getOuterWindowWithId(message.json.value);
+
+        if (listenerWindow.location.href != message.json.href) {
+          // If there is a mismatch between the calculated href and the one
+          // sent from the frame script, it means that the frame script is
+          // running in a separate process.  Currently this only happens
+          // in B2G for OOP frames registered in Marionette:switchToFrame, so
+          //  we'll acknowledge the switchToFrame message here.
+          // XXX: Should have a better way of determining that this message
+          // is from a remote frame.
+          this.sendOk();
+          this.currentRemoteFrame.targetFrameId = this.generateFrameId(message.json.value);
+        }
+
         let browserType;
         try {
           browserType = message.target.getAttribute("type");
         } catch (ex) {
           // browserType remains undefined.
         }
         let reg;
         if (!browserType || browserType != "content") {
-          reg = this.curBrowser.register(message.json.value, message.json.href); 
+          reg = this.curBrowser.register(this.generateFrameId(message.json.value),
+                                         message.json.href); 
         }
-        this.curBrowser.elementManager.seenItems[reg] = frameObject; //add to seenItems
+        this.curBrowser.elementManager.seenItems[reg] = listenerWindow; //add to seenItems
         if (nullPrevious && (this.curBrowser.curFrameId != null)) {
           this.sendAsync("newSession", {B2G: (appName == "B2G")});
           if (this.curBrowser.newSession) {
             this.sendResponse(reg);
           }
         }
         return reg;
     }
@@ -1540,18 +1669,16 @@ function BrowserObj(win) {
   this.B2G = "B2G";
   this.browser;
   this.tab = null;
   this.window = win;
   this.knownFrames = [];
   this.curFrameId = null;
   this.startPage = "about:blank";
   this.mainContentId = null; // used in B2G to identify the homescreen content page
-  this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
-                          .getService(Ci.nsIMessageBroadcaster);
   this.newSession = true; //used to set curFrameId upon new session
   this.elementManager = new ElementManager([SELECTOR, NAME, LINK_TEXT, PARTIAL_LINK_TEXT]);
   this.setBrowser(win);
 }
 
 BrowserObj.prototype = {
   /**
    * Set the browser if the application is not B2G
@@ -1637,25 +1764,25 @@ BrowserObj.prototype = {
     Services.prefs.setBoolPref("marionette.contentListener", true);
   },
 
   /**
    * Registers a new frame, and sets its current frame id to this frame
    * if it is not already assigned, and if a) we already have a session 
    * or b) we're starting a new session and it is the right start frame.
    *
-   * @param string id
-   *        frame id
+   * @param string uid
+   *        frame uid
    * @param string href
    *        frame's href 
    */
-  register: function BO_register(id, href) {
-    let uid = id + ((appName == "B2G") ? '-b2g' : '');
+  register: function BO_register(uid, href) {
     if (this.curFrameId == null) {
-      if ((!this.newSession) || (this.newSession && ((appName != "Firefox") || href.indexOf(this.startPage) > -1))) {
+      if ((!this.newSession) || (this.newSession && 
+          ((appName != "Firefox") || href.indexOf(this.startPage) > -1))) {
         this.curFrameId = uid;
         this.mainContentId = uid;
       }
     }
-    this.knownFrames.push(uid); //used to deletesessions
+    this.knownFrames.push(uid); //used to delete sessions
     return uid;
   },
 }
--- a/testing/marionette/marionette-listener.js
+++ b/testing/marionette/marionette-listener.js
@@ -742,50 +742,62 @@ function switchToFrame(msg) {
           curWindow = curWindow.frames[i]; 
           curWindow.focus();
           sendOk();
           return;
         }
       }
     }
   }
+  let frames = curWindow.document.getElementsByTagName("iframe");
   switch(typeof(msg.json.value)) {
     case "string" :
       let foundById = null;
-      let numFrames = curWindow.frames.length;
-      for (let i = 0; i < numFrames; i++) {
+      for (let i = 0; i < frames.length; i++) {
         //give precedence to name
-        let frame = curWindow.frames[i];
-        let frameElement = frame.frameElement;
-        if (frameElement.name == msg.json.value) {
+        let frame = frames[i];
+        let name = utils.getElementAttribute(frame, 'name');
+        let id = utils.getElementAttribute(frame, 'id');
+        if (name == msg.json.value) {
           foundFrame = i;
           break;
-        } else if ((foundById == null) && (frameElement.id == msg.json.value)) {
+        } else if ((foundById == null) && (id == msg.json.value)) {
           foundById = i;
         }
       }
       if ((foundFrame == null) && (foundById != null)) {
         foundFrame = foundById;
+        curWindow = frames[foundFrame];
       }
       break;
     case "number":
-      if (curWindow.frames[msg.json.value] != undefined) {
+      if (frames[msg.json.value] != undefined) {
         foundFrame = msg.json.value;
+        curWindow = frames[foundFrame];
       }
       break;
   }
   if (foundFrame == null) {
     sendError("Unable to locate frame: " + msg.json.value, 8, null);
     return;
   }
-  curWindow = curWindow.frames[foundFrame];
-  curWindow.focus();
-  sendOk();
 
   sandbox = null;
+
+  if (curWindow.contentWindow == null) {
+    // The frame we want to switch to is a remote frame; notify our parent to handle
+    // the switch.
+    curWindow = content;
+    sendToServer('Marionette:switchToFrame', {win: winUtil.outerWindowID, frame: foundFrame});
+  }
+  else {
+    curWindow = curWindow.contentWindow;
+    curWindow.focus();
+    sendOk();
+  }
 }
 
 // emulator callbacks
 let _emu_cb_id = 0;
 let _emu_cbs = {};
 
 function runEmulatorCmd(cmd, callback) {
   if (callback) {
--- a/testing/mochitest/runtestsb2g.py
+++ b/testing/mochitest/runtestsb2g.py
@@ -91,16 +91,22 @@ class B2GOptions(MochitestOptions):
                     help = "ip address where the remote web server is hosted at")
         defaults["sslPort"] = automation.DEFAULT_SSL_PORT
 
         self.add_option("--pidfile", action = "store",
                     type = "string", dest = "pidFile",
                     help = "name of the pidfile to generate")
         defaults["pidFile"] = ""
 
+        self.add_option("--gecko-path", action="store",
+                        type="string", dest="geckoPath",
+                        help="the path to a gecko distribution that should "
+                        "be installed on the emulator prior to test")
+        defaults["geckoPath"] = None
+
         defaults["remoteTestRoot"] = None
         defaults["logFile"] = "mochitest.log"
         defaults["autorun"] = True
         defaults["closeWhenDone"] = True
         defaults["testPath"] = ""
         defaults["extensionsToExclude"] = ["specialpowers"]
 
         self.set_defaults(**defaults)
@@ -111,18 +117,20 @@ class B2GOptions(MochitestOptions):
 
         if options.utilityPath == self._automation.DIST_BIN:
             options.utilityPath = productRoot + "/bin"
 
         if options.remoteWebServer == None:
             if os.name != "nt":
                 options.remoteWebServer = automation.getLanIp()
             else:
-                print "ERROR: you must specify a --remote-webserver=<ip address>\n"
-                return None
+                self.error("You must specify a --remote-webserver=<ip address>")
+
+        if options.geckoPath and not options.emulator:
+            self.error("You must specify --emulator if you specify --gecko-path")
 
         options.webServer = options.remoteWebServer
 
         #if not options.emulator and not options.deviceIP:
         #    print "ERROR: you must provide a device IP"
         #    return None
 
         if options.remoteLogFile == None:
@@ -402,16 +410,18 @@ def main():
 
     # create our Marionette instance
     kwargs = {}
     if options.emulator:
         kwargs['emulator'] = options.emulator
         auto.setEmulator(True)
         if options.noWindow:
             kwargs['noWindow'] = True
+        if options.geckoPath:
+            kwargs['gecko_path'] = options.geckoPath
     # needless to say sdcard is only valid if using an emulator
     if options.sdcard:
         kwargs['sdcard'] = options.sdcard
     if options.b2gPath:
         kwargs['homedir'] = options.b2gPath
     if options.marionette:
         host,port = options.marionette.split(':')
         kwargs['host'] = host
--- a/toolkit/devtools/sourcemap/SourceMap.jsm
+++ b/toolkit/devtools/sourcemap/SourceMap.jsm
@@ -26,25 +26,16 @@ Components.utils.import('resource://gre/
  */
 define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'source-map/util', 'source-map/binary-search', 'source-map/array-set', 'source-map/base64-vlq'], function(require, exports, module) {
 
   var util = require('source-map/util');
   var binarySearch = require('source-map/binary-search');
   var ArraySet = require('source-map/array-set').ArraySet;
   var base64VLQ = require('source-map/base64-vlq');
 
-  // TODO:  bug 673487
-  //
-  // Sometime in the future, if we decide we need to be able to query where in
-  // the generated source a piece of the original code came from, we may want to
-  // add a slot `_originalMappings` which would be an object keyed by the
-  // original source and whose value would be an array of mappings ordered by
-  // original line/col rather than generated (which is what we have now in
-  // `_generatedMappings`).
-
   /**
    * A SourceMapConsumer instance represents a parsed source map which we can
    * query for information about the original file positions by giving it a file
    * position in the generated source.
    *
    * The only parameter is the raw source map (either as a JSON string, or
    * already parsed to an object). According to the spec, source maps have the
    * following attributes:
@@ -67,53 +58,58 @@ define('source-map/source-map-consumer',
    *       mappings: "AA,AB;;ABCDE;"
    *     }
    *
    * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
    */
   function SourceMapConsumer(aSourceMap) {
     var sourceMap = aSourceMap;
     if (typeof aSourceMap === 'string') {
-      sourceMap = JSON.parse(aSourceMap);
+      sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
     }
 
     var version = util.getArg(sourceMap, 'version');
     var sources = util.getArg(sourceMap, 'sources');
     var names = util.getArg(sourceMap, 'names');
     var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
     var mappings = util.getArg(sourceMap, 'mappings');
     var file = util.getArg(sourceMap, 'file');
 
     if (version !== this._version) {
       throw new Error('Unsupported version: ' + version);
     }
 
     this._names = ArraySet.fromArray(names);
     this._sources = ArraySet.fromArray(sources);
 
-    // `this._generatedMappings` hold the parsed mapping coordinates from the
-    // source map's "mappings" attribute. Each object in the array is of the
-    // form
+    // `this._generatedMappings` and `this._originalMappings` hold the parsed
+    // mapping coordinates from the source map's "mappings" attribute. Each
+    // object in the array is of the form
     //
     //     {
     //       generatedLine: The line number in the generated code,
     //       generatedColumn: The column number in the generated code,
     //       source: The path to the original source file that generated this
     //               chunk of code,
     //       originalLine: The line number in the original source that
     //                     corresponds to this chunk of generated code,
     //       originalColumn: The column number in the original source that
     //                       corresponds to this chunk of generated code,
     //       name: The name of the original symbol which generated this chunk of
     //             code.
     //     }
     //
     // All properties except for `generatedLine` and `generatedColumn` can be
     // `null`.
+    //
+    // `this._generatedMappings` is ordered by the generated positions.
+    //
+    // `this._originalMappings` is ordered by the original positions.
     this._generatedMappings = [];
+    this._originalMappings = [];
     this._parseMappings(mappings, sourceRoot);
   }
 
   /**
    * The version of the source mapping spec that we are consuming.
    */
   SourceMapConsumer.prototype._version = 3;
 
@@ -190,18 +186,75 @@ define('source-map/source-map-consumer',
               temp = base64VLQ.decode(str);
               mapping.name = this._names.at(previousName + temp.value);
               previousName += temp.value;
               str = temp.rest;
             }
           }
 
           this._generatedMappings.push(mapping);
+          this._originalMappings.push(mapping);
         }
       }
+
+      this._originalMappings.sort(this._compareOriginalPositions);
+    };
+
+  /**
+   * Comparator between two mappings where the original positions are compared.
+   */
+  SourceMapConsumer.prototype._compareOriginalPositions =
+    function SourceMapConsumer_compareOriginalPositions(mappingA, mappingB) {
+      if (mappingA.source > mappingB.source) {
+        return 1;
+      }
+      else if (mappingA.source < mappingB.source) {
+        return -1;
+      }
+      else {
+        var cmp = mappingA.originalLine - mappingB.originalLine;
+        return cmp === 0
+          ? mappingA.originalColumn - mappingB.originalColumn
+          : cmp;
+      }
+    };
+
+  /**
+   * Comparator between two mappings where the generated positions are compared.
+   */
+  SourceMapConsumer.prototype._compareGeneratedPositions =
+    function SourceMapConsumer_compareGeneratedPositions(mappingA, mappingB) {
+      var cmp = mappingA.generatedLine - mappingB.generatedLine;
+      return cmp === 0
+        ? mappingA.generatedColumn - mappingB.generatedColumn
+        : cmp;
+    };
+
+  /**
+   * Find the mapping that best matches the hypothetical "needle" mapping that
+   * we are searching for in the given "haystack" of mappings.
+   */
+  SourceMapConsumer.prototype._findMapping =
+    function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
+                                           aColumnName, aComparator) {
+      // To return the position we are searching for, we must first find the
+      // mapping for the given position and then return the opposite position it
+      // points to. Because the mappings are sorted, we can use binary search to
+      // find the best mapping.
+
+      if (aNeedle[aLineName] <= 0) {
+        throw new TypeError('Line must be greater than or equal to 1, got '
+                            + aNeedle[aLineName]);
+      }
+      if (aNeedle[aColumnName] < 0) {
+        throw new TypeError('Column must be greater than or equal to 0, got '
+                            + aNeedle[aColumnName]);
+      }
+
+      return binarySearch.search(aNeedle, aMappings, aComparator);
     };
 
   /**
    * Returns the original source, line, and column information for the generated
    * source's line and column positions provided. The only argument is an object
    * with the following properties:
    *
    *   - line: The line number in the generated source.
@@ -211,62 +264,83 @@ define('source-map/source-map-consumer',
    *
    *   - source: The original source file, or null.
    *   - line: The line number in the original source, or null.
    *   - column: The column number in the original source, or null.
    *   - name: The original identifier, or null.
    */
   SourceMapConsumer.prototype.originalPositionFor =
     function SourceMapConsumer_originalPositionFor(aArgs) {
-      // To return the original position, we must first find the mapping for the
-      // given generated position and then return the original position it
-      // points to. Because the mappings are sorted by generated line/column, we
-      // can use binary search to find the best mapping.
-
-      // To perform a binary search on the mappings, we must be able to compare
-      // two mappings.
-      function compare(mappingA, mappingB) {
-        var cmp = mappingA.generatedLine - mappingB.generatedLine;
-        return cmp === 0
-          ? mappingA.generatedColumn - mappingB.generatedColumn
-          : cmp;
-      }
-
-      // This is the mock of the mapping we are looking for: the needle in the
-      // haystack of mappings.
       var needle = {
         generatedLine: util.getArg(aArgs, 'line'),
         generatedColumn: util.getArg(aArgs, 'column')
       };
 
-      if (needle.generatedLine <= 0) {
-        throw new TypeError('Line must be greater than or equal to 1.');
-      }
-      if (needle.generatedColumn < 0) {
-        throw new TypeError('Column must be greater than or equal to 0.');
-      }
-
-      var mapping = binarySearch.search(needle, this._generatedMappings, compare);
+      var mapping = this._findMapping(needle,
+                                      this._generatedMappings,
+                                      "generatedLine",
+                                      "generatedColumn",
+                                      this._compareGeneratedPositions)
 
       if (mapping) {
         return {
           source: util.getArg(mapping, 'source', null),
           line: util.getArg(mapping, 'originalLine', null),
           column: util.getArg(mapping, 'originalColumn', null),
           name: util.getArg(mapping, 'name', null)
         };
       }
 
       return {
         source: null,
         line: null,
         column: null,
         name: null
       };
+    };
 
+  /**
+   * Returns the generated line and column information for the original source,
+   * line, and column positions provided. The only argument is an object with
+   * the following properties:
+   *
+   *   - source: The filename of the original source.
+   *   - line: The line number in the original source.
+   *   - column: The column number in the original source.
+   *
+   * and an object is returned with the following properties:
+   *
+   *   - line: The line number in the generated source, or null.
+   *   - column: The column number in the generated source, or null.
+   */
+  SourceMapConsumer.prototype.generatedPositionFor =
+    function SourceMapConsumer_generatedPositionFor(aArgs) {
+      var needle = {
+        source: util.getArg(aArgs, 'source'),
+        originalLine: util.getArg(aArgs, 'line'),
+        originalColumn: util.getArg(aArgs, 'column')
+      };
+
+      var mapping = this._findMapping(needle,
+                                      this._originalMappings,
+                                      "originalLine",
+                                      "originalColumn",
+                                      this._compareOriginalPositions)
+
+      if (mapping) {
+        return {
+          line: util.getArg(mapping, 'generatedLine', null),
+          column: util.getArg(mapping, 'generatedColumn', null)
+        };
+      }
+
+      return {
+        line: null,
+        column: null
+      };
     };
 
   exports.SourceMapConsumer = SourceMapConsumer;
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
@@ -880,17 +954,19 @@ define('source-map/source-node', ['requi
    */
   SourceNode.prototype.add = function SourceNode_add(aChunk) {
     if (Array.isArray(aChunk)) {
       aChunk.forEach(function (chunk) {
         this.add(chunk);
       }, this);
     }
     else if (aChunk instanceof SourceNode || typeof aChunk === "string") {
-      this.children.push(aChunk);
+      if (aChunk) {
+        this.children.push(aChunk);
+      }
     }
     else {
       throw new TypeError(
         "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
       );
     }
     return this;
   };
@@ -968,17 +1044,17 @@ define('source-map/source-node', ['requi
    * @param aReplacement The thing to replace the pattern with.
    */
   SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
     var lastChild = this.children[this.children.length - 1];
     if (lastChild instanceof SourceNode) {
       lastChild.replaceRight(aPattern, aReplacement);
     }
     else if (typeof lastChild === 'string') {
-      this.children[this.children.lenth - 1] = lastChild.replace(aPattern, aReplacement);
+      this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
     }
     else {
       this.children.push(''.replace(aPattern, aReplacement));
     }
     return this;
   };
 
   /**
--- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
+++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
@@ -8,17 +8,17 @@
 /*
  * WARNING!
  *
  * Do not edit this file directly, it is built from the sources at
  * https://github.com/mozilla/source-map/
  */
 
 Components.utils.import('resource://gre/modules/devtools/Require.jsm');
-Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+Components.utils.import('resource://gre/modules/devtools/SourceMap.jsm');
 
 let EXPORTED_SYMBOLS = [ "define", "runSourceMapTests" ];
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
@@ -104,33 +104,50 @@ define('test/source-map/util', ['require
     file: 'min.js',
     names: ['bar', 'baz', 'n'],
     sources: ['one.js', 'two.js'],
     sourceRoot: '/the/root',
     mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
   };
 
   function assertMapping(generatedLine, generatedColumn, originalSource,
-                         originalLine, originalColumn, name, map, assert) {
-    var mapping = map.originalPositionFor({
-      line: generatedLine,
-      column: generatedColumn
-    });
-    assert.equal(mapping.name, name,
-                 'Incorrect name, expected ' + JSON.stringify(name)
-                 + ', got ' + JSON.stringify(mapping.name));
-    assert.equal(mapping.line, originalLine,
-                 'Incorrect line, expected ' + JSON.stringify(originalLine)
-                 + ', got ' + JSON.stringify(mapping.line));
-    assert.equal(mapping.column, originalColumn,
-                 'Incorrect column, expected ' + JSON.stringify(originalColumn)
-                 + ', got ' + JSON.stringify(mapping.column));
-    assert.equal(mapping.source, originalSource,
-                 'Incorrect source, expected ' + JSON.stringify(originalSource)
-                 + ', got ' + JSON.stringify(mapping.source));
+                         originalLine, originalColumn, name, map, assert,
+                         dontTestGenerated, dontTestOriginal) {
+    if (!dontTestOriginal) {
+      var origMapping = map.originalPositionFor({
+        line: generatedLine,
+        column: generatedColumn
+      });
+      assert.equal(origMapping.name, name,
+                   'Incorrect name, expected ' + JSON.stringify(name)
+                   + ', got ' + JSON.stringify(origMapping.name));
+      assert.equal(origMapping.line, originalLine,
+                   'Incorrect line, expected ' + JSON.stringify(originalLine)
+                   + ', got ' + JSON.stringify(origMapping.line));
+      assert.equal(origMapping.column, originalColumn,
+                   'Incorrect column, expected ' + JSON.stringify(originalColumn)
+                   + ', got ' + JSON.stringify(origMapping.column));
+      assert.equal(origMapping.source, originalSource,
+                   'Incorrect source, expected ' + JSON.stringify(originalSource)
+                   + ', got ' + JSON.stringify(origMapping.source));
+    }
+
+    if (!dontTestGenerated) {
+      var genMapping = map.generatedPositionFor({
+        source: originalSource,
+        line: originalLine,
+        column: originalColumn
+      });
+      assert.equal(genMapping.line, generatedLine,
+                   'Incorrect line, expected ' + JSON.stringify(generatedLine)
+                   + ', got ' + JSON.stringify(genMapping.line));
+      assert.equal(genMapping.column, generatedColumn,
+                   'Incorrect column, expected ' + JSON.stringify(generatedColumn)
+                   + ', got ' + JSON.stringify(genMapping.column));
+    }
   }
   exports.assertMapping = assertMapping;
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
--- a/toolkit/devtools/sourcemap/tests/unit/test_dog_fooding.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_dog_fooding.js
@@ -51,22 +51,30 @@ define("test/source-map/test-dog-fooding
 
     // Exact
     util.assertMapping(2, 2, '/wu/tang/gza.coffee', 1, 0, null, smc, assert);
     util.assertMapping(3, 2, '/wu/tang/gza.coffee', 2, 0, null, smc, assert);
     util.assertMapping(4, 2, '/wu/tang/gza.coffee', 3, 0, null, smc, assert);
     util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 0, null, smc, assert);
 
     // Fuzzy
-    util.assertMapping(2, 0, null, null, null, null, smc, assert);
-    util.assertMapping(2, 9, '/wu/tang/gza.coffee', 1, 0, null, smc, assert);
-    util.assertMapping(3, 0, '/wu/tang/gza.coffee', 1, 0, null, smc, assert);
-    util.assertMapping(3, 9, '/wu/tang/gza.coffee', 2, 0, null, smc, assert);
-    util.assertMapping(4, 0, '/wu/tang/gza.coffee', 2, 0, null, smc, assert);
-    util.assertMapping(4, 9, '/wu/tang/gza.coffee', 3, 0, null, smc, assert);
-    util.assertMapping(5, 0, '/wu/tang/gza.coffee', 3, 0, null, smc, assert);
-    util.assertMapping(5, 9, '/wu/tang/gza.coffee', 4, 0, null, smc, assert);
+
+    // Original to generated
+    util.assertMapping(2, 0, null, null, null, null, smc, assert, true);
+    util.assertMapping(2, 9, '/wu/tang/gza.coffee', 1, 0, null, smc, assert, true);
+    util.assertMapping(3, 0, '/wu/tang/gza.coffee', 1, 0, null, smc, assert, true);
+    util.assertMapping(3, 9, '/wu/tang/gza.coffee', 2, 0, null, smc, assert, true);
+    util.assertMapping(4, 0, '/wu/tang/gza.coffee', 2, 0, null, smc, assert, true);
+    util.assertMapping(4, 9, '/wu/tang/gza.coffee', 3, 0, null, smc, assert, true);
+    util.assertMapping(5, 0, '/wu/tang/gza.coffee', 3, 0, null, smc, assert, true);
+    util.assertMapping(5, 9, '/wu/tang/gza.coffee', 4, 0, null, smc, assert, true);
+
+    // Generated to original
+    util.assertMapping(2, 2, '/wu/tang/gza.coffee', 1, 1, null, smc, assert, null, true);
+    util.assertMapping(3, 2, '/wu/tang/gza.coffee', 2, 3, null, smc, assert, null, true);
+    util.assertMapping(4, 2, '/wu/tang/gza.coffee', 3, 6, null, smc, assert, null, true);
+    util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 9, null, smc, assert, null, true);
   };
 
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-dog-fooding', do_throw);
 }
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
@@ -56,20 +56,32 @@ define("test/source-map/test-source-map-
     util.assertMapping(2, 1, '/the/root/two.js', 1, 1, null, map, assert);
     util.assertMapping(2, 5, '/the/root/two.js', 1, 5, null, map, assert);
     util.assertMapping(2, 9, '/the/root/two.js', 1, 11, null, map, assert);
     util.assertMapping(2, 18, '/the/root/two.js', 1, 21, 'n', map, assert);
     util.assertMapping(2, 21, '/the/root/two.js', 2, 3, null, map, assert);
     util.assertMapping(2, 28, '/the/root/two.js', 2, 10, 'n', map, assert);
   };
 
-  exports['test mapping tokens back fuzzy'] = function (assert, util) {
+  exports['test mapping tokens fuzzy'] = function (assert, util) {
     var map = new SourceMapConsumer(util.testMap);
 
-    util.assertMapping(1, 20, '/the/root/one.js', 1, 21, 'bar', map, assert);
-    util.assertMapping(1, 30, '/the/root/one.js', 2, 10, 'baz', map, assert);
-    util.assertMapping(2, 12, '/the/root/two.js', 1, 11, null, map, assert);
+    // Finding original positions
+    util.assertMapping(1, 20, '/the/root/one.js', 1, 21, 'bar', map, assert, true);
+    util.assertMapping(1, 30, '/the/root/one.js', 2, 10, 'baz', map, assert, true);
+    util.assertMapping(2, 12, '/the/root/two.js', 1, 11, null, map, assert, true);
+
+    // Finding generated positions
+    util.assertMapping(1, 18, '/the/root/one.js', 1, 22, 'bar', map, assert, null, true);
+    util.assertMapping(1, 28, '/the/root/one.js', 2, 13, 'baz', map, assert, null, true);
+    util.assertMapping(2, 9, '/the/root/two.js', 1, 16, null, map, assert, null, true);
+  };
+
+  exports['test creating source map consumers with )]}\' prefix'] = function (assert, util) {
+    assert.doesNotThrow(function () {
+      var map = new SourceMapConsumer(")]}'" + JSON.stringify(util.testMap));
+    });
   };
 
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-source-map-consumer', do_throw);
 }
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_node.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_node.js
@@ -112,16 +112,32 @@ define("test/source-map/test-source-node
       assert.equal(expected[i].str, chunk);
       assert.equal(expected[i].source, loc.source);
       assert.equal(expected[i].line, loc.line);
       assert.equal(expected[i].column, loc.column);
       i++;
     });
   };
 
+  exports['test .replaceRight'] = function (assert, util) {
+    var node;
+
+    // Not nested
+    node = new SourceNode(null, null, null, 'hello world');
+    node.replaceRight(/world/, 'universe');
+    assert.equal(node.toString(), 'hello universe');
+
+    // Nested
+    node = new SourceNode(null, null, null,
+                          [new SourceNode(null, null, null, 'hey sexy mama, '),
+                           new SourceNode(null, null, null, 'want to kill all humans?')]);
+    node.replaceRight(/kill all humans/, 'watch Futurama');
+    assert.equal(node.toString(), 'hey sexy mama, want to watch Futurama?');
+  };
+
   exports['test .toStringWithSourceMap()'] = function (assert, util) {
     var node = new SourceNode(null, null, null,
                               ['(function () {\n',
                                '  ', new SourceNode(1, 0, 'a.js', ['someCall()']), ';\n',
                                '  ', new SourceNode(2, 0, 'b.js', ['if (foo) bar()']), ';\n',
                                '}());']);
     var map = node.toStringWithSourceMap({
       file: 'foo.js'