Merge fx-team to mozilla-central
authorWes Kocher <wkocher@mozilla.com>
Wed, 12 Feb 2014 16:26:16 -0800
changeset 168500 7920df861c8a8ea89a0fda63f6dbfcfb3b1cdc22
parent 168415 18e7634d4094be875342aae1426d619e0c6ea0bb (current diff)
parent 168499 dc26e8d484f8739c00afbaaf9795563959b6f774 (diff)
child 168501 fc32df5ed67854500a18b36001e17ea6cf233d59
push id39736
push userryanvm@gmail.com
push dateThu, 13 Feb 2014 15:31:43 +0000
treeherdermozilla-inbound@11fcd667723d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone30.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 fx-team to mozilla-central
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/GeckoApp.java
mobile/android/chrome/content/browser.js
mobile/android/chrome/content/exceptions.js
mobile/android/chrome/jar.mn
--- a/browser/components/customizableui/content/customizeMode.inc.xul
+++ b/browser/components/customizableui/content/customizeMode.inc.xul
@@ -25,16 +25,23 @@
 #NB: because oncommand fires after click, by the time we've fired, the checkbox binding
 #    will already have switched the button's state, so this is correct:
               oncommand="gCustomizeMode.toggleTitlebar(this.hasAttribute('checked'))"/>
 #endif
       <button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu">
         <menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
       </button>
       <spacer flex="1"/>
+      <label id="customization-undo-reset"
+             hidden="true"
+             onclick="gCustomizeMode.undoReset();"
+             onkeypress="gCustomizeMode.undoReset();"
+             class="text-link">
+        &undoCmd.label;
+      </label>
       <button id="customization-reset-button" oncommand="gCustomizeMode.reset();" label="&customizeMode.restoreDefaults;" class="customizationmode-button"/>
     </hbox>
   </vbox>
   <vbox id="customization-panel-container">
     <vbox id="customization-panelWrapper">
       <html:style html:type="text/html" scoped="scoped">
         @import url(chrome://global/skin/popup.css);
       </html:style>
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -284,16 +284,22 @@
               if (this.showingSubView) {
                 setTimeout(this._syncContainerWithSubView.bind(this), 0);
               } else if (!this.transitioning) {
                 setTimeout(this._syncContainerWithMainView.bind(this), 0);
               }
               break;
             case "popupshowing":
               this.setAttribute("panelopen", "true");
+              // Bug 941196 - The panel can get taller when opening a subview. Disabling
+              // autoPositioning means that the panel won't jump around if an opened
+              // subview causes the panel to exceed the dimensions of the screen in the
+              // direction that the panel originally opened in. This property resets
+              // every time the popup closes, which is why we have to set it each time.
+              this._panel.autoPosition = false;
               this._syncContainerWithMainView();
               break;
             case "popupshown":
               this._setMaxHeight();
               break;
             case "popuphidden":
               this.removeAttribute("panelopen");
               this._mainView.style.removeProperty("height");
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -123,16 +123,18 @@ let gBuildAreas = new Map();
  */
 let gBuildWindows = new Map();
 
 let gNewElementCount = 0;
 let gGroupWrapperCache = new Map();
 let gSingleWrapperCache = new WeakMap();
 let gListeners = new Set();
 
+let gUIStateBeforeReset = null;
+
 let gModuleName = "[CustomizableUI]";
 #include logging.js
 
 let CustomizableUIInternal = {
   initialize: function() {
     LOG("Initializing");
 
     this.addListener(this);
@@ -688,16 +690,20 @@ let CustomizableUIInternal = {
       child.setAttribute("wrap", "true");
     }
 
     this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
   },
 
   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
     this.insertNode(aWidgetId, aArea, aPosition, true);
+
+    if (!gResetting) {
+      gUIStateBeforeReset = null;
+    }
   },
 
   onWidgetRemoved: function(aWidgetId, aArea) {
     let areaNodes = gBuildAreas.get(aArea);
     if (!areaNodes) {
       return;
     }
 
@@ -744,20 +750,30 @@ let CustomizableUIInternal = {
         areaNode.setAttribute("currentset", gPlacements.get(aArea).join(','));
       }
 
       let windowCache = gSingleWrapperCache.get(window);
       if (windowCache) {
         windowCache.delete(aWidgetId);
       }
     }
+    if (!gResetting) {
+      gUIStateBeforeReset = null;
+    }
   },
 
   onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
     this.insertNode(aWidgetId, aArea, aNewPosition);
+    if (!gResetting) {
+      gUIStateBeforeReset = null;
+    }
+  },
+
+  onCustomizeEnd: function(aWindow) {
+    gUIStateBeforeReset = null;
   },
 
   registerBuildArea: function(aArea, aNode) {
     // We ensure that the window is registered to have its customization data
     // cleaned up when unloading.
     let window = aNode.ownerDocument.defaultView;
     if (window.closed) {
       return;
@@ -2044,46 +2060,76 @@ let CustomizableUIInternal = {
       }
     }
 
     return null;
   },
 
   reset: function() {
     gResetting = true;
+    this._resetUIState();
+
+    // Rebuild each registered area (across windows) to reflect the state that
+    // was reset above.
+    this._rebuildRegisteredAreas();
+
+    gResetting = false;
+  },
+
+  _resetUIState: function() {
+    try {
+      gUIStateBeforeReset = Services.prefs.getCharPref(kPrefCustomizationState);
+    } catch(e) { }
+
     Services.prefs.clearUserPref(kPrefCustomizationState);
     LOG("State reset");
 
     // Reset placements to make restoring default placements possible.
     gPlacements = new Map();
     gDirtyAreaCache = new Set();
     gSeenWidgets = new Set();
     // Clear the saved state to ensure that defaults will be used.
     gSavedState = null;
     // Restore the state for each area to its defaults
     for (let [areaId,] of gAreas) {
       this.restoreStateForArea(areaId);
     }
-
-    // Rebuild each registered area (across windows) to reflect the state that
-    // was reset above.
+  },
+
+  _rebuildRegisteredAreas: function() {
     for (let [areaId, areaNodes] of gBuildAreas) {
       let placements = gPlacements.get(areaId);
       for (let areaNode of areaNodes) {
         this.buildArea(areaId, placements, areaNode);
 
         let area = gAreas.get(areaId);
         if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
           let defaultCollapsed = area.get("defaultCollapsed");
           let win = areaNode.ownerDocument.defaultView;
           win.setToolbarVisibility(areaNode, !defaultCollapsed);
         }
       }
     }
-    gResetting = false;
+  },
+
+  /**
+   * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
+   */
+  undoReset: function() {
+    if (!gUIStateBeforeReset) {
+      return;
+    }
+    Services.prefs.setCharPref(kPrefCustomizationState, gUIStateBeforeReset);
+    this.loadSavedState();
+    for (let areaId of Object.keys(gSavedState.placements)) {
+      let placements = gSavedState.placements[areaId];
+      gPlacements.set(areaId, placements);
+    }
+    this._rebuildRegisteredAreas();
+    gUIStateBeforeReset = null;
   },
 
   /**
    * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance).
    * @return {Boolean} whether the widget is removable
    */
   isWidgetRemovable: function(aWidget) {
     let widgetId;
@@ -2827,16 +2873,35 @@ this.CustomizableUI = {
    *
    * This is the nuclear option. You should never call this except if the user
    * explicitly requests it. Firefox does this when the user clicks the
    * "Restore Defaults" button in customize mode.
    */
   reset: function() {
     CustomizableUIInternal.reset();
   },
+
+  /**
+   * Undo the previous reset, can only be called immediately after a reset.
+   * @return a promise that will be resolved when the operation is complete.
+   */
+  undoReset: function() {
+    CustomizableUIInternal.undoReset();
+  },
+
+  /**
+   * Can the last Restore Defaults operation be undone.
+   *
+   * @return A boolean stating whether an undo of the
+   *         Restore Defaults can be performed.
+   */
+  get canUndoReset() {
+    return !!gUIStateBeforeReset;
+  },
+
   /**
    * Get the placement of a widget. This is by far the best way to obtain
    * information about what the state of your widget is. The internals of
    * this call are cheap (no DOM necessary) and you will know where the user
    * has put your widget.
    *
    * @param aWidgetId the ID of the widget whose placement you want to know
    * @return
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -234,16 +234,17 @@ CustomizeMode.prototype = {
       this.visiblePalette.addEventListener("dragend", this, true);
 
       window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
 
       document.getElementById("PanelUI-help").setAttribute("disabled", true);
       document.getElementById("PanelUI-quit").setAttribute("disabled", true);
 
       this._updateResetButton();
+      this._updateUndoResetButton();
 
       this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL &&
                                   Services.prefs.getBoolPref(kSkipSourceNodePref);
 
       let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])");
       for (let toolbar of customizableToolbars)
         toolbar.setAttribute("customizing", true);
 
@@ -849,22 +850,45 @@ CustomizeMode.prototype = {
       CustomizableUI.reset();
 
       yield this._wrapToolbarItems();
       yield this.populatePalette();
 
       this.persistCurrentSets(true);
 
       this._updateResetButton();
+      this._updateUndoResetButton();
       this._updateEmptyPaletteNotice();
       this._showPanelCustomizationPlaceholders();
       this.resetting = false;
     }.bind(this)).then(null, ERROR);
   },
 
+  undoReset: function() {
+    this.resetting = true;
+
+    return Task.spawn(function() {
+      this._removePanelCustomizationPlaceholders();
+      yield this.depopulatePalette();
+      yield this._unwrapToolbarItems();
+
+      CustomizableUI.undoReset();
+
+      yield this._wrapToolbarItems();
+      yield this.populatePalette();
+
+      this.persistCurrentSets(true);
+
+      this._updateResetButton();
+      this._updateUndoResetButton();
+      this._updateEmptyPaletteNotice();
+      this.resetting = false;
+    }.bind(this)).then(null, ERROR);
+  },
+
   _onToolbarVisibilityChange: function(aEvent) {
     let toolbar = aEvent.target;
     if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") {
       toolbar.setAttribute("customizing", "true");
     } else {
       toolbar.removeAttribute("customizing");
     }
     this._onUIChange();
@@ -953,31 +977,37 @@ CustomizeMode.prototype = {
       }
     }
   },
 
   _onUIChange: function() {
     this._changed = true;
     if (!this.resetting) {
       this._updateResetButton();
+      this._updateUndoResetButton();
       this._updateEmptyPaletteNotice();
     }
     this.dispatchToolboxEvent("customizationchange");
   },
 
   _updateEmptyPaletteNotice: function() {
     let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
     this.paletteEmptyNotice.hidden = !!paletteItems.length;
   },
 
   _updateResetButton: function() {
     let btn = this.document.getElementById("customization-reset-button");
     btn.disabled = CustomizableUI.inDefaultState;
   },
 
+  _updateUndoResetButton: function() {
+    let undoReset =  this.document.getElementById("customization-undo-reset");
+    undoReset.hidden = !CustomizableUI.canUndoReset;
+  },
+
   handleEvent: function(aEvent) {
     switch(aEvent.type) {
       case "toolbarvisibilitychange":
         this._onToolbarVisibilityChange(aEvent);
         break;
       case "dragstart":
         this._onDragStart(aEvent);
         break;
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -60,9 +60,10 @@ skip-if = os == "linux"
 [browser_944887_destroyWidget_should_destroy_in_palette.js]
 [browser_945739_showInPrivateBrowsing_customize_mode.js]
 [browser_947987_removable_default.js]
 [browser_948985_non_removable_defaultArea.js]
 [browser_952963_areaType_getter_no_area.js]
 [browser_956602_remove_special_widget.js]
 [browser_969427_recreate_destroyed_widget_after_reset.js]
 [browser_969661_character_encoding_navbar_disabled.js]
+[browser_970511_undo_restore_default.js]
 [browser_panel_toggle.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_970511_undo_restore_default.js
@@ -0,0 +1,65 @@
+/* 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/. */
+
+"use strict";
+
+// Restoring default should show an "undo" option which undoes the restoring operation.
+add_task(function() {
+  let homeButtonId = "home-button";
+  CustomizableUI.removeWidgetFromArea(homeButtonId);
+  yield startCustomizing();
+  ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
+  is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
+  let undoReset = document.getElementById("customization-undo-reset");
+  is(undoReset.hidden, true, "The undo button is hidden before reset");
+
+  yield gCustomizeMode.reset();
+
+  ok(CustomizableUI.inDefaultState, "In default state after reset");
+  is(undoReset.hidden, false, "The undo button is visible after reset");
+
+  undoReset.click();
+  yield waitForCondition(function() !gCustomizeMode.resetting);
+  ok(!CustomizableUI.inDefaultState, "Not in default state after reset-undo");
+  is(undoReset.hidden, true, "The undo button is hidden after clicking on the undo button");
+  is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
+
+  yield gCustomizeMode.reset();
+});
+
+// Performing an action after a reset will hide the reset button.
+add_task(function() {
+  let homeButtonId = "home-button";
+  CustomizableUI.removeWidgetFromArea(homeButtonId);
+  ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
+  is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
+  let undoReset = document.getElementById("customization-undo-reset");
+  is(undoReset.hidden, true, "The undo button is hidden before reset");
+
+  yield gCustomizeMode.reset();
+
+  ok(CustomizableUI.inDefaultState, "In default state after reset");
+  is(undoReset.hidden, false, "The undo button is visible after reset");
+
+  CustomizableUI.addWidgetToArea(homeButtonId, CustomizableUI.AREA_PANEL);
+  is(undoReset.hidden, true, "The undo button is hidden after another change");
+});
+
+// "Restore defaults", exiting customize, and re-entering shouldn't show the Undo button
+add_task(function() {
+  let undoReset = document.getElementById("customization-undo-reset");
+  is(undoReset.hidden, true, "The undo button is hidden before a reset");
+  ok(!CustomizableUI.inDefaultState, "The browser should not be in default state");
+  yield gCustomizeMode.reset();
+
+  is(undoReset.hidden, false, "The undo button is hidden after a reset");
+  yield endCustomizing();
+  yield startCustomizing();
+  is(undoReset.hidden, true, "The undo reset button should be hidden after entering customization mode");
+});
+
+add_task(function asyncCleanup() {
+  yield gCustomizeMode.reset();
+  yield endCustomizing();
+});
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -8,18 +8,20 @@ support-files =
   browser_inspector_markup_765105_tooltip.png
   browser_inspector_markup_950732.html
   head.js
 
 [browser_bug896181_css_mixed_completion_new_attribute.js]
 # Bug 916763 - too many intermittent failures
 skip-if = true
 [browser_inspector_markup_edit.js]
-# Bug 904953 - too many intermittent failures on Linux
-skip-if = os == "linux"
+[browser_inspector_markup_edit_2.js]
+[browser_inspector_markup_edit_3.js]
+[browser_inspector_markup_edit_4.js]
+[browser_inspector_markup_add_attributes.js]
 [browser_inspector_markup_edit_outerhtml.js]
 [browser_inspector_markup_edit_outerhtml2.js]
 [browser_inspector_markup_mutation.js]
 [browser_inspector_markup_mutation_flashing.js]
 [browser_inspector_markup_navigation.js]
 [browser_inspector_markup_subset.js]
 [browser_inspector_markup_765105_tooltip.js]
 [browser_inspector_markup_950732.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_add_attributes.js
@@ -0,0 +1,170 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that adding various types of attributes to nodes in the markup-view
+ * works as expected. Also checks that the changes are properly undoable and
+ * redoable. For each step in the test, we:
+ * - Create a new DIV
+ * - Make the change, check that the change was made as we expect
+ * - Undo the change, check that the node is back in its original state
+ * - Redo the change, check that the node change was made again correctly.
+ */
+
+waitForExplicitFinish();
+
+let TEST_URL = "data:text/html,<div>markup-view attributes addition test</div>";
+let TEST_DATA = [{
+  desc: "Add an attribute value without closing \"",
+  enteredText: 'style="display: block;',
+  expectedAttributes: {
+    style: "display: block;"
+  }
+}, {
+  desc: "Add an attribute value without closing '",
+  enteredText: "style='display: inline;",
+  expectedAttributes: {
+    style: "display: inline;"
+  }
+}, {
+  desc: "Add an attribute wrapped with with double quotes double quote in it",
+  enteredText: 'style="display: "inline',
+  expectedAttributes: {
+    style: "display: ",
+    inline: ""
+  }
+}, {
+  desc: "Add an attribute wrapped with single quotes with single quote in it",
+  enteredText: "style='display: 'inline",
+  expectedAttributes: {
+    style: "display: ",
+    inline: ""
+  }
+}, {
+  desc: "Add an attribute with no value",
+  enteredText: "disabled",
+  expectedAttributes: {
+    disabled: ""
+  }
+}, {
+  desc: "Add multiple attributes with no value",
+  enteredText: "disabled autofocus",
+  expectedAttributes: {
+    disabled: "",
+    autofocus: ""
+  }
+}, {
+  desc: "Add multiple attributes with no value, and some with value",
+  enteredText: "disabled name='name' data-test='test' autofocus",
+  expectedAttributes: {
+    disabled: "",
+    autofocus: "",
+    name: "name",
+    'data-test': "test"
+  }
+}, {
+  desc: "Add attribute with xmlns",
+  enteredText: "xmlns:edi='http://ecommerce.example.org/schema'",
+  expectedAttributes: {
+    'xmlns:edi': "http://ecommerce.example.org/schema"
+  }
+}, {
+  desc: "Mixed single and double quotes",
+  enteredText: "name=\"hi\" maxlength='not a number'",
+  expectedAttributes: {
+    maxlength: "not a number",
+    name: "hi"
+  }
+}, {
+  desc: "Invalid attribute name",
+  enteredText: "x='y' <why-would-you-do-this>=\"???\"",
+  expectedAttributes: {
+    x: "y"
+  }
+}, {
+  desc: "Double quote wrapped in single quotes",
+  enteredText: "x='h\"i'",
+  expectedAttributes: {
+    x: "h\"i"
+  }
+}, {
+  desc: "Single quote wrapped in double quotes",
+  enteredText: "x=\"h'i\"",
+  expectedAttributes: {
+    x: "h'i"
+  }
+}, {
+  desc: "No quote wrapping",
+  enteredText: "a=b x=y data-test=Some spaced data",
+  expectedAttributes: {
+    a: "b",
+    x: "y",
+    "data-test": "Some",
+    spaced: "",
+    data: ""
+  }
+}, {
+  desc: "Duplicate Attributes",
+  enteredText: "a=b a='c' a=\"d\"",
+  expectedAttributes: {
+    a: "b"
+  }
+}, {
+  desc: "Inline styles",
+  enteredText: "style=\"font-family: 'Lucida Grande', sans-serif; font-size: 75%;\"",
+  expectedAttributes: {
+    style: "font-family: 'Lucida Grande', sans-serif; font-size: 75%;"
+  }
+}, {
+  desc: "Object attribute names",
+  enteredText: "toString=\"true\" hasOwnProperty=\"false\"",
+  expectedAttributes: {
+    toString: "true",
+    hasOwnProperty: "false"
+  }
+}, {
+  desc: "Add event handlers",
+  enteredText: "onclick=\"javascript: throw new Error('wont fire');\" onload=\"alert('here');\"",
+  expectedAttributes: {
+    onclick: "javascript: throw new Error('wont fire');",
+    onload: "alert('here');"
+  }
+}];
+
+function test() {
+  Task.spawn(function() {
+    info("Opening the inspector on the test URL");
+    let args = yield addTab(TEST_URL).then(openInspector);
+    let inspector = args.inspector;
+    let markup = inspector.markup;
+
+    info("Selecting the test node");
+    let div = getNode("div");
+    yield selectNode(div, inspector);
+    let editor = getContainerForRawNode(markup, div).editor;
+
+    for (let test of TEST_DATA) {
+      info("Starting test: " + test.desc);
+
+      info("Enter the new attribute(s) test: " + test.enteredText);
+      let nodeMutated = inspector.once("markupmutation");
+      setEditableFieldValue(editor.newAttr, test.enteredText, inspector);
+      yield nodeMutated;
+
+      info("Assert that the attribute(s) has/have been applied correctly");
+      assertAttributes(div, test.expectedAttributes);
+
+      info("Undo the change");
+      yield undoChange(inspector);
+
+      info("Assert that the attribute(s) has/have been removed correctly");
+      assertAttributes(div, {});
+    }
+
+    yield inspector.once("inspector-updated");
+
+    gBrowser.removeCurrentTab();
+  }).then(null, ok.bind(null, false)).then(finish);
+}
--- a/browser/devtools/markupview/test/browser_inspector_markup_edit.js
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
@@ -1,817 +1,441 @@
 /* Any copyright", " is dedicated to the Public Domain.
 http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 /**
- * Tests that various editors work as expected.  Also checks
+ * Tests that various editors work as expected. Also checks
  * that the various changes are properly undoable and redoable.
  * For each step in the test, we:
  * - Run the setup for that test (if any)
  * - Check that the node we're editing is as we expect
  * - Make the change, check that the change was made as we expect
  * - Undo the change, check that the node is back in its original state
  * - Redo the change, check that the node change was made again correctly.
  *
  * This test mostly tries to verify that the editor makes changes to the
  * underlying DOM, not that the UI updates - UI updates are based on
  * underlying DOM changes, and the mutation tests should cover those cases.
  */
 
-function test() {
-  let inspector;
-  let {
-    getInplaceEditorForSpan: inplaceEditor
-  } = devtools.require("devtools/shared/inplace-editor");
+waitForExplicitFinish();
+
+let doc, inspector, markup;
 
-  // Prevent intermittent "test exceeded the timeout threshold" since this is
-  // a slow test: https://bugzilla.mozilla.org/show_bug.cgi?id=904953.
-  requestLongerTimeout(2);
-
-  waitForExplicitFinish();
+let TEST_URL = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_edit.html";
+let LONG_ATTRIBUTE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+let LONG_ATTRIBUTE_COLLAPSED = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEF\u2026UVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+let DATA_URL_INLINE_STYLE='color: red; background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC");';
+let DATA_URL_INLINE_STYLE_COLLAPSED='color: red; background: url("data:image/png;base64,iVBORw0KG\u2026NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC");';
+let DATA_URL_ATTRIBUTE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC";
+let DATA_URL_ATTRIBUTE_COLLAPSED = "data:image/png;base64,iVBORw0K\u20269/AFGGFyjOXZtQAAAAAElFTkSuQmCC";
 
-  // Will hold the doc we're viewing
-  let doc;
-
-  // Holds the MarkupTool object we're testing.
-  let markup;
+let TEST_DATA = [
+  {
+    desc: "Change an attribute",
+    before: function() {
+      assertAttributes(doc.querySelector("#node1"), {
+        id: "node1",
+        class: "node1"
+      });
+    },
+    execute: function(after) {
+      inspector.once("markupmutation", after);
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node1")).editor;
+      let attr = editor.attrs["class"].querySelector(".editable");
+      setEditableFieldValue(attr, 'class="changednode1"', inspector);
+    },
+    after: function() {
+      assertAttributes(doc.querySelector("#node1"), {
+        id: "node1",
+        class: "changednode1"
+      });
+    }
+  },
+  {
+    desc: 'Try changing an attribute to a quote (") - this should result ' +
+          'in it being set to an empty string',
+    before: function() {
+      assertAttributes(doc.querySelector("#node22"), {
+        id: "node22",
+        class: "unchanged"
+      });
+    },
+    execute: function(after) {
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node22")).editor;
+      let attr = editor.attrs["class"].querySelector(".editable");
+      setEditableFieldValue(attr, 'class="""', inspector);
+      inspector.once("markupmutation", after);
+    },
+    after: function() {
+      assertAttributes(doc.querySelector("#node22"), {
+        id: "node22",
+        class: ""
+      });
+    }
+  },
+  {
+    desc: "Remove an attribute",
+    before: function() {
+      assertAttributes(doc.querySelector("#node4"), {
+        id: "node4",
+        class: "node4"
+      });
+    },
+    execute: function(after) {
+      inspector.once("markupmutation", after);
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node4")).editor;
+      let attr = editor.attrs["class"].querySelector(".editable");
+      setEditableFieldValue(attr, '', inspector);
+    },
+    after: function() {
+      assertAttributes(doc.querySelector("#node4"), {
+        id: "node4",
+      });
+    }
+  },
+  {
+    desc: "Add an attribute by clicking the empty space after a node",
+    before: function() {
+      assertAttributes(doc.querySelector("#node14"), {
+        id: "node14",
+      });
+    },
+    execute: function(after) {
+      inspector.once("markupmutation", after);
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node14")).editor;
+      let attr = editor.newAttr;
+      setEditableFieldValue(attr, 'class="newclass" style="color:green"', inspector);
+    },
+    after: function() {
+      assertAttributes(doc.querySelector("#node14"), {
+        id: "node14",
+        class: "newclass",
+        style: "color:green"
+      });
+    }
+  },
+  {
+    desc: 'Try add an attribute containing a quote (") attribute by ' +
+          'clicking the empty space after a node - this should result ' +
+          'in it being set to an empty string',
+    before: function() {
+      assertAttributes(doc.querySelector("#node23"), {
+        id: "node23",
+      });
+    },
+    execute: function(after) {
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node23")).editor;
+      let attr = editor.newAttr;
+      setEditableFieldValue(attr, 'class="newclass" style="""', inspector);
+      inspector.once("markupmutation", after);
+    },
+    after: function() {
+      assertAttributes(doc.querySelector("#node23"), {
+        id: "node23",
+        class: "newclass",
+        style: ""
+      });
+    }
+  },
+  {
+    desc: "Try add attributes by adding to an existing attribute's entry",
+    before: function() {
+      assertAttributes(doc.querySelector("#node24"), {
+        id: "node24",
+      });
+    },
+    execute: function(after) {
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
+      let attr = editor.attrs["id"].querySelector(".editable");
+      setEditableFieldValue(attr, attr.textContent + ' class="""', inspector);
+      inspector.once("markupmutation", after);
+    },
+    after: function() {
+      assertAttributes(doc.querySelector("#node24"), {
+        id: "node24",
+        class: ""
+      });
+    }
+  },
+  {
+    desc: "Try to add long attribute to make sure it is collapsed in attribute editor.",
+    before: function() {
+      assertAttributes(doc.querySelector("#node24"), {
+        id: "node24",
+        class: ""
+      });
+    },
+    execute: function(after) {
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
+      let attr = editor.newAttr;
+      setEditableFieldValue(attr, 'data-long="'+LONG_ATTRIBUTE+'"', inspector);
+      inspector.once("markupmutation", after);
+    },
+    after: function() {
 
-  let LONG_ATTRIBUTE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
-  let LONG_ATTRIBUTE_COLLAPSED = "ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEF\u2026UVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
+      let visibleAttrText = editor.attrs["data-long"].querySelector(".attr-value").textContent;
+      is (visibleAttrText, LONG_ATTRIBUTE_COLLAPSED)
 
-  let DATA_URL_INLINE_STYLE='color: red; background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC");';
-  let DATA_URL_INLINE_STYLE_COLLAPSED='color: red; background: url("data:image/png;base64,iVBORw0KG\u2026NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC");';
-
-  let DATA_URL_ATTRIBUTE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC";
-  let DATA_URL_ATTRIBUTE_COLLAPSED = "data:image/png;base64,iVBORw0K\u20269/AFGGFyjOXZtQAAAAAElFTkSuQmCC";
+      assertAttributes(doc.querySelector("#node24"), {
+        id: "node24",
+        class: "",
+        'data-long':LONG_ATTRIBUTE
+      });
+    }
+  },
+  {
+    desc: "Try to modify the collapsed long attribute, making sure it expands.",
+    before: function() {
+      assertAttributes(doc.querySelector("#node24"), {
+        id: "node24",
+        class: "",
+        'data-long': LONG_ATTRIBUTE
+      });
+    },
+    execute: function(after) {
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
+      let attr = editor.attrs["data-long"].querySelector(".editable");
 
-  /**
-   * Edit a given editableField
-   */
-  function editField(aField, aValue)
+      // Check to make sure it has expanded after focus
+      attr.focus();
+      EventUtils.sendKey("return", inspector.panelWin);
+      let input = inplaceEditor(attr).input;
+      is (input.value, 'data-long="'+LONG_ATTRIBUTE+'"');
+      EventUtils.sendKey("escape", inspector.panelWin);
+
+      setEditableFieldValue(attr, input.value  + ' data-short="ABC"', inspector);
+      inspector.once("markupmutation", after);
+    },
+    after: function() {
+
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
+      let visibleAttrText = editor.attrs["data-long"].querySelector(".attr-value").textContent;
+      is (visibleAttrText, LONG_ATTRIBUTE_COLLAPSED)
+
+      assertAttributes(doc.querySelector("#node24"), {
+        id: "node24",
+        class: "",
+        'data-long': LONG_ATTRIBUTE,
+        "data-short": "ABC"
+      });
+    }
+  },
   {
-    aField.focus();
-    EventUtils.sendKey("return", inspector.panelWin);
-    let input = inplaceEditor(aField).input;
-    input.value = aValue;
-    EventUtils.sendKey("return", inspector.panelWin);
-  }
+    desc: "Try to add long data URL to make sure it is collapsed in attribute editor.",
+    before: function() {
+      assertAttributes(doc.querySelector("#node-data-url"), {
+        id: "node-data-url"
+      });
+    },
+    execute: function(after) {
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node-data-url")).editor;
+      let attr = editor.newAttr;
+      setEditableFieldValue(attr, 'src="'+DATA_URL_ATTRIBUTE+'"', inspector);
+      inspector.once("markupmutation", after);
+    },
+    after: function() {
+
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node-data-url")).editor;
+      let visibleAttrText = editor.attrs["src"].querySelector(".attr-value").textContent;
+      is (visibleAttrText, DATA_URL_ATTRIBUTE_COLLAPSED);
+
+      let node = doc.querySelector("#node-data-url");
+      is (node.width, 16, "Image width has been set after data url src.");
+      is (node.height, 16, "Image height has been set after data url src.");
+
+      assertAttributes(node, {
+        id: "node-data-url",
+        "src": DATA_URL_ATTRIBUTE
+      });
+    }
+  },
+  {
+    desc: "Try to add long data URL to make sure it is collapsed in attribute editor.",
+    before: function() {
+      assertAttributes(doc.querySelector("#node-data-url-style"), {
+        id: "node-data-url-style"
+      });
+    },
+    execute: function(after) {
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node-data-url-style")).editor;
+      let attr = editor.newAttr;
+      setEditableFieldValue(attr, "style='"+DATA_URL_INLINE_STYLE+"'", inspector);
+      inspector.once("markupmutation", after);
+    },
+    after: function() {
+
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node-data-url-style")).editor;
+      let visibleAttrText = editor.attrs["style"].querySelector(".attr-value").textContent;
+      is (visibleAttrText, DATA_URL_INLINE_STYLE_COLLAPSED)
 
-  /**
-   * Check that the appropriate attributes are assigned to a node.
-   *
-   * @param  {HTMLNode} aElement
-   *         The node to check.
-   * @param  {Object} aAttributes
-   *         An object containing the arguments to check.
-   *         e.g. {id="id1",class="someclass"}
-   *
-   * NOTE: When checking attribute values bare in mind that node.getAttribute()
-   *       returns attribute values provided by the HTML parser. The parser only
-   *       provides unescaped entities so &amp; will return &.
-   */
-  function assertAttributes(aElement, aAttributes)
+      assertAttributes(doc.querySelector("#node-data-url-style"), {
+        id: "node-data-url-style",
+        'style': DATA_URL_INLINE_STYLE
+      });
+    }
+  },
+  {
+    desc: "Edit text",
+    before: function() {
+      let node = doc.querySelector('.node6').firstChild;
+      is(node.nodeValue, "line6", "Text should be unchanged");
+    },
+    execute: function(after) {
+      inspector.once("markupmutation", after);
+      let node = doc.querySelector('.node6').firstChild;
+      let editor = getContainerForRawNode(markup, node).editor;
+      let field = editor.elt.querySelector("pre");
+      setEditableFieldValue(field, "New text", inspector);
+    },
+    after: function() {
+      let node = doc.querySelector('.node6').firstChild;
+      is(node.nodeValue, "New text", "Text should be changed.");
+    },
+  },
+  {
+    desc: "Add an attribute value containing < > &uuml; \" & '",
+    before: function() {
+      assertAttributes(doc.querySelector("#node25"), {
+        id: "node25",
+      });
+    },
+    execute: function(after) {
+      inspector.once("markupmutation", after);
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node25")).editor;
+      let attr = editor.newAttr;
+      setEditableFieldValue(attr, 'src="somefile.html?param1=<a>&param2=&uuml;&param3=\'&quot;\'"', inspector);
+    },
+    after: function() {
+      assertAttributes(doc.querySelector("#node25"), {
+        id: "node25",
+        src: "somefile.html?param1=<a>&param2=\xfc&param3='\"'"
+      });
+    }
+  },
   {
-    let attrs = Object.getOwnPropertyNames(aAttributes);
-    is(aElement.attributes.length, attrs.length,
-      "Node has the correct number of attributes");
-    for (let attr of attrs) {
-      is(aElement.getAttribute(attr), aAttributes[attr],
-        "Node has the correct " + attr + " attribute.");
+    desc: "Modify inline style containing \"",
+    before: function() {
+      assertAttributes(doc.querySelector("#node26"), {
+        id: "node26",
+        style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");'
+      });
+    },
+    execute: function(after) {
+      inspector.once("markupmutation", after);
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node26")).editor;
+      let attr = editor.attrs["style"].querySelector(".editable");
+
+
+      attr.focus();
+      EventUtils.sendKey("return", inspector.panelWin);
+
+      let input = inplaceEditor(attr).input;
+      let value = input.value;
+
+      is (value,
+        "style='background-image: url(\"moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F\");'",
+        "Value contains actual double quotes"
+      );
+
+      value = value.replace(/mozilla\.org/, "mozilla.com");
+      input.value = value;
+
+      EventUtils.sendKey("return", inspector.panelWin);
+    },
+    after: function() {
+      assertAttributes(doc.querySelector("#node26"), {
+        id: "node26",
+        style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.com%2F");'
+      });
+    }
+  },
+  {
+    desc: "Modify inline style containing \" and \'",
+    before: function() {
+      assertAttributes(doc.querySelector("#node27"), {
+        id: "node27",
+        class: 'Double " and single \''
+      });
+    },
+    execute: function(after) {
+      inspector.once("markupmutation", after);
+      let editor = getContainerForRawNode(markup, doc.querySelector("#node27")).editor;
+      let attr = editor.attrs["class"].querySelector(".editable");
+
+      attr.focus();
+      EventUtils.sendKey("return", inspector.panelWin);
+
+      let input = inplaceEditor(attr).input;
+      let value = input.value;
+
+      is (value, "class=\"Double &quot; and single '\"", "Value contains &quot;");
+
+      value = value.replace(/Double/, "&quot;").replace(/single/, "'");
+      input.value = value;
+
+      EventUtils.sendKey("return", inspector.panelWin);
+    },
+    after: function() {
+      assertAttributes(doc.querySelector("#node27"), {
+        id: "node27",
+        class: '" " and \' \''
+      });
     }
   }
-
-  // All the mutation types we want to test.
-  let edits = [
-    {
-      desc: "Change an attribute",
-      before: function() {
-        assertAttributes(doc.querySelector("#node1"), {
-          id: "node1",
-          class: "node1"
-        });
-      },
-      execute: function(after) {
-        inspector.once("markupmutation", after);
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node1")).editor;
-        let attr = editor.attrs["class"].querySelector(".editable");
-        editField(attr, 'class="changednode1"');
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node1"), {
-          id: "node1",
-          class: "changednode1"
-        });
-      }
-    },
-    {
-      desc: 'Try changing an attribute to a quote (") - this should result ' +
-            'in it being set to an empty string',
-      before: function() {
-        assertAttributes(doc.querySelector("#node22"), {
-          id: "node22",
-          class: "unchanged"
-        });
-      },
-      execute: function(after) {
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node22")).editor;
-        let attr = editor.attrs["class"].querySelector(".editable");
-        editField(attr, 'class="""');
-        inspector.once("markupmutation", after);
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node22"), {
-          id: "node22",
-          class: ""
-        });
-      }
-    },
-
-    {
-      desc: "Remove an attribute",
-      before: function() {
-        assertAttributes(doc.querySelector("#node4"), {
-          id: "node4",
-          class: "node4"
-        });
-      },
-      execute: function(after) {
-        inspector.once("markupmutation", after);
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node4")).editor;
-        let attr = editor.attrs["class"].querySelector(".editable");
-        editField(attr, '');
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node4"), {
-          id: "node4",
-        });
-      }
-    },
-
-    {
-      desc: "Add an attribute by clicking the empty space after a node",
-      before: function() {
-        assertAttributes(doc.querySelector("#node14"), {
-          id: "node14",
-        });
-      },
-      execute: function(after) {
-        inspector.once("markupmutation", after);
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node14")).editor;
-        let attr = editor.newAttr;
-        editField(attr, 'class="newclass" style="color:green"');
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node14"), {
-          id: "node14",
-          class: "newclass",
-          style: "color:green"
-        });
-      }
-    },
-
-    {
-      desc: 'Try add an attribute containing a quote (") attribute by ' +
-            'clicking the empty space after a node - this should result ' +
-            'in it being set to an empty string',
-      before: function() {
-        assertAttributes(doc.querySelector("#node23"), {
-          id: "node23",
-        });
-      },
-      execute: function(after) {
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node23")).editor;
-        let attr = editor.newAttr;
-        editField(attr, 'class="newclass" style="""');
-        inspector.once("markupmutation", after);
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node23"), {
-          id: "node23",
-          class: "newclass",
-          style: ""
-        });
-      }
-    },
-
-    {
-      desc: "Try add attributes by adding to an existing attribute's entry",
-      before: function() {
-        assertAttributes(doc.querySelector("#node24"), {
-          id: "node24",
-        });
-      },
-      execute: function(after) {
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
-        let attr = editor.attrs["id"].querySelector(".editable");
-        editField(attr, attr.textContent + ' class="""');
-        inspector.once("markupmutation", after);
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node24"), {
-          id: "node24",
-          class: ""
-        });
-      }
-    },
-
-    {
-      desc: "Try to add long attribute to make sure it is collapsed in attribute editor.",
-      before: function() {
-        assertAttributes(doc.querySelector("#node24"), {
-          id: "node24",
-          class: ""
-        });
-      },
-      execute: function(after) {
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
-        let attr = editor.newAttr;
-        editField(attr, 'data-long="'+LONG_ATTRIBUTE+'"');
-        inspector.once("markupmutation", after);
-      },
-      after: function() {
-
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
-        let visibleAttrText = editor.attrs["data-long"].querySelector(".attr-value").textContent;
-        is (visibleAttrText, LONG_ATTRIBUTE_COLLAPSED)
-
-        assertAttributes(doc.querySelector("#node24"), {
-          id: "node24",
-          class: "",
-          'data-long':LONG_ATTRIBUTE
-        });
-      }
-    },
-
-    {
-      desc: "Try to modify the collapsed long attribute, making sure it expands.",
-      before: function() {
-        assertAttributes(doc.querySelector("#node24"), {
-          id: "node24",
-          class: "",
-          'data-long': LONG_ATTRIBUTE
-        });
-      },
-      execute: function(after) {
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
-        let attr = editor.attrs["data-long"].querySelector(".editable");
-
-        // Check to make sure it has expanded after focus
-        attr.focus();
-        EventUtils.sendKey("return", inspector.panelWin);
-        let input = inplaceEditor(attr).input;
-        is (input.value, 'data-long="'+LONG_ATTRIBUTE+'"');
-        EventUtils.sendKey("escape", inspector.panelWin);
-
-        editField(attr, input.value  + ' data-short="ABC"');
-        inspector.once("markupmutation", after);
-      },
-      after: function() {
-
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node24")).editor;
-        let visibleAttrText = editor.attrs["data-long"].querySelector(".attr-value").textContent;
-        is (visibleAttrText, LONG_ATTRIBUTE_COLLAPSED)
-
-        assertAttributes(doc.querySelector("#node24"), {
-          id: "node24",
-          class: "",
-          'data-long': LONG_ATTRIBUTE,
-          "data-short": "ABC"
-        });
-      }
-    },
-
-    {
-      desc: "Try to add long data URL to make sure it is collapsed in attribute editor.",
-      before: function() {
-        assertAttributes(doc.querySelector("#node-data-url"), {
-          id: "node-data-url"
-        });
-      },
-      execute: function(after) {
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node-data-url")).editor;
-        let attr = editor.newAttr;
-        editField(attr, 'src="'+DATA_URL_ATTRIBUTE+'"');
-        inspector.once("markupmutation", after);
-      },
-      after: function() {
-
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node-data-url")).editor;
-        let visibleAttrText = editor.attrs["src"].querySelector(".attr-value").textContent;
-        is (visibleAttrText, DATA_URL_ATTRIBUTE_COLLAPSED);
-
-        let node = doc.querySelector("#node-data-url");
-        is (node.width, 16, "Image width has been set after data url src.");
-        is (node.height, 16, "Image height has been set after data url src.");
-
-        assertAttributes(node, {
-          id: "node-data-url",
-          "src": DATA_URL_ATTRIBUTE
-        });
-      }
-    },
-
-    {
-      desc: "Try to add long data URL to make sure it is collapsed in attribute editor.",
-      before: function() {
-        assertAttributes(doc.querySelector("#node-data-url-style"), {
-          id: "node-data-url-style"
-        });
-      },
-      execute: function(after) {
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node-data-url-style")).editor;
-        let attr = editor.newAttr;
-        editField(attr, "style='"+DATA_URL_INLINE_STYLE+"'");
-        inspector.once("markupmutation", after);
-      },
-      after: function() {
-
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node-data-url-style")).editor;
-        let visibleAttrText = editor.attrs["style"].querySelector(".attr-value").textContent;
-        is (visibleAttrText, DATA_URL_INLINE_STYLE_COLLAPSED)
-
-        assertAttributes(doc.querySelector("#node-data-url-style"), {
-          id: "node-data-url-style",
-          'style':DATA_URL_INLINE_STYLE
-        });
-      }
-    },
+];
 
