Merge backout, a=bustage fix
authorBlair McBride <bmcbride@mozilla.com>
Mon, 09 Sep 2013 12:46:17 +1200
changeset 146134 968d0083e7d7488d1bde70b9a6fcfbe0cc37970a
parent 146133 78128ed0ce263b1d51076c15cb5e0f247a59fa36 (current diff)
parent 146132 6bee20c2e0c6e2f7ddcdd9bebc0c9b6d7a3d0895 (diff)
child 146135 5fa36cd1823d85e98ff906f6b1d1ddb6bc557d9a
push id25242
push useremorley@mozilla.com
push dateMon, 09 Sep 2013 12:13:52 +0000
treeherdermozilla-central@218d4334d29e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbustage
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge backout, a=bustage fix
--- a/browser/devtools/app-manager/test/Makefile.in
+++ b/browser/devtools/app-manager/test/Makefile.in
@@ -2,10 +2,11 @@
 # 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/.
 
 MOCHITEST_CHROME_FILES = \
 		test_template.html \
 		test_connection_store.html \
 		test_device_store.html \
 		test_projects_store.html \
+		test_remain_connected.html \
 		hosted_app.manifest \
 		$(NULL)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/app-manager/test/test_remain_connected.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+
+<!--
+Bug 912646 - Closing app toolbox causes phone to disconnect
+-->
+
+<html>
+
+  <head>
+    <meta charset="utf8">
+    <title></title>
+
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  </head>
+
+  <body>
+
+    <script type="application/javascript;version=1.8">
+      const Cu = Components.utils;
+
+      Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+      DebuggerServer.init(function () { return true; });
+      DebuggerServer.addBrowserActors();
+
+      window.onload = function() {
+        SimpleTest.waitForExplicitFinish();
+
+        Cu.import("resource:///modules/devtools/gDevTools.jsm");
+
+        const {devtools} =
+          Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+        const {require} = devtools;
+
+        const {Connection, ConnectionManager} =
+          require("devtools/client/connection-manager");
+        const ConnectionStore =
+          require("devtools/app-manager/connection-store");
+
+        let connection = ConnectionManager.createConnection();
+
+        connection.host = null; // force pipe
+        connection.port = null;
+
+        let been_through_connecting = false;
+        let been_through_connected = false;
+        let been_through_disconnected = false;
+
+        is(connection.status, Connection.Status.DISCONNECTED,
+           "status updated (diconnected)");
+
+        connection.once("connecting", () => {
+          SimpleTest.executeSoon(() => {
+            been_through_connecting = true;
+            is(connection.status, Connection.Status.CONNECTING,
+               "status updated (connecting)");
+          })
+        });
+
+        connection.once("connected", () => {
+          SimpleTest.executeSoon(() => {
+            been_through_connected = true;
+            is(connection.status, Connection.Status.CONNECTED,
+               "status updated (connected)");
+            cycleToolbox();
+          })
+        });
+
+        function cycleToolbox() {
+          connection.client.listTabs(response => {
+            let options = {
+              form: response.tabs[0],
+              client: connection.client,
+              chrome: true
+            };
+            devtools.TargetFactory.forRemoteTab(options).then(target => {
+              let hostType = devtools.Toolbox.HostType.WINDOW;
+              gDevTools.showToolbox(target,
+                                    null,
+                                    hostType).then(toolbox => {
+                SimpleTest.executeSoon(() => {
+                  toolbox.once("destroyed", onDestroyToolbox);
+                  toolbox.destroy();
+                });
+              });
+            });
+          });
+        }
+
+        function onDestroyToolbox() {
+          is(connection.status, Connection.Status.CONNECTED,
+             "toolbox cycled, still connected");
+          connection.disconnect();
+        }
+
+        connection.once("disconnected", () => {
+          SimpleTest.executeSoon(() => {
+            been_through_disconnected = true;
+            is(connection.status, Connection.Status.DISCONNECTED,
+               "status updated (disconnected)");
+            connection.destroy();
+            finishUp();
+          })
+        });
+
+        function finishUp() {
+          ok(been_through_connecting &&
+             been_through_connected &&
+             been_through_disconnected, "All updates happened");
+          DebuggerServer.destroy();
+          SimpleTest.finish();
+        }
+
+        connection.connect();
+      }
+
+    </script>
+  </body>
+</html>
--- a/browser/devtools/framework/connect/connect.js
+++ b/browser/devtools/framework/connect/connect.js
@@ -164,12 +164,17 @@ function handleConnectionTimeout() {
  */
 function openToolbox(form, chrome=false) {
   let options = {
     form: form,
     client: gClient,
     chrome: chrome
   };
   devtools.TargetFactory.forRemoteTab(options).then((target) => {
-    gDevTools.showToolbox(target, "webconsole", devtools.Toolbox.HostType.WINDOW);
+    let hostType = devtools.Toolbox.HostType.WINDOW;
+    gDevTools.showToolbox(target, "webconsole", hostType).then((toolbox) => {
+      toolbox.once("destroyed", function() {
+        gClient.close();
+      });
+    });
     window.close();
   });
 }
--- a/browser/devtools/framework/target.js
+++ b/browser/devtools/framework/target.js
@@ -41,17 +41,18 @@ exports.TargetFactory = {
   },
 
   /**
    * Return a promise of a Target for a remote tab.
    * @param {Object} options
    *        The options object has the following properties:
    *        {
    *          form: the remote protocol form of a tab,
-   *          client: a DebuggerClient instance,
+   *          client: a DebuggerClient instance
+   *                  (caller owns this and is responsible for closing),
    *          chrome: true if the remote target is the whole process
    *        }
    *
    * @return A promise of a target object
    */
   forRemoteTab: function TF_forRemoteTab(options) {
     let targetPromise = promiseTargets.get(options);
     if (targetPromise == null) {
@@ -328,16 +329,29 @@ TabTarget.prototype = {
     this._webProgressListener = new TabWebProgressListener(this);
     this.tab.linkedBrowser.addProgressListener(this._webProgressListener);
     this.tab.addEventListener("TabClose", this);
     this.tab.parentNode.addEventListener("TabSelect", this);
     this.tab.ownerDocument.defaultView.addEventListener("unload", this);
   },
 
   /**
+   * Teardown event listeners.
+   */
+  _teardownListeners: function TabTarget__teardownListeners() {
+    if (this._webProgressListener) {
+      this._webProgressListener.destroy();
+    }
+
+    this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
+    this._tab.removeEventListener("TabClose", this);
+    this._tab.parentNode.removeEventListener("TabSelect", this);
+  },
+
+  /**
    * Setup listeners for remote debugging, updating existing ones as necessary.
    */
   _setupRemoteListeners: function TabTarget__setupRemoteListeners() {
     this.client.addListener("tabDetached", this.destroy);
 
     this._onTabNavigated = function onRemoteTabNavigated(aType, aPacket) {
       let event = Object.create(null);
       event.url = aPacket.url;
@@ -354,16 +368,24 @@ TabTarget.prototype = {
         this.emit("navigate", event);
         this._navWindow = null;
       }
     }.bind(this);
     this.client.addListener("tabNavigated", this._onTabNavigated);
   },
 
   /**
+   * Teardown listeners for remote debugging.
+   */
+  _teardownRemoteListeners: function TabTarget__teardownRemoteListeners() {
+    this.client.removeListener("tabNavigated", this._onTabNavigated);
+    this.client.removeListener("tabDetached", this.destroy);
+  },
+
+  /**
    * Handle tabs events.
    */
   handleEvent: function (event) {
     switch (event.type) {
       case "TabClose":
       case "unload":
         this.destroy();
         break;
@@ -407,59 +429,62 @@ TabTarget.prototype = {
     this.emit("close");
 
     // First of all, do cleanup tasks that pertain to both remoted and
     // non-remoted targets.
     this.off("thread-resumed", this._handleThreadState);
     this.off("thread-paused", this._handleThreadState);
 
     if (this._tab) {
-      if (this._webProgressListener) {
-        this._webProgressListener.destroy();
-      }
-
-      this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
-      this._tab.removeEventListener("TabClose", this);
-      this._tab.parentNode.removeEventListener("TabSelect", this);
+      this._teardownListeners();
     }
 
     // If this target was not remoted, the promise will be resolved before the
     // function returns.
     if (this._tab && !this._client) {
-      targets.delete(this._tab);
-      this._tab = null;
-      this._client = null;
-      this._form = null;
-      this._remote = null;
-
+      this._cleanup();
       this._destroyer.resolve(null);
     } else if (this._client) {
       // If, on the other hand, this target was remoted, the promise will be
       // resolved after the remote connection is closed.
-      this.client.removeListener("tabNavigated", this._onTabNavigated);
-      this.client.removeListener("tabDetached", this.destroy);
+      this._teardownRemoteListeners();
 
-      this._client.close(function onClosed() {
-        if (this._tab) {
-          targets.delete(this._tab);
-        } else {
-          promiseTargets.delete(this._form);
-        }
-        this._client = null;
-        this._tab = null;
-        this._form = null;
-        this._remote = null;
-
+      if (this.isLocalTab) {
+        // We started with a local tab and created the client ourselves, so we
+        // should close it.
+        this._client.close(() => {
+          this._cleanup();
+          this._destroyer.resolve(null);
+        });
+      } else {
+        // The client was handed to us, so we are not responsible for closing
+        // it.
+        this._cleanup();
         this._destroyer.resolve(null);
-      }.bind(this));
+      }
     }
 
     return this._destroyer.promise;
   },
 
+  /**
+   * Clean up references to what this target points to.
+   */
+  _cleanup: function TabTarget__cleanup() {
+    if (this._tab) {
+      targets.delete(this._tab);
+    } else {
+      promiseTargets.delete(this._form);
+    }
+    this._client = null;
+    this._tab = null;
+    this._form = null;
+    this._remote = null;
+  },
+
   toString: function() {
     return 'TabTarget:' + (this._tab ? this._tab : (this._form && this._form.actor));
   },
 };
 
 
 /**
  * WebProgressListener for TabTarget.
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -842,18 +842,17 @@ Toolbox.prototype = {
     while(container.firstChild) {
       container.removeChild(container.firstChild);
     }
 
     outstanding.push(this._host.destroy());
 
     this._telemetry.destroy();
 
-    // Targets need to be notified that the toolbox is being torn down, so that
-    // remote protocol connections can be gracefully terminated.
+    // Targets need to be notified that the toolbox is being torn down.
     if (this._target) {
       this._target.off("close", this.destroy);
       outstanding.push(this._target.destroy());
     }
     this._target = null;
 
     promise.all(outstanding).then(function() {
       this.emit("destroyed");
--- a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
+++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
@@ -10,27 +10,25 @@ const DEFAULT_CAPTURE_TIMEOUT = 30000; /
 const DESTROY_BROWSER_TIMEOUT = 60000; // ms
 const FRAME_SCRIPT_URL = "chrome://global/content/backgroundPageThumbsContent.js";
 
 const TELEMETRY_HISTOGRAM_ID_PREFIX = "FX_THUMBNAILS_BG_";
 
 // possible FX_THUMBNAILS_BG_CAPTURE_DONE_REASON telemetry values
 const TEL_CAPTURE_DONE_OK = 0;
 const TEL_CAPTURE_DONE_TIMEOUT = 1;
-const TEL_CAPTURE_DONE_PB_BEFORE_START = 2;
-const TEL_CAPTURE_DONE_PB_AFTER_START = 3;
+// 2 and 3 were used when we had special handling for private-browsing.
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/PageThumbs.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 const BackgroundPageThumbs = {
 
   /**
    * Asynchronously captures a thumbnail of the given URL.
    *
    * The page is loaded anonymously, and plug-ins are disabled.
    *
@@ -83,42 +81,30 @@ const BackgroundPageThumbs = {
       // Already fully initialized.
       return true;
     if (this._startedParentWinInit)
       // Already started initializing.
       return false;
 
     this._startedParentWinInit = true;
 
-    PrivateBrowsingUtils.whenHiddenPrivateWindowReady(function (parentWin) {
-      parentWin.addEventListener("unload", function (event) {
-        if (event.target == parentWin.document)
-          this._destroy();
-      }.bind(this), true);
-
-      if (canHostBrowser(parentWin)) {
-        this._parentWin = parentWin;
-        this._processCaptureQueue();
-        return;
-      }
-
-      // Otherwise, create an html:iframe, stick it in the parent document, and
-      // use it to host the browser.  about:blank will not have the system
-      // principal, so it can't host, but a document with a chrome URI will.
-      let iframe = parentWin.document.createElementNS(HTML_NS, "iframe");
-      iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
-      let onLoad = function onLoadFn() {
-        iframe.removeEventListener("load", onLoad, true);
-        this._parentWin = iframe.contentWindow;
-        this._processCaptureQueue();
-      }.bind(this);
-      iframe.addEventListener("load", onLoad, true);
-      parentWin.document.documentElement.appendChild(iframe);
-      this._hostIframe = iframe;
-    }.bind(this));
+    // Create an html:iframe, stick it in the parent document, and
+    // use it to host the browser.  about:blank will not have the system
+    // principal, so it can't host, but a document with a chrome URI will.
+    let hostWindow = Services.appShell.hiddenDOMWindow;
+    let iframe = hostWindow.document.createElementNS(HTML_NS, "iframe");
+    iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
+    let onLoad = function onLoadFn() {
+      iframe.removeEventListener("load", onLoad, true);
+      this._parentWin = iframe.contentWindow;
+      this._processCaptureQueue();
+    }.bind(this);
+    iframe.addEventListener("load", onLoad, true);
+    hostWindow.document.documentElement.appendChild(iframe);
+    this._hostIframe = iframe;
 
     return false;
   },
 
   /**
    * Destroys the service.  Queued and pending captures will never complete, and
    * their consumer callbacks will never be called.
    */
@@ -139,17 +125,16 @@ const BackgroundPageThumbs = {
    */
   _ensureBrowser: function () {
     if (this._thumbBrowser)
       return;
 
     let browser = this._parentWin.document.createElementNS(XUL_NS, "browser");
     browser.setAttribute("type", "content");
     browser.setAttribute("remote", "true");
-    browser.setAttribute("privatebrowsing", "true");
 
     // Size the browser.  Make its aspect ratio the same as the canvases' that
     // the thumbnails are drawn into; the canvases' aspect ratio is the same as
     // the screen's, so use that.  Aim for a size in the ballpark of 1024x768.
     let [swidth, sheight] = [{}, {}];
     Cc["@mozilla.org/gfx/screenmanager;1"].
       getService(Ci.nsIScreenManager).
       primaryScreen.
@@ -245,30 +230,16 @@ Capture.prototype = {
    * Sends a message to the content script to start the capture.
    *
    * @param messageManager  The nsIMessageSender of the thumbnail browser.
    */
   start: function (messageManager) {
     this.startDate = new Date();
     tel("CAPTURE_QUEUE_TIME_MS", this.startDate - this.creationDate);
 
-    // The thumbnail browser uses private browsing mode and therefore shares
-    // browsing state with private windows.  To avoid capturing sites that the
-    // user is logged into in private browsing windows, (1) observe window
-    // openings, and if a private window is opened during capture, discard the
-    // capture when it finishes, and (2) don't start the capture at all if a
-    // private window is open already.
-    Services.ww.registerNotification(this);
-    if (isPrivateBrowsingActive()) {
-      tel("CAPTURE_DONE_REASON", TEL_CAPTURE_DONE_PB_BEFORE_START);
-      // Captures should always finish asyncly.
-      schedule(() => this._done(null));
-      return;
-    }
-
     // timeout timer
     let timeout = typeof(this.options.timeout) == "number" ?
                   this.options.timeout :
                   DEFAULT_CAPTURE_TIMEOUT;
     this._timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     this._timeoutTimer.initWithCallback(this, timeout,
                                         Ci.nsITimer.TYPE_ONE_SHOT);
 
@@ -312,23 +283,16 @@ Capture.prototype = {
   },
 
   // Called when the timeout timer fires.
   notify: function () {
     tel("CAPTURE_DONE_REASON", TEL_CAPTURE_DONE_TIMEOUT);
     this._done(null);
   },
 
-  // Called when the window watcher notifies us.
-  observe: function (subj, topic, data) {
-    if (topic == "domwindowopened" &&
-        PrivateBrowsingUtils.isWindowPrivate(subj))
-      this._privateWinOpenedDuringCapture = true;
-  },
-
   _done: function (data) {
     // Note that _done will be called only once, by either receiveMessage or
     // notify, since it calls destroy, which cancels the timeout timer and
     // removes the didCapture message listener.
 
     this.captureCallback(this);
     this.destroy();
 
@@ -345,62 +309,29 @@ Capture.prototype = {
           callback.call(this.options, this.url);
         }
         catch (err) {
           Cu.reportError(err);
         }
       }
     }.bind(this);
 
-    if (!data || this._privateWinOpenedDuringCapture) {
-      if (this._privateWinOpenedDuringCapture)
-        tel("CAPTURE_DONE_REASON", TEL_CAPTURE_DONE_PB_AFTER_START);
+    if (!data) {
       callOnDones();
       return;
     }
+
     PageThumbs._store(this.url, data.finalURL, data.imageData, data.wasErrorResponse)
               .then(callOnDones);
   },
 };
 
 Capture.nextID = 0;
 
 /**
- * Returns true if the given window is suitable for hosting our xul:browser.
- *
- * @param win  The window.
- * @return     True if the window can host the browser, false otherwise.
- */
-function canHostBrowser(win) {
-  // The host document needs to have the system principal since, like all code
-  // intended to be used in chrome, the browser binding does lots of things that
-  // assume it has it.  The document must also allow XUL children.  So check for
-  // both the system principal and the "allowXULXBL" permission.  (It turns out
-  // that allowXULXBL is satisfied by the system principal alone, making that
-  // check not strictly necessary, but it's here for robustness.)
-  let principal = win.document.nodePrincipal;
-  if (!Services.scriptSecurityManager.isSystemPrincipal(principal))
-    return false;
-  let permResult = Services.perms.testPermissionFromPrincipal(principal,
-                                                              "allowXULXBL");
-  return permResult == Ci.nsIPermissionManager.ALLOW_ACTION;
-}
-
-/**
- * Returns true if there are any private windows.
- */
-function isPrivateBrowsingActive() {
-  let wins = Services.ww.getWindowEnumerator();
-  while (wins.hasMoreElements())
-    if (PrivateBrowsingUtils.isWindowPrivate(wins.getNext()))
-      return true;
-  return false;
-}
-
-/**
  * Adds a value to one of this module's telemetry histograms.
  *
  * @param histogramID  This is prefixed with this module's ID.
  * @param value        The value to add.
  */
 function tel(histogramID, value) {
   let id = TELEMETRY_HISTOGRAM_ID_PREFIX + histogramID;
   Services.telemetry.getHistogramById(id).add(value);
--- a/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
+++ b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
@@ -24,16 +24,21 @@ const backgroundPageThumbsContent = {
     // etc - so set it to the lowest priority available.
     this._webNav.QueryInterface(Ci.nsIDocumentLoader).
       loadGroup.QueryInterface(Ci.nsISupportsPriority).
       priority = Ci.nsISupportsPriority.PRIORITY_LOWEST;
 
     docShell.allowMedia = false;
     docShell.allowPlugins = false;
     docShell.allowContentRetargeting = false;
+    let defaultFlags = Ci.nsIRequest.LOAD_ANONYMOUS |
+                       Ci.nsIRequest.LOAD_BYPASS_CACHE |
+                       Ci.nsIRequest.INHIBIT_CACHING |
+                       Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY;
+    docShell.defaultLoadFlags = defaultFlags;
 
     addMessageListener("BackgroundPageThumbs:capture",
                        this._onCapture.bind(this));
     docShell.
       QueryInterface(Ci.nsIInterfaceRequestor).
       getInterface(Ci.nsIWebProgress).
       addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
   },
--- a/toolkit/components/thumbnails/test/browser_thumbnails_background.js
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_background.js
@@ -17,17 +17,20 @@ function test() {
   spawnNextTest();
 }
 
 function spawnNextTest() {
   if (!tests.length) {
     finish();
     return;
   }
-  imports.Task.spawn(tests.shift()).then(spawnNextTest, function onError(err) {
+
+  let nextTest = tests.shift();
+  info("starting sub-test " + nextTest.name);
+  imports.Task.spawn(nextTest).then(spawnNextTest, function onError(err) {
     ok(false, err);
     spawnNextTest();
   });
 }
 
 let tests = [
 
   function basic() {
@@ -135,36 +138,37 @@ let tests = [
 
     yield capture(url1);
     ok(file1.exists(), "First file should exist after capture.");
     file1.remove(false);
 
     yield wait(2000);
     is(imports.BackgroundPageThumbs._thumbBrowser, undefined,
        "Thumb browser should be destroyed after timeout.");
+    imports.BackgroundPageThumbs._destroyBrowserTimeout = defaultTimeout;
 
     yield capture(url2);
     ok(file2.exists(), "Second file should exist after capture.");
     file2.remove(false);
 
-    imports.BackgroundPageThumbs._destroyBrowserTimeout = defaultTimeout;
     isnot(imports.BackgroundPageThumbs._thumbBrowser, undefined,
           "Thumb browser should exist immediately after capture.");
   },
 
   function privateBrowsingActive() {
     let url = "http://example.com/";
     let file = fileForURL(url);
     ok(!file.exists(), "Thumbnail file should not already exist.");
 
     let win = yield openPrivateWindow();
     let capturedURL = yield capture(url);
     is(capturedURL, url, "Captured URL should be URL passed to capture.");
-    ok(!file.exists(),
-       "Thumbnail file should not exist because a private window is open.");
+    ok(file.exists(),
+       "Thumbnail file should be created even when a private window is open.");
+    file.remove(false);
 
     win.close();
   },
 
   function openPrivateWindowDuringCapture() {
     let url = "http://example.com/";
     let file = fileForURL(url);
     ok(!file.exists(), "Thumbnail file should not already exist.");
@@ -175,19 +179,20 @@ let tests = [
     function maybeFinish() {
       if (++waitCount == 2)
         deferred.resolve();
     }
 
     imports.BackgroundPageThumbs.capture(url, {
       onDone: function (capturedURL) {
         is(capturedURL, url, "Captured URL should be URL passed to capture.");
-        ok(!file.exists(),
-           "Thumbnail file should not exist because a private window " +
+        ok(file.exists(),
+           "Thumbnail file should be created even though a private window " +
            "was opened during the capture.");
+        file.remove(false);
         maybeFinish();
       },
     });
 
     // Opening the private window at this point relies on a couple of
     // implementation details: (1) The capture will start immediately and
     // synchronously (since at this point in the test, the service is
     // initialized and its queue is empty), and (2) when it starts the capture
@@ -195,17 +200,17 @@ let tests = [
     openPrivateWindow().then(function (win) {
       win.close();
       maybeFinish();
     });
 
     yield deferred.promise;
   },
 
-  function noCookies() {
+  function noCookiesSent() {
     // Visit the test page in the browser and tell it to set a cookie.
     let url = testPageURL({ setGreenCookie: true });
     let tab = gBrowser.loadOneTab(url, { inBackground: false });
     let browser = tab.linkedBrowser;
     yield onPageLoad(browser);
 
     // The root element of the page shouldn't be green yet.
     let greenStr = "rgb(0, 255, 0)";
@@ -233,16 +238,37 @@ let tests = [
             "The captured page should not be green.");
       gBrowser.removeTab(tab);
       file.remove(false);
       deferred.resolve();
     });
     yield deferred.promise;
   },
 
+  // check that if a page captured in the background attempts to set a cookie,
+  // that cookie is not saved for subsequent requests.
+  function noCookiesStored() {
+    let url = testPageURL({ setRedCookie: true });
+    yield capture(url);
+    let file = fileForURL(url);
+    ok(file.exists(), "Thumbnail file should exist after capture.");
+    // now load it up in a browser - it should *not* be red, otherwise the
+    // cookie above was saved.
+    let tab = gBrowser.loadOneTab(url, { inBackground: false });
+    let browser = tab.linkedBrowser;
+    yield onPageLoad(browser);
+
+    // The root element of the page shouldn't be red.
+    let redStr = "rgb(255, 0, 0)";
+    isnot(browser.contentDocument.documentElement.style.backgroundColor,
+          redStr,
+          "The page shouldn't be red.");
+    gBrowser.removeTab(tab);
+  },
+
   // the following tests attempt to display modal dialogs.  The test just
   // relies on the fact that if the dialog was displayed the test will hang
   // and timeout.  IOW - the tests would pass if the dialogs appear and are
   // manually closed by the user - so don't do that :)  (obviously there is
   // noone available to do that when run via tbpl etc, so this should be safe,
   // and it's tricky to use the window-watcher to check a window *does not*
   // appear - how long should the watcher be active before assuming it's not
   // going to appear?)
--- a/toolkit/components/thumbnails/test/thumbnails_background.sjs
+++ b/toolkit/components/thumbnails/test/thumbnails_background.sjs
@@ -12,20 +12,30 @@ function handleRequest(req, resp) {
   resp.setHeader("Content-Type", "text/html;charset=utf-8", false);
 
   let opts = {};
   try {
     opts = JSON.parse(decodeURIComponent(req.queryString));
   }
   catch (err) {}
 
+  if (opts.setRedCookie)
+    resp.setHeader("Set-Cookie", "red", false);
+
   if (opts.setGreenCookie)
     resp.setHeader("Set-Cookie", "green", false);
 
   if (req.hasHeader("Cookie") &&
+      req.getHeader("Cookie").split(";").indexOf("red") >= 0) {
+    resp.write('<html style="background: #f00;"></html>');
+    resp.finish();
+    return;
+  }
+
+  if (req.hasHeader("Cookie") &&
       req.getHeader("Cookie").split(";").indexOf("green") >= 0) {
     resp.write('<html style="background: #0f0;"></html>');
     resp.finish();
     return;
   }
 
   if (opts.redirect) {
     resp.setHeader("Location", opts.redirect);