-    {
-      desc: "Edit text",
-      before: function() {
-        let node = doc.querySelector('.node6').firstChild;
-        is(node.nodeValue, "line6", "Text should be unchanged");
-      },
-      execute: function(after) {
-        inspector.once("markupmutation", after);
-        let node = doc.querySelector('.node6').firstChild;
-        let editor = getContainerForRawNode(markup, node).editor;
-        let field = editor.elt.querySelector("pre");
-        editField(field, "New text");
-      },
-      after: function() {
-        let node = doc.querySelector('.node6').firstChild;
-        is(node.nodeValue, "New text", "Text should be changed.");
-      },
-    },
-
-    {
-      desc: "Add an attribute value containing < > &uuml; \" & '",
-      before: function() {
-        assertAttributes(doc.querySelector("#node25"), {
-          id: "node25",
-        });
-      },
-      execute: function(after) {
-        inspector.once("markupmutation", after);
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node25")).editor;
-        let attr = editor.newAttr;
-        editField(attr, 'src="somefile.html?param1=<a>&param2=&uuml;&param3=\'&quot;\'"');
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node25"), {
-          id: "node25",
-          src: "somefile.html?param1=<a>&param2=\xfc&param3='\"'"
-        });
-      }
-    },
-
-    {
-      desc: "Modify inline style containing \"",
-      before: function() {
-        assertAttributes(doc.querySelector("#node26"), {
-          id: "node26",
-          style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F");'
-        });
-      },
-      execute: function(after) {
-        inspector.once("markupmutation", after);
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node26")).editor;
-        let attr = editor.attrs["style"].querySelector(".editable");
-
-
-        attr.focus();
-        EventUtils.sendKey("return", inspector.panelWin);
-
-        let input = inplaceEditor(attr).input;
-        let value = input.value;
-
-        is (value,
-          "style='background-image: url(\"moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F\");'",
-          "Value contains actual double quotes"
-        );
-
-        value = value.replace(/mozilla\.org/, "mozilla.com");
-        input.value = value;
-
-        EventUtils.sendKey("return", inspector.panelWin);
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node26"), {
-          id: "node26",
-          style: 'background-image: url("moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.com%2F");'
-        });
-      }
-    },
-
-    {
-      desc: "Modify inline style containing \" and \'",
-      before: function() {
-        assertAttributes(doc.querySelector("#node27"), {
-          id: "node27",
-          class: 'Double " and single \''
-        });
-      },
-      execute: function(after) {
-        inspector.once("markupmutation", after);
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node27")).editor;
-        let attr = editor.attrs["class"].querySelector(".editable");
-
-        attr.focus();
-        EventUtils.sendKey("return", inspector.panelWin);
-
-        let input = inplaceEditor(attr).input;
-        let value = input.value;
-
-        is (value, "class=\"Double &quot; and single '\"", "Value contains &quot;");
-
-        value = value.replace(/Double/, "&quot;").replace(/single/, "'");
-        input.value = value;
-
-        EventUtils.sendKey("return", inspector.panelWin);
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node27"), {
-          id: "node27",
-          class: '" " and \' \''
-        });
-      }
-    },
-
-    {
-      desc: "Add an attribute value without closing \"",
-      enteredText: 'style="display: block;',
-      expectedAttributes: {
-        style: "display: block;"
-      }
-    },
-    {
-      desc: "Add an attribute value without closing '",
-      enteredText: "style='display: inline;",
-      expectedAttributes: {
-        style: "display: inline;"
-      }
-    },
-    {
-      desc: "Add an attribute wrapped with with double quotes double quote in it",
-      enteredText: 'style="display: "inline',
-      expectedAttributes: {
-        style: "display: ",
-        inline: ""
-      }
-    },
-    {
-      desc: "Add an attribute wrapped with single quotes with single quote in it",
-      enteredText: "style='display: 'inline",
-      expectedAttributes: {
-        style: "display: ",
-        inline: ""
-      }
-    },
-    {
-      desc: "Add an attribute with no value",
-      enteredText: "disabled",
-      expectedAttributes: {
-        disabled: ""
-      }
-    },
-    {
-      desc: "Add multiple attributes with no value",
-      enteredText: "disabled autofocus",
-      expectedAttributes: {
-        disabled: "",
-        autofocus: ""
-      }
-    },
-    {
-      desc: "Add multiple attributes with no value, and some with value",
-      enteredText: "disabled name='name' data-test='test' autofocus",
-      expectedAttributes: {
-        disabled: "",
-        autofocus: "",
-        name: "name",
-        'data-test': "test"
-      }
-    },
-    {
-      desc: "Add attribute with xmlns",
-      enteredText: "xmlns:edi='http://ecommerce.example.org/schema'",
-      expectedAttributes: {
-        'xmlns:edi': "http://ecommerce.example.org/schema"
-      }
-    },
-    {
-      desc: "Mixed single and double quotes",
-      enteredText: "name=\"hi\" maxlength='not a number'",
-      expectedAttributes: {
-        maxlength: "not a number",
-        name: "hi"
-      }
-    },
-    {
-      desc: "Invalid attribute name",
-      enteredText: "x='y' <why-would-you-do-this>=\"???\"",
-      expectedAttributes: {
-        x: "y"
-      }
-    },
-    {
-      desc: "Double quote wrapped in single quotes",
-      enteredText: "x='h\"i'",
-      expectedAttributes: {
-        x: "h\"i"
-      }
-    },
-    {
-      desc: "Single quote wrapped in double quotes",
-      enteredText: "x=\"h'i\"",
-      expectedAttributes: {
-        x: "h'i"
-      }
-    },
-    {
-      desc: "No quote wrapping",
-      enteredText: "a=b x=y data-test=Some spaced data",
-      expectedAttributes: {
-        a: "b",
-        x: "y",
-        "data-test": "Some",
-        spaced: "",
-        data: ""
-      }
-    },
-    {
-      desc: "Duplicate Attributes",
-      enteredText: "a=b a='c' a=\"d\"",
-      expectedAttributes: {
-        a: "b"
-      }
-    },
-    {
-      desc: "Inline styles",
-      enteredText: "style=\"font-family: 'Lucida Grande', sans-serif; font-size: 75%;\"",
-      expectedAttributes: {
-        style: "font-family: 'Lucida Grande', sans-serif; font-size: 75%;"
-      }
-    },
-    {
-      desc: "Object attribute names",
-      enteredText: "toString=\"true\" hasOwnProperty=\"false\"",
-      expectedAttributes: {
-        toString: "true",
-        hasOwnProperty: "false"
-      }
-    },
-    {
-      desc: "Add event handlers",
-      enteredText: "onclick=\"javascript: throw new Error('wont fire');\" onload=\"alert('here');\"",
-      expectedAttributes: {
-        onclick: "javascript: throw new Error('wont fire');",
-        onload: "alert('here');"
-      }
-    }
-  ];
-
-  // Create the helper tab for parsing...
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onload() {
-    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+function test() {
+  addTab(TEST_URL).then(openInspector).then(args => {
+    inspector = args.inspector;
     doc = content.document;
-    waitForFocus(setupTest, content);
-  }, true);
-  content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_edit.html";
-
-  function setupTest() {
-    var target = TargetFactory.forTab(gBrowser.selectedTab);
-    gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
-      inspector = toolbox.getCurrentPanel();
-      startTests();
-    });
-  }
-
-  function startTests() {
     markup = inspector.markup;
 
-    // expectedAttributes - Shortcut to provide a more decalarative test when you only
-    // want to check the outcome of setting an attribute to a string.
-    edits.forEach((edit, i) => {
-      if (edit.expectedAttributes) {
-        let id = "expectedAttributes" + i;
-
-        let div = doc.createElement("div");
-        div.id = id;
-        doc.body.appendChild(div);
-
-        // Attach the ID onto the object that will assert attributes
-        edit.expectedAttributes.id = id;
-
-        edit.before = () => {
-          assertAttributes(doc.querySelector("#" + id), {
-            id: id,
-          });
-        };
-
-        edit.execute = (after) =>{
-          inspector.once("markupmutation", after);
-          let editor = getContainerForRawNode(markup, doc.querySelector("#" + id)).editor;
-          editField(editor.newAttr, edit.enteredText);
-        };
-
-        edit.after = () => {
-          assertAttributes(doc.querySelector("#" + id), edit.expectedAttributes);
-        };
-      }
-    });
-
     markup.expandAll().then(() => {
-
+      // Iterate through the items in TEST_DATA
       let cursor = 0;
 
       function nextEditTest() {
         executeSoon(function() {
-          if (cursor >= edits.length) {
-            addAttributes();
-          } else {
-            let step = edits[cursor++];
-            info("START " + step.desc);
-            if (step.setup) {
-              step.setup();
-            }
-            step.before();
-            info("before execute");
-            step.execute(function() {
-              info("after execute");
-              step.after();
-              ok(markup.undo.canUndo(), "Should be able to undo.");
-              markup.undo.undo();
-              inspector.once("markupmutation", () => {
-                step.before();
-                ok(markup.undo.canRedo(), "Should be able to redo.");
-                markup.undo.redo();
-                inspector.once("markupmutation", () => {
-                  step.after();
-                  info("END " + step.desc);
-                  nextEditTest();
-                });
+          if (cursor >= TEST_DATA.length) {
+            return finishUp();
+          }
+
+          let step = TEST_DATA[cursor++];
+          info("Start test for: " + step.desc);
+          if (step.setup) {
+            step.setup();
+          }
+          step.before();
+          step.execute(function() {
+            step.after();
+
+            undoChange(inspector).then(() => {
+              step.before();
+
+              redoChange(inspector).then(() => {
+                step.after();
+                info("End test for: " + step.desc);
+                nextEditTest();
               });
             });
-          }
-        });
-      }
-      nextEditTest();
-    });
-  }
-
-  function addAttributes() {
-    let test = {
-      desc: "Add attributes by adding to an existing attribute's entry",
-      setup: function() {
-        inspector.selection.setNode(doc.querySelector("#node18"));
-      },
-      before: function() {
-        assertAttributes(doc.querySelector("#node18"), {
-          id: "node18",
-        });
-      },
-      execute: function(after) {
-        inspector.once("markupmutation", function() {
-          // needed because we need to make sure the infobar is updated
-          // not just the markupview (which happens in this event loop)
-          executeSoon(after);
-        });
-        let editor = getContainerForRawNode(markup, doc.querySelector("#node18")).editor;
-        let attr = editor.attrs["id"].querySelector(".editable");
-        editField(attr, attr.textContent + ' class="newclass" style="color:green"');
-      },
-      after: function() {
-        assertAttributes(doc.querySelector("#node18"), {
-          id: "node18",
-          class: "newclass",
-          style: "color:green"
+          });
         });
       }
-    };
-    testAsyncSetup(test, editTagName);
-  }
 
-  function editTagName() {
-    let test =  {
-      desc: "Edit the tag name",
-      setup: function() {
-        inspector.selection.setNode(doc.querySelector("#retag-me"));
-      },
-      before: function() {
-        let node = doc.querySelector("#retag-me");
-        let container = getContainerForRawNode(markup, node);
-
-        is(node.tagName, "DIV", "retag-me should be a div.");
-        ok(container.selected, "retag-me should be selected.");
-        ok(container.expanded, "retag-me should be expanded.");
-        is(doc.querySelector("#retag-me-2").parentNode, node,
-          "retag-me-2 should be a child of the old element.");
-      },
-      execute: function(after) {
-        inspector.once("markupmutation", after);
-        let node = doc.querySelector("#retag-me");
-        let editor = getContainerForRawNode(markup, node).editor;
-        let field = editor.tag;
-        editField(field, "p");
-      },
-      after: function() {
-        let node = doc.querySelector("#retag-me");
-        let container = getContainerForRawNode(markup, node);
-        is(node.tagName, "P", "retag-me should be a p.");
-        ok(container.selected, "retag-me should be selected.");
-        ok(container.expanded, "retag-me should be expanded.");
-        is(doc.querySelector("#retag-me-2").parentNode, node,
-          "retag-me-2 should be a child of the new element.");
-      }
-    };
-    testAsyncSetup(test, removeElementWithDelete);
-  }
+      nextEditTest();
+    });
+  });
+}
 
-  function removeElementWithDelete() {
-    let test =  {
-      desc: "Remove an element with the delete key",
-      before: function() {
-        ok(!!doc.querySelector("#node18"), "Node 18 should exist.");
-      },
-      execute: function() {
-        inspector.selection.setNode(doc.querySelector("#node18"));
-      },
-      executeCont: function() {
-        EventUtils.sendKey("delete", inspector.panelWin);
-      },
-      after: function() {
-        ok(!doc.querySelector("#node18"), "Node 18 should not exist.")
-      }
-    };
-    testAsyncExecute(test, finishUp);
-  }
-
-  function testAsyncExecute(test, callback) {
-    info("START " + test.desc);
-
-    test.before();
-    inspector.once("inspector-updated", function BIMET_testAsyncExecNewNode() {
-      test.executeCont();
-      inspector.once("markupmutation", () => {
-        test.after();
-        undoRedo(test, callback);
-      });
-    });
-    executeSoon(function BIMET_setNode1() {
-      test.execute();
-    });
+function finishUp() {
+  while (markup.undo.canUndo()) {
+    markup.undo.undo();
   }
-
-  function testAsyncSetup(test, callback) {
-    info("START " + test.desc);
-
-    inspector.on("inspector-updated", function BIMET_updated(event, name) {
-      if (name === "inspector-panel") {
-        inspector.off("inspector-updated", BIMET_updated);
-
-        test.before();
-        test.execute(function() {
-          test.after();
-          undoRedo(test, callback);
-        });
-      }
-    });
-    executeSoon(test.setup);
-  }
-
-  function undoRedo(test, callback) {
-    ok(markup.undo.canUndo(), "Should be able to undo.");
-    markup.undo.undo();
-    inspector.once("markupmutation", () => {
-      test.before();
-      ok(markup.undo.canRedo(), "Should be able to redo.");
-      markup.undo.redo();
-      inspector.once("markupmutation", () => {
-        test.after();
-        info("END " + test.desc);
-        callback();
-      });
-    });
-  }
-
-  function finishUp() {
-    while (markup.undo.canUndo()) {
-      markup.undo.undo();
-    }
-    doc = inspector = null;
+  inspector.once("inspector-updated", () => {
+    doc = inspector = markup = null;
     gBrowser.removeCurrentTab();
     finish();
-  }
+  });
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit_2.js
@@ -0,0 +1,51 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that an existing attribute can be modified
+
+waitForExplicitFinish();
+
+const TEST_URL = "data:text/html,<div id='test-div'>Test modifying my ID attribute</div>";
+
+function test() {
+  Task.spawn(function() {
+    info("Opening the inspector on the test page");
+    let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
+
+    info("Selecting the test node");
+    let node = content.document.getElementById("test-div");
+    yield selectNode(node, inspector);
+
+    info("Verify attributes, only ID should be there for now");
+    assertAttributes(node, {
+      id: "test-div"
+    });
+
+    info("Focus the ID attribute and change its content");
+    let editor = getContainerForRawNode(inspector.markup, node).editor;
+    let attr = editor.attrs["id"].querySelector(".editable");
+    let mutated = inspector.once("markupmutation");
+    setEditableFieldValue(attr,
+      attr.textContent + ' class="newclass" style="color:green"', inspector);
+    yield mutated;
+
+    info("Verify attributes, should have ID, class and style");
+    assertAttributes(node, {
+      id: "test-div",
+      class: "newclass",
+      style: "color:green"
+    });
+
+    info("Trying to undo the change");
+    yield undoChange(inspector);
+    assertAttributes(node, {
+      id: "test-div"
+    });
+
+    yield inspector.once("inspector-updated");
+
+    gBrowser.removeCurrentTab();
+  }).then(null, ok.bind(null, false)).then(finish);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit_3.js
@@ -0,0 +1,45 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that a node's tagname can be edited in the markup-view
+
+waitForExplicitFinish();
+
+const TEST_URL = "data:text/html,<div id='retag-me'><div id='retag-me-2'></div></div>";
+
+function test() {
+  Task.spawn(function() {
+    info("Opening the inspector on the test page");
+    let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
+
+    yield inspector.markup.expandAll();
+
+    info("Selecting the test node");
+    let node = content.document.getElementById("retag-me");
+    let child = content.document.querySelector("#retag-me-2");
+    yield selectNode(node, inspector);
+
+    let container = getContainerForRawNode(inspector.markup, node);
+    is(node.tagName, "DIV", "We've got #retag-me element, it's a DIV");
+    ok(container.expanded, "It is expanded");
+    is(child.parentNode, node, "Child #retag-me-2 is inside #retag-me");
+
+    info("Changing the tagname");
+    let mutated = inspector.once("markupmutation");
+    let tagEditor = container.editor.tag;
+    setEditableFieldValue(tagEditor, "p", inspector);
+    yield mutated;
+
+    info("Checking that the tagname change was done");
+    let node = content.document.getElementById("retag-me");
+    let container = getContainerForRawNode(inspector.markup, node);
+    is(node.tagName, "P", "We've got #retag-me, it should now be a P");
+    ok(container.expanded, "It is still expanded");
+    ok(container.selected, "It is still selected");
+    is(child.parentNode, node, "Child #retag-me-2 is still inside #retag-me");
+
+    gBrowser.removeCurrentTab();
+  }).then(null, ok.bind(null, false)).then(finish);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit_4.js
@@ -0,0 +1,36 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that a node can be deleted from the markup-view with the delete key
+
+waitForExplicitFinish();
+
+const TEST_URL = "data:text/html,<div id='delete-me'></div>";
+
+function test() {
+  Task.spawn(function() {
+    info("Opening the inspector on the test page");
+    let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
+
+    info("Selecting the test node by clicking on it to make sure it receives focus");
+    let node = content.document.getElementById("delete-me");
+    yield clickContainer(node, inspector);
+
+    info("Deleting the element with the keyboard");
+    let mutated = inspector.once("markupmutation");
+    EventUtils.sendKey("delete", inspector.panelWin);
+    yield mutated;
+
+    info("Checking that it's gone, baby gone!");
+    ok(!content.document.getElementById("delete-me"), "The test node does not exist");
+
+    yield undoChange(inspector);
+    ok(content.document.getElementById("delete-me"), "The test node is back!");
+
+    yield inspector.once("inspector-updated");
+
+    gBrowser.removeCurrentTab();
+  }).then(null, ok.bind(null, false)).then(finish);
+}
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -3,125 +3,240 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const Cu = Components.utils;
 
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let TargetFactory = devtools.TargetFactory;
 let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 let promise = devtools.require("sdk/core/promise");
+let {getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor");
 
 // Clear preferences that may be set during the course of tests.
 function clearUserPrefs() {
   Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen");
   Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
   Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
 }
 
 registerCleanupFunction(clearUserPrefs);
 
-Services.prefs.setBoolPref("devtools.debugger.log", true);
-SimpleTest.registerCleanupFunction(() => {
-  Services.prefs.clearUserPref("devtools.debugger.log");
-});
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @return a promise that resolves when the url is loaded
+ */
+function addTab(url) {
+  let def = promise.defer();
 
-function getContainerForRawNode(markupView, rawNode) {
-  let front = markupView.walker.frontForRawNode(rawNode);
-  let container = markupView.getContainer(front);
-  return container;
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    info("URL " + url + " loading complete into new test tab");
+    waitForFocus(def.resolve, content);
+  }, true);
+  content.location = url;
+
+  return def.promise;
 }
 
 /**
  * Open the toolbox, with the inspector tool visible.
  * @return a promise that resolves when the inspector is ready
  */
 function openInspector() {
-  let deferred = promise.defer();
+  let def = promise.defer();
 
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+    info("Toolbox open");
     let inspector = toolbox.getCurrentPanel();
     inspector.once("inspector-updated", () => {
-      deferred.resolve({toolbox: toolbox, inspector: inspector});
+      info("Inspector panel active and ready");
+      def.resolve({toolbox: toolbox, inspector: inspector});
     });
   }).then(null, console.error);
 
-  return deferred.promise;
+  return def.promise;
 }
 
+/**
+ * Get the MarkupContainer object instance that corresponds to the given
+ * HTML node
+ * @param {MarkupView} markupView The instance of MarkupView currently loaded into the inspector panel
+ * @param {DOMNode} rawNode The DOM node for which the container is required
+ * @return {MarkupContainer}
+ */
+function getContainerForRawNode(markupView, rawNode) {
+  let front = markupView.walker.frontForRawNode(rawNode);
+  let container = markupView.getContainer(front);
+  ok(container, "A markup-container object was found");
+  return container;
+}
+
+/**
+ * Simple DOM node accesor function that takes either a node or a string css
+ * selector as argument and returns the corresponding node
+ * @param {String|DOMNode} nodeOrSelector
+ * @return {DOMNode}
+ */
 function getNode(nodeOrSelector) {
   let node = nodeOrSelector;
 
   if (typeof nodeOrSelector === "string") {
     node = content.document.querySelector(nodeOrSelector);
     ok(node, "A node was found for selector " + nodeOrSelector);
   }
 
   return node;
 }
 
 /**
  * Set the inspector's current selection to a node or to the first match of the
  * given css selector
+ * @param {String|DOMNode} nodeOrSelector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
+ * @param {String} reason Defaults to "test" which instructs the inspector not to highlight the node upon selection
  * @return a promise that resolves when the inspector is updated with the new
  * node
  */
-function selectNode(nodeOrSelector, inspector) {
+function selectNode(nodeOrSelector, inspector, reason="test") {
+  info("Selecting the node " + nodeOrSelector);
   let node = getNode(nodeOrSelector);
   let updated = inspector.once("inspector-updated");
-  inspector.selection.setNode(node, "test");
+  inspector.selection.setNode(node, reason);
   return updated;
 }
 
 /**
  * Simulate a mouse-over on the markup-container (a line in the markup-view)
  * that corresponds to the node or selector passed.
+ * @param {String|DOMNode} nodeOrSelector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
  * @return a promise that resolves when the container is hovered and the higlighter
  * is shown on the corresponding node
  */
 function hoverContainer(nodeOrSelector, inspector) {
+  info("Hovering over the markup-container for node " + nodeOrSelector);
   let highlit = inspector.toolbox.once("node-highlight");
   let container = getContainerForRawNode(inspector.markup, getNode(nodeOrSelector));
   EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
     inspector.markup.doc.defaultView);
   return highlit;
 }
 
 /**
  * Simulate a click on the markup-container (a line in the markup-view)
  * that corresponds to the node or selector passed.
+ * @param {String|DOMNode} nodeOrSelector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
  * @return a promise that resolves when the node has been selected.
  */
 function clickContainer(nodeOrSelector, inspector) {
+  info("Clicking on the markup-container for node " + nodeOrSelector);
   let updated = inspector.once("inspector-updated");
   let container = getContainerForRawNode(inspector.markup, getNode(nodeOrSelector));
   EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"},
     inspector.markup.doc.defaultView);
   EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mouseup"},
     inspector.markup.doc.defaultView);
   return updated;
 }
 
 /**
  * Checks if the highlighter is visible currently
+ * @return {Boolean}
  */
 function isHighlighterVisible() {
   let outline = gBrowser.selectedBrowser.parentNode.querySelector(".highlighter-container .highlighter-outline");
   return outline && !outline.hasAttribute("hidden");
 }
 
 /**
  * Simulate the mouse leaving the markup-view area
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
  * @return a promise when done
  */
 function mouseLeaveMarkupView(inspector) {
-  let deferred = promise.defer();
+  info("Leaving the markup-view area");
+  let def = promise.defer();
 
   // Find another element to mouseover over in order to leave the markup-view
   let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button");
 
   EventUtils.synthesizeMouse(btn, 2, 2, {type: "mousemove"},
     inspector.toolbox.doc.defaultView);
-  executeSoon(deferred.resolve);
+  executeSoon(def.resolve);
+
+  return def.promise;
+}
 
-  return deferred.promise;
+/**
+ * Focus a given editable element, enter edit mode, set value, and commit
+ * @param {DOMNode} field The element that gets editable after receiving focus and <ENTER> keypress
+ * @param {String} value The string value to be set into the edited field
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
+ */
+function setEditableFieldValue(field, value, inspector) {
+  field.focus();
+  EventUtils.sendKey("return", inspector.panelWin);
+  let input = inplaceEditor(field).input;
+  ok(input, "Found editable field for setting value: " + value);
+  input.value = value;
+  EventUtils.sendKey("return", inspector.panelWin);
 }
 
+/**
+ * Checks that a node has the given attributes
+ *
+ * @param {HTMLNode} element The node to check.
+ * @param {Object} attrs An object containing the attributes to check.
+ *        e.g. {id: "id1", class: "someclass"}
+ *
+ * Note that node.getAttribute() returns attribute values provided by the HTML
+ * parser. The parser only provides unescaped entities so &amp; will return &.
+ */
+function assertAttributes(element, attrs) {
+  is(element.attributes.length, Object.keys(attrs).length,
+    "Node has the correct number of attributes.");
+  for (let attr in attrs) {
+    is(element.getAttribute(attr), attrs[attr],
+      "Node has the correct " + attr + " attribute.");
+  }
+}
+
+/**
+ * Undo the last markup-view action and wait for the corresponding mutation to
+ * occur
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
+ * @return a promise that resolves when the markup-mutation has been treated or
+ * rejects if no undo action is possible
+ */
+function undoChange(inspector) {
+  let canUndo = inspector.markup.undo.canUndo();
+  ok(canUndo, "The last change in the markup-view can be undone");
+  if (!canUndo) {
+    return promise.reject();
+  }
+
+  let mutated = inspector.once("markupmutation");
+  inspector.markup.undo.undo();
+  return mutated;
+}
+
+/**
+ * Redo the last markup-view action and wait for the corresponding mutation to
+ * occur
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
+ * @return a promise that resolves when the markup-mutation has been treated or
+ * rejects if no redo action is possible
+ */
+function redoChange(inspector) {
+  let canRedo = inspector.markup.undo.canRedo();
+  ok(canRedo, "The last change in the markup-view can be redone");
+  if (!canRedo) {
+    return promise.reject();
+  }
+
+  let mutated = inspector.once("markupmutation");
+  inspector.markup.undo.redo();
+  return mutated;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/css-parsing-utils.js
@@ -0,0 +1,153 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+"use strict";
+
+const cssTokenizer  = require("devtools/sourceeditor/css-tokenizer");
+
+/**
+ * Returns the string enclosed in quotes
+ */
+function quoteString(string) {
+  let hasDoubleQuotes = string.contains('"');
+  let hasSingleQuotes = string.contains("'");
+
+  if (hasDoubleQuotes && !hasSingleQuotes) {
+    // In this case, no escaping required, just enclose in single-quotes
+    return "'" + string + "'";
+  }
+
+  // In all other cases, enclose in double-quotes, and escape any double-quote
+  // that may be in the string
+  return '"' + string.replace(/"/g, '\"') + '"';
+}
+
+/**
+ * Returns an array of CSS declarations given an string.
+ * For example, parseDeclarations("width: 1px; height: 1px") would return
+ * [{name:"width", value: "1px"}, {name: "height", "value": "1px"}]
+ *
+ * The input string is assumed to only contain declarations so { and } characters
+ * will be treated as part of either the property or value, depending where it's
+ * found.
+ *
+ * @param {string} inputString
+ *        An input string of CSS
+ * @return {Array} an array of objects with the following signature:
+ *         [{"name": string, "value": string, "priority": string}, ...]
+ */
+function parseDeclarations(inputString) {
+  let tokens = cssTokenizer(inputString);
+
+  let declarations = [{name: "", value: "", priority: ""}];
+
+  let current = "", hasBang = false, lastProp;
+  for (let token of tokens) {
+    lastProp = declarations[declarations.length - 1];
+
+    if (token.tokenType === ":") {
+      if (!lastProp.name) {
+        // Set the current declaration name if there's no name yet
+        lastProp.name = current.trim();
+        current = "";
+        hasBang = false;
+      } else {
+        // Otherwise, just append ':' to the current value (declaration value
+        // with colons)
+        current += ":";
+      }
+    } else if (token.tokenType === ";") {
+      lastProp.value = current.trim();
+      current = "";
+      hasBang = false;
+      declarations.push({name: "", value: "", priority: ""});
+    } else {
+      switch(token.tokenType) {
+        case "IDENT":
+          if (token.value === "important" && hasBang) {
+            lastProp.priority = "important";
+            hasBang = false;
+          } else {
+            if (hasBang) {
+              current += "!";
+            }
+            current += token.value;
+          }
+          break;
+        case "WHITESPACE":
+          current += " ";
+          break;
+        case "DIMENSION":
+          current += token.repr;
+          break;
+        case "HASH":
+          current += "#" + token.value;
+          break;
+        case "URL":
+          current += "url(" + quoteString(token.value) + ")";
+          break;
+        case "FUNCTION":
+          current += token.value + "(";
+          break;
+        case ")":
+          current += token.tokenType;
+          break;
+        case "EOF":
+          break;
+        case "DELIM":
+          if (token.value === "!") {
+            hasBang = true;
+          } else {
+            current += token.value;
+          }
+          break;
+        case "STRING":
+          current += quoteString(token.value);
+          break;
+        case "{":
+        case "}":
+          current += token.tokenType;
+          break;
+        default:
+          current += token.value;
+          break;
+      }
+    }
+  }
+
+  // Handle whatever trailing properties or values might still be there
+  if (current) {
+    if (!lastProp.name) {
+      // Trailing property found, e.g. p1:v1;p2:v2;p3
+      lastProp.name = current.trim();
+    } else {
+      // Trailing value found, i.e. value without an ending ;
+      lastProp.value += current.trim();
+    }
+  }
+
+  // Remove declarations that have neither a name nor a value
+  declarations = declarations.filter(prop => prop.name || prop.value);
+
+  return declarations;
+};
+exports.parseDeclarations = parseDeclarations;
+
+/**
+ * Expects a single CSS value to be passed as the input and parses the value
+ * and priority.
+ *
+ * @param {string} value The value from the text editor.
+ * @return {object} an object with 'value' and 'priority' properties.
+ */
+function parseSingleValue(value) {
+  let declaration = parseDeclarations("a: " + value + ";")[0];
+  return {
+    value: declaration ? declaration.value : "",
+    priority: declaration ? declaration.priority : ""
+  };
+};
+exports.parseSingleValue = parseSingleValue;
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -9,17 +9,18 @@
 const {Cc, Ci, Cu} = require("chrome");
 const promise = require("sdk/core/promise");
 const {CssLogic} = require("devtools/styleinspector/css-logic");
 const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
 const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 const {Tooltip, SwatchColorPickerTooltip} = require("devtools/shared/widgets/Tooltip");
 const {OutputParser} = require("devtools/output-parser");
-const { PrefObserver, PREF_ORIG_SOURCES } = require("devtools/styleeditor/utils");
+const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
+const {parseSingleValue, parseDeclarations} = require("devtools/styleinspector/css-parsing-utils");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
@@ -577,17 +578,17 @@ Rule.prototype = {
     if (disabledProps.length > 0) {
       disabled.set(this.style, disabledProps);
     } else {
       disabled.delete(this.style);
     }
 
     let promise = aModifications.apply().then(() => {
       let cssProps = {};
-      for (let cssProp of parseCSSText(this.style.cssText)) {
+      for (let cssProp of parseDeclarations(this.style.cssText)) {
         cssProps[cssProp.name] = cssProp;
       }
 
       for (let textProp of this.textProps) {
         if (!textProp.enabled) {
           continue;
         }
         let cssProp = cssProps[textProp.name];
@@ -687,17 +688,17 @@ Rule.prototype = {
 
   /**
    * Get the list of TextProperties from the style.  Needs
    * to parse the style's cssText.
    */
   _getTextProperties: function() {
     let textProps = [];
     let store = this.elementStyle.store;
-    let props = parseCSSText(this.style.cssText);
+    let props = parseDeclarations(this.style.cssText);
     for (let prop of props) {
       let name = prop.name;
       if (this.inherited && !domUtils.isInheritedProperty(name)) {
         continue;
       }
       let value = store.userProperties.getProperty(this.style, name, prop.value);
       let textProp = new TextProperty(this, name, value, prop.priority);
       textProps.push(textProp);
@@ -1845,27 +1846,23 @@ RuleEditor.prototype = {
    * @param {bool} aCommit
    *        True if the value should be committed.
    */
   _onNewProperty: function(aValue, aCommit) {
     if (!aValue || !aCommit) {
       return;
     }
 
-    // Deal with adding declarations later (once editor has been destroyed).
-    // If aValue is just a name, will make a new property with empty value.
-    this.multipleAddedProperties = parseCSSText(aValue);
-    if (!this.multipleAddedProperties.length) {
-      this.multipleAddedProperties = [{
-        name: aValue,
-        value: "",
-        priority: ""
-      }];
-    }
+    // parseDeclarations allows for name-less declarations, but in the present
+    // case, we're creating a new declaration, it doesn't make sense to accept
+    // these entries
+    this.multipleAddedProperties = parseDeclarations(aValue).filter(d => d.name);
 
+    // Blur the editor field now and deal with adding declarations later when
+    // the field gets destroyed (see _newPropertyDestroy)
     this.editor.input.blur();
   },
 
   /**
    * Called when the new property editor is destroyed.
    * This is where the properties (type TextProperty) are actually being
    * added, since we want to wait until after the inplace editor `destroy`
    * event has been fired to keep consistent UI state.
@@ -2258,27 +2255,26 @@ TextPropertyEditor.prototype = {
    */
   _onNameDone: function(aValue, aCommit) {
     if (aCommit) {
       // Unlike the value editor, if a name is empty the entire property
       // should always be removed.
       if (aValue.trim() === "") {
         this.remove();
       } else {
-
         // Adding multiple rules inside of name field overwrites the current
         // property with the first, then adds any more onto the property list.
-        let properties = parseCSSText(aValue);
-        if (properties.length > 0) {
+        let properties = parseDeclarations(aValue);
+
+        if (properties.length) {
           this.prop.setName(properties[0].name);
-          this.prop.setValue(properties[0].value, properties[0].priority);
-
-          this.ruleEditor.addProperties(properties.slice(1), this.prop);
-        } else {
-          this.prop.setName(aValue);
+          if (properties.length > 1) {
+            this.prop.setValue(properties[0].value, properties[0].priority);
+            this.ruleEditor.addProperties(properties.slice(1), this.prop);
+          }
         }
       }
     }
   },
 
 
   /**
    * Remove property from style and the editors from DOM.
@@ -2315,17 +2311,17 @@ TextPropertyEditor.prototype = {
          this.prop.setValue(this.committed.value, this.committed.priority);
        }
        return;
     }
 
     let {propertiesToAdd,firstValue} = this._getValueAndExtraProperties(aValue);
 
     // First, set this property value (common case, only modified a property)
-    let val = parseCSSValue(firstValue);
+    let val = parseSingleValue(firstValue);
     this.prop.setValue(val.value, val.priority);
     this.removeOnRevert = false;
     this.committed.value = this.prop.value;
     this.committed.priority = this.prop.priority;
 
     // If needed, add any new properties after this.prop.
     this.ruleEditor.addProperties(propertiesToAdd, this.prop);
 
@@ -2351,56 +2347,51 @@ TextPropertyEditor.prototype = {
    * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] }
    *
    * @param {string} aValue
    *        The string to parse
    * @return {object} An object with the following properties:
    *        firstValue: A string containing a simple value, like
    *                    "red" or "100px!important"
    *        propertiesToAdd: An array with additional properties, following the
-   *                         parseCSSText format of {name,value,priority}
+   *                         parseDeclarations format of {name,value,priority}
    */
   _getValueAndExtraProperties: function(aValue) {
     // The inplace editor will prevent manual typing of multiple properties,
     // but we need to deal with the case during a paste event.
     // Adding multiple properties inside of value editor sets value with the
     // first, then adds any more onto the property list (below this property).
-    let properties = parseCSSText(aValue);
+    let firstValue = aValue;
     let propertiesToAdd = [];
-    let firstValue = aValue;
 
-    if (properties.length > 0) {
-      // If text like "red; width: 1px;" was entered in, handle this as two
-      // separate properties (setting value here to red and adding a new prop).
-      let propertiesNoName = parseCSSText("a:" + aValue);
-      let enteredValueFirst = propertiesNoName.length > properties.length;
+    let properties = parseDeclarations(aValue);
 
-      let firstProp = properties[0];
-      propertiesToAdd = properties.slice(1);
-
-      if (enteredValueFirst) {
-        firstProp = propertiesNoName[0];
-        propertiesToAdd = propertiesNoName.slice(1);
+    // Check to see if the input string can be parsed as multiple properties
+    if (properties.length) {
+      // Get the first property value (if any), and any remaining properties (if any)
+      if (!properties[0].name && properties[0].value) {
+        firstValue = properties[0].value;
+        propertiesToAdd = properties.slice(1);
       }
-
-      // If "red; width: 1px", then set value to "red"
-      // If "color: red; width: 1px;", then set value to "color: red;"
-      firstValue = enteredValueFirst ?
-        firstProp.value + "!" + firstProp.priority :
-        firstProp.name + ": " + firstProp.value + "!" + firstProp.priority;
+      // In some cases, the value could be a property:value pair itself.
+      // Join them as one value string and append potentially following properties
+      else if (properties[0].name && properties[0].value) {
+        firstValue = properties[0].name + ": " + properties[0].value;
+        propertiesToAdd = properties.slice(1);
+      }
     }
 
     return {
       propertiesToAdd: propertiesToAdd,
       firstValue: firstValue
     };
   },
 
   _applyNewValue: function(aValue) {
-    let val = parseCSSValue(aValue);
+    let val = parseSingleValue(aValue);
     // Any property should be removed if has an empty value.
     if (val.value.trim() === "") {
       this.remove();
     } else {
       this.prop.setValue(val.value, val.priority);
       this.removeOnRevert = false;
       this.committed.value = this.prop.value;
       this.committed.priority = this.prop.priority;
@@ -2414,17 +2405,17 @@ TextPropertyEditor.prototype = {
    *        The value to set the current property to.
    */
   _livePreview: function(aValue) {
     // Since function call is throttled, we need to make sure we are still editing
     if (!this.editing) {
       return;
     }
 
-    let val = parseCSSValue(aValue);
+    let val = parseSingleValue(aValue);
 
     // Live previewing the change without committing just yet, that'll be done in _onValueDone
     // If it was not a valid value, apply an empty string to reset the live preview
     this.ruleEditor.rule.setPropertyValue(this.prop, val.value, val.priority);
   },
 
   /**
    * Validate this property. Does it make sense for this value to be assigned
@@ -2434,17 +2425,17 @@ TextPropertyEditor.prototype = {
    *        The property value used for validation.
    *        Defaults to the current value for this.prop
    *
    * @return {bool} true if the property value is valid, false otherwise.
    */
   isValid: function(aValue) {
     let name = this.prop.name;
     let value = typeof aValue == "undefined" ? this.prop.value : aValue;
-    let val = parseCSSValue(value);
+    let val = parseSingleValue(value);
 
     let style = this.doc.createElementNS(HTML_NS, "div").style;
     let prefs = Services.prefs;
 
     // We toggle output of errors whilst the user is typing a property value.
     let prefVal = prefs.getBoolPref("layout.css.report_errors");
     prefs.setBoolPref("layout.css.report_errors", false);
 
@@ -2601,73 +2592,23 @@ function throttle(func, wait, scope) {
     timer = setTimeout(function() {
       timer = null;
       func.apply(scope, args);
     }, wait);
   };
 }
 
 /**
- * Pull priority (!important) out of the value provided by a
- * value editor.
- *
- * @param {string} aValue
- *        The value from the text editor.
- * @return {object} an object with 'value' and 'priority' properties.
- */
-function parseCSSValue(aValue) {
-  let pieces = aValue.split("!", 2);
-  return {
-    value: pieces[0].trim(),
-    priority: (pieces.length > 1 ? pieces[1].trim() : "")
-  };
-}
-
-/**
- * Return an array of CSS properties given an input string
- * For example, parseCSSText("width: 1px; height: 1px") would return
- * [{name:"width", value: "1px"}, {name: "height", "value": "1px"}]
- *
- * @param {string} aCssText
- *        An input string of CSS
- * @return {Array} an array of objects with the following signature:
- *         [{"name": string, "value": string, "priority": string}, ...]
- */
-function parseCSSText(aCssText) {
-  let lines = aCssText.match(CSS_LINE_RE);
-  let props = [];
-
-  [].forEach.call(lines, (line, i) => {
-    let [, name, value, priority] = CSS_PROP_RE.exec(line) || [];
-
-    // If this is ending with an unfinished line, add it onto the end
-    // with an empty value
-    if (!name && line && i > 0) {
-      name = line;
-    }
-
-    if (name) {
-      props.push({
-        name: name.trim(),
-        value: value || "",
-        priority: priority || ""
-      });
-    }
-  });
-
-  return props;
-}
-
-/**
  * Event handler that causes a blur on the target if the input has
  * multiple CSS properties as the value.
  */
 function blurOnMultipleProperties(e) {
   setTimeout(() => {
-    if (parseCSSText(e.target.value).length) {
+    let props = parseDeclarations(e.target.value);
+    if (props.length > 1) {
       e.target.blur();
     }
   }, 0);
 }
 
 /**
  * Append a text node to an element.
  */
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -50,20 +50,20 @@ support-files =
 [browser_ruleview_bug_902966_revert_value_on_ESC.js]
 [browser_ruleview_pseudoelement.js]
 support-files = browser_ruleview_pseudoelement.html
 [browser_computedview_bug835808_keyboard_nav.js]
 [browser_bug913014_matched_expand.js]
 [browser_bug765105_background_image_tooltip.js]
 [browser_bug889638_rule_view_color_picker.js]
 [browser_bug726427_csstransform_tooltip.js]
-
 [browser_bug940500_rule_view_pick_gradient_color.js]
 [browser_ruleview_original_source_link.js]
 support-files =
   sourcemaps.html
   sourcemaps.css
   sourcemaps.css.map
   sourcemaps.scss
 [browser_computedview_original_source_link.js]
 [browser_bug946331_close_tooltip_on_new_selection.js]
 [browser_bug942297_user_property_reset.js]
 [browser_styleinspector_outputparser.js]
+[browser_bug970532_mathml_element.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug970532_mathml_element.js
@@ -0,0 +1,70 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule-view displays correctly on MathML elements
+
+waitForExplicitFinish();
+
+const TEST_URL = [
+  "data:text/html,",
+  "<div>",
+  "  <math xmlns=\"http://www.w3.org/1998/Math/MathML\">",
+  "    <mfrac>",
+  "      <msubsup>",
+  "        <mi>a</mi>",
+  "        <mi>i</mi>",
+  "        <mi>j</mi>",
+  "      </msubsup>",
+  "      <msub>",
+  "        <mi>x</mi>",
+  "        <mn>0</mn>",
+  "      </msub>",
+  "    </mfrac>",
+  "  </math>",
+  "</div>"
+].join("");
+
+function test() {
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload(evt) {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(runTests, content);
+  }, true);
+  content.location = TEST_URL;
+}
+
+function runTests() {
+  openRuleView((inspector, ruleView) => {
+    Task.spawn(function() {
+      info("Select the DIV node and verify the rule-view shows rules");
+      yield selectNode("div", inspector);
+      ok(ruleView.element.querySelectorAll(".ruleview-rule").length,
+        "The rule-view shows rules for the div element");
+
+      info("Select various MathML nodes and verify the rule-view is empty");
+      yield selectNode("math", inspector);
+      ok(!ruleView.element.querySelectorAll(".ruleview-rule").length,
+        "The rule-view is empty for the math element");
+
+      yield selectNode("msubsup", inspector);
+      ok(!ruleView.element.querySelectorAll(".ruleview-rule").length,
+        "The rule-view is empty for the msubsup element");
+
+      yield selectNode("mn", inspector);
+      ok(!ruleView.element.querySelectorAll(".ruleview-rule").length,
+        "The rule-view is empty for the mn element");
+
+      info("Select again the DIV node and verify the rule-view shows rules");
+      yield selectNode("div", inspector);
+      ok(ruleView.element.querySelectorAll(".ruleview-rule").length,
+        "The rule-view shows rules for the div element");
+    }).then(null, ok.bind(null, false)).then(finishUp);
+  });
+}
+
+function finishUp() {
+  gBrowser.removeCurrentTab();
+  finish();
+}
--- a/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js
@@ -1,16 +1,18 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let doc;
 let ruleWindow;
 let ruleView;
 let inspector;
+let TEST_URL = 'url("http://example.com/browser/browser/devtools/' +
+               'styleinspector/test/test-image.png")';
 
 function startTest()
 {
   let style = '' +
     '#testid {' +
     '  background-color: blue;' +
     '} ' +
     '.testclass {' +
@@ -135,17 +137,17 @@ function testEditProperty()
         input = aEditor.input;
         is(inplaceEditor(propEditor.valueSpan), aEditor, "Focus should have moved to the value.");
 
         waitForEditorBlur(aEditor, function() {
           promiseDone(expectRuleChange(idRuleEditor.rule).then(() => {
             let value = idRuleEditor.rule.domRule._rawStyle().getPropertyValue("border-color");
             is(value, "red", "border-color should have been set.");
             is(propEditor.isValid(), true, "red should be a valid entry");
-            finishTest();
+            testEditPropertyWithColon();
           }));
         });
 
         for (let ch of "red;") {
           EventUtils.sendChar(ch, ruleWindow);
         }
       }));
     });
@@ -154,16 +156,53 @@ function testEditProperty()
     }
   });
 
   EventUtils.synthesizeMouse(propEditor.nameSpan, 32, 1,
                              { },
                              ruleWindow);
 }
 
+function testEditPropertyWithColon()
+{
+  let idRuleEditor = ruleView.element.children[1]._ruleEditor;
+  let propEditor = idRuleEditor.rule.textProps[0].editor;
+  waitForEditorFocus(propEditor.element, function onNewElement(aEditor) {
+    is(inplaceEditor(propEditor.nameSpan), aEditor, "Next focused editor should be the name editor.");
+    let input = aEditor.input;
+    waitForEditorFocus(propEditor.element, function onNewName(aEditor) {
+      promiseDone(expectRuleChange(idRuleEditor.rule).then(() => {
+        input = aEditor.input;
+        is(inplaceEditor(propEditor.valueSpan), aEditor, "Focus should have moved to the value.");
+
+        waitForEditorBlur(aEditor, function() {
+          promiseDone(expectRuleChange(idRuleEditor.rule).then(() => {
+            let value = idRuleEditor.rule.domRule._rawStyle().getPropertyValue("background-image");
+            is(value, TEST_URL, "background-image should have been set.");
+            is(propEditor.isValid(), true, "the test URL should be a valid entry");
+            finishTest();
+          }));
+        });
+
+
+        for (let ch of (TEST_URL + ";")) {
+          EventUtils.sendChar(ch, ruleWindow);
+        }
+      }));
+    });
+    for (let ch of "background-image:") {
+      EventUtils.sendChar(ch, ruleWindow);
+    }
+  });
+
+  EventUtils.synthesizeMouse(propEditor.nameSpan, 32, 1,
+                             { },
+                             ruleWindow);
+}
+
 function finishTest()
 {
   inspector = ruleWindow = ruleView = null;
   doc = null;
   gBrowser.removeCurrentTab();
   finish();
 }
 
--- a/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties.js
@@ -23,23 +23,25 @@ function startTest()
 /*
  * Add a new node to the DOM and resolve the promise once it is ready to use
  */
 function selectNewElement()
 {
   let newElement = doc.createElement("div");
   newElement.textContent = "Test Element";
   doc.body.appendChild(newElement);
-  inspector.selection.setNode(newElement);
+
+  inspector.selection.setNode(newElement, "test");
   let def = promise.defer();
   ruleView.element.addEventListener("CssRuleViewRefreshed", function changed() {
     ruleView.element.removeEventListener("CssRuleViewRefreshed", changed);
     elementRuleEditor = ruleView.element.children[0]._ruleEditor;
     def.resolve();
   });
+
   return def.promise;
 }
 
 /*
  * Begin the creation of a new property, resolving after the editor
  * has been created.
  */
 function beginNewProp()
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -74,16 +74,52 @@ function openComputedView(callback)
     inspector.sidebar.once("computedview-ready", () => {
       inspector.sidebar.select("computedview");
       let computedView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
       callback(inspector, computedView);
     })
   });
 }
 
+/**
+ * Simple DOM node accesor function that takes either a node or a string css
+ * selector as argument and returns the corresponding node
+ * @param {String|DOMNode} nodeOrSelector
+ * @return {DOMNode}
+ */
+function getNode(nodeOrSelector)
+{
+  let node = nodeOrSelector;
+
+  if (typeof nodeOrSelector === "string") {
+    node = content.document.querySelector(nodeOrSelector);
+    ok(node, "A node was found for selector " + nodeOrSelector);
+  }
+
+  return node;
+}
+
+/**
+ * Set the inspector's current selection to a node or to the first match of the
+ * given css selector
+ * @param {String|DOMNode} nodeOrSelector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
+ * @param {String} reason Defaults to "test" which instructs the inspector not to highlight the node upon selection
+ * @return a promise that resolves when the inspector is updated with the new
+ * node
+ */
+function selectNode(nodeOrSelector, inspector, reason="test")
+{
+  info("Selecting the node " + nodeOrSelector);
+  let node = getNode(nodeOrSelector);
+  let updated = inspector.once("inspector-updated");
+  inspector.selection.setNode(node, reason);
+  return updated;
+}
+
 function addStyle(aDocument, aString)
 {
   let node = aDocument.createElement('style');
   node.setAttribute("type", "text/css");
   node.textContent = aString;
   aDocument.getElementsByTagName("head")[0].appendChild(node);
   return node;
 }
--- a/browser/devtools/styleinspector/test/moz.build
+++ b/browser/devtools/styleinspector/test/moz.build
@@ -1,7 +1,8 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 BROWSER_CHROME_MANIFESTS += ['browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/unit/test_parseDeclarations.js
@@ -0,0 +1,206 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
+
+const TEST_DATA = [
+  // Simple test
+  {
+    input: "p:v;",
+    expected: [{name: "p", value: "v", priority: ""}]
+  },
+  // Simple test
+  {
+    input: "this:is;a:test;",
+    expected: [
+      {name: "this", value: "is", priority: ""},
+      {name: "a", value: "test", priority: ""}
+    ]
+  },
+  // Test a single declaration with semi-colon
+  {
+    input: "name:value;",
+    expected: [{name: "name", value: "value", priority: ""}]
+  },
+  // Test a single declaration without semi-colon
+  {
+    input: "name:value",
+    expected: [{name: "name", value: "value", priority: ""}]
+  },
+  // Test multiple declarations separated by whitespaces and carriage returns and tabs
+  {
+    input: "p1 : v1 ; \t\t  \n p2:v2;   \n\n\n\n\t  p3    :   v3;",
+    expected: [
+      {name: "p1", value: "v1", priority: ""},
+      {name: "p2", value: "v2", priority: ""},
+      {name: "p3", value: "v3", priority: ""},
+    ]
+  },
+  // Test simple priority
+  {
+    input: "p1: v1; p2: v2 !important;",
+    expected: [
+      {name: "p1", value: "v1", priority: ""},
+      {name: "p2", value: "v2", priority: "important"}
+    ]
+  },
+  // Test simple priority
+  {
+    input: "p1: v1 !important; p2: v2",
+    expected: [
+      {name: "p1", value: "v1", priority: "important"},
+      {name: "p2", value: "v2", priority: ""}
+    ]
+  },
+  // Test simple priority
+  {
+    input: "p1: v1 !  important; p2: v2 ! important;",
+    expected: [
+      {name: "p1", value: "v1", priority: "important"},
+      {name: "p2", value: "v2", priority: "important"}
+    ]
+  },
+  // Test invalid priority
+  {
+    input: "p1: v1 important;",
+    expected: [
+      {name: "p1", value: "v1 important", priority: ""}
+    ]
+  },
+  // Test various types of background-image urls
+  {
+    input: "background-image: url(../../relative/image.png)",
+    expected: [{name: "background-image", value: "url(\"../../relative/image.png\")", priority: ""}]
+  },
+  {
+    input: "background-image: url(http://site.com/test.png)",
+    expected: [{name: "background-image", value: "url(\"http://site.com/test.png\")", priority: ""}]
+  },
+  {
+    input: "background-image: url(wow.gif)",
+    expected: [{name: "background-image", value: "url(\"wow.gif\")", priority: ""}]
+  },
+  // Test that urls with :;{} characters in them are parsed correctly
+  {
+    input: "background: red url(\"http://site.com/image{}:;.png?id=4#wat\") repeat top right",
+    expected: [
+      {name: "background", value: "red url(\"http://site.com/image{}:;.png?id=4#wat\") repeat top right", priority: ""}
+    ]
+  },
+  // Test that an empty string results in an empty array
+  {input: "", expected: []},
+  // Test that a string comprised only of whitespaces results in an empty array
+  {input: "       \n \n   \n   \n \t  \t\t\t  ", expected: []},
+  // Test that a null input throws an exception
+  {input: null, throws: true},
+  // Test that a undefined input throws an exception
+  {input: undefined, throws: true},
+  // Test that :;{} characters in quoted content are not parsed as multiple declarations
+  {
+    input: "content: \";color:red;}selector{color:yellow;\"",
+    expected: [
+      {name: "content", value: "\";color:red;}selector{color:yellow;\"", priority: ""}
+    ]
+  },
+  // Test that rules aren't parsed, just declarations. So { and } found after a
+  // property name should be part of the property name, same for values.
+  {
+    input: "body {color:red;} p {color: blue;}",
+    expected: [
+      {name: "body {color", value: "red", priority: ""},
+      {name: "} p {color", value: "blue", priority: ""},
+      {name: "}", value: "", priority: ""}
+    ]
+  },
+  // Test unbalanced : and ;
+  {
+    input: "color :red : font : arial;",
+    expected : [
+      {name: "color", value: "red : font : arial", priority: ""}
+    ]
+  },
+  {input: "background: red;;;;;", expected: [{name: "background", value: "red", priority: ""}]},
+  {input: "background:;", expected: [{name: "background", value: "", priority: ""}]},
+  {input: ";;;;;", expected: []},
+  {input: ":;:;", expected: []},
+  // Test name only
+  {input: "color", expected: [
+    {name: "color", value: "", priority: ""}
+  ]},
+  // Test trailing name without :
+  {input: "color:blue;font", expected: [
+    {name: "color", value: "blue", priority: ""},
+    {name: "font", value: "", priority: ""}
+  ]},
+  // Test trailing name with :
+  {input: "color:blue;font:", expected: [
+    {name: "color", value: "blue", priority: ""},
+    {name: "font", value: "", priority: ""}
+  ]},
+  // Test leading value
+  {input: "Arial;color:blue;", expected: [
+    {name: "", value: "Arial", priority: ""},
+    {name: "color", value: "blue", priority: ""}
+  ]},
+  // Test hex colors
+  {input: "color: #333", expected: [{name: "color", value: "#333", priority: ""}]},
+  {input: "color: #456789", expected: [{name: "color", value: "#456789", priority: ""}]},
+  {input: "wat: #XYZ", expected: [{name: "wat", value: "#XYZ", priority: ""}]},
+  // Test string/url quotes escaping
+  {input: "content: \"this is a 'string'\"", expected: [{name: "content", value: "\"this is a 'string'\"", priority: ""}]},
+  {input: 'content: "this is a \\"string\\""', expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]},
+  {input: "content: 'this is a \"string\"'", expected: [{name: "content", value: '\'this is a "string"\'', priority: ""}]},
+  {input: "content: 'this is a \\'string\\'", expected: [{name: "content", value: '"this is a \'string\'"', priority: ""}]},
+  {input: "content: 'this \\' is a \" really strange string'", expected: [{name: "content", value: '"this \' is a \" really strange string"', priority: ""}]},
+  {
+    input: "content: \"a not s\\\
+          o very long title\"",
+    expected: [
+      {name: "content", value: '"a not s\
+          o very long title"', priority: ""}
+    ]
+  }
+];
+
+function run_test() {
+  for (let test of TEST_DATA) {
+    do_print("Test input string " + test.input);
+    let output;
+    try {
+      output = parseDeclarations(test.input);
+    } catch (e) {
+      do_print("parseDeclarations threw an exception with the given input string");
+      if (test.throws) {
+        do_print("Exception expected");
+        do_check_true(true);
+      } else {
+        do_print("Exception unexpected\n" + e);
+        do_check_true(false);
+      }
+    }
+    if (output) {
+      assertOutput(output, test.expected);
+    }
+  }
+}
+
+function assertOutput(actual, expected) {
+  if (actual.length === expected.length) {
+    for (let i = 0; i < expected.length; i ++) {
+      do_check_true(!!actual[i]);
+      do_print("Check that the output item has the expected name, value and priority");
+      do_check_eq(expected[i].name, actual[i].name);
+      do_check_eq(expected[i].value, actual[i].value);
+      do_check_eq(expected[i].priority, actual[i].priority);
+    }
+  } else {
+    for (let prop of actual) {
+      do_print("Actual output contained: {name: "+prop.name+", value: "+prop.value+", priority: "+prop.priority+"}");
+    }
+    do_check_eq(actual.length, expected.length);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/unit/test_parseSingleValue.js
@@ -0,0 +1,76 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+const {parseSingleValue} = devtools.require("devtools/styleinspector/css-parsing-utils");
+
+const TEST_DATA = [
+  {input: null, throws: true},
+  {input: undefined, throws: true},
+  {input: "", expected: {value: "", priority: ""}},
+  {input: "  \t \t \n\n  ", expected: {value: "", priority: ""}},
+  {input: "blue", expected: {value: "blue", priority: ""}},
+  {input: "blue !important", expected: {value: "blue", priority: "important"}},
+  {input: "blue!important", expected: {value: "blue", priority: "important"}},
+  {input: "blue ! important", expected: {value: "blue", priority: "important"}},
+  {input: "blue !  important", expected: {value: "blue", priority: "important"}},
+  {input: "blue !", expected: {value: "blue", priority: ""}},
+  {input: "blue !mportant", expected: {value: "blue !mportant", priority: ""}},
+  {input: "  blue   !important ", expected: {value: "blue", priority: "important"}},
+  {
+    input: "url(\"http://url.com/whyWouldYouDoThat!important.png\") !important",
+    expected: {
+      value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+      priority: "important"
+    }
+  },
+  {
+    input: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+    expected: {
+      value: "url(\"http://url.com/whyWouldYouDoThat!important.png\")",
+      priority: ""
+    }
+  },
+  {
+    input: "\"content!important\" !important",
+    expected: {
+      value: "\"content!important\"",
+      priority: "important"
+    }
+  },
+  {
+    input: "\"content!important\"",
+    expected: {
+      value: "\"content!important\"",
+      priority: ""
+    }
+  }
+];
+
+function run_test() {
+  for (let test of TEST_DATA) {
+    do_print("Test input value " + test.input);
+    try {
+      let output = parseSingleValue(test.input);
+      assertOutput(output, test.expected);
+    } catch (e) {
+      do_print("parseSingleValue threw an exception with the given input value");
+      if (test.throws) {
+        do_print("Exception expected");
+        do_check_true(true);
+      } else {
+        do_print("Exception unexpected\n" + e);
+        do_check_true(false);
+      }
+    }
+  }
+}
+
+function assertOutput(actual, expected) {
+  do_print("Check that the output has the expected value and priority");
+  do_check_eq(expected.value, actual.value);
+  do_check_eq(expected.priority, actual.priority);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/unit/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head =
+tail =
+firefox-appdir = browser
+
+[test_parseDeclarations.js]
+[test_parseSingleValue.js]
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_589162_css_filter.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_589162_css_filter.js
@@ -7,56 +7,41 @@
  *  Mihai Șucan <mihai.sucan@gmail.com>
  *  Patrick Walton <pcwalton@mozilla.com>
  *
  * ***** END LICENSE BLOCK ***** */
 
 const TEST_URI = "data:text/html;charset=utf-8,<div style='font-size:3em;" +
   "foobarCssParser:baz'>test CSS parser filter</div>";
 
-function onContentLoaded()
-{
-  browser.removeEventListener("load", onContentLoaded, true);
-
-  let HUD = HUDService.getHudByWindow(content);
-  let hudId = HUD.hudId;
-  let outputNode = HUD.outputNode;
-
-  HUD.jsterm.clearOutput();
-
-  waitForSuccess({
-    name: "css error displayed",
-    validatorFn: function()
-    {
-      return outputNode.textContent.indexOf("foobarCssParser") > -1;
-    },
-    successFn: function()
-    {
-      HUD.setFilterState("cssparser", false);
-
-      let msg = "the unknown CSS property warning is not displayed, " +
-                "after filtering";
-      testLogEntry(outputNode, "foobarCssParser", msg, true, true);
-
-      HUD.setFilterState("cssparser", true);
-      finishTest();
-    },
-    failureFn: finishTest,
-  });
-}
-
 /**
  * Unit test for bug 589162:
  * CSS filtering on the console does not work
  */
-function test()
-{
-  addTab(TEST_URI);
-  browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
+function test() {
+  Task.spawn(runner).then(finishTest);
+
+  function* runner() {
+    let {tab} = yield loadTab(TEST_URI);
+    let hud = yield openConsole(tab);
+
+    // CSS warnings are disabled by default.
+    hud.setFilterState("cssparser", true);
+    hud.jsterm.clearOutput();
+
+    content.location.reload();
 
-    openConsole(null, function() {
-      browser.addEventListener("load", onContentLoaded, true);
-      content.location.reload();
+    yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        text: "foobarCssParser",
+        category: CATEGORY_CSS,
+        severity: SEVERITY_WARNING,
+      }],
     });
-  }, true);
+
+    hud.setFilterState("cssparser", false);
+
+    let msg = "the unknown CSS property warning is not displayed, " +
+              "after filtering";
+    testLogEntry(hud.outputNode, "foobarCssParser", msg, true, true);
+  }
 }
-
--- a/browser/metro/components/SessionStore.js
+++ b/browser/metro/components/SessionStore.js
@@ -11,16 +11,19 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/WindowsPrefSync.jsm");
 
 #ifdef MOZ_CRASHREPORTER
 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
   "@mozilla.org/xre/app-info;1", "nsICrashReporter");
 #endif
 
+XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor",
+  "resource://gre/modules/CrashMonitor.jsm");
+
 XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
   "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
 
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
   Cu.import("resource://gre/modules/NetUtil.jsm");
   return NetUtil;
 });
 
@@ -71,16 +74,35 @@ SessionStore.prototype = {
 
     try {
       UITelemetry.addSimpleMeasureFunction("metro-tabs",
                                           this._getTabStats.bind(this));
     } catch (ex) {
       // swallow exception that occurs if metro-tabs measure is already set up
     }
 
+    CrashMonitor.previousCheckpoints.then(checkpoints => {
+      let previousSessionCrashed = false;
+
+      if (checkpoints) {
+        // If the previous session finished writing the final state, we'll
+        // assume there was no crash.
+        previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"];
+      } else {
+        // If no checkpoints are saved, this is the first run with CrashMonitor or the
+        // metroSessionCheckpoints file was corrupted/deleted, so fallback to defining
+        // a crash as init-ing with an unexpected previousExecutionState
+        // 1 == RUNNING, 2 == SUSPENDED
+        previousSessionCrashed = Services.metro.previousExecutionState == 1 ||
+          Services.metro.previousExecutionState == 2;
+      }
+
+      Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!previousSessionCrashed);
+    });
+
     try {
       let shutdownWasUnclean = false;
 
       if (this._sessionFileBackup.exists()) {
         this._sessionFileBackup.remove(false);
         shutdownWasUnclean = true;
       }
 
@@ -286,18 +308,18 @@ SessionStore.prototype = {
         observerService.removeObserver(this, "quit-application-granted");
         observerService.removeObserver(this, "quit-application");
         observerService.removeObserver(this, "reset-telemetry-vars");
 
         // If a save has been queued, kill the timer and save state now
         if (this._saveTimer) {
           this._saveTimer.cancel();
           this._saveTimer = null;
-          this.saveState();
         }
+        this.saveState();
         break;
       case "browser:purge-session-history": // catch sanitization
         this._clearDisk();
 
         // If the browser is shutting down, simply return after clearing the
         // session data on disk as this notification fires after the
         // quit-application notification so the browser is about to exit.
         if (this._loadState == STATE_QUITTING)
@@ -639,16 +661,19 @@ SessionStore.prototype = {
     // Obtain a converter to convert our data to a UTF-8 encoded input stream.
     let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
     converter.charset = "UTF-8";
 
     // Asynchronously copy the data to the file.
     let istream = converter.convertToInputStream(aData);
     NetUtil.asyncCopy(istream, ostream, function(rc) {
       if (Components.isSuccessCode(rc)) {
+        if (Services.startup.shuttingDown) {
+          Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", "");
+        }
         Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
       }
     });
   },
 
   _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) {
 #ifdef MOZ_CRASHREPORTER
     try {
--- a/browser/themes/linux/devtools/ruleview.css
+++ b/browser/themes/linux/devtools/ruleview.css
@@ -104,16 +104,20 @@
   vertical-align: text-top;
   -moz-margin-end: 5px;
 }
 
 .ruleview-overridden {
   text-decoration: line-through;
 }
 
+.theme-light .ruleview-overridden {
+  -moz-text-decoration-color: #667380; /*  Content (Text) - Dark Grey */
+}
+
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
   padding: 0;
 }
 
 .ruleview-property {
   border-left: 2px solid transparent;
   clear: right;
--- a/browser/themes/osx/devtools/ruleview.css
+++ b/browser/themes/osx/devtools/ruleview.css
@@ -108,16 +108,20 @@
   vertical-align: text-top;
   -moz-margin-end: 5px;
 }
 
 .ruleview-overridden {
   text-decoration: line-through;
 }
 
+.theme-light .ruleview-overridden {
+  -moz-text-decoration-color: #667380; /*  Content (Text) - Dark Grey */
+}
+
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
   padding: 0;
 }
 
 .ruleview-property {
   border-left: 2px solid transparent;
   clear: right;
--- a/browser/themes/shared/customizableui/customizeMode.inc.css
+++ b/browser/themes/shared/customizableui/customizeMode.inc.css
@@ -99,16 +99,25 @@
   -moz-image-region: rect(0, 48px, 24px, 24px);
   background-color: rgb(218, 218, 218);
   border-color: rgb(168, 168, 168);
   text-shadow: 0 1px rgb(236, 236, 236);
   box-shadow: 0 1px rgba(255, 255, 255, 0.5),
               inset 0 1px rgb(196, 196, 196);
 }
 
+#customization-undo-reset {
+  padding-left: 12px;
+  padding-right: 12px;
+%ifdef XP_MACOSX
+  padding-top: 6px;
+%else
+  padding-top: 7px;
+%endif
+}
 
 #main-window[customize-entered] #customization-panel-container {
   background-image: url("chrome://browser/skin/customizableui/customizeMode-separatorHorizontal.png"),
                     url("chrome://browser/skin/customizableui/customizeMode-separatorVertical.png"),
                     url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png"),
                     url("chrome://browser/skin/customizableui/background-noise-toolbar.png"),
                     linear-gradient(to bottom, #3e86ce, #3878ba);
   background-position: center top, left center, left top, left top, left top;
--- a/browser/themes/windows/devtools/ruleview.css
+++ b/browser/themes/windows/devtools/ruleview.css
@@ -104,16 +104,20 @@
   vertical-align: text-top;
   -moz-margin-end: 5px;
 }
 
 .ruleview-overridden {
   text-decoration: line-through;
 }
 
+.theme-light .ruleview-overridden {
+  -moz-text-decoration-color: #667380; /*  Content (Text) - Dark Grey */
+}
+
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
   padding: 0;
 }
 
 .ruleview-property {
   border-left: 2px solid transparent;
   clear: right;
--- a/build/docs/mozinfo.rst
+++ b/build/docs/mozinfo.rst
@@ -68,29 +68,36 @@ buildapp
 crashreporter
    Whether the crash reporter is enabled for this build.
 
    Values are ``true`` and ``false``.
 
    Always defined.
 
 datareporting
-  Whether data reporting (MOZ_DATA_REPORTING) is enabled for this build.
+   Whether data reporting (MOZ_DATA_REPORTING) is enabled for this build.
 
    Values are ``true`` and ``false``.
 
    Always defined.
 
 debug
    Whether this is a debug build.
 
    Values are ``true`` and ``false``.
 
    Always defined.
 
+healthreport
+   Whether the Health Report feature is enabled.
+
+   Values are ``true`` and ``false``.
+
+   Always defined.
+
 mozconfig
    The path of the :ref:`mozconfig file <mozconfig>` used to produce this build.
 
    Optional.
 
 os
    The operating system the build is produced for. Values for tier-1
    supported platforms are ``linux``, ``win``, ``mac``, ``b2g``, and
@@ -128,8 +135,21 @@ toolkit
 
    Always defined.
 
 topsrcdir
    The path to the source directory the build came from.
 
    Always defined.
 
+wave
+   Whether Wave audio support is enabled.
+
+   Values are ``true`` and ``false``.
+
+   Always defined.
+
+webm
+   Whether WebM support is enabled.
+
+   Values are ``true`` and ``false``.
+
+   Always defined.
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -244,16 +244,17 @@
 #include ../services/manifests/SyncAndroidManifest_activities.xml.in
 #include ../services/manifests/HealthReportAndroidManifest_activities.xml.in
 
 #if MOZ_CRASHREPORTER
   <activity android:name="org.mozilla.gecko.CrashReporter"
             android:label="@string/crash_reporter_title"
             android:icon="@drawable/crash_reporter"
             android:theme="@style/Gecko"
+            android:exported="false"
             android:excludeFromRecents="true">
           <intent-filter>
             <action android:name="org.mozilla.gecko.reportCrash" />
           </intent-filter>
 	</activity>
 #endif
 
         <activity android:name="org.mozilla.gecko.VideoPlayer"
--- a/mobile/android/base/home/PanelListRow.java
+++ b/mobile/android/base/home/PanelListRow.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.HomeItems;
 
 import com.squareup.picasso.Picasso;
 
 import android.content.Context;
 import android.database.Cursor;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.ImageView;
 
 public class PanelListRow extends TwoLineRow {
 
     private final ImageView mIcon;
 
@@ -45,14 +46,19 @@ public class PanelListRow extends TwoLin
 
         int descriptionIndex = cursor.getColumnIndexOrThrow(HomeItems.DESCRIPTION);
         final String description = cursor.getString(descriptionIndex);
         setDescription(description);
 
         int imageIndex = cursor.getColumnIndexOrThrow(HomeItems.IMAGE_URL);
         final String imageUrl = cursor.getString(imageIndex);
 
-        Picasso.with(getContext())
-               .load(imageUrl)
-               .error(R.drawable.favicon)
-               .into(mIcon);
+        final boolean hasImageUrl = !TextUtils.isEmpty(imageUrl);
+        mIcon.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE);
+
+        if (hasImageUrl) {
+            Picasso.with(getContext())
+                   .load(imageUrl)
+                   .error(R.drawable.favicon)
+                   .into(mIcon);
+        }
     }
 }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -51,16 +51,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Sanitizer.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
                                   "resource://gre/modules/Prompt.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
                                   "resource://gre/modules/HelperApps.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
+                                  "resource://gre/modules/SSLExceptions.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
@@ -284,16 +287,24 @@ var BrowserApp = {
 
   deck: null,
 
   startup: function startup() {
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
     dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
 
     this.deck = document.getElementById("browsers");
+    this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
+      try {
+        BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
+        Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
+        sendMessageToJava({ type: "Gecko:DelayedStartup" });
+      } catch(ex) { console.log(ex); }
+    }, false);
+
     BrowserEventHandler.init();
     ViewportHandler.init();
 
     Services.androidBridge.browserApp = this;
 
     Services.obs.addObserver(this, "Locale:Changed", false);
     Services.obs.addObserver(this, "Tab:Load", false);
     Services.obs.addObserver(this, "Tab:Selected", false);
@@ -3785,27 +3796,28 @@ Tab.prototype = {
 
         if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
           if (!this._linkifier)
             this._linkifier = new Linkifier();
           this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
         }
 
         // Show page actions for helper apps.
-        if (BrowserApp.selectedTab == this)
-          ExternalApps.updatePageAction(this.browser.currentURI);
+        let uri = this.browser.currentURI;
+        if (BrowserApp.selectedTab == this && ExternalApps.shouldCheckUri(uri))
+          ExternalApps.updatePageAction(uri);
 
         if (!Reader.isEnabledForParseOnLoad)
           return;
 
         // Once document is fully loaded, parse it
         Reader.parseDocumentFromTab(this.id, function (article) {
           // Do nothing if there's no article or the page in this tab has
           // changed
-          let tabURL = this.browser.currentURI.specIgnoringRef;
+          let tabURL = uri.specIgnoringRef;
           if (article == null || (article.url != tabURL)) {
             // Don't clear the article for about:reader pages since we want to
             // use the article from the previous page
             if (!tabURL.startsWith("about:reader")) {
               this.savedArticle = null;
               this.readerEnabled = false;
               this.readerActive = false;
             } else {
@@ -7999,16 +8011,24 @@ var ExternalApps = {
     }
   },
 
   openExternal: function(aElement) {
     let uri = ExternalApps._getMediaLink(aElement);
     HelperApps.launchUri(uri);
   },
 
+  shouldCheckUri: function(uri) {
+    if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
+      return false;
+    }
+
+    return true;
+  },
+
   updatePageAction: function updatePageAction(uri) {
     let apps = HelperApps.getAppsForUri(uri);
 
     if (apps.length > 0)
       this._setUriForPageAction(uri, apps);
     else
       this._removePageAction();
   },
--- a/mobile/android/chrome/content/browser.xul
+++ b/mobile/android/chrome/content/browser.xul
@@ -7,13 +7,12 @@
 
 <window id="main-window"
         onload="BrowserApp.startup();"
         windowtype="navigator:browser"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script type="application/javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/javascript" src="chrome://browser/content/downloads.js"/>
-  <script type="application/javascript" src="chrome://browser/content/exceptions.js"/>
 
   <deck id="browsers" flex="1"/>
 
 </window>
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -29,17 +29,16 @@ chrome.jar:
 * content/aboutApps.xhtml              (content/aboutApps.xhtml)
 * content/aboutApps.js                 (content/aboutApps.js)
   content/blockedSite.xhtml            (content/blockedSite.xhtml)
   content/languages.properties         (content/languages.properties)
   content/browser.xul                  (content/browser.xul)
 * content/browser.js                   (content/browser.js)
   content/bindings/checkbox.xml        (content/bindings/checkbox.xml)
   content/bindings/settings.xml        (content/bindings/settings.xml)
-  content/exceptions.js                (content/exceptions.js)
   content/downloads.js                 (content/downloads.js)
   content/netError.xhtml               (content/netError.xhtml)
   content/SelectHelper.js              (content/SelectHelper.js)
   content/SelectionHandler.js          (content/SelectionHandler.js)
   content/dbg-browser-actors.js        (content/dbg-browser-actors.js)
 * content/WebappRT.js                  (content/WebappRT.js)
   content/InputWidgetHelper.js         (content/InputWidgetHelper.js)
   content/WebrtcUI.js                  (content/WebrtcUI.js)
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -83,16 +83,19 @@ BrowserCLH.prototype = {
       height = aCmdLine.handleFlagWithParam("height", false);
     } catch (e) { /* Optional */ }
 
     try {
       let uri = resolveURIInternal(aCmdLine, openURL);
       if (!uri)
         return;
 
+      // Let's get a head start on opening the network connection to the URI we are about to load
+      Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+
       let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
       if (browserWin) {
         if (!pinned) {
           browserWin.browserDOMWindow.openURI(uri, null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
         }
       } else {
         let args = {
           url: openURL,
rename from mobile/android/chrome/content/exceptions.js
rename to mobile/android/modules/SSLExceptions.jsm
--- a/mobile/android/chrome/content/exceptions.js
+++ b/mobile/android/modules/SSLExceptions.jsm
@@ -1,18 +1,21 @@
 /* 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/. */
+"use strict"
 
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 
+this.EXPORTED_SYMBOLS = ["SSLExceptions"];
+
 /**
   A class to add exceptions to override SSL certificate problems. The functionality
   itself is borrowed from exceptionDialog.js.
 */
 function SSLExceptions() {
   this._overrideService = Cc["@mozilla.org/security/certoverride;1"]
                           .getService(Ci.nsICertOverrideService);
 }
@@ -43,18 +46,18 @@ SSLExceptions.prototype = {
   },
 
   /**
     Attempt to download the certificate for the location specified to get the SSLState
     for the certificate and the errors.
    */
   _checkCert: function SSLE_checkCert(aURI) {
     this._sslStatus = null;
-  
-    let req = new XMLHttpRequest();
+
+    let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
     try {
       if (aURI) {
         req.open("GET", aURI.prePath, false);
         req.channel.notificationCallbacks = this;
         req.send(null);
       }
     } catch (e) {
       // We *expect* exceptions if there are problems with the certificate
--- a/mobile/android/modules/moz.build
+++ b/mobile/android/modules/moz.build
@@ -12,15 +12,16 @@ EXTRA_JS_MODULES += [
     'JNI.jsm',
     'LightweightThemeConsumer.jsm',
     'Notifications.jsm',
     'OrderedBroadcast.jsm',
     'Prompt.jsm',
     'Sanitizer.jsm',
     'SharedPreferences.jsm',
     'SimpleServiceDiscovery.jsm',
+    'SSLExceptions.jsm',
 ]
 
 if CONFIG['MOZ_ANDROID_SYNTHAPKS']:
     EXTRA_JS_MODULES += [
         'WebappManager.jsm',
         'WebappManagerWorker.js',
     ]
--- a/python/mozbuild/mozbuild/mozinfo.py
+++ b/python/mozbuild/mozbuild/mozinfo.py
@@ -73,16 +73,17 @@ def build_dict(config, env=os.environ):
     # hardcoded list of known 32-bit CPUs
     elif p in ["x86", "arm", "ppc"]:
         d["bits"] = 32
     # other CPUs will wind up with unknown bits
 
     d['debug'] = substs.get('MOZ_DEBUG') == '1'
     d['crashreporter'] = bool(substs.get('MOZ_CRASHREPORTER'))
     d['datareporting'] = bool(substs.get('MOZ_DATA_REPORTING'))
+    d['healthreport'] = substs.get('MOZ_SERVICES_HEALTHREPORT') == '1'
     d['asan'] = substs.get('MOZ_ASAN') == '1'
     d['tests_enabled'] = substs.get('ENABLE_TESTS') == "1"
     d['bin_suffix'] = substs.get('BIN_SUFFIX', '')
 
     d['ogg'] = bool(substs.get('MOZ_OGG'))
     d['webm'] = bool(substs.get('MOZ_WEBM'))
     d['wave'] = bool(substs.get('MOZ_WAVE'))
 
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -10,18 +10,17 @@ Cu.import("resource://gre/modules/Log.js
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-common/hawk.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 Cu.import("resource://gre/modules/Credentials.jsm");
 
-// Default can be changed by the preference 'identity.fxaccounts.auth.uri'
-let _host = "https://api-accounts.dev.lcip.org/v1";
+let _host = "https://api.accounts.firefox.com/v1"
 try {
   _host = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
 } catch(keepDefault) {}
 
 const HOST = _host;
 this.FxAccountsClient = function(host = HOST) {
   this.host = host;
 
--- a/services/healthreport/docs/dataformat.rst
+++ b/services/healthreport/docs/dataformat.rst
@@ -1308,16 +1308,84 @@ Example
 ::
 
     "org.mozilla.searches.counts": {
       "_v": 1,
       "google.searchbar": 3,
       "google.urlbar": 7
     },
 
+org.mozilla.sync.sync
+---------------------
+
+This daily measurement contains information about the Sync service.
+
+Values should be recorded for every day FHR measurements occurred.
+
+Version 1
+^^^^^^^^^
+
+This version debuted with Firefox 30 on desktop. It contains the following
+properties:
+
+enabled
+   Daily numeric indicating whether Sync is configured and enabled. 1 if so,
+   0 otherwise.
+
+preferredProtocol
+   String version of the maximum Sync protocol version the client supports.
+   This will be ``1.1`` for for legacy Sync and ``1.5`` for clients that
+   speak the Firefox Accounts protocol.
+
+actualProtocol
+   The actual Sync protocol version the client is configured to use.
+
+   This will be ``1.1`` if the client is configured with the legacy Sync
+   service or if the client only supports ``1.1``.
+
+   It will be ``1.5`` if the client supports ``1.5`` and either a) the
+   client is not configured b) the client is using Firefox Accounts Sync.
+
+syncStart
+   Count of sync operations performed.
+
+syncSuccess
+   Count of sync operations that completed successfully.
+
+syncError
+   Count of sync operations that did not complete successfully.
+
+   This is a measure of overall sync success. This does *not* reflect
+   recoverable errors (such as record conflict) that can occur during
+   sync. This is thus a rough proxy of whether the sync service is
+   operating without error.
+
+org.mozilla.sync.devices
+------------------------
+
+This daily measurement contains information about the device type composition
+for the configured Sync account.
+
+Version 1
+^^^^^^^^^
+
+Version 1 was introduced with Firefox 30.
+
+Field names are dynamic according to the client-reported device types from
+Sync records. All fields are daily last seen integer values corresponding to
+the number of devices of that type.
+
+Common values include:
+
+desktop
+   Corresponds to a Firefox desktop client.
+
+mobile
+   Corresponds to a Fennec client.
+
 org.mozilla.sysinfo.sysinfo
 ---------------------------
 
 This measurement contains basic information about the system the application
 is running on.
 
 Version 2
 ^^^^^^^^^
--- a/services/metrics/providermanager.jsm
+++ b/services/metrics/providermanager.jsm
@@ -155,20 +155,24 @@ this.ProviderManager.prototype = Object.
    * provider.init().
    *
    * @param provider
    *        (Metrics.Provider) The provider instance to register.
    *
    * @return Promise<null>
    */
   registerProvider: function (provider) {
-    if (!(provider instanceof Provider)) {
-      throw new Error("Argument must be a Provider instance.");
+    // We should perform an instanceof check here. However, due to merged
+    // compartments, the Provider type may belong to one of two JSMs
+    // isinstance gets confused depending on which module Provider comes
+    // from. Some code references Provider from dataprovider.jsm; others from
+    // Metrics.jsm.
+    if (!provider.name) {
+      throw new Error("Provider is not valid: does not have a name.");
     }
-
     if (this._providers.has(provider.name)) {
       return CommonUtils.laterTickResolvingPromise();
     }
 
     let deferred = Promise.defer();
     this._providerInitQueue.push([provider, deferred]);
 
     if (this._providerInitQueue.length == 1) {
--- a/services/metrics/tests/xpcshell/test_metrics_provider_manager.js
+++ b/services/metrics/tests/xpcshell/test_metrics_provider_manager.js
@@ -42,17 +42,17 @@ add_task(function test_register_provider
   yield manager.registerProvider(dummy);
   do_check_eq(manager._providers.size, 1);
   do_check_eq(manager.getProvider(dummy.name), dummy);
 
   let failed = false;
   try {
     manager.registerProvider({});
   } catch (ex) {
-    do_check_true(ex.message.startsWith("Argument must be a Provider"));
+    do_check_true(ex.message.startsWith("Provider is not valid"));
     failed = true;
   } finally {
     do_check_true(failed);
     failed = false;
   }
 
   manager.unregisterProvider(dummy.name);
   do_check_eq(manager._providers.size, 0);
--- a/services/sync/Makefile.in
+++ b/services/sync/Makefile.in
@@ -15,16 +15,17 @@ SYNC_PP_PATH = $(FINAL_TARGET)/modules/s
 PP_TARGETS += SYNC_PP
 
 # The set of core JavaScript modules for Sync. These are copied as-is.
 sync_modules := \
   addonsreconciler.js \
   addonutils.js \
   browserid_identity.js \
   engines.js \
+  healthreport.jsm \
   identity.js \
   jpakeclient.js \
   keys.js \
   main.js \
   notifications.js \
   policies.js \
   record.js \
   resource.js \
--- a/services/sync/SyncComponents.manifest
+++ b/services/sync/SyncComponents.manifest
@@ -18,8 +18,12 @@ component {74b89fb0-f200-4ae8-a3ec-dd164
 contract @mozilla.org/weave/service;1 {74b89fb0-f200-4ae8-a3ec-dd164117f6de}
 category app-startup WeaveService service,@mozilla.org/weave/service;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} application={99bceaaa-e3c6-48c1-b981-ef9b46b67d60}
 component {d28f8a0b-95da-48f4-b712-caf37097be41} Weave.js
 contract @mozilla.org/network/protocol/about;1?what=sync-log {d28f8a0b-95da-48f4-b712-caf37097be41}
 
 # Register resource aliases
 # (Note, for tests these are also set up in addResourceAlias)
 resource services-sync resource://gre/modules/services-sync/
+
+#ifdef MOZ_SERVICES_HEALTHREPORT
+category healthreport-js-provider-default SyncProvider resource://services-sync/healthreport.jsm
+#endif
--- a/services/sync/Weave.js
+++ b/services/sync/Weave.js
@@ -87,16 +87,21 @@ WeaveService.prototype = {
     Services.obs.addObserver(function onReady() {
       Services.obs.removeObserver(onReady, "weave:service:ready");
       deferred.resolve();
     }, "weave:service:ready", false);
     this.ensureLoaded();
     return deferred.promise;
   },
 
+  /**
+   * Whether Firefox Accounts is enabled.
+   *
+   * @return bool
+   */
   get fxAccountsEnabled() {
     // work out what identity manager to use.  This is stored in a preference;
     // if the preference exists, we trust it.
     let fxAccountsEnabled;
     try {
       fxAccountsEnabled = Services.prefs.getBoolPref("services.sync.fxaccounts.enabled");
     } catch (_) {
       // That pref doesn't exist - so let's assume this is a first-run.
@@ -107,16 +112,31 @@ WeaveService.prototype = {
       Services.prefs.setBoolPref("services.sync.fxaccounts.enabled", fxAccountsEnabled);
     }
     // Currently we don't support toggling this pref after initialization -
     // except when sync is reset - but this 1 exception is enough that we can't
     // cache the value.
     return fxAccountsEnabled;
   },
 
+  /**
+   * Whether Sync appears to be enabled.
+   *
+   * This returns true if all the Sync preferences for storing account
+   * and server configuration are populated.
+   *
+   * It does *not* perform a robust check to see if the client is working.
+   * For that, you'll want to check Weave.Status.checkSetup().
+   */
+  get enabled() {
+    let prefs = Services.prefs.getBranch(SYNC_PREFS_BRANCH);
+    return prefs.prefHasUserValue("username") &&
+           prefs.prefHasUserValue("clusterURL");
+  },
+
   observe: function (subject, topic, data) {
     switch (topic) {
     case "app-startup":
       let os = Cc["@mozilla.org/observer-service;1"].
                getService(Ci.nsIObserverService);
       os.addObserver(this, "final-ui-startup", true);
       break;
 
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -66,16 +66,38 @@ ClientEngine.prototype = {
       stats.hasMobile = stats.hasMobile || type == "mobile";
       stats.names.push(name);
       stats.numClients++;
     }
 
     return stats;
   },
 
+  /**
+   * Obtain information about device types.
+   *
+   * Returns a Map of device types to integer counts.
+   */
+  get deviceTypes() {
+    let counts = new Map();
+
+    counts.set(this.localType, 1);
+
+    for each (let record in this._store._remoteClients) {
+      let type = record.type;
+      if (!counts.has(type)) {
+        counts.set(type, 0);
+      }
+
+      counts.set(type, counts.get(type) + 1);
+    }
+
+    return counts;
+  },
+
   get localID() {
     // Generate a random GUID id we don't have one
     let localID = Svc.Prefs.get("client.GUID", "");
     return localID == "" ? this.localID = Utils.makeGUID() : localID;
   },
   set localID(value) Svc.Prefs.set("client.GUID", value),
 
   get localName() {
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/healthreport.jsm
@@ -0,0 +1,180 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "SyncProvider",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+const DAILY_LAST_NUMERIC_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC};
+const DAILY_LAST_TEXT_FIELD = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
+const DAILY_COUNTER_FIELD = {type: Metrics.Storage.FIELD_DAILY_COUNTER};
+
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+                                  "resource://services-sync/main.js");
+
+function SyncMeasurement1() {
+  Metrics.Measurement.call(this);
+}
+
+SyncMeasurement1.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "sync",
+  version: 1,
+
+  fields: {
+    enabled: DAILY_LAST_NUMERIC_FIELD,
+    preferredProtocol: DAILY_LAST_TEXT_FIELD,
+    activeProtocol: DAILY_LAST_TEXT_FIELD,
+    syncStart: DAILY_COUNTER_FIELD,
+    syncSuccess: DAILY_COUNTER_FIELD,
+    syncError: DAILY_COUNTER_FIELD,
+  },
+});
+
+function SyncDevicesMeasurement1() {
+  Metrics.Measurement.call(this);
+}
+
+SyncDevicesMeasurement1.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "devices",
+  version: 1,
+
+  fields: {},
+
+  shouldIncludeField: function (name) {
+    return true;
+  },
+
+  fieldType: function (name) {
+    return Metrics.Storage.FIELD_DAILY_COUNTER;
+  },
+});
+
+this.SyncProvider = function () {
+  Metrics.Provider.call(this);
+};
+SyncProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.sync",
+
+  measurementTypes: [
+    SyncDevicesMeasurement1,
+    SyncMeasurement1,
+  ],
+
+  _OBSERVERS: [
+    "weave:service:sync:start",
+    "weave:service:sync:finish",
+    "weave:service:sync:error",
+  ],
+
+  postInit: function () {
+    for (let o of this._OBSERVERS) {
+      Services.obs.addObserver(this, o, false);
+    }
+
+    return Promise.resolve();
+  },
+
+  onShutdown: function () {
+    for (let o of this._OBSERVERS) {
+      Services.obs.removeObserver(this, o);
+    }
+
+    return Promise.resolve();
+  },
+
+  observe: function (subject, topic, data) {
+    let field;
+    switch (topic) {
+      case "weave:service:sync:start":
+        field = "syncStart";
+        break;
+
+      case "weave:service:sync:finish":
+        field = "syncSuccess";
+        break;
+
+      case "weave:service:sync:error":
+        field = "syncError";
+        break;
+    }
+
+    let m = this.getMeasurement(SyncMeasurement1.prototype.name,
+                                SyncMeasurement1.prototype.version);
+    return this.enqueueStorageOperation(function recordSyncEvent() {
+      return m.incrementDailyCounter(field);
+    });
+  },
+
+  collectDailyData: function () {
+    return this.storage.enqueueTransaction(this._populateDailyData.bind(this));
+  },
+
+  _populateDailyData: function* () {
+    let m = this.getMeasurement(SyncMeasurement1.prototype.name,
+                                SyncMeasurement1.prototype.version);
+
+    let svc = Cc["@mozilla.org/weave/service;1"]
+                .getService(Ci.nsISupports)
+                .wrappedJSObject;
+
+    let enabled = svc.enabled;
+    yield m.setDailyLastNumeric("enabled", enabled ? 1 : 0);
+
+    // preferredProtocol is constant and only changes as the client
+    // evolves.
+    yield m.setDailyLastText("preferredProtocol", "1.5");
+
+    let protocol = svc.fxAccountsEnabled ? "1.5" : "1.1";
+    yield m.setDailyLastText("activeProtocol", protocol);
+
+    if (!enabled) {
+      return;
+    }
+
+    // Before grabbing more information, be sure the Sync service
+    // is fully initialized. This has the potential to initialize
+    // Sync on the spot. This may be undesired if Sync appears to
+    // be enabled but it really isn't. That responsibility should
+    // be up to svc.enabled to not return false positives, however.
+    yield svc.whenLoaded();
+
+    if (Weave.Status.service != Weave.STATUS_OK) {
+      return;
+    }
+
+    // Device types are dynamic. So we need to dynamically create fields if
+    // they don't exist.
+    let dm = this.getMeasurement(SyncDevicesMeasurement1.prototype.name,
+                                 SyncDevicesMeasurement1.prototype.version);
+    let devices = Weave.Service.clientsEngine.deviceTypes;
+    for (let [field, count] of devices) {
+      let hasField = this.storage.hasFieldFromMeasurement(dm.id, field,
+                                    this.storage.FIELD_DAILY_LAST_NUMERIC);
+      let fieldID;
+      if (hasField) {
+        fieldID = this.storage.fieldIDFromMeasurement(dm.id, field);
+      } else {
+        fieldID = yield this.storage.registerField(dm.id, field,
+                                       this.storage.FIELD_DAILY_LAST_NUMERIC);
+      }
+
+      yield this.storage.setDailyLastNumericFromFieldID(fieldID, count);
+    }
+  },
+});
--- a/services/sync/moz.build
+++ b/services/sync/moz.build
@@ -3,11 +3,14 @@
 # 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/.
 
 DIRS += ['locales']
 TEST_DIRS += ['tests']
 
 EXTRA_COMPONENTS += [
-    'SyncComponents.manifest',
     'Weave.js',
 ]
+
+EXTRA_PP_COMPONENTS += [
+    'SyncComponents.manifest',
+]
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_healthreport.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Metrics.jsm", this);
+Cu.import("resource://gre/modules/Preferences.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://services-sync/main.js", this);
+Cu.import("resource://services-sync/healthreport.jsm", this);
+Cu.import("resource://testing-common/services-common/logging.js", this);
+Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
+
+function run_test() {
+  initTestLogging();
+
+  // A head JS file always sets the
+  // services.sync.fxaccounts.enabled pref. This prevents us from testing
+  // pristine profile conditions and likely indicates there isn't test
+  // coverage of the Sync service's fxAccountsEnabled property. Check
+  // that pre-condition and hack around it.
+  let branch = new Preferences("services.sync.");
+  Assert.ok(branch.isSet("fxaccounts.enabled"), "Check precondition");
+  branch.reset("fxaccounts.enabled");
+
+  run_next_test();
+}
+
+add_task(function test_constructor() {
+  let provider = new SyncProvider();
+});
+
+// Provider can initialize and de-initialize properly.
+add_task(function* test_init() {
+  let storage = yield Metrics.Storage("init");
+  let provider = new SyncProvider();
+  yield provider.init(storage);
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+add_task(function* test_collect() {
+  let storage = yield Metrics.Storage("collect");
+  let provider = new SyncProvider();
+  yield provider.init(storage);
+
+  // Initially nothing should be configured.
+  let now = new Date();
+  yield provider.collectDailyData();
+
+  let m = provider.getMeasurement("sync", 1);
+  let values = yield m.getValues();
+  Assert.equal(values.days.size, 1);
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+  Assert.ok(day.has("enabled"));
+  Assert.ok(day.has("activeProtocol"));
+  Assert.ok(day.has("preferredProtocol"));
+  Assert.equal(day.get("enabled"), 0);
+  Assert.equal(day.get("preferredProtocol"), "1.5");
+  Assert.equal(day.get("activeProtocol"), "1.5",
+               "Protocol without setup should be FX Accounts version.");
+
+  // Now check for old Sync setup.
+  let branch = new Preferences("services.sync.");
+  branch.set("username", "foo");
+  branch.reset("fxaccounts.enabled");
+  yield provider.collectDailyData();
+  values = yield m.getValues();
+  Assert.equal(values.days.getDay(now).get("activeProtocol"), "1.1",
+               "Protocol with old Sync setup is correct.");
+
+  Assert.equal(Weave.Status.__authManager, undefined, "Detect code changes");
+
+  // Let's enable Sync so we can get more useful data.
+  // We need to do this because the FHR probe only records more info if Sync
+  // is configured properly.
+  Weave.Service.identity.account = "johndoe";
+  Weave.Service.identity.basicPassword = "ilovejane";
+  Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase();
+  Weave.Service.clusterURL = "http://localhost/";
+  Assert.equal(Weave.Status.checkSetup(), Weave.STATUS_OK);
+
+  yield provider.collectDailyData();
+  values = yield m.getValues();
+  day = values.days.getDay(now);
+  Assert.equal(day.get("enabled"), 1);
+
+  // An empty account should have 1 device: us.
+  let dm = provider.getMeasurement("devices", 1);
+  values = yield dm.getValues();
+  Assert.ok(values.days.hasDay(now));
+  day = values.days.getDay(now);
+  Assert.equal(day.size, 1);
+  let engine = Weave.Service.clientsEngine;
+  Assert.ok(engine);
+  Assert.ok(day.has(engine.localType));
+  Assert.equal(day.get(engine.localType), 1);
+
+  // Add some devices and ensure they show up.
+  engine._store._remoteClients["id1"] = {type: "mobile"};
+  engine._store._remoteClients["id2"] = {type: "tablet"};
+  engine._store._remoteClients["id3"] = {type: "mobile"};
+
+  yield provider.collectDailyData();
+  values = yield dm.getValues();
+  day = values.days.getDay(now);
+
+  let expected = {
+    "foobar": 0,
+    "tablet": 1,
+    "mobile": 2,
+    "desktop": 0,
+  };
+
+  for (let type in expected) {
+    let count = expected[type];
+
+    if (engine.localType == type) {
+      count++;
+    }
+
+    if (!count) {
+      Assert.ok(!day.has(type));
+    } else {
+      Assert.ok(day.has(type));
+      Assert.equal(day.get(type), count);
+    }
+  }
+
+  engine._store._remoteClients = {};
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+add_task(function* test_sync_events() {
+  let storage = yield Metrics.Storage("sync_events");
+  let provider = new SyncProvider();
+  yield provider.init(storage);
+
+  let m = provider.getMeasurement("sync", 1);
+
+  for (let i = 0; i < 5; i++) {
+    Services.obs.notifyObservers(null, "weave:service:sync:start", null);
+  }
+
+  for (let i = 0; i < 3; i++) {
+    Services.obs.notifyObservers(null, "weave:service:sync:finish", null);
+  }
+
+  for (let i = 0; i < 2; i++) {
+    Services.obs.notifyObservers(null, "weave:service:sync:error", null);
+  }
+
+  // Wait for storage to complete.
+  yield m.storage.enqueueOperation(() => {
+    return Promise.resolve();
+  });
+
+  let values = yield m.getValues();
+  let now = new Date();
+  Assert.ok(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+
+  Assert.ok(day.has("syncStart"));
+  Assert.ok(day.has("syncSuccess"));
+  Assert.ok(day.has("syncError"));
+  Assert.equal(day.get("syncStart"), 5);
+  Assert.equal(day.get("syncSuccess"), 3);
+  Assert.equal(day.get("syncError"), 2);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+add_task(function* test_healthreporter_json() {
+  let reporter = yield getHealthReporter("healthreporter_json");
+  yield reporter.init();
+  try {
+    yield reporter._providerManager.registerProvider(new SyncProvider());
+    yield reporter.collectMeasurements();
+    let payload = yield reporter.getJSONPayload(true);
+    let now = new Date();
+    let today = reporter._formatDate(now);
+
+    Assert.ok(today in payload.data.days);
+    let day = payload.data.days[today];
+
+    Assert.ok("org.mozilla.sync.sync" in day);
+    Assert.ok("org.mozilla.sync.devices" in day);
+
+    let devices = day["org.mozilla.sync.devices"];
+    let engine = Weave.Service.clientsEngine;
+    Assert.ok(engine);
+    let type = engine.localType;
+    Assert.ok(type);
+    Assert.ok(type in devices);
+    Assert.equal(devices[type], 1);
+  } finally {
+    reporter._shutdown();
+  }
+});
--- a/services/sync/tests/unit/test_service_startup.js
+++ b/services/sync/tests/unit/test_service_startup.js
@@ -8,39 +8,47 @@ Cu.import("resource://testing-common/ser
 
 Svc.Prefs.set("registerEngines", "Tab,Bookmarks,Form,History");
 Cu.import("resource://services-sync/service.js");
 
 function run_test() {
   _("When imported, Service.onStartup is called");
   initTestLogging("Trace");
 
+  let xps = Cc["@mozilla.org/weave/service;1"]
+              .getService(Ci.nsISupports)
+              .wrappedJSObject;
+  do_check_false(xps.enabled);
+
   // Test fixtures
   Service.identity.username = "johndoe";
+  do_check_false(xps.enabled);
 
   Cu.import("resource://services-sync/service.js");
 
   _("Service is enabled.");
   do_check_eq(Service.enabled, true);
 
   _("Engines are registered.");
   let engines = Service.engineManager.getAll();
   do_check_true(Utils.deepEquals([engine.name for each (engine in engines)],
                                  ['tabs', 'bookmarks', 'forms', 'history']));
 
   _("Observers are notified of startup");
   do_test_pending();
 
-  let xps = Cc["@mozilla.org/weave/service;1"]
-              .getService(Ci.nsISupports)
-              .wrappedJSObject;
-
   do_check_false(Service.status.ready);
   do_check_false(xps.ready);
   Observers.add("weave:service:ready", function (subject, data) {
     do_check_true(Service.status.ready);
     do_check_true(xps.ready);
 
     // Clean up.
     Svc.Prefs.resetBranch("");
     do_test_finished();
   });
+
+  do_check_false(xps.enabled);
+
+  Service.identity.account = "johndoe";
+  Service.clusterURL = "http://localhost/";
+  do_check_true(xps.enabled);
 }
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -159,8 +159,11 @@ skip-if = debug
 [test_password_tracker.js]
 # Too many intermittent "ASSERTION: thread pool wasn't shutdown: '!mPool'" (bug 804479)
 skip-if = debug
 [test_prefs_store.js]
 [test_prefs_tracker.js]
 [test_tab_engine.js]
 [test_tab_store.js]
 [test_tab_tracker.js]
+
+[test_healthreport.js]
+skip-if = ! healthreport
--- a/testing/xpcshell/mach_commands.py
+++ b/testing/xpcshell/mach_commands.py
@@ -41,18 +41,19 @@ class TestStartFilter(logging.Filter):
 # runners.
 class InvalidTestPathError(Exception):
     """Exception raised when the test path is not valid."""
 
 
 class XPCShellRunner(MozbuildObject):
     """Run xpcshell tests."""
     def run_suite(self, **kwargs):
-        manifest = os.path.join(self.topobjdir, '_tests', 'xpcshell',
-            'xpcshell.ini')
+        from manifestparser import TestManifest
+        manifest = TestManifest(manifests=[os.path.join(self.topobjdir,
+            '_tests', 'xpcshell', 'xpcshell.ini')])
 
         return self._run_xpcshell_harness(manifest=manifest, **kwargs)
 
     def run_test(self, test_file, interactive=False,
                  keep_going=False, sequential=False, shuffle=False,
                  debugger=None, debuggerArgs=None, debuggerInteractive=None,
                  rerun_failures=False,
                  # ignore parameters from other platforms' options
--- a/toolkit/components/crashmonitor/CrashMonitor.jsm
+++ b/toolkit/components/crashmonitor/CrashMonitor.jsm
@@ -81,19 +81,23 @@ let CrashMonitorInternal = {
 
   /* Deferred for AsyncShutdown blocker */
   profileBeforeChangeDeferred: Promise.defer(),
 
   /**
    * Path to checkpoint file.
    *
    * Each time a new notification is received, this file is written to
-   * disc to reflect the information in |checkpoints|.
+   * disc to reflect the information in |checkpoints|. Although Firefox for
+   * Desktop and Metro share the same profile, they need to keep record of
+   * crashes separately.
    */
-  path: OS.Path.join(OS.Constants.Path.profileDir, "sessionCheckpoints.json"),
+  path: (Services.metro && Services.metro.immersive) ?
+    OS.Path.join(OS.Constants.Path.profileDir, "metro", "sessionCheckpoints.json"):
+    OS.Path.join(OS.Constants.Path.profileDir, "sessionCheckpoints.json"),
 
   /**
    * Load checkpoints from previous session asynchronously.
    *
    * @return {Promise} A promise that resolves/rejects once loading is complete
    */
   loadPreviousCheckpoints: function () {
     let deferred = Promise.defer();
@@ -176,16 +180,17 @@ this.CrashMonitor = {
 
     // Add shutdown blocker for profile-before-change
     AsyncShutdown.profileBeforeChange.addBlocker(
       "CrashMonitor: Writing notifications to file after receiving profile-before-change",
       CrashMonitorInternal.profileBeforeChangeDeferred.promise
     );
 
     CrashMonitorInternal.initialized = true;
+    OS.File.makeDir(OS.Path.join(OS.Constants.Path.profileDir, "metro"));
     return promise;
   },
 
   /**
    * Handle registered notifications.
    *
    * Update checkpoint file for every new notification received.
    */
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -45,16 +45,17 @@ if (SharedAll.Constants.Win) {
 let OSError = SysAll.Error;
 let Type = SysAll.Type;
 
 let Path = {};
 Cu.import("resource://gre/modules/osfile/ospath.jsm", Path);
 
 // The library of promises.
 Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
 
 // The implementation of communications
 Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
 
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
 Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
 
@@ -78,16 +79,19 @@ const EXCEPTION_CONSTRUCTORS = {
   SyntaxError: function(error) {
     return new SyntaxError(error.message, error.fileName, error.lineNumber);
   },
   TypeError: function(error) {
     return new TypeError(error.message, error.fileName, error.lineNumber);
   },
   URIError: function(error) {
     return new URIError(error.message, error.fileName, error.lineNumber);
+  },
+  OSError: function(error) {
+    return OS.File.Error.fromMsg(error);
   }
 };
 
 // It's possible for osfile.jsm to get imported before the profile is
 // set up. In this case, some path constants aren't yet available.
 // Here, we make them lazy loaders.
 
 function lazyPathGetter(constProp, dirKey) {
@@ -140,19 +144,35 @@ let Scheduler = {
 
   /**
    * |true| once shutdown has begun i.e. we should reject any
    * message, including resets.
    */
   shutdown: false,
 
   /**
-   * The latest promise returned.
+   * A promise resolved once all operations are complete.
+   *
+   * This promise is never rejected and the result is always undefined.
    */
-  latestPromise: Promise.resolve("OS.File scheduler hasn't been launched yet"),
+  queue: Promise.resolve(),
+
+  /**
+   * The latest message sent and still waiting for a reply. This
+   * field is stored only in DEBUG builds, to avoid hoarding memory in
+   * release builds.
+   */
+  latestSent: undefined,
+
+  /**
+   * The latest reply received, or null if we are waiting for a reply.
+   * This field is stored only in DEBUG builds, to avoid hoarding
+   * memory in release builds.
+   */
+  latestReceived: undefined,
 
   /**
    * A timer used to automatically shut down the worker after some time.
    */
   resetTimer: null,
 
   restartTimer: function(arg) {
     let delay;
@@ -164,16 +184,42 @@ let Scheduler = {
     }
 
     if (this.resetTimer) {
       clearTimeout(this.resetTimer);
     }
     this.resetTimer = setTimeout(File.resetWorker, delay);
   },
 
+  /**
+   * Push a task at the end of the queue.
+   *
+   * @param {function} code A function returning a Promise.
+   * This function will be executed once all the previously
+   * pushed tasks have completed.
+   * @return {Promise} A promise with the same behavior as
+   * the promise returned by |code|.
+   */
+  push: function(code) {
+    let promise = this.queue.then(code);
+    // By definition, |this.queue| can never reject.
+    this.queue = promise.then(null, () => undefined);
+    // Fork |promise| to ensure that uncaught errors are reported
+    return promise.then(null, null);
+  },
+
+  /**
+   * Post a message to the worker thread.
+   *
+   * @param {string} method The name of the method to call.
+   * @param {...} args The arguments to pass to the method. These arguments
+   * must be clonable.
+   * @return {Promise} A promise conveying the result/error caused by
+   * calling |method| with arguments |args|.
+   */
   post: function post(method, ...args) {
     if (this.shutdown) {
       LOG("OS.File is not available anymore. The following request has been rejected.",
         method, args);
       return Promise.reject(new Error("OS.File has been shut down."));
     }
     if (!worker) {
       // Either the worker has never been created or it has been reset
@@ -189,87 +235,81 @@ let Scheduler = {
     }
 
     // By convention, the last argument of any message may be an |options| object.
     let options;
     let methodArgs = args[0];
     if (methodArgs) {
       options = methodArgs[methodArgs.length - 1];
     }
-    let promise = worker.post(method,...args);
-    return this.latestPromise = promise.then(
-      function onSuccess(data) {
+    return this.push(() => Task.spawn(function*() {
+      if (OS.Constants.Sys.DEBUG) {
+        // Update possibly memory-expensive debugging information
+        Scheduler.latestReceived = null;
+        Scheduler.latestSent = [method, ...args];
+      }
+      let data;
+      let reply;
+      try {
+        data = yield worker.post(method, ...args);
+        reply = data;
+      } catch (error if error instanceof PromiseWorker.WorkerError) {
+        reply = error;
+        throw EXCEPTION_CONSTRUCTORS[error.data.exn || "OSError"](error.data);
+      } catch (error if error instanceof ErrorEvent) {
+        reply = error;
+        let message = error.message;
+        if (message == "uncaught exception: [object StopIteration]") {
+          throw StopIteration;
+        }
+        throw new Error(message, error.filename, error.lineno);
+      } finally {
+        if (OS.Constants.Sys.DEBUG) {
+          // Update possibly memory-expensive debugging information
+          Scheduler.latestSent = null;
+          Scheduler.latestReceived = reply;
+        }
         if (firstLaunch) {
           Scheduler._updateTelemetry();
         }
 
         // Don't restart the timer when reseting the worker, since that will
         // lead to an endless "resetWorker()" loop.
         if (method != "Meta_reset") {
           Scheduler.restartTimer();
         }
+      }
 
-        // Check for duration and return result.
-        if (!options) {
-          return data.ok;
-        }
-        // Check for options.outExecutionDuration.
-        if (typeof options !== "object" ||
-          !("outExecutionDuration" in options)) {
-          return data.ok;
-        }
-        // If data.durationMs is not present, return data.ok (there was an
-        // exception applying the method).
-        if (!("durationMs" in data)) {
-          return data.ok;
-        }
-        // Bug 874425 demonstrates that two successive calls to Date.now()
-        // can actually produce an interval with negative duration.
-        // We assume that this is due to an operation that is so short
-        // that Date.now() is not monotonic, so we round this up to 0.
-        let durationMs = Math.max(0, data.durationMs);
-        // Accumulate (or initialize) outExecutionDuration
-        if (typeof options.outExecutionDuration == "number") {
-          options.outExecutionDuration += durationMs;
-        } else {
-          options.outExecutionDuration = durationMs;
-        }
+      // Check for duration and return result.
+      if (!options) {
+        return data.ok;
+      }
+      // Check for options.outExecutionDuration.
+      if (typeof options !== "object" ||
+        !("outExecutionDuration" in options)) {
+        return data.ok;
+      }
+      // If data.durationMs is not present, return data.ok (there was an
+      // exception applying the method).
+      if (!("durationMs" in data)) {
         return data.ok;
-      },
-      function onError(error) {
-        if (firstLaunch) {
-          Scheduler._updateTelemetry();
-        }
-
-        // Don't restart the timer when reseting the worker, since that will
-        // lead to an endless "resetWorker()" loop.
-        if (method != "Meta_reset") {
-          Scheduler.restartTimer();
-        }
-        // Check and throw EvalError | InternalError | RangeError
-        // | ReferenceError | SyntaxError | TypeError | URIError
-        if (error.data && error.data.exn in EXCEPTION_CONSTRUCTORS) {
-          throw EXCEPTION_CONSTRUCTORS[error.data.exn](error.data);
-        }
-        // Decode any serialized error
-        if (error instanceof PromiseWorker.WorkerError) {
-          throw OS.File.Error.fromMsg(error.data);
-        }
-        // Extract something meaningful from ErrorEvent
-        if (error instanceof ErrorEvent) {
-          let message = error.message;
-          if (message == "uncaught exception: [object StopIteration]") {
-            throw StopIteration;
-          }
-          throw new Error(message, error.filename, error.lineno);
-        }
-
-        throw error;
       }
-    );
+      // Bug 874425 demonstrates that two successive calls to Date.now()
+      // can actually produce an interval with negative duration.
+      // We assume that this is due to an operation that is so short
+      // that Date.now() is not monotonic, so we round this up to 0.
+      let durationMs = Math.max(0, data.durationMs);
+      // Accumulate (or initialize) outExecutionDuration
+      if (typeof options.outExecutionDuration == "number") {
+        options.outExecutionDuration += durationMs;
+      } else {
+        options.outExecutionDuration = durationMs;
+      }
+      return data.ok;
+    }));
   },
 
   /**
    * Post Telemetry statistics.
    *
    * This is only useful on first launch.
    */
   _updateTelemetry: function() {
@@ -1253,13 +1293,23 @@ this.OS.Path = Path;
 
 
 // Auto-flush OS.File during profile-before-change. This ensures that any I/O
 // that has been queued *before* profile-before-change is properly completed.
 // To ensure that I/O queued *during* profile-before-change is completed,
 // clients should register using AsyncShutdown.addBlocker.
 AsyncShutdown.profileBeforeChange.addBlocker(
   "OS.File: flush I/O queued before profile-before-change",
-  () =>
-    // Wait until the latest currently enqueued promise is satisfied/rejected
-    Scheduler.latestPromise.then(null,
-      function onError() { /* ignore error */})
+  // Wait until the latest currently enqueued promise is satisfied/rejected
+  (() => Scheduler.queue),
+  function getDetails() {
+    let result = {
+      launched: Scheduler.launched,
+      shutdown: Scheduler.shutdown,
+      pendingReset: !!Scheduler.resetTimer,
+    };
+    if (OS.Constants.Sys.DEBUG) {
+      result.latestSent = Scheduler.latestSent;
+      result.latestReceived - Scheduler.latestReceived;
+    };
+    return result;
+  }
 );
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -18,23 +18,24 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://testing-common/httpd.js", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/TelemetryFile.jsm", this);
 Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
-Cu.import("resource://gre/modules/osfile.jsm", this);
+let {OS: {File, Path, Constants}} = Cu.import("resource://gre/modules/osfile.jsm", {});
 
 // We increment TelemetryFile's MAX_PING_FILE_AGE and
-// OVERDUE_PING_FILE_AGE by 1ms so that our test pings exceed
-// those points in time.
-const EXPIRED_PING_FILE_AGE = TelemetryFile.MAX_PING_FILE_AGE + 1;
-const OVERDUE_PING_FILE_AGE = TelemetryFile.OVERDUE_PING_FILE_AGE + 1;
+// OVERDUE_PING_FILE_AGE by 1 minute so that our test pings exceed
+// those points in time, even taking into account file system imprecision.
+const ONE_MINUTE_MS = 60 * 1000;
+const EXPIRED_PING_FILE_AGE = TelemetryFile.MAX_PING_FILE_AGE + ONE_MINUTE_MS;
+const OVERDUE_PING_FILE_AGE = TelemetryFile.OVERDUE_PING_FILE_AGE + ONE_MINUTE_MS;
 
 const PING_SAVE_FOLDER = "saved-telemetry-pings";
 const PING_TIMEOUT_LENGTH = 5000;
 const EXPIRED_PINGS = 5;
 const OVERDUE_PINGS = 6;
 const RECENT_PINGS = 4;
 
 const TOTAL_EXPECTED_PINGS = OVERDUE_PINGS + RECENT_PINGS;
@@ -47,17 +48,18 @@ let gSeenPings = 0;
  * Creates some TelemetryPings for the current session and
  * saves them to disk. Each ping gets a unique ID slug based on
  * an incrementor.
  *
  * @param aNum the number of pings to create.
  * @param aAge the age in milliseconds to offset from now. A value
  *             of 10 would make the ping 10ms older than now, for
  *             example.
- * @returns an Array with the created pings.
+ * @returns Promise
+ * @resolve an Array with the created pings.
  */
 function createSavedPings(aNum, aAge) {
   return Task.spawn(function*(){
     // Create a TelemetryPing service that we can generate payloads from.
     // Luckily, the TelemetryPing constructor does nothing that we need to
     // clean up.
     let pings = [];
     let age = Date.now() - aAge;
@@ -66,80 +68,81 @@ function createSavedPings(aNum, aAge) {
       let payload = TelemetryPing.getPayload();
       let ping = { slug: "test-ping-" + gCreatedPings, reason: "test", payload: payload };
 
       yield TelemetryFile.savePing(ping);
 
       if (aAge) {
         // savePing writes to the file synchronously, so we're good to
         // modify the lastModifedTime now.
-        let file = getSaveFileForPing(ping);
-        file.lastModifiedTime = age;
+        let file = getSavePathForPing(ping);
+        yield File.setDates(file, null, age);
       }
       gCreatedPings++;
       pings.push(ping);
     }
     return pings;
   });
 }
 
 /**
  * Deletes locally saved pings in aPings if they
  * exist.
  *
  * @param aPings an Array of pings to delete.
+ * @returns Promise
  */
 function clearPings(aPings) {
-  for (let ping of aPings) {
-    let file = getSaveFileForPing(ping);
-    if (file.exists()) {
-      file.remove(false);
+  return Task.spawn(function*() {
+    for (let ping of aPings) {
+      let path = getSavePathForPing(ping);
+      yield File.remove(path);
     }
-  }
+  });
 }
 
 /**
  * Returns a handle for the file that aPing should be
  * stored in locally.
  *
- * @returns nsILocalFile
+ * @returns path
  */
-function getSaveFileForPing(aPing) {
-  let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile).clone();
-  file.append(PING_SAVE_FOLDER);
-  file.append(aPing.slug);
-  return file;
+function getSavePathForPing(aPing) {
+  return Path.join(Constants.Path.profileDir, PING_SAVE_FOLDER, aPing.slug);
 }
 
 /**
- * Check if the number of TelemetryPings received by the 
+ * Check if the number of TelemetryPings received by the
  * HttpServer is not equal to aExpectedNum.
  *
  * @param aExpectedNum the number of pings we expect to receive.
  */
 function assertReceivedPings(aExpectedNum) {
   do_check_eq(gSeenPings, aExpectedNum);
 }
 
 /**
  * Throws if any pings in aPings is saved locally.
  *
  * @param aPings an Array of pings to check.
+ * @returns Promise
  */
 function assertNotSaved(aPings) {
-  let saved = 0;
-  for (let ping of aPings) {
-    let file = getSaveFileForPing(ping);
-    if (file.exists()) {
-      saved++;
+  return Task.spawn(function*() {
+    let saved = 0;
+    for (let ping of aPings) {
+      let file = getSavePathForPing(ping);
+      if (yield File.exists()) {
+        saved++;
+      }
     }
-  }
-  if (saved > 0) {
-    do_throw("Found " + saved + " unexpected saved pings.");
-  }
+    if (saved > 0) {
+      do_throw("Found " + saved + " unexpected saved pings.");
+    }
+  });
 }
 
 /**
  * Our handler function for the HttpServer that simply
  * increments the gSeenPings global when it successfully
  * receives and decodes a TelemetryPing payload.
  *
  * @param aRequest the HTTP request sent from HttpServer.
@@ -195,45 +198,45 @@ function run_test() {
 /**
  * Test that pings that are considered too old are just chucked out
  * immediately and never sent.
  */
 add_task(function test_expired_pings_are_deleted() {
   let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
   yield startTelemetry();
   assertReceivedPings(0);
-  assertNotSaved(expiredPings);
+  yield assertNotSaved(expiredPings);
   yield resetTelemetry();
 });
 
 /**
  * Test that really recent pings are not sent on Telemetry initialization.
  */
 add_task(function test_recent_pings_not_sent() {
   let recentPings = yield createSavedPings(RECENT_PINGS);
   yield startTelemetry();
   assertReceivedPings(0);
   yield resetTelemetry();
-  clearPings(recentPings);
+  yield clearPings(recentPings);
 });
 
 /**
  * Create some recent, expired and overdue pings. The overdue pings should
  * trigger a send of all recent and overdue pings, but the expired pings
  * should just be deleted.
  */
 add_task(function test_overdue_pings_trigger_send() {
   let recentPings = yield createSavedPings(RECENT_PINGS);
   let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
   let overduePings = yield createSavedPings(OVERDUE_PINGS, OVERDUE_PING_FILE_AGE);
 
   yield startTelemetry();
   assertReceivedPings(TOTAL_EXPECTED_PINGS);
 
-  assertNotSaved(recentPings);
-  assertNotSaved(expiredPings);
-  assertNotSaved(overduePings);
+  yield assertNotSaved(recentPings);
+  yield assertNotSaved(expiredPings);
+  yield assertNotSaved(overduePings);
   yield resetTelemetry();
 });
 
 add_task(function teardown() {
   yield stopHttpServer();
 });
--- a/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js
+++ b/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js
@@ -17,16 +17,18 @@ function setup_crash() {
     return deferred.promise;
   });
 
   Services.obs.notifyObservers(null, TOPIC, null);
   dump("Waiting for crash\n");
 }
 
 function after_crash(mdump, extra) {
+  do_print("after crash: " + extra.AsyncShutdownTimeout);
   let info = JSON.parse(extra.AsyncShutdownTimeout);
-  do_check_true(info.phase == "testing-async-shutdown-crash");
-  do_check_true(info.conditions.indexOf("A blocker that is never satisfied") != -1);
+  do_check_eq(info.phase, "testing-async-shutdown-crash");
+  do_print("Condition: " + JSON.stringify(info.conditions));
+  do_check_true(JSON.stringify(info.conditions).indexOf("A blocker that is never satisfied") != -1);
 }
 
 function run_test() {
   do_crash(setup_crash, after_crash);
 }
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -352,16 +352,20 @@ var PageStyleActor = protocol.ActorClass
   },
 
   /**
    * Helper function for getApplied, adds all the rules from a given
    * element.
    */
   addElementRules: function(element, inherited, options, rules)
   {
+    if (!element.style) {
+      return;
+    }
+
     let elementStyle = this._styleRef(element);
 
     if (!inherited || this._hasInheritedProps(element.style)) {
       rules.push({
         rule: elementStyle,
         inherited: inherited,
       });
     }
--- a/toolkit/devtools/styleinspector/css-logic.js
+++ b/toolkit/devtools/styleinspector/css-logic.js
@@ -612,17 +612,17 @@ CssLogic.prototype = {
 
         rule._matchId = this._matchId;
         rule._passId = this._passId;
         this._matchedRules.push([rule, status]);
       }
 
 
       // Add element.style information.
-      if (element.style.length > 0) {
+      if (element.style && element.style.length > 0) {
         let rule = new CssRule(null, { style: element.style }, element);
         rule._matchId = this._matchId;
         rule._passId = this._passId;
         this._matchedRules.push([rule, status]);
       }
     } while ((element = element.parentNode) &&
               element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE);
   },
--- a/toolkit/modules/AsyncShutdown.jsm
+++ b/toolkit/modules/AsyncShutdown.jsm
@@ -103,16 +103,44 @@ function log(msg, prefix = "", error = n
 }
 function warn(msg, error = null) {
   return log(msg, "WARNING: ", error);
 }
 function err(msg, error = null) {
   return log(msg, "ERROR: ", error);
 }
 
+// Utility function designed to get the current state of execution
+// of a blocker.
+// We are a little paranoid here to ensure that in case of evaluation
+// error we do not block the AsyncShutdown.
+function safeGetState(state) {
+  if (!state) {
+    return "(none)";
+  }
+  try {
+    // Evaluate state(), normalize the result into something that we can
+    // safely stringify or upload.
+    let string = JSON.stringify(state());
+    let data = JSON.parse(string);
+    // Simplify the rest of the code by ensuring that we can simply
+    // concatenate the result to a message.
+    data.toString = function() {
+      return string;
+    };
+    return data;
+  } catch (ex) {
+    try {
+      return "Error getting state: " + ex;
+    } catch (ex2) {
+      return "Could not display error";
+    }
+  }
+}
+
 /**
  * Countdown for a given duration, skipping beats if the computer is too busy,
  * sleeping or otherwise unavailable.
  *
  * @param {number} delay An approximate delay to wait in milliseconds (rounded
  * up to the closest second).
  *
  * @return Deferred
@@ -181,16 +209,20 @@ function getPhase(topic) {
      * for instance "OS.File: flushing all pending I/O";
      * @param {function|promise|*} condition A condition blocking the
      * completion of the phase. Generally, this is a function
      * returning a promise. This function is evaluated during the
      * phase and the phase is guaranteed to not terminate until the
      * resulting promise is either resolved or rejected. If
      * |condition| is not a function but another value |v|, it behaves
      * as if it were a function returning |v|.
+     * @param {function*} state Optionally, a function returning
+     * information about the current state of the blocker as an
+     * object. Used for providing more details when logging errors or
+     * crashing.
      *
      * Examples:
      * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise",
      *      promise); // profileBeforeChange will not complete until
      *                // promise is resolved or rejected
      *
      * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback",
      *     function callback() {
@@ -204,21 +236,24 @@ function getPhase(topic) {
      * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback",
      *     function callback() {
      *       // ...
      *       // Execute this code during profileBeforeChange
      *       // No specific guarantee about completion of profileBeforeChange
      * });
      *
      */
-    addBlocker: function(name, condition) {
+    addBlocker: function(name, condition, state = null) {
       if (typeof name != "string") {
         throw new TypeError("Expected a human-readable name as first argument");
       }
-      spinner.addBlocker({name: name, condition: condition});
+      if (state && typeof state != "function") {
+        throw new TypeError("Expected nothing or a function as third argument");
+      }
+      spinner.addBlocker({name: name, condition: condition, state: state});
     }
   });
   gPhases.set(topic, phase);
   return phase;
 }
 
 /**
  * Utility class used to spin the event loop until all blockers for a
@@ -269,17 +304,17 @@ Spinner.prototype = {
 
     // The promises for which we are waiting.
     let allPromises = [];
 
     // Information to determine and report to the user which conditions
     // are not satisfied yet.
     let allMonitors = [];
 
-    for (let {condition, name} of conditions) {
+    for (let {condition, name, state} of conditions) {
       // Gather all completion conditions
 
       try {
         if (typeof condition == "function") {
           // Normalize |condition| to the result of the function.
           try {
             condition = condition(topic);
           } catch (ex) {
@@ -298,45 +333,49 @@ Spinner.prototype = {
         //
         // If it takes way too long, we need to crash.
 
         let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
         timer.initWithCallback(function() {
           let msg = "A phase completion condition is" +
             " taking too long to complete." +
             " Condition: " + monitor.name +
-            " Phase: " + topic;
+            " Phase: " + topic +
+            " State: " + safeGetState(state);
           warn(msg);
         }, DELAY_WARNING_MS, Ci.nsITimer.TYPE_ONE_SHOT);
 
         let monitor = {
           isFrozen: true,
-          name: name
+          name: name,
+          state: state
         };
         condition = condition.then(function onSuccess() {
             timer.cancel(); // As a side-effect, this prevents |timer| from
                             // being garbage-collected too early.
             monitor.isFrozen = false;
           }, function onError(error) {
             timer.cancel();
             let msg = "A completion condition encountered an error" +
                 " while we were spinning the event loop." +
                 " Condition: " + name +
-                " Phase: " + topic;
+                " Phase: " + topic +
+                " State: " + safeGetState(state);
             warn(msg, error);
             monitor.isFrozen = false;
         });
         allMonitors.push(monitor);
         allPromises.push(condition);
 
       } catch (error) {
           let msg = "A completion condition encountered an error" +
                 " while we were initializing the phase." +
                 " Condition: " + name +
-                " Phase: " + topic;
+                " Phase: " + topic +
+                " State: " + safeGetState(state);
           warn(msg, error);
       }
 
     }
     conditions = null;
 
     let promise = Promise.all(allPromises);
     allPromises = null;
@@ -357,27 +396,28 @@ Spinner.prototype = {
     // presumably deadlocked. At this stage, the only thing we can do to
     // avoid leaving the user's computer in an unstable (and battery-sucking)
     // situation is report the issue and crash.
     let timeToCrash = looseTimer(DELAY_CRASH_MS);
     timeToCrash.promise.then(
       function onTimeout() {
         // Report the problem as best as we can, then crash.
         let frozen = [];
-        for (let {name, isFrozen} of allMonitors) {
+        let states = [];
+        for (let {name, isFrozen, state} of allMonitors) {
           if (isFrozen) {
-            frozen.push(name);
+            frozen.push({name: name, state: safeGetState(state)});
           }
         }
 
         let msg = "At least one completion condition failed to complete" +
               " within a reasonable amount of time. Causing a crash to" +
               " ensure that we do not leave the user with an unresponsive" +
               " process draining resources." +
-              " Conditions: " + frozen.join(", ") +
+              " Conditions: " + JSON.stringify(frozen) +
               " Phase: " + topic;
         err(msg);
         if (gCrashReporter && gCrashReporter.enabled) {
           let data = {
             phase: topic,
             conditions: frozen
           };
           gCrashReporter.annotateCrashReport("AsyncShutdownTimeout",
--- a/toolkit/modules/tests/xpcshell/test_AsyncShutdown.js
+++ b/toolkit/modules/tests/xpcshell/test_AsyncShutdown.js
@@ -49,33 +49,47 @@ add_task(function test_no_condition() {
   do_print("Phase with no condition didn't lock");
 });
 
 add_task(function test_simple_async() {
   do_print("Testing various combinations of a phase with a single condition");
   for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
     for (let resolution of [arg, Promise.reject(arg)]) {
       for (let success of [false, true]) {
-        // Asynchronous phase
-        do_print("Asynchronous test with " + arg + ", " + resolution);
-        let topic = getUniqueTopic();
-        let outParam = { isFinished: false };
-        AsyncShutdown._getPhase(topic).addBlocker(
-          "Async test",
-            function() {
-              if (success) {
-                return longRunningAsyncTask(resolution, outParam);
-              } else {
-                throw resolution;
-              }
-            }
-        );
-        do_check_false(outParam.isFinished);
-        Services.obs.notifyObservers(null, topic, null);
-        do_check_eq(outParam.isFinished, success);
+        for (let state of [[null],
+                           [],
+                           [() => "some state"],
+                           [function() {
+                             throw new Error("State BOOM"); }],
+                           [function() {
+                             return {
+                               toJSON: function() {
+                                 throw new Error("State.toJSON BOOM");
+                               }
+                             };
+                           }]]) {
+          // Asynchronous phase
+          do_print("Asynchronous test with " + arg + ", " + resolution);
+          let topic = getUniqueTopic();
+          let outParam = { isFinished: false };
+          AsyncShutdown._getPhase(topic).addBlocker(
+            "Async test",
+              function() {
+                if (success) {
+                  return longRunningAsyncTask(resolution, outParam);
+                } else {
+                  throw resolution;
+                }
+              },
+              ...state
+          );
+          do_check_false(outParam.isFinished);
+          Services.obs.notifyObservers(null, topic, null);
+          do_check_eq(outParam.isFinished, success);
+        }
       }
 
       // Synchronous phase - just test that we don't throw/freeze
       do_print("Synchronous test with " + arg + ", " + resolution);
       let topic = getUniqueTopic();
       AsyncShutdown._getPhase(topic).addBlocker(
         "Sync test",
         resolution
@@ -123,16 +137,19 @@ add_task(function test_various_failures(
 
   do_print("Ensure that an incomplete blocker causes a TypeError");
 
   exn = get_exn(() => phase.addBlocker());
   do_check_eq(exn.name, "TypeError");
 
   exn = get_exn(() => phase.addBlocker(null, true));
   do_check_eq(exn.name, "TypeError");
+
+  exn = get_exn(() => phase.addBlocker("Test 2", () => true, "not a function"));
+  do_check_eq(exn.name, "TypeError");
 });
 
 add_task(function() {
   Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
 });
 
 function run_test() {
   run_next_test();
--- a/toolkit/mozapps/extensions/AddonRepository.jsm
+++ b/toolkit/mozapps/extensions/AddonRepository.jsm
@@ -91,16 +91,19 @@ const HTML_KEY_MAP = {
 // A map between XML keys to AddonSearchResult keys for integer values
 // that require no extra parsing from XML
 const INTEGER_KEY_MAP = {
   total_downloads:  "totalDownloads",
   weekly_downloads: "weeklyDownloads",
   daily_users:      "dailyUsers"
 };
 
+// Wrap the XHR factory so that tests can override with a mock
+let XHRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
+                                       "nsIXMLHttpRequest");
 
 function convertHTMLToPlainText(html) {
   if (!html)
     return html;
   var converter = Cc["@mozilla.org/widget/htmlformatconverter;1"].
                   createInstance(Ci.nsIFormatConverter);
 
   var input = Cc["@mozilla.org/supports-string;1"].
@@ -597,22 +600,25 @@ this.AddonRepository = {
    * Asynchronously repopulate cache so it only contains the add-ons
    * corresponding to the specified ids. If caching is disabled,
    * the cache is completely removed.
    *
    * @param  aIds
    *         The array of add-on ids to repopulate the cache with
    * @param  aCallback
    *         The optional callback to call once complete
+   * @param  aTimeout
+   *         (Optional) timeout in milliseconds to abandon the XHR request
+   *         if we have not received a response from the server.
    */
-  repopulateCache: function AddonRepo_repopulateCache(aIds, aCallback) {
-    this._repopulateCacheInternal(aIds, aCallback, false);
+  repopulateCache: function(aIds, aCallback, aTimeout) {
+    this._repopulateCacheInternal(aIds, aCallback, false, aTimeout);
   },
 
-  _repopulateCacheInternal: function AddonRepo_repopulateCacheInternal(aIds, aCallback, aSendPerformance) {
+  _repopulateCacheInternal: function(aIds, aCallback, aSendPerformance, aTimeout) {
     // Completely remove cache if caching is not enabled
     if (!this.cacheEnabled) {
       this._addons = null;
       this._pendingCallbacks = null;
       AddonDatabase.delete(aCallback);
       return;
     }
 
@@ -632,17 +638,17 @@ this.AddonRepository = {
           aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
           AddonDatabase.repopulate(aAddons, aCallback);
         },
         searchFailed: function repopulateCacheInternal_searchFailed() {
           WARN("Search failed when repopulating cache");
           if (aCallback)
             aCallback();
         }
-      }, aSendPerformance);
+      }, aSendPerformance, aTimeout);
     });
   },
 
   /**
    * Asynchronously add add-ons to the cache corresponding to the specified
    * ids. If caching is disabled, the cache is unchanged and the callback is
    * immediatly called if it is defined.
    *
@@ -753,18 +759,21 @@ this.AddonRepository = {
    *
    * @param  aIDs
    *         Array of ids to search for.
    * @param  aCallback
    *         Function to pass results to.
    * @param  aSendPerformance
    *         Boolean indicating whether to send performance data with the
    *         request.
+   * @param  aTimeout
+   *         (Optional) timeout in milliseconds to abandon the XHR request
+   *         if we have not received a response from the server.
    */
-  _beginGetAddons: function AddonRepo_beginGetAddons(aIDs, aCallback, aSendPerformance) {
+  _beginGetAddons: function(aIDs, aCallback, aSendPerformance, aTimeout) {
     let ids = aIDs.slice(0);
 
     let params = {
       API_VERSION : API_VERSION,
       IDS : ids.map(encodeURIComponent).join(',')
     };
 
     let pref = PREF_GETADDONS_BYIDS;
@@ -836,17 +845,17 @@ this.AddonRepository = {
         };
         results.push(result);
       }
 
       // aTotalResults irrelevant
       self._reportSuccess(results, -1);
     }
 
-    this._beginSearch(url, ids.length, aCallback, handleResults);
+    this._beginSearch(url, ids.length, aCallback, handleResults, aTimeout);
   },
 
   /**
    * Performs the daily background update check.
    *
    * This API both searches for the add-on IDs specified and sends performance
    * data. It is meant to be called as part of the daily update ping. It should
    * not be used for any other purpose. Use repopulateCache instead.
@@ -1383,58 +1392,58 @@ this.AddonRepository = {
   // Parses addon_compatibility elements.
   _parseAddonCompatData: function AddonRepo_parseAddonCompatData(aElements) {
     let compatData = {};
     Array.forEach(aElements, this._parseAddonCompatElement.bind(this, compatData));
     return compatData;
   },
 
   // Begins a new search if one isn't currently executing
-  _beginSearch: function AddonRepo_beginSearch(aURI, aMaxResults, aCallback, aHandleResults) {
+  _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) {
     if (this._searching || aURI == null || aMaxResults <= 0) {
       aCallback.searchFailed();
       return;
     }
 
     this._searching = true;
     this._callback = aCallback;
     this._maxResults = aMaxResults;
 
     LOG("Requesting " + aURI);
 
-    this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
-                    createInstance(Ci.nsIXMLHttpRequest);
+    this._request = new XHRequest();
     this._request.mozBackgroundRequest = true;
     this._request.open("GET", aURI, true);
     this._request.overrideMimeType("text/xml");
+    if (aTimeout) {
+      this._request.timeout = aTimeout;
+    }
 
-    let self = this;
-    this._request.addEventListener("error", function beginSearch_errorListener(aEvent) {
-      self._reportFailure();
-    }, false);
-    this._request.addEventListener("load", function beginSearch_loadListener(aEvent) {
+    this._request.addEventListener("error", aEvent => this._reportFailure(), false);
+    this._request.addEventListener("timeout", aEvent => this._reportFailure(), false);
+    this._request.addEventListener("load", aEvent => {
       let request = aEvent.target;
       let responseXML = request.responseXML;
 
       if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
           (request.status != 200 && request.status != 0)) {
-        self._reportFailure();
+        this._reportFailure();
         return;
       }
 
       let documentElement = responseXML.documentElement;
       let elements = documentElement.getElementsByTagName("addon");
       let totalResults = elements.length;
       let parsedTotalResults = parseInt(documentElement.getAttribute("total_results"));
       // Parsed value of total results only makes sense if >= elements.length
       if (parsedTotalResults >= totalResults)
         totalResults = parsedTotalResults;
 
       let compatElements = documentElement.getElementsByTagName("addon_compatibility");
-      let compatData = self._parseAddonCompatData(compatElements);
+      let compatData = this._parseAddonCompatData(compatElements);
 
       aHandleResults(elements, totalResults, compatData);
     }, false);
     this._request.send(null);
   },
 
   // Gets the id's of local add-ons, and the sourceURI's of local installs,
   // passing the results to aCallback
--- a/toolkit/mozapps/extensions/AddonUpdateChecker.jsm
+++ b/toolkit/mozapps/extensions/AddonUpdateChecker.jsm
@@ -389,20 +389,16 @@ function parseRDFManifest(aId, aUpdateKe
  *         An observer to pass results to
  */
 function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
   this.id = aId;
   this.updateKey = aUpdateKey;
   this.observer = aObserver;
   this.url = aUrl;
 
-  this.timer = Cc["@mozilla.org/timer;1"].
-               createInstance(Ci.nsITimer);
-  this.timer.initWithCallback(this, TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
-
   let requireBuiltIn = true;
   try {
     requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
   }
   catch (e) {
   }
 
   LOG("Requesting " + aUrl);
@@ -410,40 +406,39 @@ function UpdateParser(aId, aUpdateKey, a
     this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
                    createInstance(Ci.nsIXMLHttpRequest);
     this.request.open("GET", this.url, true);
     this.request.channel.notificationCallbacks = new CertUtils.BadCertHandler(!requireBuiltIn);
     this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
     // Prevent the request from writing to cache.
     this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
     this.request.overrideMimeType("text/xml");
+    this.request.timeout = TIMEOUT;
     var self = this;
     this.request.addEventListener("load", function loadEventListener(event) { self.onLoad() }, false);
     this.request.addEventListener("error", function errorEventListener(event) { self.onError() }, false);
+    this.request.addEventListener("timeout", function timeoutEventListener(event) { self.onTimeout() }, false);
     this.request.send(null);
   }
   catch (e) {
     ERROR("Failed to request update manifest", e);
   }
 }
 
 UpdateParser.prototype = {
   id: null,
   updateKey: null,
   observer: null,
   request: null,
-  timer: null,
   url: null,
 
   /**
    * Called when the manifest has been successfully loaded.
    */
   onLoad: function UP_onLoad() {
-    this.timer.cancel();
-    this.timer = null;
     let request = this.request;
     this.request = null;
 
     let requireBuiltIn = true;
     try {
       requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
     }
     catch (e) {
@@ -501,22 +496,28 @@ UpdateParser.prototype = {
       return;
     }
 
     WARN("Update manifest had an unrecognised namespace: " + xml.documentElement.namespaceURI);
     this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT);
   },
 
   /**
+   * Called when the request times out
+   */
+  onTimeout: function() {
+    this.request = null;
+    WARN("Request for " + this.url + " timed out");
+    this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
+  },
+
+  /**
    * Called when the manifest failed to load.
    */
   onError: function UP_onError() {
-    this.timer.cancel();
-    this.timer = null;
-
     if (!Components.isSuccessCode(this.request.status)) {
       WARN("Request failed: " + this.url + " - " + this.request.status);
     }
     else if (this.request.channel instanceof Ci.nsIHttpChannel) {
       try {
         if (this.request.channel.requestSucceeded) {
           WARN("Request failed: " + this.url + " - " +
                this.request.channel.responseStatus + ": " +
@@ -546,34 +547,19 @@ UpdateParser.prototype = {
       }
       catch (e) {
         WARN("onUpdateCheckError notification failed", e);
       }
     }
   },
 
   /**
-   * Called when the request has timed out and should be canceled.
-   */
-  notify: function UP_notify(aTimer) {
-    this.timer = null;
-    this.request.abort();
-    this.request = null;
-
-    WARN("Request timed out");
-
-    this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
-  },
-
-  /**
    * Called to cancel an in-progress update check.
    */
   cancel: function UP_cancel() {
-    this.timer.cancel();
-    this.timer = null;
     this.request.abort();
     this.request = null;
     this.notifyError(AddonUpdateChecker.ERROR_CANCELLED);
   }
 };
 
 /**
  * Tests if an update matches a version of the application or platform
--- a/toolkit/mozapps/extensions/content/update.js
+++ b/toolkit/mozapps/extensions/content/update.js
@@ -7,16 +7,19 @@
 // This UI is only opened from the Extension Manager when the app is upgraded.
 
 "use strict";
 
 const PREF_UPDATE_EXTENSIONS_ENABLED            = "extensions.update.enabled";
 const PREF_XPINSTALL_ENABLED                    = "xpinstall.enabled";
 const PREF_EM_HOTFIX_ID                         = "extensions.hotfix.id";
 
+// timeout (in milliseconds) to wait for response to the metadata ping
+const METADATA_TIMEOUT    = 30000;
+
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/AddonManager.jsm");
 Components.utils.import("resource://gre/modules/AddonRepository.jsm");
 
 
 var gInteruptable = true;
 var gPendingClose = false;
 
@@ -170,17 +173,17 @@ var gVersionInfoPage = {
           if (gPendingClose) {
             window.close();
             return;
           }
 
           for (let addon of gUpdateWizard.addons)
             addon.findUpdates(gVersionInfoPage, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
         });
-      });
+      }, METADATA_TIMEOUT);
     });
   },
 
   onAllUpdatesFinished: function gVersionInfoPage_onAllUpdatesFinished() {
     // Filter out any add-ons that were disabled before the application was
     // upgraded or are already compatible
     gUpdateWizard.addons = gUpdateWizard.addons.filter(function onAllUpdatesFinished_filterAddons(a) {
       return a.appDisabled && gUpdateWizard.inactiveAddonIDs.indexOf(a.id) < 0;
--- a/toolkit/mozapps/extensions/test/browser/Makefile.in
+++ b/toolkit/mozapps/extensions/test/browser/Makefile.in
@@ -33,16 +33,17 @@ MOCHITEST_BROWSER_MAIN_FILES = \
   browser_bug618502.js \
   browser_bug679604.js \
   browser_bug714593.js \
   browser_bug590347.js \
   browser_details.js \
   browser_discovery.js \
   browser_dragdrop.js \
   browser_list.js \
+  browser_metadataTimeout.js \
   browser_searching.js \
   browser_sorting.js \
   browser_uninstalling.js \
   browser_install.js \
   browser_recentupdates.js \
   browser_manualupdates.js \
   browser_globalwarnings.js \
   browser_globalinformations.js \
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_metadataTimeout.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test how update window behaves when metadata ping times out
+// bug 965788
+
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+
+const PREF_GETADDONS_BYIDS            = "extensions.getAddons.get.url";
+const PREF_MIN_PLATFORM_COMPAT        = "extensions.minCompatiblePlatformVersion";
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+let repo = {};
+let ARContext = Components.utils.import("resource://gre/modules/AddonRepository.jsm", repo);
+info("ARContext: " + Object.keys(ARContext).join(", "));
+
+// Mock out the XMLHttpRequest factory for AddonRepository so
+// we can reply with a timeout
+let pXHRStarted = Promise.defer();
+let oldXHRConstructor = ARContext.XHRequest;
+ARContext.XHRequest = function() {
+  this._handlers = new Map();
+  this.mozBackgroundRequest = false;
+  this.timeout = undefined;
+  this.open = function(aMethod, aURI, aAsync) {
+      this.method = aMethod;
+      this.uri = aURI;
+      this.async = aAsync;
+      info("Opened XHR for " + aMethod + " " + aURI);
+    };
+  this.overrideMimeType = function(aMimeType) {
+      this.mimeType = aMimeType;
+    };
+  this.addEventListener = function(aEvent, aHandler, aCapture) {
+      this._handlers.set(aEvent, aHandler);
+    };
+  this.send = function(aBody) {
+      info("Send XHR for " + this.method + " " + this.uri + " handlers: " + [this._handlers.keys()].join(", "));
+      pXHRStarted.resolve(this);
+    }
+};
+
+
+// Returns promise{window}, resolves with a handle to the compatibility
+// check window
+function promise_open_compatibility_window(aInactiveAddonIds) {
+  let deferred = Promise.defer();
+  // This will reset the longer timeout multiplier to 2 which will give each
+  // test that calls open_compatibility_window a minimum of 60 seconds to
+  // complete.
+  requestLongerTimeout(100 /* XXX was 2 */);
+
+  var variant = Cc["@mozilla.org/variant;1"].
+                createInstance(Ci.nsIWritableVariant);
+  variant.setFromVariant(aInactiveAddonIds);
+
+  // Cannot be modal as we want to interract with it, shouldn't cause problems
+  // with testing though.
+  var features = "chrome,centerscreen,dialog,titlebar";
+  var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+           getService(Ci.nsIWindowWatcher);
+  var win = ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
+
+  win.addEventListener("load", function() {
+    function page_shown(aEvent) {
+      if (aEvent.target.pageid)
+        info("Page " + aEvent.target.pageid + " shown");
+    }
+
+    win.removeEventListener("load", arguments.callee, false);
+
+    info("Compatibility dialog opened");
+
+    win.addEventListener("pageshow", page_shown, false);
+    win.addEventListener("unload", function() {
+      win.removeEventListener("unload", arguments.callee, false);
+      win.removeEventListener("pageshow", page_shown, false);
+      dump("Compatibility dialog closed\n");
+    }, false);
+
+    deferred.resolve(win);
+  }, false);
+  return deferred.promise;
+}
+
+function promise_window_close(aWindow) {
+  let deferred = Promise.defer();
+  aWindow.addEventListener("unload", function() {
+    aWindow.removeEventListener("unload", arguments.callee, false);
+    deferred.resolve(aWindow);
+  }, false);
+  return deferred.promise;
+}
+
+// Start the compatibility update dialog, but use the mock XHR to respond with
+// a timeout
+add_task(function* amo_ping_timeout() {
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+  let compatWindow = yield promise_open_compatibility_window([]);
+
+  let xhr = yield pXHRStarted.promise;
+  is(xhr.timeout, 30000, "XHR request should have 30 second timeout");
+  ok(xhr._handlers.has("timeout"), "Timeout handler set on XHR");
+  // call back the timeout handler
+  xhr._handlers.get("timeout")();
+
+  // Put the old XHR constructor back
+  ARContext.XHRequest = oldXHRConstructor;
+  // The window should close without further interaction
+  yield promise_window_close(compatWindow);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -18,16 +18,21 @@ const PREF_GETADDONS_BYIDS_PERFORMANCE  
 // Forcibly end the test if it runs longer than 15 minutes
 const TIMEOUT_MS = 900000;
 
 Components.utils.import("resource://gre/modules/AddonRepository.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+Services.prefs.setBoolPref("toolkit.osfile.log", true);
 
 // We need some internal bits of AddonManager
 let AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm");
 let AddonManager = AMscope.AddonManager;
 let AddonManagerInternal = AMscope.AddonManagerInternal;
 // Mock out AddonManager's reference to the AsyncShutdown module so we can shut
 // down AddonManager from the test
 let MockAsyncShutdown = {
@@ -439,16 +444,17 @@ function shutdownManager() {
   gAppInfo.annotations = {};
 
   // Force the XPIProvider provider to reload to better
   // simulate real-world usage.
   let XPIscope = Components.utils.import("resource://gre/modules/XPIProvider.jsm");
   // This would be cleaner if I could get it as the rejection reason from
   // the AddonManagerInternal.shutdown() promise
   gXPISaveError = XPIscope.XPIProvider._shutdownError;
+  do_print("gXPISaveError set to: " + gXPISaveError);
   AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
   Components.utils.unload("resource://gre/modules/XPIProvider.jsm");
 }
 
 function loadAddonsList() {
   function readDirectories(aSection) {
     var dirs = [];
     var keys = parser.getKeys(aSection);
@@ -686,27 +692,48 @@ function writeInstallRDFForExtension(aDa
 
 /**
  * Sets the last modified time of the extension, usually to trigger an update
  * of its metadata. If the extension is unpacked, this function assumes that
  * the extension contains only the install.rdf file.
  *
  * @param aExt   a file pointing to either the packed extension or its unpacked directory.
  * @param aTime  the time to which we set the lastModifiedTime of the extension
+ *
+ * @deprecated Please use promiseSetExtensionModifiedTime instead
  */
 function setExtensionModifiedTime(aExt, aTime) {
   aExt.lastModifiedTime = aTime;
   if (aExt.isDirectory()) {
     let entries = aExt.directoryEntries
                       .QueryInterface(AM_Ci.nsIDirectoryEnumerator);
     while (entries.hasMoreElements())
       setExtensionModifiedTime(entries.nextFile, aTime);
     entries.close();
   }
 }
+function promiseSetExtensionModifiedTime(aPath, aTime) {
+  return Task.spawn(function* () {
+    yield OS.File.setDates(aPath, aTime, aTime);
+    let entries, iterator;
+    try {
+      let iterator = new OS.File.DirectoryIterator(aPath);
+      entries = yield iterator.nextBatch();
+    } catch (ex if ex instanceof OS.File.Error) {
+      return;
+    } finally {
+      if (iterator) {
+        iterator.close();
+      }
+    }
+    for (let entry of entries) {
+      yield promiseSetExtensionModifiedTime(entry.path, aTime);
+    }
+  });
+}
 
 /**
  * Manually installs an XPI file into an install location by either copying the
  * XPI there or extracting it depending on whether unpacking is being tested
  * or not.
  *
  * @param aXPIFile
  *        The XPI file to install.
@@ -1077,32 +1104,43 @@ function completeAllInstalls(aInstalls, 
  *          The callback to call when all installs have finished
  * @param   aIgnoreIncompatible
  *          Optional parameter to ignore add-ons that are incompatible in
  *          aome way with the application
  */
 function installAllFiles(aFiles, aCallback, aIgnoreIncompatible) {
   let count = aFiles.length;
   let installs = [];
-
+  function callback() {
+    if (aCallback) {
+      aCallback();
+    }
+  }
   aFiles.forEach(function(aFile) {
     AddonManager.getInstallForFile(aFile, function(aInstall) {
       if (!aInstall)
         do_throw("No AddonInstall created for " + aFile.path);
       do_check_eq(aInstall.state, AddonManager.STATE_DOWNLOADED);
 
       if (!aIgnoreIncompatible || !aInstall.addon.appDisabled)
         installs.push(aInstall);
 
       if (--count == 0)
-        completeAllInstalls(installs, aCallback);
+        completeAllInstalls(installs, callback);
     });
   });
 }
 
+function promiseInstallAllFiles(aFiles, aIgnoreIncompatible) {
+  let deferred = Promise.defer();
+  installAllFiles(aFiles, deferred.resolve, aIgnoreIncompatible);
+  return deferred.promise;
+
+}
+
 if ("nsIWindowsRegKey" in AM_Ci) {
   var MockRegistry = {
     LOCAL_MACHINE: {},
     CURRENT_USER: {},
     CLASSES_ROOT: {},
 
     getRoot: function(aRoot) {
       switch (aRoot) {
@@ -1468,8 +1506,22 @@ function saveJSON(aData, aFile) {
  */
 function callback_soon(aFunction) {
   return function(...args) {
     do_execute_soon(function() {
       aFunction.apply(null, args);
     }, aFunction.name ? "delayed callback " + aFunction.name : "delayed callback");
   }
 }
+
+/**
+ * A promise-based variant of AddonManager.getAddonsByIDs.
+ *
+ * @param {array} list As the first argument of AddonManager.getAddonsByIDs
+ * @return {promise}
+ * @resolve {array} The list of add-ons sent by AddonManaget.getAddonsByIDs to
+ * its callback.
+ */
+function promiseAddonsByIDs(list) {
+  let deferred = Promise.defer();
+  AddonManager.getAddonsByIDs(list, deferred.resolve);
+  return deferred.promise;
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
@@ -410,17 +410,30 @@ function run_test() {
       // Create a non-active AddonInstall so can check that it is returned in the results
       AddonManager.getInstallForURL(BASE_URL + INSTALL_URL3,
                                     run_test_1, "application/x-xpinstall");
     }, "application/x-xpinstall");
   });
 }
 
 function end_test() {
-  gServer.stop(do_test_finished);
+  let testDir = gProfD.clone();
+  testDir.append("extensions");
+  testDir.append("staged");
+  gServer.stop(function() {
+    function loop() {
+      if (!testDir.exists()) {
+        do_print("Staged directory has been cleaned up");
+        do_test_finished();
+      }
+      do_print("Waiting 1 second until cleanup is complete");
+      do_timeout(1000, loop);
+    }
+    loop();
+  });
 }
 
 // Tests homepageURL, getRecommendedURL() and getSearchURL()
 function run_test_1() {
   function check_urls(aPreference, aGetURL, aTests) {
     aTests.forEach(function(aTest) {
       Services.prefs.setCharPref(aPreference, aTest.preferenceValue);
       do_check_eq(aGetURL(aTest), aTest.expectedURL);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_locked.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked.js
@@ -129,18 +129,17 @@ var theme2 = {
     minVersion: "2",
     maxVersion: "2"
   }]
 };
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
-function run_test() {
-  do_test_pending();
+add_task(function* init() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
 
   writeInstallRDFForExtension(addon1, profileDir);
   writeInstallRDFForExtension(addon2, profileDir);
   writeInstallRDFForExtension(addon3, profileDir);
   writeInstallRDFForExtension(addon4, profileDir);
   writeInstallRDFForExtension(addon5, profileDir);
   writeInstallRDFForExtension(addon6, profileDir);
@@ -149,383 +148,382 @@ function run_test() {
   writeInstallRDFForExtension(theme2, profileDir);
 
   // Startup the profile and setup the initial state
   startupManager();
 
   // New profile so new add-ons are ignored
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
-  AddonManager.getAddonsByIDs(["addon2@tests.mozilla.org",
+  let [a2, a3, a4, a7, t2] =
+    yield promiseAddonsByIDs(["addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org",
                                "addon7@tests.mozilla.org",
-                               "theme2@tests.mozilla.org"], function([a2, a3, a4,
-                                                                      a7, t2]) {
-    // Set up the initial state
-    a2.userDisabled = true;
-    a4.userDisabled = true;
-    a7.userDisabled = true;
-    t2.userDisabled = false;
-    a3.findUpdates({
-      onUpdateFinished: function() {
-        a4.findUpdates({
-          onUpdateFinished: function() {
-            // Let the updates finish before restarting the manager
-            do_execute_soon(run_test_1);
-          }
-        }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
-      }
-    }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
-  });
-}
+                               "theme2@tests.mozilla.org"]);
+  let deferredUpdateFinished = Promise.defer();
+  // Set up the initial state
+  a2.userDisabled = true;
+  a4.userDisabled = true;
+  a7.userDisabled = true;
+  t2.userDisabled = false;
+  a3.findUpdates({
+    onUpdateFinished: function() {
+      a4.findUpdates({
+        onUpdateFinished: function() {
+          // Let the updates finish before restarting the manager
+          deferredUpdateFinished.resolve();
+        }
+      }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+    }
+  }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+
+  yield deferredUpdateFinished.promise;
+});
+
+
+add_task(function* run_test_1() {
+  restartManager();
+  let [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+    yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                              "addon2@tests.mozilla.org",
+                              "addon3@tests.mozilla.org",
+                              "addon4@tests.mozilla.org",
+                              "addon5@tests.mozilla.org",
+                              "addon6@tests.mozilla.org",
+                              "addon7@tests.mozilla.org",
+                              "theme1@tests.mozilla.org",
+                              "theme2@tests.mozilla.org"]);
+
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_false(a1.appDisabled);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+  do_check_neq(a2, null);
+  do_check_false(a2.isActive);
+  do_check_true(a2.userDisabled);
+  do_check_false(a2.appDisabled);
+  do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+  do_check_neq(a3, null);
+  do_check_true(a3.isActive);
+  do_check_false(a3.userDisabled);
+  do_check_false(a3.appDisabled);
+  do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+  do_check_neq(a4, null);
+  do_check_false(a4.isActive);
+  do_check_true(a4.userDisabled);
+  do_check_false(a4.appDisabled);
+  do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+  do_check_neq(a5, null);
+  do_check_true(a5.isActive);
+  do_check_false(a5.userDisabled);
+  do_check_false(a5.appDisabled);
+  do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+  do_check_neq(a6, null);
+  do_check_true(a6.isActive);
+  do_check_false(a6.userDisabled);
+  do_check_false(a6.appDisabled);
+  do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(a7, null);
+  do_check_false(a7.isActive);
+  do_check_true(a7.userDisabled);
+  do_check_false(a7.appDisabled);
+  do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_false(t1.appDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+  do_check_neq(t2, null);
+  do_check_true(t2.isActive);
+  do_check_false(t2.userDisabled);
+  do_check_false(t2.appDisabled);
+  do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isThemeInAddonsList(profileDir, t2.id));
 
-function end_test() {
-  testserver.stop(do_test_finished);
-}
+  // Open another handle on the JSON DB with as much Unix and Windows locking
+  // as we can to simulate some other process interfering with it
+  shutdownManager();
+  do_print("Locking " + gExtensionsJSON.path);
+  let options = {
+    winShare: 0
+  };
+  if (OS.Constants.libc.O_EXLOCK)
+    options.unixFlags = OS.Constants.libc.O_EXLOCK;
+
+  let file = yield OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options);
+
+  let filePermissions = gExtensionsJSON.permissions;
+  if (!OS.Constants.Win) {
+    gExtensionsJSON.permissions = 0;
+  }
+  startupManager(false);
+
+  // Shouldn't have seen any startup changes
+  check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+  // Accessing the add-ons should open and recover the database
+  [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+    yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                              "addon2@tests.mozilla.org",
+                              "addon3@tests.mozilla.org",
+                              "addon4@tests.mozilla.org",
+                              "addon5@tests.mozilla.org",
+                              "addon6@tests.mozilla.org",
+                              "addon7@tests.mozilla.org",
+                              "theme1@tests.mozilla.org",
+                              "theme2@tests.mozilla.org"]);
+
+   // Should be correctly recovered
+   do_check_neq(a1, null);
+   do_check_true(a1.isActive);
+   do_check_false(a1.userDisabled);
+   do_check_false(a1.appDisabled);
+   do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+   // Should be correctly recovered
+   do_check_neq(a2, null);
+   do_check_false(a2.isActive);
+   do_check_true(a2.userDisabled);
+   do_check_false(a2.appDisabled);
+   do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_false(isExtensionInAddonsList(profileDir, a2.id));
 
-function run_test_1() {
-  restartManager();
-  AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+   // The compatibility update won't be recovered but it should still be
+   // active for this session
+   do_check_neq(a3, null);
+   do_check_true(a3.isActive);
+   do_check_false(a3.userDisabled);
+   do_check_false(a3.appDisabled);
+   do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+   // The compatibility update won't be recovered and with strict
+   // compatibility it would not have been able to tell that it was
+   // previously userDisabled. However, without strict compat, it wasn't
+   // appDisabled, so it knows it must have been userDisabled.
+   do_check_neq(a4, null);
+   do_check_false(a4.isActive);
+   do_check_true(a4.userDisabled);
+   do_check_false(a4.appDisabled);
+   do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+   do_check_neq(a5, null);
+   do_check_true(a5.isActive);
+   do_check_false(a5.userDisabled);
+   do_check_false(a5.appDisabled);
+   do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+   do_check_neq(a6, null);
+   do_check_true(a6.isActive);
+   do_check_false(a6.userDisabled);
+   do_check_false(a6.appDisabled);
+   do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+   do_check_neq(a7, null);
+   do_check_false(a7.isActive);
+   do_check_true(a7.userDisabled);
+   do_check_false(a7.appDisabled);
+   do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+   // Should be correctly recovered
+   do_check_neq(t1, null);
+   do_check_false(t1.isActive);
+   do_check_true(t1.userDisabled);
+   do_check_false(t1.appDisabled);
+   do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+   // Should be correctly recovered
+   do_check_neq(t2, null);
+   do_check_true(t2.isActive);
+   do_check_false(t2.userDisabled);
+   do_check_false(t2.appDisabled);
+   do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+   // Restarting will actually apply changes to extensions.ini which will
+   // then be put into the in-memory database when we next fail to load the
+   // real thing
+   restartManager();
+
+   // Shouldn't have seen any startup changes
+   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+   [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+     yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org",
                                "addon5@tests.mozilla.org",
                                "addon6@tests.mozilla.org",
                                "addon7@tests.mozilla.org",
                                "theme1@tests.mozilla.org",
-                               "theme2@tests.mozilla.org"],
-                               callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
-    do_check_neq(a1, null);
-    do_check_true(a1.isActive);
-    do_check_false(a1.userDisabled);
-    do_check_false(a1.appDisabled);
-    do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_true(isExtensionInAddonsList(profileDir, a1.id));
-
-    do_check_neq(a2, null);
-    do_check_false(a2.isActive);
-    do_check_true(a2.userDisabled);
-    do_check_false(a2.appDisabled);
-    do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_false(isExtensionInAddonsList(profileDir, a2.id));
-
-    do_check_neq(a3, null);
-    do_check_true(a3.isActive);
-    do_check_false(a3.userDisabled);
-    do_check_false(a3.appDisabled);
-    do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+                               "theme2@tests.mozilla.org"]);
 
-    do_check_neq(a4, null);
-    do_check_false(a4.isActive);
-    do_check_true(a4.userDisabled);
-    do_check_false(a4.appDisabled);
-    do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_false(isExtensionInAddonsList(profileDir, a4.id));
-
-    do_check_neq(a5, null);
-    do_check_true(a5.isActive);
-    do_check_false(a5.userDisabled);
-    do_check_false(a5.appDisabled);
-    do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_true(isExtensionInAddonsList(profileDir, a5.id));
-
-    do_check_neq(a6, null);
-    do_check_true(a6.isActive);
-    do_check_false(a6.userDisabled);
-    do_check_false(a6.appDisabled);
-    do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
-
-    do_check_neq(a7, null);
-    do_check_false(a7.isActive);
-    do_check_true(a7.userDisabled);
-    do_check_false(a7.appDisabled);
-    do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
-
-    do_check_neq(t1, null);
-    do_check_false(t1.isActive);
-    do_check_true(t1.userDisabled);
-    do_check_false(t1.appDisabled);
-    do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_false(isThemeInAddonsList(profileDir, t1.id));
+   do_check_neq(a1, null);
+   do_check_true(a1.isActive);
+   do_check_false(a1.userDisabled);
+   do_check_false(a1.appDisabled);
+   do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isExtensionInAddonsList(profileDir, a1.id));
 
-    do_check_neq(t2, null);
-    do_check_true(t2.isActive);
-    do_check_false(t2.userDisabled);
-    do_check_false(t2.appDisabled);
-    do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_true(isThemeInAddonsList(profileDir, t2.id));
-
-    // Open another handle on the JSON DB with as much Unix and Windows locking
-    // as we can to simulate some other process interfering with it
-    shutdownManager();
-    do_print("Locking " + gExtensionsJSON.path);
-    let options = {
-      winShare: 0
-    };
-    if (OS.Constants.libc.O_EXLOCK)
-      options.unixFlags = OS.Constants.libc.O_EXLOCK;
-
-    OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options).then(
-      file => {
-        filePermissions = gExtensionsJSON.permissions;
-        if (!OS.Constants.Win) {
-          gExtensionsJSON.permissions = 0;
-        }
-        startupManager(false);
-
-        // Shouldn't have seen any startup changes
-        check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+   do_check_neq(a2, null);
+   do_check_false(a2.isActive);
+   do_check_true(a2.userDisabled);
+   do_check_false(a2.appDisabled);
+   do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_false(isExtensionInAddonsList(profileDir, a2.id));
 
-        // Accessing the add-ons should open and recover the database
-        AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                                     "addon2@tests.mozilla.org",
-                                     "addon3@tests.mozilla.org",
-                                     "addon4@tests.mozilla.org",
-                                     "addon5@tests.mozilla.org",
-                                     "addon6@tests.mozilla.org",
-                                     "addon7@tests.mozilla.org",
-                                     "theme1@tests.mozilla.org",
-                                     "theme2@tests.mozilla.org"],
-                                     callback_soon(function get_after_lock([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
-          // Should be correctly recovered
-          do_check_neq(a1, null);
-          do_check_true(a1.isActive);
-          do_check_false(a1.userDisabled);
-          do_check_false(a1.appDisabled);
-          do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+   do_check_neq(a3, null);
+   do_check_true(a3.isActive);
+   do_check_false(a3.userDisabled);
+   do_check_false(a3.appDisabled);
+   do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isExtensionInAddonsList(profileDir, a3.id));
 
-          // Should be correctly recovered
-          do_check_neq(a2, null);
-          do_check_false(a2.isActive);
-          do_check_true(a2.userDisabled);
-          do_check_false(a2.appDisabled);
-          do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_false(isExtensionInAddonsList(profileDir, a2.id));
-
-          // The compatibility update won't be recovered but it should still be
-          // active for this session
-          do_check_neq(a3, null);
-          do_check_true(a3.isActive);
-          do_check_false(a3.userDisabled);
-          do_check_false(a3.appDisabled);
-          do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+   do_check_neq(a4, null);
+   do_check_false(a4.isActive);
+   do_check_true(a4.userDisabled);
+   do_check_false(a4.appDisabled);
+   do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_false(isExtensionInAddonsList(profileDir, a4.id));
 
-          // The compatibility update won't be recovered and with strict
-          // compatibility it would not have been able to tell that it was
-          // previously userDisabled. However, without strict compat, it wasn't
-          // appDisabled, so it knows it must have been userDisabled.
-          do_check_neq(a4, null);
-          do_check_false(a4.isActive);
-          do_check_true(a4.userDisabled);
-          do_check_false(a4.appDisabled);
-          do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_false(isExtensionInAddonsList(profileDir, a4.id));
-
-          do_check_neq(a5, null);
-          do_check_true(a5.isActive);
-          do_check_false(a5.userDisabled);
-          do_check_false(a5.appDisabled);
-          do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_true(isExtensionInAddonsList(profileDir, a5.id));
-
-          do_check_neq(a6, null);
-          do_check_true(a6.isActive);
-          do_check_false(a6.userDisabled);
-          do_check_false(a6.appDisabled);
-          do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_neq(a5, null);
+   do_check_true(a5.isActive);
+   do_check_false(a5.userDisabled);
+   do_check_false(a5.appDisabled);
+   do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isExtensionInAddonsList(profileDir, a5.id));
 
-          do_check_neq(a7, null);
-          do_check_false(a7.isActive);
-          do_check_true(a7.userDisabled);
-          do_check_false(a7.appDisabled);
-          do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
-
-          // Should be correctly recovered
-          do_check_neq(t1, null);
-          do_check_false(t1.isActive);
-          do_check_true(t1.userDisabled);
-          do_check_false(t1.appDisabled);
-          do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_false(isThemeInAddonsList(profileDir, t1.id));
+   do_check_neq(a6, null);
+   do_check_true(a6.isActive);
+   do_check_false(a6.userDisabled);
+   do_check_false(a6.appDisabled);
+   do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
 
-          // Should be correctly recovered
-          do_check_neq(t2, null);
-          do_check_true(t2.isActive);
-          do_check_false(t2.userDisabled);
-          do_check_false(t2.appDisabled);
-          do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_true(isThemeInAddonsList(profileDir, t2.id));
-
-          // Restarting will actually apply changes to extensions.ini which will
-          // then be put into the in-memory database when we next fail to load the
-          // real thing
-          restartManager();
-
-          // Shouldn't have seen any startup changes
-          check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+   do_check_neq(a7, null);
+   do_check_false(a7.isActive);
+   do_check_true(a7.userDisabled);
+   do_check_false(a7.appDisabled);
+   do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
 
-          AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                                       "addon2@tests.mozilla.org",
-                                       "addon3@tests.mozilla.org",
-                                       "addon4@tests.mozilla.org",
-                                       "addon5@tests.mozilla.org",
-                                       "addon6@tests.mozilla.org",
-                                       "addon7@tests.mozilla.org",
-                                       "theme1@tests.mozilla.org",
-                                       "theme2@tests.mozilla.org"],
-                                       callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
-            do_check_neq(a1, null);
-            do_check_true(a1.isActive);
-            do_check_false(a1.userDisabled);
-            do_check_false(a1.appDisabled);
-            do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_true(isExtensionInAddonsList(profileDir, a1.id));
-
-            do_check_neq(a2, null);
-            do_check_false(a2.isActive);
-            do_check_true(a2.userDisabled);
-            do_check_false(a2.appDisabled);
-            do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_false(isExtensionInAddonsList(profileDir, a2.id));
-
-            do_check_neq(a3, null);
-            do_check_true(a3.isActive);
-            do_check_false(a3.userDisabled);
-            do_check_false(a3.appDisabled);
-            do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_true(isExtensionInAddonsList(profileDir, a3.id));
-
-            do_check_neq(a4, null);
-            do_check_false(a4.isActive);
-            do_check_true(a4.userDisabled);
-            do_check_false(a4.appDisabled);
-            do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+   do_check_neq(t1, null);
+   do_check_false(t1.isActive);
+   do_check_true(t1.userDisabled);
+   do_check_false(t1.appDisabled);
+   do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_false(isThemeInAddonsList(profileDir, t1.id));
 
-            do_check_neq(a5, null);
-            do_check_true(a5.isActive);
-            do_check_false(a5.userDisabled);
-            do_check_false(a5.appDisabled);
-            do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_true(isExtensionInAddonsList(profileDir, a5.id));
-
-            do_check_neq(a6, null);
-            do_check_true(a6.isActive);
-            do_check_false(a6.userDisabled);
-            do_check_false(a6.appDisabled);
-            do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
-
-            do_check_neq(a7, null);
-            do_check_false(a7.isActive);
-            do_check_true(a7.userDisabled);
-            do_check_false(a7.appDisabled);
-            do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_neq(t2, null);
+   do_check_true(t2.isActive);
+   do_check_false(t2.userDisabled);
+   do_check_false(t2.appDisabled);
+   do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isThemeInAddonsList(profileDir, t2.id));
 
-            do_check_neq(t1, null);
-            do_check_false(t1.isActive);
-            do_check_true(t1.userDisabled);
-            do_check_false(t1.appDisabled);
-            do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_false(isThemeInAddonsList(profileDir, t1.id));
-
-            do_check_neq(t2, null);
-            do_check_true(t2.isActive);
-            do_check_false(t2.userDisabled);
-            do_check_false(t2.appDisabled);
-            do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_true(isThemeInAddonsList(profileDir, t2.id));
-
-            // After allowing access to the original DB things should go back to as
-            // they were previously
-            shutdownManager();
-            do_print("Unlocking " + gExtensionsJSON.path);
-            file.close();
-            gExtensionsJSON.permissions = filePermissions;
-            startupManager();
+   // After allowing access to the original DB things should go back to as
+   // they were previously
+   shutdownManager();
+   do_print("Unlocking " + gExtensionsJSON.path);
+   yield file.close();
+   gExtensionsJSON.permissions = filePermissions;
+   startupManager();
 
 
-            // Shouldn't have seen any startup changes
-            check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+   // Shouldn't have seen any startup changes
+   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
-            AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                                         "addon2@tests.mozilla.org",
-                                         "addon3@tests.mozilla.org",
-                                         "addon4@tests.mozilla.org",
-                                         "addon5@tests.mozilla.org",
-                                         "addon6@tests.mozilla.org",
-                                         "addon7@tests.mozilla.org",
-                                         "theme1@tests.mozilla.org",
-                                         "theme2@tests.mozilla.org"],
-                                         callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
-              do_check_neq(a1, null);
-              do_check_true(a1.isActive);
-              do_check_false(a1.userDisabled);
-              do_check_false(a1.appDisabled);
-              do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+   [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+     yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                               "addon2@tests.mozilla.org",
+                               "addon3@tests.mozilla.org",
+                               "addon4@tests.mozilla.org",
+                               "addon5@tests.mozilla.org",
+                               "addon6@tests.mozilla.org",
+                               "addon7@tests.mozilla.org",
+                               "theme1@tests.mozilla.org",
+                               "theme2@tests.mozilla.org"]);
 
-              do_check_neq(a2, null);
-              do_check_false(a2.isActive);
-              do_check_true(a2.userDisabled);
-              do_check_false(a2.appDisabled);
-              do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+   do_check_neq(a1, null);
+   do_check_true(a1.isActive);
+   do_check_false(a1.userDisabled);
+   do_check_false(a1.appDisabled);
+   do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isExtensionInAddonsList(profileDir, a1.id));
 
-              do_check_neq(a3, null);
-              do_check_true(a3.isActive);
-              do_check_false(a3.userDisabled);
-              do_check_false(a3.appDisabled);
-              do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+   do_check_neq(a2, null);
+   do_check_false(a2.isActive);
+   do_check_true(a2.userDisabled);
+   do_check_false(a2.appDisabled);
+   do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_false(isExtensionInAddonsList(profileDir, a2.id));
 
-              do_check_neq(a4, null);
-              do_check_false(a4.isActive);
-              do_check_true(a4.userDisabled);
-              do_check_false(a4.appDisabled);
-              do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+   do_check_neq(a3, null);
+   do_check_true(a3.isActive);
+   do_check_false(a3.userDisabled);
+   do_check_false(a3.appDisabled);
+   do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isExtensionInAddonsList(profileDir, a3.id));
 
-              do_check_neq(a5, null);
-              do_check_true(a5.isActive);
-              do_check_false(a5.userDisabled);
-              do_check_false(a5.appDisabled);
-              do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+   do_check_neq(a4, null);
+   do_check_false(a4.isActive);
+   do_check_true(a4.userDisabled);
+   do_check_false(a4.appDisabled);
+   do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_false(isExtensionInAddonsList(profileDir, a4.id));
 
-              do_check_neq(a6, null);
-              do_check_true(a6.isActive);
-              do_check_false(a6.userDisabled);
-              do_check_false(a6.appDisabled);
-              do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_neq(a5, null);
+   do_check_true(a5.isActive);
+   do_check_false(a5.userDisabled);
+   do_check_false(a5.appDisabled);
+   do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isExtensionInAddonsList(profileDir, a5.id));
 
-              do_check_neq(a7, null);
-              do_check_false(a7.isActive);
-              do_check_true(a7.userDisabled);
-              do_check_false(a7.appDisabled);
-              do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_neq(a6, null);
+   do_check_true(a6.isActive);
+   do_check_false(a6.userDisabled);
+   do_check_false(a6.appDisabled);
+   do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
 
-              do_check_neq(t1, null);
-              do_check_false(t1.isActive);
-              do_check_true(t1.userDisabled);
-              do_check_false(t1.appDisabled);
-              do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_false(isThemeInAddonsList(profileDir, t1.id));
+   do_check_neq(a7, null);
+   do_check_false(a7.isActive);
+   do_check_true(a7.userDisabled);
+   do_check_false(a7.appDisabled);
+   do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+   do_check_neq(t1, null);
+   do_check_false(t1.isActive);
+   do_check_true(t1.userDisabled);
+   do_check_false(t1.appDisabled);
+   do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_false(isThemeInAddonsList(profileDir, t1.id));
 
-              do_check_neq(t2, null);
-              do_check_true(t2.isActive);
-              do_check_false(t2.userDisabled);
-              do_check_false(t2.appDisabled);
-              do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isThemeInAddonsList(profileDir, t2.id));
+   do_check_neq(t2, null);
+   do_check_true(t2.isActive);
+   do_check_false(t2.userDisabled);
+   do_check_false(t2.appDisabled);
+   do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+   do_check_true(isThemeInAddonsList(profileDir, t2.id));
+});
 
-              end_test();
-            }));
-          }));
-        }));
-      },
-      do_report_unexpected_exception
-    );
-  }));
+
+function run_test() {
+ run_next_test();
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
@@ -66,230 +66,227 @@ var addon5 = {
     minVersion: "2",
     maxVersion: "2"
   }]
 };
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
-function run_test() {
-  do_test_pending();
+add_task(function() {
+
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
 
   writeInstallRDFForExtension(addon1, profileDir);
   writeInstallRDFForExtension(addon2, profileDir);
   writeInstallRDFForExtension(addon3, profileDir);
   writeInstallRDFForExtension(addon4, profileDir);
   writeInstallRDFForExtension(addon5, profileDir);
 
   // Make it look like add-on 5 was installed some time in the past so the update is
   // detected
-  setExtensionModifiedTime(getFileForAddon(profileDir, addon5.id), Date.now() - (60000));
+  let path = getFileForAddon(profileDir, addon5.id).path;
+  yield promiseSetExtensionModifiedTime(path, Date.now() - (60000));
 
   // Startup the profile and setup the initial state
   startupManager();
 
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
   check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
 
-  AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(a2) {
-    a2.userDisabled = true;
+  let a1, a2, a3, a4, a5, a6;
 
-    restartManager();
+  [a2] = yield promiseAddonsByIDs(["addon2@tests.mozilla.org"]);
+  a2.userDisabled = true;
+
+  restartManager();
 
-    AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                                 "addon2@tests.mozilla.org",
-                                 "addon3@tests.mozilla.org",
-                                 "addon4@tests.mozilla.org",
-                                 "addon5@tests.mozilla.org"],
-                                function([a1, a2, a3, a4, a5]) {
-      a2.userDisabled = false;
-      a3.userDisabled = true;
-      a4.uninstall();
+  [a1, a2, a3, a4, a5] =
+    yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                              "addon2@tests.mozilla.org",
+                              "addon3@tests.mozilla.org",
+                              "addon4@tests.mozilla.org",
+                              "addon5@tests.mozilla.org"]);
+
+  a2.userDisabled = false;
+  a3.userDisabled = true;
+  a4.uninstall();
 
-      installAllFiles([do_get_addon("test_locked2_5"),
-                       do_get_addon("test_locked2_6")], function locked_installed() {
-        do_check_neq(a1, null);
-        do_check_true(a1.isActive);
-        do_check_false(a1.userDisabled);
-        do_check_false(a1.appDisabled);
-        do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-        do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+  yield promiseInstallAllFiles([do_get_addon("test_locked2_5"),
+                                do_get_addon("test_locked2_6")]);
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_false(a1.appDisabled);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
 
-        do_check_neq(a2, null);
-        do_check_false(a2.isActive);
-        do_check_false(a2.userDisabled);
-        do_check_false(a2.appDisabled);
-        do_check_eq(a2.pendingOperations, AddonManager.PENDING_ENABLE);
-        do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+  do_check_neq(a2, null);
+  do_check_false(a2.isActive);
+  do_check_false(a2.userDisabled);
+  do_check_false(a2.appDisabled);
+  do_check_eq(a2.pendingOperations, AddonManager.PENDING_ENABLE);
+  do_check_false(isExtensionInAddonsList(profileDir, a2.id));
 
-        do_check_neq(a3, null);
-        do_check_true(a3.isActive);
-        do_check_true(a3.userDisabled);
-        do_check_false(a3.appDisabled);
-        do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
-        do_check_true(isExtensionInAddonsList(profileDir, a3.id));
-
-        do_check_neq(a4, null);
-        do_check_true(a4.isActive);
-        do_check_false(a4.userDisabled);
-        do_check_false(a4.appDisabled);
-        do_check_eq(a4.pendingOperations, AddonManager.PENDING_UNINSTALL);
-        do_check_true(isExtensionInAddonsList(profileDir, a4.id));
+  do_check_neq(a3, null);
+  do_check_true(a3.isActive);
+  do_check_true(a3.userDisabled);
+  do_check_false(a3.appDisabled);
+  do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
+  do_check_true(isExtensionInAddonsList(profileDir, a3.id));
 
-        do_check_neq(a5, null);
-        do_check_eq(a5.version, "1.0");
-        do_check_true(a5.isActive);
-        do_check_false(a5.userDisabled);
-        do_check_false(a5.appDisabled);
-        do_check_eq(a5.pendingOperations, AddonManager.PENDING_UPGRADE);
-        do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+  do_check_neq(a4, null);
+  do_check_true(a4.isActive);
+  do_check_false(a4.userDisabled);
+  do_check_false(a4.appDisabled);
+  do_check_eq(a4.pendingOperations, AddonManager.PENDING_UNINSTALL);
+  do_check_true(isExtensionInAddonsList(profileDir, a4.id));
 
-        // Open another handle on the JSON DB with as much Unix and Windows locking
-        // as we can to simulate some other process interfering with it
-        shutdownManager();
-        do_print("Locking " + gExtensionsJSON.path);
-        let options = {
-          winShare: 0
-        };
-        if (OS.Constants.libc.O_EXLOCK)
-          options.unixFlags = OS.Constants.libc.O_EXLOCK;
+  do_check_neq(a5, null);
+  do_check_eq(a5.version, "1.0");
+  do_check_true(a5.isActive);
+  do_check_false(a5.userDisabled);
+  do_check_false(a5.appDisabled);
+  do_check_eq(a5.pendingOperations, AddonManager.PENDING_UPGRADE);
+  do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+  // Open another handle on the JSON DB with as much Unix and Windows locking
+  // as we can to simulate some other process interfering with it
+  shutdownManager();
+  do_print("Locking " + gExtensionsJSON.path);
+  let options = {
+    winShare: 0
+  };
+  if (OS.Constants.libc.O_EXLOCK)
+    options.unixFlags = OS.Constants.libc.O_EXLOCK;
 
-        OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options).then(
-          file => {
-            filePermissions = gExtensionsJSON.permissions;
-            if (!OS.Constants.Win) {
-              gExtensionsJSON.permissions = 0;
-            }
-            startupManager(false);
+  let file = yield OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options);
 
-            check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
-            check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+  let filePermissions = gExtensionsJSON.permissions;
+  if (!OS.Constants.Win) {
+    gExtensionsJSON.permissions = 0;
+  }
+  startupManager(false);
+
+  check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+  check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
 
-            AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                                         "addon2@tests.mozilla.org",
-                                         "addon3@tests.mozilla.org",
-                                         "addon4@tests.mozilla.org",
-                                         "addon5@tests.mozilla.org",
-                                         "addon6@tests.mozilla.org"],
-                                        callback_soon(function([a1, a2, a3, a4, a5, a6]) {
-              do_check_neq(a1, null);
-              do_check_true(a1.isActive);
-              do_check_false(a1.userDisabled);
-              do_check_false(a1.appDisabled);
-              do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+  [a1, a2, a3, a4, a5, a6] =
+    yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                              "addon2@tests.mozilla.org",
+                              "addon3@tests.mozilla.org",
+                              "addon4@tests.mozilla.org",
+                              "addon5@tests.mozilla.org",
+                              "addon6@tests.mozilla.org"]);
 
-              do_check_neq(a2, null);
-              do_check_true(a2.isActive);
-              do_check_false(a2.userDisabled);
-              do_check_false(a2.appDisabled);
-              do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_false(a1.appDisabled);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
 
-              do_check_neq(a3, null);
-              do_check_false(a3.isActive);
-              do_check_true(a3.userDisabled);
-              do_check_false(a3.appDisabled);
-              do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_false(isExtensionInAddonsList(profileDir, a3.id));
-
-              do_check_eq(a4, null);
+  do_check_neq(a2, null);
+  do_check_true(a2.isActive);
+  do_check_false(a2.userDisabled);
+  do_check_false(a2.appDisabled);
+  do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a2.id));
 
-              do_check_neq(a5, null);
-              do_check_eq(a5.version, "2.0");
-              do_check_true(a5.isActive);
-              do_check_false(a5.userDisabled);
-              do_check_false(a5.appDisabled);
-              do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+  do_check_neq(a3, null);
+  do_check_false(a3.isActive);
+  do_check_true(a3.userDisabled);
+  do_check_false(a3.appDisabled);
+  do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a3.id));
 
-              do_check_neq(a6, null);
-              do_check_true(a6.isActive);
-              do_check_false(a6.userDisabled);
-              do_check_false(a6.appDisabled);
-              do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+  do_check_eq(a4, null);
 
-              // After allowing access to the original DB things should still be
-              // back how they were before the lock
-              shutdownManager();
-              file.close();
-              gExtensionsJSON.permissions = filePermissions;
-              startupManager();
+  do_check_neq(a5, null);
+  do_check_eq(a5.version, "2.0");
+  do_check_true(a5.isActive);
+  do_check_false(a5.userDisabled);
+  do_check_false(a5.appDisabled);
+  do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a5.id));
 
-              // On Unix, we can save the DB even when the original file wasn't
-              // readable, so our changes were saved. On Windows,
-              // these things happened when we had no access to the database so
-              // they are seen as external changes when we get the database back
-              if (gXPISaveError) {
-                do_print("Previous XPI save failed");
-                check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED,
-                    ["addon6@tests.mozilla.org"]);
-                check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED,
-                    ["addon4@tests.mozilla.org"]);
-              }
-              else {
-                do_print("Previous XPI save succeeded");
-                check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
-                check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
-              }
+  do_check_neq(a6, null);
+  do_check_true(a6.isActive);
+  do_check_false(a6.userDisabled);
+  do_check_false(a6.appDisabled);
+  do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+
+  // After allowing access to the original DB things should still be
+  // back how they were before the lock
+  shutdownManager();
+  yield file.close();
+  gExtensionsJSON.permissions = filePermissions;
+  startupManager();
 
-              AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                                           "addon2@tests.mozilla.org",
-                                           "addon3@tests.mozilla.org",
-                                           "addon4@tests.mozilla.org",
-                                           "addon5@tests.mozilla.org",
-                                           "addon6@tests.mozilla.org"],
-                                          function([a1, a2, a3, a4, a5, a6]) {
-                do_check_neq(a1, null);
-                do_check_true(a1.isActive);
-                do_check_false(a1.userDisabled);
-                do_check_false(a1.appDisabled);
-                do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-                do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+  // On Unix, we can save the DB even when the original file wasn't
+  // readable, so our changes were saved. On Windows,
+  // these things happened when we had no access to the database so
+  // they are seen as external changes when we get the database back
+  if (gXPISaveError) {
+    do_print("Previous XPI save failed");
+    check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED,
+        ["addon6@tests.mozilla.org"]);
+    check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED,
+        ["addon4@tests.mozilla.org"]);
+  }
+  else {
+    do_print("Previous XPI save succeeded");
+    check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+    check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+  }
 
-                do_check_neq(a2, null);
-                do_check_true(a2.isActive);
-                do_check_false(a2.userDisabled);
-                do_check_false(a2.appDisabled);
-                do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-                do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+  [a1, a2, a3, a4, a5, a6] =
+    yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                              "addon2@tests.mozilla.org",
+                              "addon3@tests.mozilla.org",
+                              "addon4@tests.mozilla.org",
+                              "addon5@tests.mozilla.org",
+                              "addon6@tests.mozilla.org"]);
+
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_false(a1.appDisabled);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
 
-                do_check_neq(a3, null);
-                do_check_false(a3.isActive);
-                do_check_true(a3.userDisabled);
-                do_check_false(a3.appDisabled);
-                do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
-                do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+  do_check_neq(a2, null);
+  do_check_true(a2.isActive);
+  do_check_false(a2.userDisabled);
+  do_check_false(a2.appDisabled);
+  do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a2.id));
 
-                do_check_eq(a4, null);
+  do_check_neq(a3, null);
+  do_check_false(a3.isActive);
+  do_check_true(a3.userDisabled);
+  do_check_false(a3.appDisabled);
+  do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a3.id));
 
-                do_check_neq(a5, null);
-                do_check_eq(a5.version, "2.0");
-                do_check_true(a5.isActive);
-                do_check_false(a5.userDisabled);
-                do_check_false(a5.appDisabled);
-                do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-                do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+  do_check_eq(a4, null);
 
-                do_check_neq(a6, null);
-                do_check_true(a6.isActive);
-                do_check_false(a6.userDisabled);
-                do_check_false(a6.appDisabled);
-                do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
-                do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+  do_check_neq(a5, null);
+  do_check_eq(a5.version, "2.0");
+  do_check_true(a5.isActive);
+  do_check_false(a5.userDisabled);
+  do_check_false(a5.appDisabled);
+  do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a5.id));
 
-                end_test();
-              });
-            }));
-          },
-          do_report_unexpected_exception
-          );
-      });
-    });
-  }));
+  do_check_neq(a6, null);
+  do_check_true(a6.isActive);
+  do_check_false(a6.userDisabled);
+  do_check_false(a6.appDisabled);
+  do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+});
+
+function run_test() {
+  run_next_test();
 }
 
-function end_test() {
-  do_execute_soon(do_test_finished);
-}
--- a/toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
@@ -129,18 +129,17 @@ var theme2 = {
     minVersion: "2",
     maxVersion: "2"
   }]
 };
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
-function run_test() {
-  do_test_pending();
+add_task(function* init() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
 
   writeInstallRDFForExtension(addon1, profileDir);
   writeInstallRDFForExtension(addon2, profileDir);
   writeInstallRDFForExtension(addon3, profileDir);
   writeInstallRDFForExtension(addon4, profileDir);
   writeInstallRDFForExtension(addon5, profileDir);
   writeInstallRDFForExtension(addon6, profileDir);
@@ -149,402 +148,404 @@ function run_test() {
   writeInstallRDFForExtension(theme2, profileDir);
 
   // Startup the profile and setup the initial state
   startupManager();
 
   // New profile so new add-ons are ignored
   check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
 
-  AddonManager.getAddonsByIDs(["addon2@tests.mozilla.org",
-                               "addon3@tests.mozilla.org",
-                               "addon4@tests.mozilla.org",
-                               "addon7@tests.mozilla.org",
-                               "theme2@tests.mozilla.org"], function([a2, a3, a4,
-                                                                      a7, t2]) {
-    // Set up the initial state
-    a2.userDisabled = true;
-    a4.userDisabled = true;
-    a7.userDisabled = true;
-    t2.userDisabled = false;
-    a3.findUpdates({
-      onUpdateFinished: function() {
-        a4.findUpdates({
-          onUpdateFinished: function() {
-            do_execute_soon(run_test_1);
-          }
-        }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
-      }
-    }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
-  });
-}
+  let a1, a2, a3, a4, a5, a6, a7, t1, t2;
+
+  [a2, a3, a4, a7, t2] =
+    yield promiseAddonsByIDs(["addon2@tests.mozilla.org",
+                              "addon3@tests.mozilla.org",
+                              "addon4@tests.mozilla.org",
+                              "addon7@tests.mozilla.org",
+                              "theme2@tests.mozilla.org"]);
+
+  // Set up the initial state
+  let deferredUpdateFinished = Promise.defer();
+
+  a2.userDisabled = true;
+  a4.userDisabled = true;
+  a7.userDisabled = true;
+  t2.userDisabled = false;
+  a3.findUpdates({
+    onUpdateFinished: function() {
+      a4.findUpdates({
+        onUpdateFinished: function() {
+          deferredUpdateFinished.resolve();
+        }
+      }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+    }
+  }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+  yield deferredUpdateFinished.promise;
+});
+
+add_task(function* run_test_1() {
+  let a1, a2, a3, a4, a5, a6, a7, t1, t2;
+
+  restartManager();
+  [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+    yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                             "addon2@tests.mozilla.org",
+                             "addon3@tests.mozilla.org",
+                             "addon4@tests.mozilla.org",
+                             "addon5@tests.mozilla.org",
+                             "addon6@tests.mozilla.org",
+                             "addon7@tests.mozilla.org",
+                             "theme1@tests.mozilla.org",
+                             "theme2@tests.mozilla.org"]);
+
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_false(a1.appDisabled);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+  do_check_neq(a2, null);
+  do_check_false(a2.isActive);
+  do_check_true(a2.userDisabled);
+  do_check_false(a2.appDisabled);
+  do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+  do_check_neq(a3, null);
+  do_check_true(a3.isActive);
+  do_check_false(a3.userDisabled);
+  do_check_false(a3.appDisabled);
+  do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+  do_check_neq(a4, null);
+  do_check_false(a4.isActive);
+  do_check_true(a4.userDisabled);
+  do_check_false(a4.appDisabled);
+  do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+  do_check_neq(a5, null);
+  do_check_false(a5.isActive);
+  do_check_false(a5.userDisabled);
+  do_check_true(a5.appDisabled);
+  do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+
+  do_check_neq(a6, null);
+  do_check_true(a6.isActive);
+  do_check_false(a6.userDisabled);
+  do_check_false(a6.appDisabled);
+  do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(a7, null);
+  do_check_false(a7.isActive);
+  do_check_true(a7.userDisabled);
+  do_check_false(a7.appDisabled);
+  do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_false(t1.appDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+  do_check_neq(t2, null);
+  do_check_true(t2.isActive);
+  do_check_false(t2.userDisabled);
+  do_check_false(t2.appDisabled);
+  do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isThemeInAddonsList(profileDir, t2.id));
 
-function end_test() {
-  testserver.stop(do_test_finished);
-}
+  // Open another handle on the JSON DB with as much Unix and Windows locking
+  // as we can to simulate some other process interfering with it
+  shutdownManager();
+  do_print("Locking " + gExtensionsJSON.path);
+  let options = {
+    winShare: 0
+  };
+  if (OS.Constants.libc.O_EXLOCK)
+    options.unixFlags = OS.Constants.libc.O_EXLOCK;
+
+  let file = yield OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options);
+
+  let filePermissions = gExtensionsJSON.permissions;
+  if (!OS.Constants.Win) {
+    gExtensionsJSON.permissions = 0;
+  }
+  startupManager(false);
+
+  // Shouldn't have seen any startup changes
+  check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+  // Accessing the add-ons should open and recover the database
+  [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+    yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                              "addon2@tests.mozilla.org",
+                              "addon3@tests.mozilla.org",
+                              "addon4@tests.mozilla.org",
+                              "addon5@tests.mozilla.org",
+                              "addon6@tests.mozilla.org",
+                              "addon7@tests.mozilla.org",
+                              "theme1@tests.mozilla.org",
+                              "theme2@tests.mozilla.org"]);
+
+  // Should be correctly recovered
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_false(a1.appDisabled);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+  // Should be correctly recovered
+  do_check_neq(a2, null);
+  do_check_false(a2.isActive);
+  do_check_true(a2.userDisabled);
+  do_check_false(a2.appDisabled);
+  do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a2.id));
 
-function run_test_1() {
+  // The compatibility update won't be recovered but it should still be
+  // active for this session
+  do_check_neq(a3, null);
+  do_check_true(a3.isActive);
+  do_check_false(a3.userDisabled);
+  do_check_true(a3.appDisabled);
+  do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
+  do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+  // The compatibility update won't be recovered and it will not have been
+  // able to tell that it was previously userDisabled
+  do_check_neq(a4, null);
+  do_check_false(a4.isActive);
+  do_check_false(a4.userDisabled);
+  do_check_true(a4.appDisabled);
+  do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+  do_check_neq(a5, null);
+  do_check_false(a5.isActive);
+  do_check_false(a5.userDisabled);
+  do_check_true(a5.appDisabled);
+  do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+
+  do_check_neq(a6, null);
+  do_check_true(a6.isActive);
+  do_check_false(a6.userDisabled);
+  do_check_false(a6.appDisabled);
+  do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(a7, null);
+  do_check_false(a7.isActive);
+  do_check_true(a7.userDisabled);
+  do_check_false(a7.appDisabled);
+  do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+  // Should be correctly recovered
+  do_check_neq(t1, null);
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_false(t1.appDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+  // Should be correctly recovered
+  do_check_neq(t2, null);
+  do_check_true(t2.isActive);
+  do_check_false(t2.userDisabled);
+  do_check_false(t2.appDisabled);
+  do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+  // Restarting will actually apply changes to extensions.ini which will
+  // then be put into the in-memory database when we next fail to load the
+  // real thing
   restartManager();
 
-  AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+  // Shouldn't have seen any startup changes
+  check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+  [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+    yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org",
                                "addon5@tests.mozilla.org",
                                "addon6@tests.mozilla.org",
                                "addon7@tests.mozilla.org",
                                "theme1@tests.mozilla.org",
-                               "theme2@tests.mozilla.org"],
-                               callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
-    do_check_neq(a1, null);
-    do_check_true(a1.isActive);
-    do_check_false(a1.userDisabled);
-    do_check_false(a1.appDisabled);
-    do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+                               "theme2@tests.mozilla.org"]);
+
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_false(a1.appDisabled);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+  do_check_neq(a2, null);
+  do_check_false(a2.isActive);
+  do_check_true(a2.userDisabled);
+  do_check_false(a2.appDisabled);
+  do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+  do_check_neq(a3, null);
+  do_check_false(a3.isActive);
+  do_check_false(a3.userDisabled);
+  do_check_true(a3.appDisabled);
+  do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+
+  do_check_neq(a4, null);
+  do_check_false(a4.isActive);
+  do_check_false(a4.userDisabled);
+  do_check_true(a4.appDisabled);
+  do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+  do_check_neq(a5, null);
+  do_check_false(a5.isActive);
+  do_check_false(a5.userDisabled);
+  do_check_true(a5.appDisabled);
+  do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+
+  do_check_neq(a6, null);
+  do_check_true(a6.isActive);
+  do_check_false(a6.userDisabled);
+  do_check_false(a6.appDisabled);
+  do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(a7, null);
+  do_check_false(a7.isActive);
+  do_check_true(a7.userDisabled);
+  do_check_false(a7.appDisabled);
+  do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_false(t1.appDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+  do_check_neq(t2, null);
+  do_check_true(t2.isActive);
+  do_check_false(t2.userDisabled);
+  do_check_false(t2.appDisabled);
+  do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isThemeInAddonsList(profileDir, t2.id));
 
-    do_check_neq(a2, null);
-    do_check_false(a2.isActive);
-    do_check_true(a2.userDisabled);
-    do_check_false(a2.appDisabled);
-    do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+  // After allowing access to the original DB things should go back to as
+  // back how they were before the lock
+  shutdownManager();
+  do_print("Unlocking " + gExtensionsJSON.path);
+  yield file.close();
+  gExtensionsJSON.permissions = filePermissions;
+  startupManager(false);
+
+  // Shouldn't have seen any startup changes
+  check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+  [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+    yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                              "addon2@tests.mozilla.org",
+                              "addon3@tests.mozilla.org",
+                              "addon4@tests.mozilla.org",
+                              "addon5@tests.mozilla.org",
+                              "addon6@tests.mozilla.org",
+                              "addon7@tests.mozilla.org",
+                              "theme1@tests.mozilla.org",
+                              "theme2@tests.mozilla.org"]);
+
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_false(a1.appDisabled);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
 
-    do_check_neq(a3, null);
+  do_check_neq(a2, null);
+  do_check_false(a2.isActive);
+  do_check_true(a2.userDisabled);
+  do_check_false(a2.appDisabled);
+  do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+  do_check_neq(a3, null);
+  do_check_false(a3.userDisabled);
+  // On Unix, we may be able to save our changes over the locked DB so we
+  // remember that this extension was changed to disabled. On Windows we
+  // couldn't replace the old DB so we read the older version of the DB
+  // where the extension is enabled
+  if (gXPISaveError) {
+    do_print("XPI save failed");
     do_check_true(a3.isActive);
-    do_check_false(a3.userDisabled);
     do_check_false(a3.appDisabled);
-    do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
     do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+  }
+  else {
+    do_print("XPI save succeeded");
+    do_check_false(a3.isActive);
+    do_check_true(a3.appDisabled);
+    do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+  }
+  do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
 
-    do_check_neq(a4, null);
-    do_check_false(a4.isActive);
+  do_check_neq(a4, null);
+  do_check_false(a4.isActive);
+  // The reverse of the platform difference for a3 - Unix should
+  // stay the same as the last iteration because the save succeeded,
+  // Windows should still say userDisabled
+  if (OS.Constants.Win) {
     do_check_true(a4.userDisabled);
     do_check_false(a4.appDisabled);
-    do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_false(isExtensionInAddonsList(profileDir, a4.id));
-
-    do_check_neq(a5, null);
-    do_check_false(a5.isActive);
-    do_check_false(a5.userDisabled);
-    do_check_true(a5.appDisabled);
-    do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_false(isExtensionInAddonsList(profileDir, a5.id));
-
-    do_check_neq(a6, null);
-    do_check_true(a6.isActive);
-    do_check_false(a6.userDisabled);
-    do_check_false(a6.appDisabled);
-    do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
-
-    do_check_neq(a7, null);
-    do_check_false(a7.isActive);
-    do_check_true(a7.userDisabled);
-    do_check_false(a7.appDisabled);
-    do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
-
-    do_check_neq(t1, null);
-    do_check_false(t1.isActive);
-    do_check_true(t1.userDisabled);
-    do_check_false(t1.appDisabled);
-    do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_false(isThemeInAddonsList(profileDir, t1.id));
-
-    do_check_neq(t2, null);
-    do_check_true(t2.isActive);
-    do_check_false(t2.userDisabled);
-    do_check_false(t2.appDisabled);
-    do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
-    do_check_true(isThemeInAddonsList(profileDir, t2.id));
-
-    // Open another handle on the JSON DB with as much Unix and Windows locking
-    // as we can to simulate some other process interfering with it
-    shutdownManager();
-    do_print("Locking " + gExtensionsJSON.path);
-    let options = {
-      winShare: 0
-    };
-    if (OS.Constants.libc.O_EXLOCK)
-      options.unixFlags = OS.Constants.libc.O_EXLOCK;
-
-    OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options).then(
-      file => {
-        filePermissions = gExtensionsJSON.permissions;
-        if (!OS.Constants.Win) {
-          gExtensionsJSON.permissions = 0;
-        }
-        startupManager(false);
-
-        // Shouldn't have seen any startup changes
-        check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
-
-        // Accessing the add-ons should open and recover the database
-        AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                                     "addon2@tests.mozilla.org",
-                                     "addon3@tests.mozilla.org",
-                                     "addon4@tests.mozilla.org",
-                                     "addon5@tests.mozilla.org",
-                                     "addon6@tests.mozilla.org",
-                                     "addon7@tests.mozilla.org",
-                                     "theme1@tests.mozilla.org",
-                                     "theme2@tests.mozilla.org"],
-                                     callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
-          // Should be correctly recovered
-          do_check_neq(a1, null);
-          do_check_true(a1.isActive);
-          do_check_false(a1.userDisabled);
-          do_check_false(a1.appDisabled);
-          do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+  }
+  else {
+    do_check_false(a4.userDisabled);
+    do_check_true(a4.appDisabled);
+  }
+  do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+  do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
 
-          // Should be correctly recovered
-          do_check_neq(a2, null);
-          do_check_false(a2.isActive);
-          do_check_true(a2.userDisabled);
-          do_check_false(a2.appDisabled);
-          do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_false(isExtensionInAddonsList(profileDir, a2.id));
-
-          // The compatibility update won't be recovered but it should still be
-          // active for this session
-          do_check_neq(a3, null);
-          do_check_true(a3.isActive);
-          do_check_false(a3.userDisabled);
-          do_check_true(a3.appDisabled);
-          do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
-          do_check_true(isExtensionInAddonsList(profileDir, a3.id));
-
-          // The compatibility update won't be recovered and it will not have been
-          // able to tell that it was previously userDisabled
-          do_check_neq(a4, null);
-          do_check_false(a4.isActive);
-          do_check_false(a4.userDisabled);
-          do_check_true(a4.appDisabled);
-          do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_false(isExtensionInAddonsList(profileDir, a4.id));
-
-          do_check_neq(a5, null);
-          do_check_false(a5.isActive);
-          do_check_false(a5.userDisabled);
-          do_check_true(a5.appDisabled);
-          do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_false(isExtensionInAddonsList(profileDir, a5.id));
-
-          do_check_neq(a6, null);
-          do_check_true(a6.isActive);
-          do_check_false(a6.userDisabled);
-          do_check_false(a6.appDisabled);
-          do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_neq(a5, null);
+  do_check_false(a5.isActive);
+  do_check_false(a5.userDisabled);
+  do_check_true(a5.appDisabled);
+  do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isExtensionInAddonsList(profileDir, a5.id));
 
-          do_check_neq(a7, null);
-          do_check_false(a7.isActive);
-          do_check_true(a7.userDisabled);
-          do_check_false(a7.appDisabled);
-          do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
-
-          // Should be correctly recovered
-          do_check_neq(t1, null);
-          do_check_false(t1.isActive);
-          do_check_true(t1.userDisabled);
-          do_check_false(t1.appDisabled);
-          do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_false(isThemeInAddonsList(profileDir, t1.id));
-
-          // Should be correctly recovered
-          do_check_neq(t2, null);
-          do_check_true(t2.isActive);
-          do_check_false(t2.userDisabled);
-          do_check_false(t2.appDisabled);
-          do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
-          do_check_true(isThemeInAddonsList(profileDir, t2.id));
-
-          // Restarting will actually apply changes to extensions.ini which will
-          // then be put into the in-memory database when we next fail to load the
-          // real thing
-          restartManager();
-
-          // Shouldn't have seen any startup changes
-          check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
-
-          AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                                       "addon2@tests.mozilla.org",
-                                       "addon3@tests.mozilla.org",
-                                       "addon4@tests.mozilla.org",
-                                       "addon5@tests.mozilla.org",
-                                       "addon6@tests.mozilla.org",
-                                       "addon7@tests.mozilla.org",
-                                       "theme1@tests.mozilla.org",
-                                       "theme2@tests.mozilla.org"],
-                                       callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
-            do_check_neq(a1, null);
-            do_check_true(a1.isActive);
-            do_check_false(a1.userDisabled);
-            do_check_false(a1.appDisabled);
-            do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+  do_check_neq(a6, null);
+  do_check_true(a6.isActive);
+  do_check_false(a6.userDisabled);
+  do_check_false(a6.appDisabled);
+  do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
 
-            do_check_neq(a2, null);
-            do_check_false(a2.isActive);
-            do_check_true(a2.userDisabled);
-            do_check_false(a2.appDisabled);
-            do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_false(isExtensionInAddonsList(profileDir, a2.id));
-
-            do_check_neq(a3, null);
-            do_check_false(a3.isActive);
-            do_check_false(a3.userDisabled);
-            do_check_true(a3.appDisabled);
-            do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_false(isExtensionInAddonsList(profileDir, a3.id));
-
-            do_check_neq(a4, null);
-            do_check_false(a4.isActive);
-            do_check_false(a4.userDisabled);
-            do_check_true(a4.appDisabled);
-            do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_false(isExtensionInAddonsList(profileDir, a4.id));
-
-            do_check_neq(a5, null);
-            do_check_false(a5.isActive);
-            do_check_false(a5.userDisabled);
-            do_check_true(a5.appDisabled);
-            do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_false(isExtensionInAddonsList(profileDir, a5.id));
-
-            do_check_neq(a6, null);
-            do_check_true(a6.isActive);
-            do_check_false(a6.userDisabled);
-            do_check_false(a6.appDisabled);
-            do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
-
-            do_check_neq(a7, null);
-            do_check_false(a7.isActive);
-            do_check_true(a7.userDisabled);
-            do_check_false(a7.appDisabled);
-            do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_neq(a7, null);
+  do_check_false(a7.isActive);
+  do_check_true(a7.userDisabled);
+  do_check_false(a7.appDisabled);
+  do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
 
-            do_check_neq(t1, null);
-            do_check_false(t1.isActive);
-            do_check_true(t1.userDisabled);
-            do_check_false(t1.appDisabled);
-            do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_false(isThemeInAddonsList(profileDir, t1.id));
-
-            do_check_neq(t2, null);
-            do_check_true(t2.isActive);
-            do_check_false(t2.userDisabled);
-            do_check_false(t2.appDisabled);
-            do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
-            do_check_true(isThemeInAddonsList(profileDir, t2.id));
-
-            // After allowing access to the original DB things should go back to as
-            // back how they were before the lock
-            shutdownManager();
-            do_print("Unlocking " + gExtensionsJSON.path);
-            file.close();
-            gExtensionsJSON.permissions = filePermissions;
-            startupManager(false);
-
-            // Shouldn't have seen any startup changes
-            check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
-
-            AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                                         "addon2@tests.mozilla.org",
-                                         "addon3@tests.mozilla.org",
-                                         "addon4@tests.mozilla.org",
-                                         "addon5@tests.mozilla.org",
-                                         "addon6@tests.mozilla.org",
-                                         "addon7@tests.mozilla.org",
-                                         "theme1@tests.mozilla.org",
-                                         "theme2@tests.mozilla.org"],
-                                         callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
-              do_check_neq(a1, null);
-              do_check_true(a1.isActive);
-              do_check_false(a1.userDisabled);
-              do_check_false(a1.appDisabled);
-              do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+  do_check_neq(t1, null);
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_false(t1.appDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(isThemeInAddonsList(profileDir, t1.id));
 
-              do_check_neq(a2, null);
-              do_check_false(a2.isActive);
-              do_check_true(a2.userDisabled);
-              do_check_false(a2.appDisabled);
-              do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_false(isExtensionInAddonsList(profileDir, a2.id));
-
-              do_check_neq(a3, null);
-              do_check_false(a3.userDisabled);
-              // On Unix, we may be able to save our changes over the locked DB so we
-              // remember that this extension was changed to disabled. On Windows we
-              // couldn't replace the old DB so we read the older version of the DB
-              // where the extension is enabled
-              if (gXPISaveError) {
-                do_print("XPI save failed");
-                do_check_true(a3.isActive);
-                do_check_false(a3.appDisabled);
-                do_check_true(isExtensionInAddonsList(profileDir, a3.id));
-              }
-              else {
-                do_print("XPI save succeeded");
-                do_check_false(a3.isActive);
-                do_check_true(a3.appDisabled);
-                do_check_false(isExtensionInAddonsList(profileDir, a3.id));
-              }
-              do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
-
-              do_check_neq(a4, null);
-              do_check_false(a4.isActive);
-              // The reverse of the platform difference for a3 - Unix should
-              // stay the same as the last iteration because the save succeeded,
-              // Windows should still say userDisabled
-              if (OS.Constants.Win) {
-                do_check_true(a4.userDisabled);
-                do_check_false(a4.appDisabled);
-              }
-              else {
-                do_check_false(a4.userDisabled);
-                do_check_true(a4.appDisabled);
-              }
-              do_check_false(isExtensionInAddonsList(profileDir, a4.id));
-              do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_neq(t2, null);
+  do_check_true(t2.isActive);
+  do_check_false(t2.userDisabled);
+  do_check_false(t2.appDisabled);
+  do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(isThemeInAddonsList(profileDir, t2.id));
+});
 
-              do_check_neq(a5, null);
-              do_check_false(a5.isActive);
-              do_check_false(a5.userDisabled);
-              do_check_true(a5.appDisabled);
-              do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_false(isExtensionInAddonsList(profileDir, a5.id));
-
-              do_check_neq(a6, null);
-              do_check_true(a6.isActive);
-              do_check_false(a6.userDisabled);
-              do_check_false(a6.appDisabled);
-              do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
-
-              do_check_neq(a7, null);
-              do_check_false(a7.isActive);
-              do_check_true(a7.userDisabled);
-              do_check_false(a7.appDisabled);
-              do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+function run_test() {
+  run_next_test();
+}
 
-              do_check_neq(t1, null);
-              do_check_false(t1.isActive);
-              do_check_true(t1.userDisabled);
-              do_check_false(t1.appDisabled);
-              do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_false(isThemeInAddonsList(profileDir, t1.id));
-
-              do_check_neq(t2, null);
-              do_check_true(t2.isActive);
-              do_check_false(t2.userDisabled);
-              do_check_false(t2.appDisabled);
-              do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
-              do_check_true(isThemeInAddonsList(profileDir, t2.id));
-
-              end_test();
-            }));
-          }));
-        }));
-      },
-      do_report_unexpected_exception
-    );
-  }));
-}