Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 10 Feb 2015 15:54:32 -0500
changeset 242093 9ac2e8cd4faff33702ff849bea34e54db18c2f0b
parent 242092 3d3f1b07ef0f3aee7bb227031e822de6259d24f9 (current diff)
parent 242086 38a668c3efaa990ab0585749f21b4ccc76fe71cc (diff)
child 242096 ee093ca706662491d356484bf729ef32d55bc19c
push id634
push usermozilla@noorenberghe.ca
push dateTue, 10 Feb 2015 22:34:30 +0000
reviewersmerge
milestone38.0a1
Merge fx-team to m-c. a=merge CLOSED TREE
browser/devtools/webide/test/test_deviceinfo.html
toolkit/themes/osx/global/config.css
toolkit/themes/windows/global/config.css
--- a/accessible/tests/mochitest/tree/test_txtctrl.xul
+++ b/accessible/tests/mochitest/tree/test_txtctrl.xul
@@ -35,33 +35,42 @@
       // default textbox
       testAccessibleTree("txc", accTree);
 
       // multiline
       testAccessibleTree("txc_multiline", accTree);
 
       //////////////////////////////////////////////////////////////////////////
       // search textbox
+      accTree =
+        { SECTION: [
+          { ENTRY: [ { TEXT_LEAF: [] } ] },
+          { MENUPOPUP: [] }
+        ] };
+      testAccessibleTree("txc_search", accTree);
+
+      //////////////////////////////////////////////////////////////////////////
+      // search textbox with search button
 
       if (MAC) {
         accTree =
           { SECTION: [
             { ENTRY: [ { TEXT_LEAF: [] } ] },
             { MENUPOPUP: [] }
           ] };
       } else {
         accTree =
           { SECTION: [
             { ENTRY: [ { TEXT_LEAF: [] } ] },
             { PUSHBUTTON: [] },
             { MENUPOPUP: [] }
           ] };
       }
 
-      testAccessibleTree("txc_search", accTree);
+      testAccessibleTree("txc_search_searchbutton", accTree);
 
       //////////////////////////////////////////////////////////////////////////
       // number textbox
 
       accTree =
         { SECTION: [
           { ENTRY: [ { TEXT_LEAF: [] } ] },
           { MENUPOPUP: [] },
@@ -190,16 +199,17 @@
       </div>
       <pre id="test">
       </pre>
     </body>
 
     <vbox flex="1">
       <textbox id="txc" value="hello"/>
       <textbox id="txc_search" type="search" value="hello"/>
+      <textbox id="txc_search_searchbutton" searchbutton="true" type="search" value="hello"/>
       <textbox id="txc_number" type="number" value="44"/>
       <textbox id="txc_password" type="password" value="hello"/>
       <textbox id="txc_multiline" multiline="true" value="hello"/>
       <textbox id="txc_autocomplete" type="autocomplete" value="hello"/>
     </vbox>
   </hbox>
 
 </window>
--- a/b2g/chrome/content/desktop.js
+++ b/b2g/chrome/content/desktop.js
@@ -133,31 +133,31 @@ function initResponsiveDesign() {
     responsive.rotatebutton.addEventListener('command', function (evt) {
       GlobalSimulatorScreen.flipScreen();
       evt.stopImmediatePropagation();
       evt.preventDefault();
     }, true);
 
     // Enable touch events
     responsive.enableTouch();
+
+    // Automatically toggle responsive design mode
+    let width = 320, height = 480;
+    // We have to take into account padding and border introduced with the
+    // device look'n feel:
+    width += 15*2; // Horizontal padding
+    width += 1*2; // Vertical border
+    height += 60; // Top Padding
+    height += 1; // Top border
+    responsive.setSize(width, height);
   });
 
-  // Automatically toggle responsive design mode
-  let width = 320, height = 480;
-  // We have to take into account padding and border introduced with the
-  // device look'n feel:
-  width += 15*2; // Horizontal padding
-  width += 1*2; // Vertical border
-  height += 60; // Top Padding
-  height += 1; // Top border
-  let args = {'width': width, 'height': height};
+
   let mgr = browserWindow.ResponsiveUI.ResponsiveUIManager;
   mgr.toggle(browserWindow, browserWindow.gBrowser.selectedTab);
-  let responsive = browserWindow.gBrowser.selectedTab.__responsiveUI;
-  responsive.setSize(width, height);
 
 }
 
 function openDevtools() {
   // Open devtool panel while maximizing its size according to screen size
   Services.prefs.setIntPref('devtools.toolbox.sidebar.width',
                             browserWindow.outerWidth - 550);
   Services.prefs.setCharPref('devtools.toolbox.host', 'side');
--- a/b2g/dev/app/moz.build
+++ b/b2g/dev/app/moz.build
@@ -2,10 +2,11 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIST_SUBDIR = 'browser'
 export('DIST_SUBDIR')
 
 JS_PREFERENCE_FILES += [
     '/b2g/app/b2g.js',
+    '/b2g/dev/app/mulet.js',
 ]
 
new file mode 100644
--- /dev/null
+++ b/b2g/dev/app/mulet.js
@@ -0,0 +1,19 @@
+// Automatically open b2g in a tab
+pref("browser.startup.homepage", "chrome://b2g/content/shell.html");
+
+// Disable some painful behavior of fx
+pref("startup.homepage_welcome_url", "");
+pref("browser.shell.checkDefaultBrowser", "");
+pref("browser.sessionstore.max_tabs_undo", 0);
+pref("browser.sessionstore.max_windows_undo", 0);
+pref("browser.sessionstore.restore_on_demand", false);
+pref("browser.sessionstore.resume_from_crash", false);
+
+// Display the devtools on the right of the phone
+pref("devtools.toolbox.host", "side");
+pref("devtools.toolbox.sidebar.width", 800);
+
+// Disable e10s as we don't want to run shell.html,
+// nor the system app OOP, but only inner apps
+pref("browser.tabs.remote.autostart", false);
+pref("browser.tabs.remote.autostart.1", false);
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -365,16 +365,20 @@ var LightWeightThemeWebInstaller = {
 
     this._manager.resetPreview();
   },
 
   _isAllowed: function (node) {
     var pm = Services.perms;
 
     var uri = node.ownerDocument.documentURIObject;
+
+    if (!uri.schemeIs("https"))
+      return false;
+
     return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
   },
 
   _getThemeFromNode: function (node) {
     return this._manager.parseTheme(node.getAttribute("data-browsertheme"),
                                     node.baseURI);
   }
 }
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -13,32 +13,45 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 (function() {
   LoopUI = {
     get toolbarButton() {
       delete this.toolbarButton;
       return this.toolbarButton = CustomizableUI.getWidget("loop-button").forWindow(window);
     },
 
+    get panel() {
+      delete this.panel;
+      return this.panel = document.getElementById("loop-notification-panel");
+    },
+
     /**
      * @return {Promise}
      */
     promiseDocumentVisible(aDocument) {
       if (!aDocument.hidden) {
         return Promise.resolve();
       }
 
       return new Promise((resolve) => {
         aDocument.addEventListener("visibilitychange", function onVisibilityChanged() {
           aDocument.removeEventListener("visibilitychange", onVisibilityChanged);
           resolve();
         });
       });
     },
 
+    togglePanel: function(event, tabId = null) {
+      if (this.panel.state == "open") {
+        this.panel.hidePopup();
+      } else {
+        this.openCallPanel(event, tabId);
+      }
+    },
+
     /**
      * Opens the panel for Loop and sizes it appropriately.
      *
      * @param {event}  event   The event opening the panel, used to anchor
      *                         the panel to the button which triggers it.
      * @param {String} [tabId] Identifier of the tab to select when the panel is
      *                         opened. Example: 'rooms', 'contacts', etc.
      * @return {Promise}
@@ -188,16 +201,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
      */
     updateToolbarState: function(aReason = null) {
       if (!this.toolbarButton.node) {
         return;
       }
       let state = "";
       if (MozLoopService.errors.size) {
         state = "error";
+      } else if (MozLoopService.screenShareActive) {
+        state = "action";
       } else if (aReason == "login" && MozLoopService.userProfile) {
         state = "active";
       } else if (MozLoopService.doNotDisturb) {
         state = "disabled";
       } else if (MozLoopService.roomsParticipantsCount > 0) {
         state = "active";
       }
       this.toolbarButton.node.setAttribute("state", state);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2529,18 +2529,18 @@ let gMenuButtonUpdateBadge = {
       case STATE_PENDING:
       case STATE_PENDING_SVC:
         // If the update is successfully applied, or if the updater has fallen back
         // to non-staged updates, add a badge to the hamburger menu to indicate an
         // update will be applied once the browser restarts.
         let badge = document.getAnonymousElementByAttribute(PanelUI.menuButton,
                                                             "class",
                                                             "toolbarbutton-badge");
-        badge.style.backgroundColor = 'green';
-        PanelUI.menuButton.setAttribute("badge", "\u2605");
+        badge.style.backgroundColor = '#74BF43';
+        PanelUI.menuButton.setAttribute("badge", "\u2B06");
 
         let brandBundle = document.getElementById("bundle_brand");
         let brandShortName = brandBundle.getString("brandShortName");
         stringId = "appmenu.restartNeeded.description";
         updateButtonText = gNavigatorBundle.getFormattedString(stringId,
                                                                [brandShortName]);
 
         updateButton.setAttribute("label", updateButtonText);
--- a/browser/base/content/test/general/browser_bug553455.js
+++ b/browser/base/content/test/general/browser_bug553455.js
@@ -835,16 +835,17 @@ var XPInstallObserver = {
 };
 
 function test() {
   requestLongerTimeout(4);
   waitForExplicitFinish();
 
   Services.prefs.setBoolPref("extensions.logging.enabled", true);
   Services.prefs.setBoolPref("extensions.strictCompatibility", true);
+  Services.prefs.setBoolPref("extensions.install.requireSecureOrigin", false);
 
   Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
   Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
   Services.obs.addObserver(XPInstallObserver, "addon-install-failed", false);
   Services.obs.addObserver(XPInstallObserver, "addon-install-complete", false);
 
   registerCleanupFunction(function() {
     // Make sure no more test parts run in case we were timed out
@@ -854,16 +855,17 @@ function test() {
     AddonManager.getAllInstalls(function(aInstalls) {
       aInstalls.forEach(function(aInstall) {
         aInstall.cancel();
       });
     });
 
     Services.prefs.clearUserPref("extensions.logging.enabled");
     Services.prefs.clearUserPref("extensions.strictCompatibility");
+    Services.prefs.clearUserPref("extensions.install.requireSecureOrigin");
 
     Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
     Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
     Services.obs.removeObserver(XPInstallObserver, "addon-install-failed");
     Services.obs.removeObserver(XPInstallObserver, "addon-install-complete");
   });
 
   runNextTest();
--- a/browser/base/content/test/general/browser_bug592338.js
+++ b/browser/base/content/test/general/browser_bug592338.js
@@ -11,23 +11,51 @@ var LightweightThemeManager = tempScope.
 function wait_for_notification(aCallback) {
   PopupNotifications.panel.addEventListener("popupshown", function() {
     PopupNotifications.panel.removeEventListener("popupshown", arguments.callee, false);
     aCallback(PopupNotifications.panel);
   }, false);
 }
 
 var TESTS = [
+function test_install_http() {
+  is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected");
+
+  var pm = Services.perms;
+  pm.add(makeURI("http://example.org/"), "install", pm.ALLOW_ACTION);
+
+  gBrowser.selectedTab = gBrowser.addTab("http://example.org/browser/browser/base/content/test/general/bug592338.html");
+  gBrowser.selectedBrowser.addEventListener("pageshow", function() {
+    if (gBrowser.contentDocument.location.href == "about:blank")
+      return;
+
+    gBrowser.selectedBrowser.removeEventListener("pageshow", arguments.callee, false);
+
+    executeSoon(function() {
+      var link = gBrowser.contentDocument.getElementById("theme-install");
+      EventUtils.synthesizeMouse(link, 2, 2, {}, gBrowser.contentWindow);
+
+      is(LightweightThemeManager.currentTheme, null, "Should not have installed the test theme");
+
+      gBrowser.removeTab(gBrowser.selectedTab);
+
+      pm.remove("example.org", "install");
+
+      runNextTest();
+    });
+  }, false);
+},
+
 function test_install_lwtheme() {
   is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected");
 
   var pm = Services.perms;
   pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
 
-  gBrowser.selectedTab = gBrowser.addTab("http://example.com/browser/browser/base/content/test/general/bug592338.html");
+  gBrowser.selectedTab = gBrowser.addTab("https://example.com/browser/browser/base/content/test/general/bug592338.html");
   gBrowser.selectedBrowser.addEventListener("pageshow", function() {
     if (gBrowser.contentDocument.location.href == "about:blank")
       return;
 
     gBrowser.selectedBrowser.removeEventListener("pageshow", arguments.callee, false);
 
     executeSoon(function() {
       var link = gBrowser.contentDocument.getElementById("theme-install");
@@ -49,19 +77,19 @@ function test_lwtheme_switch_theme() {
   is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected");
 
   AddonManager.getAddonByID("theme-xpi@tests.mozilla.org", function(aAddon) {
     aAddon.userDisabled = false;
     ok(aAddon.isActive, "Theme should have immediately enabled");
     Services.prefs.setBoolPref("extensions.dss.enabled", false);
 
     var pm = Services.perms;
-    pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+    pm.add(makeURI("https://example.com/"), "install", pm.ALLOW_ACTION);
 
-    gBrowser.selectedTab = gBrowser.addTab("http://example.com/browser/browser/base/content/test/general/bug592338.html");
+    gBrowser.selectedTab = gBrowser.addTab("https://example.com/browser/browser/base/content/test/general/bug592338.html");
     gBrowser.selectedBrowser.addEventListener("pageshow", function() {
       if (gBrowser.contentDocument.location.href == "about:blank")
         return;
 
       gBrowser.selectedBrowser.removeEventListener("pageshow", arguments.callee, false);
 
       executeSoon(function() {
         var link = gBrowser.contentDocument.getElementById("theme-install");
--- a/browser/base/content/test/general/browser_sanitizeDialog.js
+++ b/browser/base/content/test/general/browser_sanitizeDialog.js
@@ -63,17 +63,17 @@ var gAllTests = [
     let places = [];
     let pURI;
     for (let i = 0; i < 30; i++) {
       pURI = makeURI("http://" + i + "-minutes-ago.com/");
       places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
       uris.push(pURI);
     }
 
-    addVisits(places, function() {
+    PlacesTestUtils.addVisits(places).then(() => {
       let wh = new WindowHelper();
       wh.onload = function () {
         this.selectDuration(Sanitizer.TIMESPAN_HOUR);
         this.checkPrefCheckbox("history", false);
         this.checkDetails(false);
 
         // Show details
         this.toggleDetails();
@@ -127,17 +127,17 @@ var gAllTests = [
     // Add history (over an hour ago).
     let olderURIs = [];
     for (let i = 0; i < 5; i++) {
       pURI = makeURI("http://" + (61 + i) + "-minutes-ago.com/");
       places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i)});
       olderURIs.push(pURI);
     }
 
-    addVisits(places, function() {
+    PlacesTestUtils.addVisits(places).then(() => {
       let totalHistoryVisits = uris.length + olderURIs.length;
 
       let wh = new WindowHelper();
       wh.onload = function () {
         this.selectDuration(Sanitizer.TIMESPAN_HOUR);
         this.checkPrefCheckbox("history", true);
         this.acceptDialog();
       };
@@ -208,17 +208,17 @@ var gAllTests = [
     let places = [];
     let pURI;
     for (let i = 0; i < 5; i++) {
       pURI = makeURI("http://" + i + "-minutes-ago.com/");
       places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
       uris.push(pURI);
     }
 
-    addVisits(places, function() {
+    PlacesTestUtils.addVisits(places).then(() => {
       let wh = new WindowHelper();
       wh.onload = function () {
         is(this.isWarningPanelVisible(), false,
            "Warning panel should be hidden after previously accepting dialog " +
            "with a predefined timespan");
         this.selectDuration(Sanitizer.TIMESPAN_HOUR);
 
         // Remove only form entries, leave history (including downloads).
@@ -265,17 +265,17 @@ var gAllTests = [
     let pURI;
     // within past hour, within past two hours, within past four hours and 
     // outside past four hours
     [10, 70, 130, 250].forEach(function(aValue) {
       pURI = makeURI("http://" + aValue + "-minutes-ago.com/");
       places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)});
       uris.push(pURI);
     });
-    addVisits(places, function() {
+    PlacesTestUtils.addVisits(places).then(() => {
       let wh = new WindowHelper();
       wh.onload = function () {
         is(this.isWarningPanelVisible(), false,
            "Warning panel should be hidden after previously accepting dialog " +
            "with a predefined timespan");
         this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
         this.checkPrefCheckbox("history", true);
         this.checkDetails(true);
@@ -312,17 +312,17 @@ var gAllTests = [
     let pURI;
     // within past hour, within past two hours, within past four hours and 
     // outside past four hours
     [10, 70, 130, 250].forEach(function(aValue) {
       pURI = makeURI("http://" + aValue + "-minutes-ago.com/");
       places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)});
       uris.push(pURI);
     });
-    addVisits(places, function() {
+    PlacesTestUtils.addVisits(places).then(() => {
       let wh = new WindowHelper();
       wh.onload = function () {
         is(this.isWarningPanelVisible(), true,
            "Warning panel should be visible after previously accepting dialog " +
            "with clearing everything");
         this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
         this.checkPrefCheckbox("history", true);
         this.acceptDialog();
@@ -354,17 +354,17 @@ var gAllTests = [
   /**
    * The next three tests checks that when a certain history item cannot be
    * cleared then the checkbox should be both disabled and unchecked.
    * In addition, we ensure that this behavior does not modify the preferences.
    */
   function () {
     // Add history.
     let pURI = makeURI("http://" + 10 + "-minutes-ago.com/");
-    addVisits({uri: pURI, visitDate: visitTimeForMinutesAgo(10)}, function() {
+    PlacesTestUtils.addVisits({uri: pURI, visitDate: visitTimeForMinutesAgo(10)}).then(() => {
       let uris = [ pURI ];
 
       let wh = new WindowHelper();
       wh.onload = function() {
         // Check that the relevant checkboxes are enabled
         var cb = this.win.document.querySelectorAll(
                    "#itemList > [preference='privacy.cpd.formdata']");
         ok(cb.length == 1 && !cb[0].disabled, "There is formdata, checkbox to " +
--- a/browser/base/content/test/general/browser_search_favicon.js
+++ b/browser/base/content/test/general/browser_search_favicon.js
@@ -1,77 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let gOriginalEngine;
 let gEngine;
 let gUnifiedCompletePref = "browser.urlbar.unifiedcomplete";
 let gRestyleSearchesPref = "browser.urlbar.restyleSearches";
 
-/**
- * Asynchronously adds visits to a page.
- *
- * @param aPlaceInfo
- *        Can be an nsIURI, in such a case a single LINK visit will be added.
- *        Otherwise can be an object describing the visit to add, or an array
- *        of these objects:
- *          { uri: nsIURI of the page,
- *            transition: one of the TRANSITION_* from nsINavHistoryService,
- *            [optional] title: title of the page,
- *            [optional] visitDate: visit date in microseconds from the epoch
- *            [optional] referrer: nsIURI of the referrer for this visit
- *          }
- *
- * @return {Promise}
- * @resolves When all visits have been added successfully.
- * @rejects JavaScript exception.
- */
-function promiseAddVisits(aPlaceInfo) {
-  return new Promise((resolve, reject) => {
-    let places = [];
-    if (aPlaceInfo instanceof Ci.nsIURI) {
-      places.push({ uri: aPlaceInfo });
-    }
-    else if (Array.isArray(aPlaceInfo)) {
-      places = places.concat(aPlaceInfo);
-    } else {
-      places.push(aPlaceInfo)
-    }
-
-    // Create mozIVisitInfo for each entry.
-    let now = Date.now();
-    for (let i = 0, len = places.length; i < len; ++i) {
-      if (!places[i].title) {
-        places[i].title = "test visit for " + places[i].uri.spec;
-      }
-      places[i].visits = [{
-        transitionType: places[i].transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
-                                                           : places[i].transition,
-        visitDate: places[i].visitDate || (now++) * 1000,
-        referrerURI: places[i].referrer
-      }];
-    }
-
-    PlacesUtils.asyncHistory.updatePlaces(
-      places,
-      {
-        handleError: function AAV_handleError(aResultCode, aPlaceInfo) {
-          let ex = new Components.Exception("Unexpected error in adding visits.",
-                                            aResultCode);
-          reject(ex);
-        },
-        handleResult: function () {},
-        handleCompletion: function UP_handleCompletion() {
-          resolve();
-        }
-      }
-    );
-  });
-}
-
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref(gUnifiedCompletePref);
   Services.prefs.clearUserPref(gRestyleSearchesPref);
   Services.search.currentEngine = gOriginalEngine;
   Services.search.removeEngine(gEngine);
   return PlacesTestUtils.clearHistory();
 });
 
@@ -85,17 +24,17 @@ add_task(function*() {
   Services.search.addEngineWithDetails("SearchEngine", "", "", "",
                                        "GET", "http://s.example.com/search");
   gEngine = Services.search.getEngineByName("SearchEngine");
   gEngine.addParam("q", "{searchTerms}", null);
   gOriginalEngine = Services.search.currentEngine;
   Services.search.currentEngine = gEngine;
 
   let uri = NetUtil.newURI("http://s.example.com/search?q=foo&client=1");
-  yield promiseAddVisits({ uri: uri, title: "Foo - SearchEngine Search" });
+  yield PlacesTestUtils.addVisits({ uri: uri, title: "Foo - SearchEngine Search" });
 
   let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
   yield promiseTabLoaded(gBrowser.selectedTab);
 
   // The first autocomplete result has the action searchengine, while
   // the second result is the "search favicon" element.
   yield promiseAutocompleteResultPopup("foo");
   let result = gURLBar.popup.richlistbox.children[1];
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -312,55 +312,16 @@ function whenTabLoaded(aTab, aCallback) 
 }
 
 function promiseTabLoaded(aTab) {
   let deferred = Promise.defer();
   whenTabLoaded(aTab, deferred.resolve);
   return deferred.promise;
 }
 
-function addVisits(aPlaceInfo, aCallback) {
-  let places = [];
-  if (aPlaceInfo instanceof Ci.nsIURI) {
-    places.push({ uri: aPlaceInfo });
-  } else if (Array.isArray(aPlaceInfo)) {
-    places = places.concat(aPlaceInfo);
-  } else {
-    places.push(aPlaceInfo);
-   }
-
-  // Create mozIVisitInfo for each entry.
-  let now = Date.now();
-  for (let i = 0; i < places.length; i++) {
-    if (!places[i].title) {
-      places[i].title = "test visit for " + places[i].uri.spec;
-    }
-    places[i].visits = [{
-      transitionType: places[i].transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
-                                                         : places[i].transition,
-      visitDate: places[i].visitDate || (now++) * 1000,
-      referrerURI: places[i].referrer
-    }];
-  }
-
-  PlacesUtils.asyncHistory.updatePlaces(
-    places,
-    {
-      handleError: function AAV_handleError() {
-        throw("Unexpected error in adding visit.");
-      },
-      handleResult: function () {},
-      handleCompletion: function UP_handleCompletion() {
-        if (aCallback)
-          aCallback();
-      }
-    }
-  );
-}
-
 /**
  * Ensures that the specified URIs are either cleared or not.
  *
  * @param aURIs
  *        Array of page URIs
  * @param aShouldBeCleared
  *        True if each visit to the URI should be cleared, false otherwise
  */
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -939,17 +939,17 @@ const CustomizableWidgets = [
       node.setAttribute("id", this.id);
       node.classList.add("toolbarbutton-1");
       node.classList.add("chromeclass-toolbar-additional");
       node.classList.add("badged-button");
       node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
       node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
       node.setAttribute("removable", "true");
       node.addEventListener("command", function(event) {
-        aDocument.defaultView.LoopUI.openCallPanel(event);
+        aDocument.defaultView.LoopUI.togglePanel(event);
       });
 
       return node;
     }
   }, {
     id: "web-apps-button",
     label: "web-apps-button.label",
     tooltiptext: "web-apps-button.tooltiptext",
--- a/browser/components/customizableui/content/customizeMode.inc.xul
+++ b/browser/components/customizableui/content/customizeMode.inc.xul
@@ -4,18 +4,18 @@
 
 <hbox id="customization-container" flex="1" hidden="true">
   <vbox flex="1" id="customization-palette-container">
     <label id="customization-header">
       &customizeMode.menuAndToolbars.header2;
     </label>
     <hbox id="customization-empty" hidden="true">
       <label>&customizeMode.menuAndToolbars.empty;</label>
-      <label onclick="BrowserOpenAddonsMgr('addons://discovery/');"
-             onkeypress="BrowserOpenAddonsMgr('addons://discovery/');"
+      <label onclick="BrowserOpenAddonsMgr('addons://discover/');"
+             onkeypress="BrowserOpenAddonsMgr('addons://discover/');"
              id="customization-more-tools"
              class="text-link">
         &customizeMode.menuAndToolbars.emptyLink;
       </label>
     </hbox>
     <vbox id="customization-palette" class="customization-palette"/>
     <spacer id="customization-spacer"/>
     <hbox id="customization-footer">
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -742,16 +742,32 @@ function injectLoopAPI(targetWindow) {
      */
     notifyUITour: {
       enumerable: true,
       writable: true,
       value: function(subject) {
         UITour.notify(subject);
       }
     },
+
+    /**
+     * Used to record the screen sharing state for a window so that it can
+     * be reflected on the toolbar button.
+     *
+     * @param {String} windowId The id of the conversation window the state
+     *                          is being changed for.
+     * @param {Boolean} active  Whether or not screen sharing is now active.
+     */
+    setScreenShareState: {
+      enumerable: true,
+      writable: true,
+      value: function(windowId, active) {
+        MozLoopService.setScreenShareState(windowId, active);
+      }
+    }
   };
 
   function onStatusChanged(aSubject, aTopic, aData) {
     let event = new targetWindow.CustomEvent("LoopStatusChanged");
     targetWindow.dispatchEvent(event);
   };
 
   function onDOMWindowDestroyed(aSubject, aTopic, aData) {
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -1041,16 +1041,17 @@ let gInitializeTimerFunc = (deferredInit
 };
 
 
 /**
  * Public API
  */
 this.MozLoopService = {
   _DNSService: gDNSService,
+  _activeScreenShares: [],
 
   get channelIDs() {
     // Channel ids that will be registered with the PushServer for notifications
     return {
       callsFxA: "25389583-921f-4169-a426-a4673658944b",
       callsGuest: "801f754b-686b-43ec-bd83-1419bbf58388",
       roomsFxA: "6add272a-d316-477c-8335-f00f73dfde71",
       roomsGuest: "19d3f799-a8f3-4328-9822-b7cd02765832",
@@ -1649,10 +1650,38 @@ this.MozLoopService = {
   },
 
   getConversationContext: function(winId) {
     return MozLoopServiceInternal.conversationContexts.get(winId);
   },
 
   addConversationContext: function(windowId, context) {
     MozLoopServiceInternal.conversationContexts.set(windowId, context);
+  },
+
+  /**
+   * Used to record the screen sharing state for a window so that it can
+   * be reflected on the toolbar button.
+   *
+   * @param {String} windowId The id of the conversation window the state
+   *                          is being changed for.
+   * @param {Boolean} active  Whether or not screen sharing is now active.
+   */
+  setScreenShareState: function(windowId, active) {
+    if (active) {
+      this._activeScreenShares.push(windowId);
+    } else {
+      var index = this._activeScreenShares.indexOf(windowId);
+      if (index != -1) {
+        this._activeScreenShares.splice(index, 1);
+      }
+    }
+
+    MozLoopServiceInternal.notifyStatusChanged();
+  },
+
+  /**
+   * Returns true if screen sharing is active in at least one window.
+   */
+  get screenShareActive() {
+    return this._activeScreenShares.length > 0;
   }
 };
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -375,16 +375,20 @@ loop.store.ActiveRoomStore = (function()
       this.setStoreState(muteState);
     },
 
     /**
      * Used to note the current screensharing state.
      */
     screenSharingState: function(actionData) {
       this.setStoreState({screenSharingState: actionData.state});
+
+      this._mozLoop.setScreenShareState(
+        this.getStoreState().windowId,
+        actionData.state === SCREEN_SHARE_STATES.ACTIVE);
     },
 
     /**
      * Used to note the current state of receiving screenshare data.
      */
     receivingScreenShare: function(actionData) {
       this.setStoreState({receivingScreenShare: actionData.receiving});
     },
@@ -412,16 +416,23 @@ loop.store.ActiveRoomStore = (function()
     },
 
     /**
      * Handles the window being unloaded. Ensures the room is left.
      */
     windowUnload: function() {
       this._leaveRoom(ROOM_STATES.CLOSING);
 
+      // If we're closing the window, then ensure the screensharing state
+      // is cleared. We don't do this on leave room, as we might still be
+      // sharing.
+      this._mozLoop.setScreenShareState(
+        this.getStoreState().windowId,
+        false);
+
       if (!this._onUpdateListener) {
         return;
       }
 
       // If we're closing the window, we can stop listening to updates.
       var roomToken = this.getStoreState().roomToken;
       this._mozLoop.rooms.off("update:" + roomToken, this._onUpdateListener);
       this._mozLoop.rooms.off("delete:" + roomToken, this._onDeleteListener);
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -10,23 +10,32 @@
 
     <link rel="stylesheet" type="text/css" href="shared/css/reset.css">
     <link rel="stylesheet" type="text/css" href="shared/css/common.css">
     <link rel="stylesheet" type="text/css" href="shared/css/conversation.css">
     <link rel="stylesheet" type="text/css" href="css/webapp.css">
     <link rel="localization" href="l10n/{locale}/loop.properties">
 
     <script>
-      (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-        (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-        m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-      })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+      // window.navigator.doNotTrack "yes" is for old versions of FF
+      // window.navigator.doNotTrack "1" is for current versions of FF + Chrome + Opera
+      // window.doNotTrack is safari
+      // window.navigator.msDoNotTrack
+      if (window.navigator.doNotTrack !== "yes" &&
+          window.navigator.doNotTrack !== "1" &&
+          window.doNotTrack !== "1" &&
+          window.navigator.msDoNotTrack !== "1") {
+        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+          (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+          m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+        })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
 
-      ga('create', 'UA-36116321-15', 'auto');
-      ga('send', 'pageview');
+        ga('create', 'UA-36116321-15', 'auto');
+        ga('send', 'pageview');
+      }
     </script>
 
   </head>
   <body class="standalone">
 
     <div id="main"></div>
 
     <!-- libs -->
--- a/browser/components/loop/standalone/content/js/standaloneMozLoop.js
+++ b/browser/components/loop/standalone/content/js/standaloneMozLoop.js
@@ -263,15 +263,16 @@ loop.StandaloneMozLoop = (function(mozL1
      *
      * @param {String} prefName The name of the pref
      * @param {String} value The value to set.
      */
     getLoopPref: function(prefName) {
       return localStorage.getItem(prefName);
     },
 
-    // Dummy function to reflect those in the desktop mozLoop that we
-    // don't currently use.
-    addConversationContext: function() {}
+    // Dummy functions to reflect those in the desktop mozLoop that we
+    // don't currently use in standalone.
+    addConversationContext: function() {},
+    setScreenShareState: function() {}
   };
 
   return StandaloneMozLoop;
 })(navigator.mozL10n);
--- a/browser/components/loop/test/mochitest/browser_toolbarbutton.js
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -31,18 +31,17 @@ add_task(function* test_doNotDisturb_wit
   yield MozLoopService.doNotDisturb = true;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
   MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
   MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
   yield MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
   yield loadLoopPanel();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state after opening panel");
-  let loopPanel = document.getElementById("loop-notification-panel");
-  loopPanel.hidePopup();
+  LoopUI.panel.hidePopup();
   yield MozLoopService.doNotDisturb = false;
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   MozLoopServiceInternal.fxAOAuthTokenData = null;
   yield MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
 add_task(function* test_error() {
@@ -70,26 +69,46 @@ add_task(function* test_error_with_login
 add_task(function* test_active() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
   MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
   yield MozLoopServiceInternal.notifyStatusChanged("login");
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
   yield loadLoopPanel();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state after opening panel");
-  let loopPanel = document.getElementById("loop-notification-panel");
-  loopPanel.hidePopup();
+  LoopUI.panel.hidePopup();
   MozLoopServiceInternal.fxAOAuthTokenData = null;
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
 add_task(function* test_room_participants() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   LoopRoomsInternal.rooms.set("test_room", {participants: [{displayName: "hugh", id: "008"}]});
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
   LoopRoomsInternal.rooms.set("test_room", {participants: []});
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
   LoopRoomsInternal.rooms.delete("test_room");
 });
 
+add_task(function* test_panelToggle_on_click() {
+  // Since we _know_ the first click on the button will open the panel, we'll
+  // open it using the test utility and check the correct state by clicking the
+  // button. This should hide the panel.
+  // If we'd open the panel with a simulated click on the button, we won't know
+  // for sure when the panel has opened, because the event loop spins a few times
+  // in the mean time.
+  yield loadLoopPanel();
+  Assert.strictEqual(LoopUI.panel.state, "open", "Panel should be open");
+  // The panel should now be visible. Clicking the button should hide it.
+  LoopUI.toolbarButton.node.click();
+  Assert.strictEqual(LoopUI.panel.state, "closed", "Panel should be closed");
+});
+
+add_task(function* test_screen_share() {
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+  MozLoopService.setScreenShareState("1", true);
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "action", "Check button is in action state");
+  MozLoopService.setScreenShareState("1", false);
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+});
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -25,17 +25,18 @@ describe("loop.store.ActiveRoomStore", f
       addConversationContext: sandbox.stub(),
       rooms: {
         get: sinon.stub(),
         join: sinon.stub(),
         refreshMembership: sinon.stub(),
         leave: sinon.stub(),
         on: sinon.stub(),
         off: sinon.stub()
-      }
+      },
+      setScreenShareState: sinon.stub()
     };
 
     fakeSdkDriver = {
       connectSession: sandbox.stub(),
       disconnectSession: sandbox.stub(),
       forceDisconnectAll: sandbox.stub().callsArg(0)
     };
 
@@ -643,23 +644,45 @@ describe("loop.store.ActiveRoomStore", f
         enabled: false
       }));
 
       expect(store.getStoreState().videoMuted).eql(true);
     });
   });
 
   describe("#screenSharingState", function() {
+    beforeEach(function() {
+      store.setStoreState({windowId: "1234"});
+    });
+
     it("should save the state", function() {
       store.screenSharingState(new sharedActions.ScreenSharingState({
         state: SCREEN_SHARE_STATES.ACTIVE
       }));
 
       expect(store.getStoreState().screenSharingState).eql(SCREEN_SHARE_STATES.ACTIVE);
     });
+
+    it("should set screen sharing active when the state is active", function() {
+      store.screenSharingState(new sharedActions.ScreenSharingState({
+        state: SCREEN_SHARE_STATES.ACTIVE
+      }));
+
+      sinon.assert.calledOnce(fakeMozLoop.setScreenShareState);
+      sinon.assert.calledWithExactly(fakeMozLoop.setScreenShareState, "1234", true);
+    });
+
+    it("should set screen sharing inactive when the state is inactive", function() {
+      store.screenSharingState(new sharedActions.ScreenSharingState({
+        state: SCREEN_SHARE_STATES.INACTIVE
+      }));
+
+      sinon.assert.calledOnce(fakeMozLoop.setScreenShareState);
+      sinon.assert.calledWithExactly(fakeMozLoop.setScreenShareState, "1234", false);
+    });
   });
 
   describe("#receivingScreenShare", function() {
     it("should save the state", function() {
       store.receivingScreenShare(new sharedActions.ReceivingScreenShare({
         receiving: true
       }));
 
@@ -691,20 +714,30 @@ describe("loop.store.ActiveRoomStore", f
     });
   });
 
   describe("#windowUnload", function() {
     beforeEach(function() {
       store.setStoreState({
         roomState: ROOM_STATES.JOINED,
         roomToken: "fakeToken",
-        sessionToken: "1627384950"
+        sessionToken: "1627384950",
+        windowId: "1234"
       });
     });
 
+    it("should set screen sharing inactive", function() {
+      store.screenSharingState(new sharedActions.ScreenSharingState({
+        state: SCREEN_SHARE_STATES.INACTIVE
+      }));
+
+      sinon.assert.calledOnce(fakeMozLoop.setScreenShareState);
+      sinon.assert.calledWithExactly(fakeMozLoop.setScreenShareState, "1234", false);
+    });
+
     it("should reset the multiplexGum", function() {
       store.windowUnload();
 
       sinon.assert.calledOnce(fakeMultiplexGum.reset);
     });
 
     it("should disconnect from the servers via the sdk", function() {
       store.windowUnload();
--- a/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
+++ b/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
@@ -61,20 +61,19 @@ function onClipboardReady() {
 }
 
 let tests = {
 
   makeHistVisit: function(aCallback) {
     // need to add a history object
     let testURI1 = NetUtil.newURI(MOZURISPEC);
     isnot(testURI1, null, "testURI is not null");
-    addVisits(
-      {uri: testURI1, transition: PlacesUtils.history.TRANSITION_TYPED},
-      window,
-      aCallback);
+    PlacesTestUtils.addVisits(
+      {uri: testURI1, transition: PlacesUtils.history.TRANSITION_TYPED}
+      ).then(aCallback);
   },
 
   makeTag: function() {
     // create an initial tag to work with
     let bmId = add_bookmark(NetUtil.newURI(TEST_URL));
     ok(bmId > 0, "A bookmark was added");
     PlacesUtils.tagging.tagURI(NetUtil.newURI(TEST_URL), ["foo"]);
     let tags = PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(TEST_URL));
--- a/browser/components/places/tests/browser/browser_bookmarksProperties.js
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -413,21 +413,20 @@ gTests.push({
   desc: " Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog",
   sidebar: SIDEBAR_HISTORY_ID,
   action: ACTION_ADD,
   historyView: SIDEBAR_HISTORY_BYLASTVISITED_VIEW,
   window: null,
 
   setup: function(aCallback) {
     // Add a visit.
-    addVisits(
+    PlacesTestUtils.addVisits(
       {uri: PlacesUtils._uri(TEST_URL),
-        transition: PlacesUtils.history.TRANSITION_TYPED},
-      window,
-      aCallback);
+        transition: PlacesUtils.history.TRANSITION_TYPED}
+      ).then(aCallback);
   },
 
   selectNode: function(tree) {
     var visitNode = tree.view.nodeForTreeIndex(0);
     tree.selectNode(visitNode);
     is(tree.selectedNode.uri, TEST_URL, "The correct visit has been selected");
     is(tree.selectedNode.itemId, -1, "The selected node is not bookmarked");
   },
--- a/browser/components/places/tests/browser/browser_forgetthissite_single.js
+++ b/browser/components/places/tests/browser/browser_forgetthissite_single.js
@@ -11,17 +11,17 @@ function test() {
   // Add a history entry.
   let TEST_URIs = ["http://www.mozilla.org/test1", "http://www.mozilla.org/test2"];
   ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
   let places = [];
   TEST_URIs.forEach(function(TEST_URI) {
     places.push({uri: PlacesUtils._uri(TEST_URI),
                  transition: PlacesUtils.history.TRANSITION_TYPED});
   });
-  addVisits(places, window, function() {
+  PlacesTestUtils.addVisits(places).then(() => {
     testForgetThisSiteVisibility(1, function() {
       testForgetThisSiteVisibility(2, function() {
         // Cleanup
         PlacesTestUtils.clearHistory().then(finish);
       });
     });
   });
 
--- a/browser/components/places/tests/browser/browser_history_sidebar_search.js
+++ b/browser/components/places/tests/browser/browser_history_sidebar_search.js
@@ -38,17 +38,17 @@ function continue_test() {
   // Add some visited page.
   var time = Date.now();
   var pagesLength = pages.length;
   var places = [];
   for (var i = 0; i < pagesLength; i++) {
     places.push({uri: uri(pages[i]), visitDate: (time - i) * 1000,
                  transition: hs.TRANSITION_TYPED});
   }
-  addVisits(places, window, function() {
+  PlacesTestUtils.addVisits(places).then(() => {
     toggleSidebar("viewHistorySidebar", true);
   });
 
   sidebar.addEventListener("load", function() {
     sidebar.removeEventListener("load", arguments.callee, true);
     executeSoon(function() {
       // Set "by last visited" in the sidebar (sort by visit date descendind).
       sidebar.contentDocument.getElementById("bylastvisited").doCommand();
--- a/browser/components/places/tests/browser/browser_library_commands.js
+++ b/browser/components/places/tests/browser/browser_library_commands.js
@@ -14,17 +14,17 @@ registerCleanupFunction(function* () {
   yield PlacesUtils.bookmarks.eraseEverything();
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function* test_date_container() {
   let library = yield promiseLibrary();
   info("Ensure date containers under History cannot be cut but can be deleted");
 
-  yield promiseAddVisits(TEST_URI);
+  yield PlacesTestUtils.addVisits(TEST_URI);
 
   // Select and open the left pane "History" query.
   let PO = library.PlacesOrganizer;
 
   PO.selectLeftPaneQuery('History');
   isnot(PO._places.selectedNode, null, "We correctly selected History");
 
   // Check that both delete and cut commands are disabled, cause this is
--- a/browser/components/places/tests/browser/browser_library_infoBox.js
+++ b/browser/components/places/tests/browser/browser_library_infoBox.js
@@ -106,21 +106,20 @@ gTests.push({
       checkAddInfoFieldsNotCollapsed(PO);
       checkAddInfoFields(PO, "bookmark item");
 
       menuNode.containerOpen = false;
 
       PlacesTestUtils.clearHistory().then(nextTest);
     }
     // add a visit to browser history
-    addVisits(
+    PlacesTestUtils.addVisits(
       { uri: PlacesUtils._uri(TEST_URI), visitDate: Date.now() * 1000,
-        transition: PlacesUtils.history.TRANSITION_TYPED },
-      window,
-      addVisitsCallback);
+        transition: PlacesUtils.history.TRANSITION_TYPED }
+      ).then(addVisitsCallback);
   }
 });
 
 function checkInfoBoxSelected(PO) {
   is(getAndCheckElmtById("detailsDeck").selectedIndex, 1,
      "Selected element in detailsDeck is infoBox.");
 }
 
--- a/browser/components/places/tests/browser/browser_library_panel_leak.js
+++ b/browser/components/places/tests/browser/browser_library_panel_leak.js
@@ -40,16 +40,15 @@ function test() {
     organizer.close();
     // Clean up history.
     PlacesTestUtils.clearHistory().then(finish);
   }
 
   waitForExplicitFinish();
   // Add an history entry.
   ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
-  addVisits(
+  PlacesTestUtils.addVisits(
     {uri: PlacesUtils._uri(TEST_URI), visitDate: Date.now() * 1000,
-      transition: PlacesUtils.history.TRANSITION_TYPED},
-    window,
-    function() {
+      transition: PlacesUtils.history.TRANSITION_TYPED}
+    ).then(() => {
       openLibrary(onLibraryReady);
     });
 }
--- a/browser/components/places/tests/browser/browser_library_search.js
+++ b/browser/components/places/tests/browser/browser_library_search.js
@@ -164,23 +164,22 @@ function onLibraryAvailable() {
 
 function test() {
   waitForExplicitFinish();
 
   // Sanity:
   ok(PlacesUtils, "PlacesUtils in context");
 
   // Add visits, a bookmark and a tag.
-  addVisits(
+  PlacesTestUtils.addVisits(
     [{ uri: PlacesUtils._uri(TEST_URL), visitDate: Date.now() * 1000,
        transition: PlacesUtils.history.TRANSITION_TYPED },
      { uri: PlacesUtils._uri(TEST_DOWNLOAD_URL), visitDate: Date.now() * 1000,
-       transition: PlacesUtils.history.TRANSITION_DOWNLOAD }],
-    window,
-    function() {
+       transition: PlacesUtils.history.TRANSITION_DOWNLOAD }]
+    ).then(() => {
       PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                            PlacesUtils._uri(TEST_URL),
                                            PlacesUtils.bookmarks.DEFAULT_INDEX,
                                            "dummy");
       PlacesUtils.tagging.tagURI(PlacesUtils._uri(TEST_URL), ["dummyTag"]);
 
       gLibrary = openLibrary(onLibraryAvailable);
     });
--- a/browser/components/places/tests/browser/browser_sidebarpanels_click.js
+++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
@@ -48,21 +48,20 @@ function test() {
     treeName: BOOKMARKS_SIDEBAR_TREE_ID,
     desc: "Bookmarks sidebar test"
   });
 
   tests.push({
     init: function(aCallback) {
       // Add a history entry.
       let uri = PlacesUtils._uri(TEST_URL);
-      addVisits(
-        { uri: uri, visitDate: Date.now() * 1000,
-          transition: PlacesUtils.history.TRANSITION_TYPED },
-        window,
-        aCallback);
+      PlacesTestUtils.addVisits({
+        uri: uri, visitDate: Date.now() * 1000,
+        transition: PlacesUtils.history.TRANSITION_TYPED
+      }).then(aCallback);
     },
     prepare: function() {
       sidebar.contentDocument.getElementById("byvisited").doCommand();
     },
     selectNode: function(tree) {
       tree.selectNode(tree.view.nodeForTreeIndex(0));
       is(tree.selectedNode.uri, TEST_URL, "The correct visit has been selected");
       is(tree.selectedNode.itemId, -1, "The selected node is not bookmarked");
--- a/browser/components/places/tests/browser/head.js
+++ b/browser/components/places/tests/browser/head.js
@@ -109,75 +109,16 @@ function waitForAsyncUpdates(aCallback, 
     handleCompletion: function(aReason)
     {
       aCallback.apply(scope, args);
     }
   });
   commit.finalize();
 }
 
-/**
- * Asynchronously adds visits to a page, invoking a callback function when done.
- *
- * @param aPlaceInfo
- *        Can be an nsIURI, in such a case a single LINK visit will be added.
- *        Otherwise can be an object describing the visit to add, or an array
- *        of these objects:
- *          { uri: nsIURI of the page,
- *            transition: one of the TRANSITION_* from nsINavHistoryService,
- *            [optional] title: title of the page,
- *            [optional] visitDate: visit date in microseconds from the epoch
- *            [optional] referrer: nsIURI of the referrer for this visit
- *          }
- * @param [optional] aCallback
- *        Function to be invoked on completion.
- * @param [optional] aStack
- *        The stack frame used to report errors.
- */
-function addVisits(aPlaceInfo, aWindow, aCallback, aStack) {
-  let stack = aStack || Components.stack.caller;
-  let places = [];
-  if (aPlaceInfo instanceof Ci.nsIURI) {
-    places.push({ uri: aPlaceInfo });
-  }
-  else if (Array.isArray(aPlaceInfo)) {
-    places = places.concat(aPlaceInfo);
-  } else {
-    places.push(aPlaceInfo)
-  }
-
-  // Create mozIVisitInfo for each entry.
-  let now = Date.now();
-  for (let i = 0; i < places.length; i++) {
-    if (!places[i].title) {
-      places[i].title = "test visit for " + places[i].uri.spec;
-    }
-    places[i].visits = [{
-      transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK
-                                                         : places[i].transition,
-      visitDate: places[i].visitDate || (now++) * 1000,
-      referrerURI: places[i].referrer
-    }];
-  }
-
-  aWindow.PlacesUtils.asyncHistory.updatePlaces(
-    places,
-    {
-      handleError: function AAV_handleError() {
-        throw("Unexpected error in adding visit.");
-      },
-      handleResult: function () {},
-      handleCompletion: function UP_handleCompletion() {
-        if (aCallback)
-          aCallback();
-      }
-    }
-  );
-}
-
 function synthesizeClickOnSelectedTreeCell(aTree, aOptions) {
   let tbo = aTree.treeBoxObject;
   if (tbo.view.selection.count != 1)
      throw new Error("The test node should be successfully selected");
   // Get selection rowID.
   let min = {}, max = {};
   tbo.view.selection.getRangeAt(0, min, max);
   let rowID = min.value;
@@ -187,79 +128,16 @@ function synthesizeClickOnSelectedTreeCe
   var x = rect.x + rect.width / 2;
   var y = rect.y + rect.height / 2;
   // Simulate the click.
   EventUtils.synthesizeMouse(aTree.body, x, y, aOptions || {},
                              aTree.ownerDocument.defaultView);
 }
 
 /**
- * Asynchronously adds visits to a page.
- *
- * @param aPlaceInfo
- *        Can be an nsIURI, in such a case a single LINK visit will be added.
- *        Otherwise can be an object describing the visit to add, or an array
- *        of these objects:
- *          { uri: nsIURI of the page,
- *            transition: one of the TRANSITION_* from nsINavHistoryService,
- *            [optional] title: title of the page,
- *            [optional] visitDate: visit date in microseconds from the epoch
- *            [optional] referrer: nsIURI of the referrer for this visit
- *          }
- *
- * @return {Promise}
- * @resolves When all visits have been added successfully.
- * @rejects JavaScript exception.
- */
-function promiseAddVisits(aPlaceInfo)
-{
-  let deferred = Promise.defer();
-  let places = [];
-  if (aPlaceInfo instanceof Ci.nsIURI) {
-    places.push({ uri: aPlaceInfo });
-  }
-  else if (Array.isArray(aPlaceInfo)) {
-    places = places.concat(aPlaceInfo);
-  } else {
-    places.push(aPlaceInfo)
-  }
-
-  // Create mozIVisitInfo for each entry.
-  let now = Date.now();
-  for (let i = 0; i < places.length; i++) {
-    if (!places[i].title) {
-      places[i].title = "test visit for " + places[i].uri.spec;
-    }
-    places[i].visits = [{
-      transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK
-                                                         : places[i].transition,
-      visitDate: places[i].visitDate || (now++) * 1000,
-      referrerURI: places[i].referrer
-    }];
-  }
-
-  PlacesUtils.asyncHistory.updatePlaces(
-    places,
-    {
-      handleError: function AAV_handleError(aResultCode, aPlaceInfo) {
-        let ex = new Components.Exception("Unexpected error in adding visits.",
-                                          aResultCode);
-        deferred.reject(ex);
-      },
-      handleResult: function () {},
-      handleCompletion: function UP_handleCompletion() {
-        deferred.resolve();
-      }
-    }
-  );
-
-  return deferred.promise;
-}
-
-/**
  * Asynchronously check a url is visited.
  *
  * @param aURI The URI.
  * @return {Promise}
  * @resolves When the check has been added successfully.
  * @rejects JavaScript exception.
  */
 function promiseIsURIVisited(aURI) {
--- a/browser/components/places/tests/chrome/head.js
+++ b/browser/components/places/tests/chrome/head.js
@@ -1,63 +1,7 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
   "resource://testing-common/PlacesTestUtils.jsm");
-
-/**
- * Asynchronously adds visits to a page, invoking a callback function when done.
- *
- * @param aPlaceInfo
- *        Can be an nsIURI, in such a case a single LINK visit will be added.
- *        Otherwise can be an object describing the visit to add, or an array
- *        of these objects:
- *          { uri: nsIURI of the page,
- *            transition: one of the TRANSITION_* from nsINavHistoryService,
- *            [optional] title: title of the page,
- *            [optional] visitDate: visit date in microseconds from the epoch
- *            [optional] referrer: nsIURI of the referrer for this visit
- *          }
- * @param [optional] aCallback
- *        Function to be invoked on completion.
- */
-function addVisits(aPlaceInfo, aCallback) {
-  let places = [];
-  if (aPlaceInfo instanceof Ci.nsIURI) {
-    places.push({ uri: aPlaceInfo });
-  }
-  else if (Array.isArray(aPlaceInfo)) {
-    places = places.concat(aPlaceInfo);
-  } else {
-    places.push(aPlaceInfo)
-  }
-
-  // Create mozIVisitInfo for each entry.
-  let now = Date.now();
-  for (let i = 0; i < places.length; i++) {
-    if (!places[i].title) {
-      places[i].title = "test visit for " + places[i].uri.spec;
-    }
-    places[i].visits = [{
-      transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK
-                                                         : places[i].transition,
-      visitDate: places[i].visitDate || (now++) * 1000,
-      referrerURI: places[i].referrer
-    }];
-  }
-
-  PlacesUtils.asyncHistory.updatePlaces(
-    places,
-    {
-      handleError: function AAV_handleError() {
-        throw("Unexpected error in adding visit.");
-      },
-      handleResult: function () {},
-      handleCompletion: function UP_handleCompletion() {
-        if (aCallback)
-          aCallback();
-      }
-    }
-  );
-}
--- a/browser/components/places/tests/chrome/test_bug549192.xul
+++ b/browser/components/places/tests/chrome/test_bug549192.xul
@@ -53,17 +53,17 @@
         let places =
           [{ uri: Services.io.newURI("http://example.tld/", null, null),
              visitDate: ++vtime, transition: ttype },
            { uri: Services.io.newURI("http://example2.tld/", null, null),
              visitDate: ++vtime, transition: ttype },
            { uri: Services.io.newURI("http://example3.tld/", null, null),
              visitDate: ++vtime, transition: ttype }];
 
-        yield new Promise(resolve => addVisits(places, resolve));
+        yield PlacesTestUtils.addVisits(places);
 
         // Make a history query.
         let query = PlacesUtils.history.getNewQuery();
         let opts = PlacesUtils.history.getNewQueryOptions();
         opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
         let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
 
         // Setup the places tree contents.
@@ -81,17 +81,17 @@
           is(node.uri, places[rc - i - 1].uri.spec,
              "Found expected node at position " + i + ".");
         }
 
         is(rc, 3, "Found expected number of rows.");
 
         // First check live-update of the view when adding visits.
         places.forEach(place => place.visitDate = ++vtime);
-        yield new Promise(resolve => addVisits(places, resolve));
+        yield PlacesTestUtils.addVisits(places);
 
         for (let i = 0; i < rc; i++) {
           selection.select(i);
           let node = tree.selectedNode;
           is(node.uri, places[rc - i - 1].uri.spec,
              "Found expected node at position " + i + ".");
         }
 
--- a/browser/components/places/tests/chrome/test_bug549491.xul
+++ b/browser/components/places/tests/chrome/test_bug549491.xul
@@ -45,21 +45,20 @@
      */
 
     function runTest() {
       SimpleTest.waitForExplicitFinish();
 
       Task.spawn(function* () {
         yield PlacesTestUtils.clearHistory();
 
-        let visits = {
+        yield PlacesTestUtils.addVisits({
           uri: Services.io.newURI("http://example.tld/", null, null),
           transition: PlacesUtils.history.TRANSITION_TYPED
-        };
-        yield new Promise(resolve => addVisits(visits, resolve));
+        });
 
         // Make a history query.
         let query = PlacesUtils.history.getNewQuery();
         let opts = PlacesUtils.history.getNewQueryOptions();
         let queryURI = PlacesUtils.history.queriesToQueryString([query], 1, opts);
 
         // Setup the places tree contents.
         let tree = document.getElementById("tree");
--- a/browser/components/places/tests/chrome/test_treeview_date.xul
+++ b/browser/components/places/tests/chrome/test_treeview_date.xul
@@ -66,27 +66,27 @@
         let midnight = new Date();
         midnight.setHours(0);
         midnight.setMinutes(0);
         midnight.setSeconds(0);
         midnight.setMilliseconds(0);
 
         // Add a visit 1ms before midnight, a visit at midnight, and
         // a visit 1ms after midnight.
-        yield new Promise(resolve => addVisits(
-          [{uri: uri("http://before.midnight.com/"),
-             visitDate: (midnight.getTime() - 1) * 1000,
-             transition: PlacesUtils.history.TRANSITION_TYPED},
-           {uri: uri("http://at.midnight.com/"),
-             visitDate: (midnight.getTime()) * 1000,
-             transition: PlacesUtils.history.TRANSITION_TYPED},
-           {uri: uri("http://after.midnight.com/"),
-             visitDate: (midnight.getTime() + 1) * 1000,
-             transition: PlacesUtils.history.TRANSITION_TYPED}],
-          resolve));
+        yield PlacesTestUtils.addVisits([
+          {uri: uri("http://before.midnight.com/"),
+           visitDate: (midnight.getTime() - 1) * 1000,
+           transition: PlacesUtils.history.TRANSITION_TYPED},
+          {uri: uri("http://at.midnight.com/"),
+           visitDate: (midnight.getTime()) * 1000,
+           transition: PlacesUtils.history.TRANSITION_TYPED},
+          {uri: uri("http://after.midnight.com/"),
+           visitDate: (midnight.getTime() + 1) * 1000,
+           transition: PlacesUtils.history.TRANSITION_TYPED}
+        ]);
 
         // add a bookmark to the midnight visit
         let bm = yield PlacesUtils.bookmarks.insert({
           parentGuid: PlacesUtils.bookmarks.toolbarGuid,
           index: PlacesUtils.bookmarks.DEFAULT_INDEX,
           url: "http://at.midnight.com/",
           title: "A bookmark at midnight",
           type: PlacesUtils.bookmarks.TYPE_BOOKMARK
--- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -102,18 +102,20 @@ add_task(function test_execute() {
   Services.prefs.setBoolPref("privacy.clearOnShutdown.passwords", true);
   Services.prefs.setBoolPref("privacy.clearOnShutdown.sessions", true);
   Services.prefs.setBoolPref("privacy.clearOnShutdown.siteSettings", true);
 
   Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
 
   print("Add visits.");
   for (let aUrl of URIS) {
-    yield promiseAddVisits({uri: uri(aUrl), visitDate: timeInMicroseconds++,
-                            transition: PlacesUtils.history.TRANSITION_TYPED})
+    yield PlacesTestUtils.addVisits({
+      uri: uri(aUrl), visitDate: timeInMicroseconds++,
+      transition: PlacesUtils.history.TRANSITION_TYPED
+    });
   }
   print("Add cache.");
   storeCache(FTP_URL, "testData");
 });
 
 function run_test_continue()
 {
   print("Simulate and wait shutdown.");
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -338,16 +338,21 @@ EngineStore.prototype = {
     for (var i = 0; i < this._defaultEngines.length; ++i) {
       var e = this._defaultEngines[i];
 
       // If the engine is already in the list, just move it.
       if (this._engines.some(this._isSameEngine, e)) {
         this.moveEngine(this._getEngineByName(e.name), i);
       } else {
         // Otherwise, add it back to our internal store
+
+        // The search service removes the alias when an engine is hidden,
+        // so clear any alias we may have cached before unhiding the engine.
+        e.alias = "";
+
         this._engines.splice(i, 0, e);
         let engine = e.originalEngine;
         engine.hidden = false;
         Services.search.moveEngine(engine, i);
         added++;
       }
     }
     gSearchPane.showRestoreDefaults(false);
--- a/browser/components/preferences/search.js
+++ b/browser/components/preferences/search.js
@@ -367,16 +367,21 @@ EngineStore.prototype = {
     for (var i = 0; i < this._defaultEngines.length; ++i) {
       var e = this._defaultEngines[i];
 
       // If the engine is already in the list, just move it.
       if (this._engines.some(this._isSameEngine, e)) {
         this.moveEngine(this._getEngineByName(e.name), i);
       } else {
         // Otherwise, add it back to our internal store
+
+        // The search service removes the alias when an engine is hidden,
+        // so clear any alias we may have cached before unhiding the engine.
+        e.alias = "";
+
         this._engines.splice(i, 0, e);
         this._ops.push(new EngineUnhideOp(e, i));
         added++;
       }
     }
     gSearchPane.showRestoreDefaults(false);
     gSearchPane.buildDefaultEngineDropDown();
     return added;
--- a/browser/components/preferences/tests/browser_chunk_permissions.js
+++ b/browser/components/preferences/tests/browser_chunk_permissions.js
@@ -29,17 +29,17 @@ function test() {
   registerCleanupFunction(cleanUp);
   setup(function() {
     runNextTest();
   });
 }
 
 function setup(aCallback) {
   // add test history visit
-  addVisits(TEST_URI_1, function() {
+  PlacesTestUtils.addVisits(TEST_URI_1).then(() => {
     // set permissions ourselves to avoid problems with different defaults
     // from test harness configuration
     for (let type in TEST_PERMS) {
       if (type == "password") {
         Services.logins.setLoginSavingEnabled(TEST_URI_2.prePath, true);
       } else {
         // set permissions on a site without history visits to test enumerateServices
         Services.perms.add(TEST_URI_2, type, TEST_PERMS[type]);
--- a/browser/components/preferences/tests/browser_permissions.js
+++ b/browser/components/preferences/tests/browser_permissions.js
@@ -39,17 +39,17 @@ const NO_GLOBAL_ALLOW = [
 // number of managed permissions in the interface
 const TEST_PERMS_COUNT = 8;
 
 function test() {
   waitForExplicitFinish();
   registerCleanupFunction(cleanUp);
 
   // add test history visit
-  addVisits(TEST_URI_1, function() {
+  PlacesTestUtils.addVisits(TEST_URI_1).then(() => {
     // set permissions ourselves to avoid problems with different defaults
     // from test harness configuration
     for (let type in TEST_PERMS) {
       if (type == "password") {
         Services.logins.setLoginSavingEnabled(TEST_URI_2.prePath, true);
       } else {
         // set permissions on a site without history visits to test enumerateServices
         Services.perms.addFromPrincipal(TEST_PRINCIPAL_2, type, TEST_PERMS[type]);
--- a/browser/components/preferences/tests/head.js
+++ b/browser/components/preferences/tests/head.js
@@ -1,60 +1,4 @@
 Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
   "resource://testing-common/PlacesTestUtils.jsm");
-
-/**
- * Asynchronously adds visits to a page, invoking a callback function when done.
- *
- * @param aPlaceInfo
- *        Can be an nsIURI, in such a case a single LINK visit will be added.
- *        Otherwise can be an object describing the visit to add, or an array
- *        of these objects:
- *          { uri: nsIURI of the page,
- *            transition: one of the TRANSITION_* from nsINavHistoryService,
- *            [optional] title: title of the page,
- *            [optional] visitDate: visit date in microseconds from the epoch
- *            [optional] referrer: nsIURI of the referrer for this visit
- *          }
- * @param [optional] aCallback
- *        Function to be invoked on completion.
- */
-function addVisits(aPlaceInfo, aCallback) {
-  let places = [];
-  if (aPlaceInfo instanceof Ci.nsIURI) {
-    places.push({ uri: aPlaceInfo });
-  }
-  else if (Array.isArray(aPlaceInfo)) {
-    places = places.concat(aPlaceInfo);
-  } else {
-    places.push(aPlaceInfo)
-  }
-
-  // Create mozIVisitInfo for each entry.
-  let now = Date.now();
-  for (let i = 0; i < places.length; i++) {
-    if (!places[i].title) {
-      places[i].title = "test visit for " + places[i].uri.spec;
-    }
-    places[i].visits = [{
-      transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK
-                                                         : places[i].transition,
-      visitDate: places[i].visitDate || (now++) * 1000,
-      referrerURI: places[i].referrer
-    }];
-  }
-
-  PlacesUtils.asyncHistory.updatePlaces(
-    places,
-    {
-      handleError: function AAV_handleError() {
-        throw("Unexpected error in adding visit.");
-      },
-      handleResult: function () {},
-      handleCompletion: function UP_handleCompletion() {
-        if (aCallback)
-          aCallback();
-      }
-    }
-  );
-}
\ No newline at end of file
--- a/browser/components/safebrowsing/content/test/browser.ini
+++ b/browser/components/safebrowsing/content/test/browser.ini
@@ -1,11 +1,11 @@
 [DEFAULT]
 support-files = head.js
 
 [browser_bug400731.js]
-skip-if = (os == "win") || e10s
+skip-if = e10s
 [browser_bug415846.js]
 skip-if = true
 # Disabled because it seems to now touch network resources
 # skip-if = os == "mac"
 # Disabled on Mac because of its bizarre special-and-unique
 # snowflake of a help menu.
--- a/browser/devtools/framework/target.js
+++ b/browser/devtools/framework/target.js
@@ -588,17 +588,20 @@ TabTarget.prototype = {
       if (this.isLocalTab) {
         // We started with a local tab and created the client ourselves, so we
         // should close it.
         this._client.close(cleanupAndResolve);
       } else {
         // The client was handed to us, so we are not responsible for closing
         // it. We just need to detach from the tab, if already attached.
         if (this.activeTab) {
-          this.activeTab.detach(cleanupAndResolve);
+          // |detach| may fail if the connection is already dead, so proceed
+          // cleanup directly after this.
+          this.activeTab.detach();
+          cleanupAndResolve();
         } else {
           cleanupAndResolve();
         }
       }
     }
 
     return this._destroyer.promise;
   },
--- a/browser/devtools/framework/toolbox-hosts.js
+++ b/browser/devtools/framework/toolbox-hosts.js
@@ -282,16 +282,19 @@ function CustomHost(hostTab, options) {
 
 CustomHost.prototype = {
   type: "custom",
 
   _sendMessageToTopWindow: function CH__sendMessageToTopWindow(msg, data) {
     // It's up to the custom frame owner (parent window) to honor
     // "close" or "raise" instructions.
     let topWindow = this.frame.ownerDocument.defaultView;
+    if (!topWindow) {
+      return;
+    }
     let json = {name:"toolbox-" + msg, uid: this.uid};
     if (data) {
       json.data = data;
     }
     topWindow.postMessage(JSON.stringify(json), "*");
   },
 
   /**
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -42,16 +42,17 @@ loader.lazyGetter(this, "toolboxStrings"
       Services.console.logStringMessage("Error reading '" + name + "'");
       return null;
     }
   };
 });
 
 loader.lazyGetter(this, "Selection", () => require("devtools/framework/selection").Selection);
 loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront);
+loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/toolkit/DevToolsUtils");
 
 // White-list buttons that can be toggled to prevent adding prefs for
 // addons that have manually inserted toolbarbuttons into DOM.
 // (By default, supported target is only local tab)
 const ToolboxButtons = [
   { id: "command-button-pick",
     isTargetSupported: target =>
       target.getTrait("highlightable")
@@ -1654,18 +1655,21 @@ Toolbox.prototype = {
       this._requisition.destroy();
     }
     this._telemetry.toolClosed("toolbox");
     this._telemetry.destroy();
 
     // Finish all outstanding tasks (which means finish destroying panels and
     // then destroying the host, successfully or not) before destroying the
     // target.
-    this._destroyer = promise.all(outstanding)
-      .then(() => this.destroyHost()).then(null, console.error).then(() => {
+    this._destroyer = DevToolsUtils.settleAll(outstanding)
+                                   .catch(console.error)
+                                   .then(() => this.destroyHost())
+                                   .catch(console.error)
+                                   .then(() => {
       // Targets need to be notified that the toolbox is being torn down.
       // This is done after other destruction tasks since it may tear down
       // fronts and the debugger transport which earlier destroy methods may
       // require to complete.
       if (!this._target) {
         return null;
       }
       let target = this._target;
--- a/browser/devtools/inspector/test/browser.ini
+++ b/browser/devtools/inspector/test/browser.ini
@@ -1,10 +1,9 @@
 [DEFAULT]
-skip-if = e10s # Bug 1074836 - inspector tests disabled with e10s
 subsuite = devtools
 support-files =
   doc_frame_script.js
   doc_inspector_breadcrumbs.html
   doc_inspector_delete-selected-node-01.html
   doc_inspector_delete-selected-node-02.html
   doc_inspector_gcli-inspect-command.html
   doc_inspector_highlight_after_transition.html
@@ -27,16 +26,17 @@ support-files =
 [browser_inspector_breadcrumbs.js]
 [browser_inspector_breadcrumbs_highlight_hover.js]
 [browser_inspector_delete-selected-node-01.js]
 [browser_inspector_delete-selected-node-02.js]
 [browser_inspector_delete-selected-node-03.js]
 [browser_inspector_destroy-after-navigation.js]
 [browser_inspector_destroy-before-ready.js]
 [browser_inspector_gcli-inspect-command.js]
+skip-if = e10s # GCLI isn't e10s compatible. See bug 1128988.
 [browser_inspector_highlighter-01.js]
 [browser_inspector_highlighter-02.js]
 [browser_inspector_highlighter-03.js]
 [browser_inspector_highlighter-04.js]
 [browser_inspector_highlighter-by-type.js]
 [browser_inspector_highlighter-comments.js]
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
@@ -62,16 +62,17 @@ support-files =
 [browser_inspector_picker-stop-on-destroy.js]
 [browser_inspector_picker-stop-on-tool-change.js]
 [browser_inspector_pseudoclass-lock.js]
 [browser_inspector_pseudoclass-menu.js]
 [browser_inspector_reload-01.js]
 [browser_inspector_reload-02.js]
 [browser_inspector_remove-iframe-during-load.js]
 [browser_inspector_scrolling.js]
+skip-if = e10s # Test synthesize scrolling events in content. Also, see bug 1035661.
 [browser_inspector_search-01.js]
 [browser_inspector_search-02.js]
 [browser_inspector_search-03.js]
 [browser_inspector_select-docshell.js]
 [browser_inspector_select-last-selected.js]
 [browser_inspector_search-navigation.js]
 [browser_inspector_sidebarstate.js]
 [browser_inspector_switch-to-inspector-on-pick.js]
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-03.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-03.js
@@ -60,13 +60,13 @@ add_task(function* () {
   is(highlightedNode, iframeBodyNode, "highlighter shows the right node");
   yield isNodeCorrectlyHighlighted(iframeBodyNode, toolbox);
 
   info("Waiting for the element picker to deactivate.");
   yield inspector.toolbox.highlighterUtils.stopPicker();
 
   function moveMouseOver(node, x, y) {
     info("Waiting for element " + node + " to be highlighted");
-    executeInContent("Test:SynthesizeMouse", {x, y, type: "mousemove"},
+    executeInContent("Test:SynthesizeMouse", {x, y, options: {type: "mousemove"}},
                      {node}, false);
     return inspector.toolbox.once("picker-node-hovered");
   }
 });
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-04.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-04.js
@@ -29,19 +29,15 @@ const ELEMENTS = ["box-model-root",
 add_task(function*() {
   let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
 
   info("Show the box-model highlighter");
   let divFront = yield getNodeFront("div", inspector);
   yield toolbox.highlighter.showBoxModel(divFront);
 
   for (let id of ELEMENTS) {
-    let {data: foundId} = yield executeInContent("Test:GetHighlighterAttribute", {
-      nodeID: id,
-      name: "id",
-      actorID: getHighlighterActorID(toolbox)
-    });
+    let foundId = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "id");
     is(foundId, id, "Element " + id + " found");
   }
 
   info("Hide the box-model highlighter");
   yield toolbox.highlighter.hideBoxModel();
 });
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-csstransform_01.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-csstransform_01.js
@@ -26,92 +26,106 @@ add_task(function*() {
   yield linesLinkThePolygons(highlighter, inspector);
 
   yield highlighter.finalize();
 });
 
 function* isHiddenByDefault(highlighterFront, inspector) {
   info("Checking that the highlighter is hidden by default");
 
-  let hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
+  let hidden = yield getHighlighterNodeAttribute(highlighterFront,
+    "css-transform-elements", "hidden");
   ok(hidden, "The highlighter is hidden by default");
 }
 
 function* has2PolygonsAnd4Lines(highlighterFront, inspector) {
   info("Checking that the highlighter is made up of 4 lines and 2 polygons");
 
-  let value = yield getAttribute("css-transform-untransformed", "class", highlighterFront);
+  let value = yield getHighlighterNodeAttribute(highlighterFront,
+    "css-transform-untransformed", "class");
   is(value, "css-transform-untransformed", "The untransformed polygon exists");
 
-  value = yield getAttribute("css-transform-transformed", "class", highlighterFront);
+  value = yield getHighlighterNodeAttribute(highlighterFront,
+    "css-transform-transformed", "class");
   is(value, "css-transform-transformed", "The transformed polygon exists");
 
   for (let nb of ["1", "2", "3", "4"]) {
-    value = yield getAttribute("css-transform-line" + nb, "class", highlighterFront);
+    value = yield getHighlighterNodeAttribute(highlighterFront,
+      "css-transform-line" + nb, "class");
     is(value, "css-transform-line", "The line " + nb + " exists");
   }
 }
 
 function* isNotShownForUntransformed(highlighterFront, inspector) {
   info("Asking to show the highlighter on the untransformed test node");
 
   let node = yield getNodeFront("#untransformed", inspector);
   yield highlighterFront.show(node);
 
-  let hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
+  let hidden = yield getHighlighterNodeAttribute(highlighterFront,
+    "css-transform-elements", "hidden");
   ok(hidden, "The highlighter is still hidden");
 }
 
 function* isNotShownForInline(highlighterFront, inspector) {
   info("Asking to show the highlighter on the inline test node");
 
   let node = yield getNodeFront("#inline", inspector);
   yield highlighterFront.show(node);
 
-  let hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
+  let hidden = yield getHighlighterNodeAttribute(highlighterFront,
+    "css-transform-elements", "hidden");
   ok(hidden, "The highlighter is still hidden");
 }
 
 function* isVisibleWhenShown(highlighterFront, inspector) {
   info("Asking to show the highlighter on the test node");
 
   let node = yield getNodeFront("#transformed", inspector);
   yield highlighterFront.show(node);
 
-  let hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
+  let hidden = yield getHighlighterNodeAttribute(highlighterFront,
+    "css-transform-elements", "hidden");
   ok(!hidden, "The highlighter is visible");
 
   info("Hiding the highlighter");
   yield highlighterFront.hide();
 
-  hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
+  hidden = yield getHighlighterNodeAttribute(highlighterFront,
+    "css-transform-elements", "hidden");
   ok(hidden, "The highlighter is hidden");
 }
 
 function* linesLinkThePolygons(highlighterFront, inspector) {
   info("Showing the highlighter on the transformed node");
 
   let node = yield getNodeFront("#transformed", inspector);
   yield highlighterFront.show(node);
 
   info("Checking that the 4 lines do link the 2 shape's corners");
 
   let lines = [];
   for (let nb of ["1", "2", "3", "4"]) {
-    let x1 = yield getAttribute("css-transform-line" + nb, "x1", highlighterFront);
-    let y1 = yield getAttribute("css-transform-line" + nb, "y1", highlighterFront);
-    let x2 = yield getAttribute("css-transform-line" + nb, "x2", highlighterFront);
-    let y2 = yield getAttribute("css-transform-line" + nb, "y2", highlighterFront);
+    let x1 = yield getHighlighterNodeAttribute(highlighterFront,
+      "css-transform-line" + nb, "x1");
+    let y1 = yield getHighlighterNodeAttribute(highlighterFront,
+      "css-transform-line" + nb, "y1");
+    let x2 = yield getHighlighterNodeAttribute(highlighterFront,
+      "css-transform-line" + nb, "x2");
+    let y2 = yield getHighlighterNodeAttribute(highlighterFront,
+      "css-transform-line" + nb, "y2");
     lines.push({x1, y1, x2, y2});
   }
 
-  let points1 = yield getAttribute("css-transform-untransformed", "points", highlighterFront);
+  let points1 = yield getHighlighterNodeAttribute(highlighterFront,
+    "css-transform-untransformed", "points");
   points1 = points1.split(" ");
 
-  let points2 = yield getAttribute("css-transform-transformed", "points", highlighterFront);
+  let points2 = yield getHighlighterNodeAttribute(highlighterFront,
+    "css-transform-transformed", "points");
   points2 = points2.split(" ");
 
   for (let i = 0; i < lines.length; i++) {
     info("Checking line nb " + i);
     let line = lines[i];
 
     let p1 = points1[i].split(",");
     is(p1[0], line.x1, "line " + i + "'s first point matches the untransformed x coordinate");
@@ -119,14 +133,8 @@ function* linesLinkThePolygons(highlight
 
     let p2 = points2[i].split(",");
     is(p2[0], line.x2, "line " + i + "'s first point matches the transformed x coordinate");
     is(p2[1], line.y2, "line " + i + "'s first point matches the transformed y coordinate");
   }
 
   yield highlighterFront.hide();
 }
-
-function* getAttribute(nodeID, name, {actorID}) {
-  let {data} = yield executeInContent("Test:GetHighlighterAttribute",
-    {nodeID, name, actorID});
-  return data;
-}
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-csstransform_02.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-csstransform_02.js
@@ -30,17 +30,18 @@ add_task(function*() {
   let nodeFront = yield getNodeFront("#test-node", inspector);
 
   info("Displaying the transform highlighter on test node");
   yield highlighter.show(nodeFront);
 
   let {data} = yield executeInContent("Test:GetAllAdjustedQuads", null, {node});
   let expected = data.border;
 
-  let points = yield getAttribute("css-transform-transformed", "points", highlighter);
+  let points = yield getHighlighterNodeAttribute(highlighter,
+    "css-transform-transformed", "points");
   let polygonPoints = points.split(" ").map(p => {
     return {
       x: +p.substring(0, p.indexOf(",")),
       y: +p.substring(p.indexOf(",")+1)
     };
   });
 
   for (let i = 1; i < 5; i ++) {
@@ -49,14 +50,8 @@ add_task(function*() {
     is(polygonPoints[i - 1].y, expected["p" + i].y,
       "p" + i + " y coordinate is correct");
   }
 
   info("Hiding the transform highlighter");
   yield highlighter.hide();
   yield highlighter.finalize();
 });
-
-function* getAttribute(nodeID, name, {actorID}) {
-  let {data} = yield executeInContent("Test:GetHighlighterAttribute",
-    {nodeID, name, actorID});
-  return data;
-}
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-hover_02.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-hover_02.js
@@ -16,18 +16,21 @@ add_task(function*() {
   info("hover over the <p> line in the markup-view so that it's the currently hovered node");
   yield hoverContainer("p", inspector);
 
   info("select the <p> markup-container line by clicking");
   yield clickContainer("p", inspector);
   let isVisible = yield isHighlighting(toolbox);
   ok(isVisible, "the highlighter is shown");
 
+  info("listen to the highlighter's hidden event");
+  let onHidden = waitForHighlighterEvent("hidden", toolbox.highlighter);
   info("mouse-leave the markup-view");
   yield mouseLeaveMarkupView(inspector);
+  yield onHidden;
   isVisible = yield isHighlighting(toolbox);
   ok(!isVisible, "the highlighter is hidden after mouseleave");
 
   info("hover over the <p> line again, which is still selected");
   yield hoverContainer("p", inspector);
   isVisible = yield isHighlighting(toolbox);
   ok(isVisible, "the highlighter is visible again");
 });
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-iframes.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-iframes.js
@@ -49,13 +49,15 @@ add_task(function*() {
 
   is(inspector.breadcrumbs.nodeHierarchy.length, 9, "Breadcrumbs have 9 items.");
 
   info("Waiting for element picker to deactivate.");
   yield inspector.toolbox.highlighterUtils.stopPicker();
 
   function moveMouseOver(node) {
     info("Waiting for element " + node + " to be highlighted");
-    executeInContent("Test:SynthesizeMouse", {type: "mousemove", center: true},
-                     {node}, false);
+    executeInContent("Test:SynthesizeMouse", {
+      options: {type: "mousemove"},
+      center: true
+    }, {node}, false);
     return inspector.toolbox.once("picker-node-hovered");
   }
 });
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-options.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-options.js
@@ -16,118 +16,142 @@ const TEST_URL = "data:text/html;charset
 // - options: json object to be passed as options to the highlighter.
 // - checkHighlighter: a generator (async) function that should check the
 //   highlighter is correct.
 const TEST_DATA = [
   {
     desc: "Guides and infobar should be shown by default",
     options: {},
     checkHighlighter: function*(toolbox) {
-      let hidden = yield getAttribute("box-model-nodeinfobar-container", "hidden", toolbox);
+      let hidden = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-nodeinfobar-container", "hidden");
       ok(!hidden, "Node infobar is visible");
 
-      hidden = yield getAttribute("box-model-elements", "hidden", toolbox);
+      hidden = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-elements", "hidden");
       ok(!hidden, "SVG container is visible");
 
       for (let side of ["top", "right", "bottom", "left"]) {
-        hidden = yield getAttribute("box-model-guide-" + side, "hidden", toolbox);
+        hidden = yield getHighlighterNodeAttribute(toolbox.highlighter,
+          "box-model-guide-" + side, "hidden");
         ok(!hidden, side + " guide is visible");
       }
     }
   },
   {
     desc: "All regions should be shown by default",
     options: {},
     checkHighlighter: function*(toolbox) {
       for (let region of ["margin", "border", "padding", "content"]) {
-        let points = yield getAttribute("box-model-" + region, "points", toolbox);
+        let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+          "box-model-" + region, "points");
         ok(points, "Region " + region + " has set coordinates");
       }
     }
   },
   {
     desc: "Guides can be hidden",
     options: {hideGuides: true},
     checkHighlighter: function*(toolbox) {
       for (let side of ["top", "right", "bottom", "left"]) {
-        let hidden = yield getAttribute("box-model-guide-" + side, "hidden", toolbox);
+        let hidden = yield getHighlighterNodeAttribute(toolbox.highlighter,
+          "box-model-guide-" + side, "hidden");
         is(hidden, "true", side + " guide has been hidden");
       }
     }
   },
   {
     desc: "Infobar can be hidden",
     options: {hideInfoBar: true},
     checkHighlighter: function*(toolbox) {
-      let hidden = yield getAttribute("box-model-nodeinfobar-container", "hidden", toolbox);
+      let hidden = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-nodeinfobar-container", "hidden");
       is(hidden, "true", "nodeinfobar has been hidden");
     }
   },
   {
     desc: "One region only can be shown (1)",
     options: {showOnly: "content"},
     checkHighlighter: function*(toolbox) {
-      let points = yield getAttribute("box-model-margin", "points", toolbox);
+      let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-margin", "points");
       ok(!points, "margin region is hidden");
 
-      points = yield getAttribute("box-model-border", "points", toolbox);
+      points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-border", "points");
       ok(!points, "border region is hidden");
 
-      points = yield getAttribute("box-model-padding", "points", toolbox);
+      points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-padding", "points");
       ok(!points, "padding region is hidden");
 
-      points = yield getAttribute("box-model-content", "points", toolbox);
+      points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-content", "points");
       ok(points, "content region is shown");
     }
   },
   {
     desc: "One region only can be shown (2)",
     options: {showOnly: "margin"},
     checkHighlighter: function*(toolbox) {
-      let points = yield getAttribute("box-model-margin", "points", toolbox);
+      let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-margin", "points");
       ok(points, "margin region is shown");
 
-      points = yield getAttribute("box-model-border", "points", toolbox);
+      points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-border", "points");
       ok(!points, "border region is hidden");
 
-      points = yield getAttribute("box-model-padding", "points", toolbox);
+      points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-padding", "points");
       ok(!points, "padding region is hidden");
 
-      points = yield getAttribute("box-model-content", "points", toolbox);
+      points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-content", "points");
       ok(!points, "content region is hidden");
     }
   },
   {
     desc: "Guides can be drawn around a given region (1)",
     options: {region: "padding"},
     checkHighlighter: function*(toolbox) {
-      let topY1 = yield getAttribute("box-model-guide-top", "y1", toolbox);
-      let rightX1 = yield getAttribute("box-model-guide-right", "x1", toolbox);
-      let bottomY1 = yield getAttribute("box-model-guide-bottom", "y1", toolbox);
-      let leftX1 = yield getAttribute("box-model-guide-left", "x1", toolbox);
+      let topY1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-guide-top", "y1");
+      let rightX1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-guide-right", "x1");
+      let bottomY1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-guide-bottom", "y1");
+      let leftX1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-guide-left", "x1");
 
-      let points = yield getAttribute("box-model-padding", "points", toolbox);
+      let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-padding", "points");
       points = points.split(" ").map(xy => xy.split(","));
 
       is(Math.ceil(topY1), points[0][1], "Top guide's y1 is correct");
       is(Math.floor(rightX1), points[1][0], "Right guide's x1 is correct");
       is(Math.floor(bottomY1), points[2][1], "Bottom guide's y1 is correct");
       is(Math.ceil(leftX1), points[3][0], "Left guide's x1 is correct");
     }
   },
   {
     desc: "Guides can be drawn around a given region (2)",
     options: {region: "margin"},
     checkHighlighter: function*(toolbox) {
-      let topY1 = yield getAttribute("box-model-guide-top", "y1", toolbox);
-      let rightX1 = yield getAttribute("box-model-guide-right", "x1", toolbox);
-      let bottomY1 = yield getAttribute("box-model-guide-bottom", "y1", toolbox);
-      let leftX1 = yield getAttribute("box-model-guide-left", "x1", toolbox);
+      let topY1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-guide-top", "y1");
+      let rightX1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-guide-right", "x1");
+      let bottomY1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-guide-bottom", "y1");
+      let leftX1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-guide-left", "x1");
 
-      let points = yield getAttribute("box-model-margin", "points", toolbox);
+      let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+        "box-model-margin", "points");
 
       points = points.split(" ").map(xy => xy.split(","));
       is(Math.ceil(topY1), points[0][1], "Top guide's y1 is correct");
       is(Math.floor(rightX1), points[1][0], "Right guide's x1 is correct");
       is(Math.floor(bottomY1), points[2][1], "Bottom guide's y1 is correct");
       is(Math.ceil(leftX1), points[3][0], "Left guide's x1 is correct");
     }
   }
@@ -145,15 +169,8 @@ add_task(function*() {
     yield toolbox.highlighter.showBoxModel(divFront, options);
 
     yield checkHighlighter(toolbox);
 
     info("Hide the box-model highlighter");
     yield toolbox.highlighter.hideBoxModel();
   }
 });
-
-function* getAttribute(nodeID, name, toolbox) {
-  let actorID = getHighlighterActorID(toolbox);
-  let {data: value} = yield executeInContent("Test:GetHighlighterAttribute",
-                                             {nodeID, name, actorID});
-  return value;
-}
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-rect_01.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-rect_01.js
@@ -21,97 +21,88 @@ add_task(function*() {
   ok(highlighter, "The RectHighlighter custom type was created");
   is(highlighter.typeName, "customhighlighter",
     "The RectHighlighter has the right type");
   ok(highlighter.show && highlighter.hide,
     "The RectHighlighter has the expected show/hide methods");
 
   info("Check that the highlighter is hidden by default");
 
-  let hidden = yield getAttribute(highlighter, "hidden");
+  let hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   is(hidden, "true", "The highlighter is hidden by default");
 
   info("Check that nothing is shown if no rect is passed");
 
   yield highlighter.show(body);
-  hidden = yield getAttribute(highlighter, "hidden");
+  hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   is(hidden, "true", "The highlighter is hidden when no rect is passed");
 
   info("Check that nothing is shown if rect is incomplete or invalid");
 
   yield highlighter.show(body, {
     rect: {x: 0, y: 0}
   });
-  hidden = yield getAttribute(highlighter, "hidden");
+  hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   is(hidden, "true", "The highlighter is hidden when the rect is incomplete");
 
   yield highlighter.show(body, {
     rect: {x: 0, y: 0, width: -Infinity, height: 0}
   });
-  hidden = yield getAttribute(highlighter, "hidden");
+  hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   is(hidden, "true", "The highlighter is hidden when the rect is invalid (1)");
 
   yield highlighter.show(body, {
     rect: {x: 0, y: 0, width: 5, height: -45}
   });
-  hidden = yield getAttribute(highlighter, "hidden");
+  hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   is(hidden, "true", "The highlighter is hidden when the rect is invalid (2)");
 
   yield highlighter.show(body, {
     rect: {x: "test", y: 0, width: 5, height: 5}
   });
-  hidden = yield getAttribute(highlighter, "hidden");
+  hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   is(hidden, "true", "The highlighter is hidden when the rect is invalid (3)");
 
   info("Check that the highlighter is displayed when valid options are passed");
 
   yield highlighter.show(body, {
     rect: {x: 5, y: 5, width: 50, height: 50}
   });
-  hidden = yield getAttribute(highlighter, "hidden");
+  hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   ok(!hidden, "The highlighter is displayed");
-  let style = yield getAttribute(highlighter, "style");
+  let style = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "style");
   is(style, "left:5px;top:5px;width:50px;height:50px;",
     "The highlighter is positioned correctly");
 
   info("Check that the highlighter can be displayed at x=0 y=0");
 
   yield highlighter.show(body, {
     rect: {x: 0, y: 0, width: 50, height: 50}
   });
-  hidden = yield getAttribute(highlighter, "hidden");
+  hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   ok(!hidden, "The highlighter is displayed when x=0 and y=0");
-  style = yield getAttribute(highlighter, "style");
+  style = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "style");
   is(style, "left:0px;top:0px;width:50px;height:50px;",
     "The highlighter is positioned correctly");
 
   info("Check that the highlighter is hidden when dimensions are 0");
 
   yield highlighter.show(body, {
     rect: {x: 0, y: 0, width: 0, height: 0}
   });
-  hidden = yield getAttribute(highlighter, "hidden");
+  hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   is(hidden, "true", "The highlighter is hidden width and height are 0");
 
   info("Check that a fill color can be passed");
 
   yield highlighter.show(body, {
     rect: {x: 100, y: 200, width: 500, height: 200},
     fill: "red"
   });
-  hidden = yield getAttribute(highlighter, "hidden");
+  hidden = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "hidden");
   ok(!hidden, "The highlighter is displayed");
-  style = yield getAttribute(highlighter, "style");
+  style = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "style");
   is(style, "left:100px;top:200px;width:500px;height:200px;background:red;",
     "The highlighter has the right background color");
 
   yield highlighter.hide();
   yield highlighter.finalize();
 });
-
-function* getAttribute(highlighter, name) {
-  let {data: value} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "highlighted-rect",
-    name: name,
-    actorID: highlighter.actorID
-  });
-  return value;
-}
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-rect_02.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-rect_02.js
@@ -17,29 +17,20 @@ add_task(function*() {
   info("Showing the rect highlighter in the context of the iframe");
 
   // Get the reference to a context node inside the iframe
   let childBody = yield getNodeFrontInFrame("body", "iframe", inspector);
   yield highlighter.show(childBody, {
     rect: {x: 50, y: 50, width: 100, height: 100}
   });
 
-  let style = yield getAttribute(highlighter, "style");
+  let style = yield getHighlighterNodeAttribute(highlighter, "highlighted-rect", "style");
 
   // The parent body has margin=50px and border=10px
   // The parent iframe also has margin=50px and border=10px
   // = 50 + 10 + 50 + 10 = 120px
   // The rect is aat x=50 and y=50, so left and top should be 170px
   is(style, "left:170px;top:170px;width:100px;height:100px;",
     "The highlighter is correctly positioned");
 
   yield highlighter.hide();
   yield highlighter.finalize();
 });
-
-function* getAttribute(highlighter, name) {
-  let {data: value} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "highlighted-rect",
-    name: name,
-    actorID: highlighter.actorID
-  });
-  return value;
-}
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-selector_01.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-selector_01.js
@@ -45,19 +45,19 @@ add_task(function*() {
   let contextNode = yield getNodeFront("body", inspector);
 
   for (let {selector, containerCount} of TEST_DATA) {
     info("Showing the highlighter on " + selector + ". Expecting " +
       containerCount + " highlighter containers");
 
     yield highlighter.show(contextNode, {selector});
 
-    let {data: nb} = yield executeInContent("Test:GetSelectorHighlighterBoxNb", {
-      actorID: highlighter.actorID
-    });
+    let {actorID, connPrefix} = getHighlighterActorID(highlighter);
+    let {data: nb} = yield executeInContent("Test:GetSelectorHighlighterBoxNb",
+                                            {actorID, connPrefix});
     ok(nb !== null, "The number of highlighters was retrieved");
 
     is(nb, containerCount, "The correct number of highlighers were created");
     yield highlighter.hide();
   }
 
   yield highlighter.finalize();
 });
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-selector_02.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-selector_02.js
@@ -43,19 +43,19 @@ add_task(function*() {
     if (inIframe) {
       contextNode = yield getNodeFrontInFrame("body", "iframe", inspector);
     } else {
       contextNode = yield getNodeFront("body", inspector);
     }
 
     yield highlighter.show(contextNode, {selector});
 
-    let {data: nb} = yield executeInContent("Test:GetSelectorHighlighterBoxNb", {
-      actorID: highlighter.actorID
-    });
+    let {actorID, connPrefix} = getHighlighterActorID(highlighter);
+    let {data: nb} = yield executeInContent("Test:GetSelectorHighlighterBoxNb",
+                                            {actorID, connPrefix});
     ok(nb !== null, "The number of highlighters was retrieved");
 
     is(nb, containerCount, "The correct number of highlighers were created");
     yield highlighter.hide();
   }
 
   yield highlighter.finalize();
 });
--- a/browser/devtools/inspector/test/browser_inspector_highlighter-zoom.js
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-zoom.js
@@ -31,20 +31,21 @@ add_task(function*() {
 
   yield hoverElement("div", inspector);
   let isVisible = yield isHighlighting(toolbox);
   ok(isVisible, "The highlighter is visible");
 
   for (let {level, expected} of TEST_LEVELS) {
     info("Zoom to level " + level + " and check that the highlighter is correct");
 
-    yield zoomPageTo(level, getHighlighterActorID(toolbox));
+    let {actorID, connPrefix} = getHighlighterActorID(toolbox.highlighter);
+    yield zoomPageTo(level, actorID, connPrefix);
     isVisible = yield isHighlighting(toolbox);
     ok(isVisible, "The highlighter is still visible at zoom level " + level);
-  
+
     yield isNodeCorrectlyHighlighted(getNode("div"), toolbox);
 
     info("Check that the highlighter root wrapper node was scaled down");
 
     let style = yield getRootNodeStyle(toolbox);
     is(style, expected, "The style attribute of the root element is correct");
   }
 });
@@ -58,15 +59,12 @@ function* hoverElement(selector, inspect
 function* hoverContainer(container, inspector) {
   let onHighlight = inspector.toolbox.once("node-highlight");
   EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
       inspector.markup.doc.defaultView);
   yield onHighlight;
 }
 
 function* getRootNodeStyle(toolbox) {
-  let {data: value} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-root",
-    name: "style",
-    actorID: getHighlighterActorID(toolbox)
-  });
+  let value = yield getHighlighterNodeAttribute(toolbox.highlighter,
+                                                "box-model-root", "style");
   return value;
 }
--- a/browser/devtools/inspector/test/browser_inspector_iframe-navigation.js
+++ b/browser/devtools/inspector/test/browser_inspector_iframe-navigation.js
@@ -14,20 +14,21 @@ add_task(function* () {
   let { inspector, toolbox } = yield openInspectorForURL(TEST_URI);
   let iframe = getNode("iframe");
 
   info("Starting element picker.");
   yield toolbox.highlighterUtils.startPicker();
 
   info("Waiting for highlighter to activate.");
   let highlighterShowing = toolbox.once("highlighter-ready");
-  executeInContent("Test:SynthesizeMouse",
-                   {type: "mousemove", x: 1, y: 1},
-                   {node: content.document.body},
-                   false);
+  executeInContent("Test:SynthesizeMouse", {
+    options: {type: "mousemove"},
+    x: 1,
+    y: 1
+  }, {node: content.document.body}, false);
   yield highlighterShowing;
 
   let isVisible = yield isHighlighting(toolbox);
   ok(isVisible, "Inspector is highlighting.");
 
   yield reloadFrame();
   info("Frame reloaded. Reloading again.");
 
--- a/browser/devtools/inspector/test/browser_inspector_infobar_01.js
+++ b/browser/devtools/inspector/test/browser_inspector_infobar_01.js
@@ -50,47 +50,35 @@ add_task(function*() {
   for (let currTest of testData) {
     yield testPosition(currTest, inspector);
   }
 });
 
 function* testPosition(test, inspector) {
   info("Testing " + test.selector);
 
-  let actorID = getHighlighterActorID(inspector.toolbox);
-
   yield selectAndHighlightNode(test.selector, inspector);
 
-  let {data: position} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-nodeinfobar-container",
-    name: "position",
-    actorID: actorID
-  });
+  let highlighter = inspector.toolbox.highlighter;
+  let position = yield getHighlighterNodeAttribute(highlighter,
+    "box-model-nodeinfobar-container", "position");
   is(position, test.position, "Node " + test.selector + ": position matches");
 
-  let {data: tag} = yield executeInContent("Test:GetHighlighterTextContent", {
-    nodeID: "box-model-nodeinfobar-tagname",
-    actorID: actorID
-  });
+  let tag = yield getHighlighterNodeTextContent(highlighter,
+    "box-model-nodeinfobar-tagname");
   is(tag, test.tag, "node " + test.selector + ": tagName matches.");
 
   if (test.id) {
-    let {data: id} = yield executeInContent("Test:GetHighlighterTextContent", {
-      nodeID: "box-model-nodeinfobar-id",
-      actorID: actorID
-    });
+    let id = yield getHighlighterNodeTextContent(highlighter,
+      "box-model-nodeinfobar-id");
     is(id, "#" + test.id, "node " + test.selector  + ": id matches.");
   }
 
-  let {data: classes} = yield executeInContent("Test:GetHighlighterTextContent", {
-    nodeID: "box-model-nodeinfobar-classes",
-    actorID: actorID
-  });
+  let classes = yield getHighlighterNodeTextContent(highlighter,
+    "box-model-nodeinfobar-classes");
   is(classes, test.classes, "node " + test.selector  + ": classes match.");
 
   if (test.dims) {
-    let {data: dims} = yield executeInContent("Test:GetHighlighterTextContent", {
-      nodeID: "box-model-nodeinfobar-dimensions",
-      actorID: actorID
-    });
+    let dims = yield getHighlighterNodeTextContent(highlighter,
+      "box-model-nodeinfobar-dimensions");
     is(dims, test.dims, "node " + test.selector  + ": dims match.");
   }
 }
--- a/browser/devtools/inspector/test/browser_inspector_initialization.js
+++ b/browser/devtools/inspector/test/browser_inspector_initialization.js
@@ -108,22 +108,34 @@ function* testBreadcrumbs(selector, insp
   let b = inspector.breadcrumbs;
   let expectedText = b.prettyPrintNodeAsText(nodeFront);
   let button = b.container.querySelector("button[checked=true]");
   ok(button, "A crumbs is checked=true");
   is(button.getAttribute("tooltiptext"), expectedText, "Crumb refers to the right node");
 }
 
 function* clickOnInspectMenuItem(node) {
-  info("Clicking on 'Inspect Element' context menu item of " + node);
-  document.popupNode = node;
-  var contentAreaContextMenu = getNode("#contentAreaContextMenu", { document });
-  var contextMenu = new nsContextMenu(contentAreaContextMenu);
+  info("Showing the contextual menu on node " + node);
+  yield executeInContent("Test:SynthesizeMouse", {
+    center: true,
+    options: {type: "contextmenu", button: 2}
+  }, {node});
 
-  info("Triggering inspect action.");
+  // nsContextMenu also requires the popupNode to be set, but we can't set it to
+  // node under e10s as it's a CPOW, not a DOM node. But under e10s,
+  // nsContextMenu won't use the property anyway, so just try/catching is ok.
+  try {
+    document.popupNode = node;
+  } catch (e) {}
+
+  let contentAreaContextMenu = document.querySelector("#contentAreaContextMenu");
+  let contextMenu = new nsContextMenu(contentAreaContextMenu);
+
+  info("Triggering inspect action and hiding the menu.");
   yield contextMenu.inspectNode();
 
-  // Clean up context menu:
+  contentAreaContextMenu.hidden = true;
+  contentAreaContextMenu.hidePopup();
   contextMenu.hiding();
 
   info("Waiting for inspector to update.");
   yield getActiveInspector().once("inspector-updated");
 }
--- a/browser/devtools/inspector/test/browser_inspector_invalidate.js
+++ b/browser/devtools/inspector/test/browser_inspector_invalidate.js
@@ -17,20 +17,22 @@ add_task(function*() {
 
   info("Waiting for highlighter to activate");
   yield inspector.toolbox.highlighter.showBoxModel(divFront);
 
   let rect = yield getSimpleBorderRect(toolbox);
   is(rect.width, 100, "The highlighter has the right width.");
 
   info("Changing the test element's size and waiting for the highlighter to update");
+  let {actorID, connPrefix} = getHighlighterActorID(toolbox.highlighter);
   yield executeInContent("Test:ChangeHighlightedNodeWaitForUpdate", {
     name: "style",
     value: "width: 200px; height: 100px; background:yellow;",
-    actorID: getHighlighterActorID(toolbox)
+    actorID,
+    connPrefix
   });
 
   rect = yield getSimpleBorderRect(toolbox);
   is(rect.width, 200, "The highlighter has the right width after update");
 
   info("Waiting for highlighter to hide");
   yield inspector.toolbox.highlighter.hideBoxModel();
 });
--- a/browser/devtools/inspector/test/browser_inspector_pseudoclass-lock.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass-lock.js
@@ -103,20 +103,18 @@ function* assertPseudoAddedToNode(inspec
   let rules = ruleview.element.querySelectorAll(".ruleview-rule.theme-separator");
   is(rules.length, 3, "rule view is showing 3 rules for pseudo-class locked div");
   is(rules[1]._ruleEditor.rule.selectorText, "div:hover", "rule view is showing " + PSEUDO + " rule");
 
   info("Show the highlighter on #div-1");
   yield showPickerOn("#div-1", inspector);
 
   info("Check that the infobar selector contains the pseudo-class");
-  let {data: value} = yield executeInContent("Test:GetHighlighterTextContent", {
-    nodeID: "box-model-nodeinfobar-pseudo-classes",
-    actorID: getHighlighterActorID(inspector.toolbox)
-  });
+  let value = yield getHighlighterNodeTextContent(inspector.toolbox.highlighter,
+    "box-model-nodeinfobar-pseudo-classes");
   is(value, PSEUDO, "pseudo-class in infobar selector");
   yield inspector.toolbox.highlighter.hideBoxModel();
 }
 
 function* assertPseudoRemovedFromNode() {
   info("Make sure the pseudoclass lock is removed from #div-1 and its ancestors");
   let node = getNode("#div-1");
   do {
@@ -129,20 +127,18 @@ function* assertPseudoRemovedFromNode() 
 
 function* assertPseudoRemovedFromView(inspector, ruleview) {
   info("Check that the ruleview no longer contains the pseudo-class rule");
   let rules = ruleview.element.querySelectorAll(".ruleview-rule.theme-separator");
   is(rules.length, 2, "rule view is showing 2 rules after removing lock");
 
   yield showPickerOn("#div-1", inspector);
 
-  let {data: value} = yield executeInContent("Test:GetHighlighterTextContent", {
-    nodeID: "box-model-nodeinfobar-pseudo-classes",
-    actorID: getHighlighterActorID(inspector.toolbox)
-  });
+  let value = yield getHighlighterNodeTextContent(inspector.toolbox.highlighter,
+    "box-model-nodeinfobar-pseudo-classes");
   is(value, "", "pseudo-class removed from infobar selector");
   yield inspector.toolbox.highlighter.hideBoxModel();
 }
 
 function* ensureRuleView(inspector) {
   if (!inspector.sidebar.getWindowForTab("ruleview")) {
     info("Waiting for ruleview initialization to complete.");
     yield inspector.sidebar.once("ruleview-ready");
--- a/browser/devtools/inspector/test/browser_inspector_remove-iframe-during-load.js
+++ b/browser/devtools/inspector/test/browser_inspector_remove-iframe-during-load.js
@@ -4,35 +4,41 @@
 "use strict";
 
 // Testing that the inspector doesn't go blank when navigating to a page that
 // deletes an iframe while loading.
 
 const TEST_URL = TEST_URL_ROOT + "doc_inspector_remove-iframe-during-load.html";
 
 add_task(function* () {
-  let { inspector, toolbox } = yield openInspectorForURL("about:blank");
-
+  let {inspector, toolbox} = yield openInspectorForURL("about:blank");
   yield selectNode("body", inspector);
 
-  let loaded = once(gBrowser.selectedBrowser, "load", true);
+  // We do not want to wait for the inspector to be fully ready before testing
+  // so we load TEST_URL and just wait for the content window to be done loading.
+  let done = waitForContentMessage("Test:TestPageProcessingDone");
   content.location = TEST_URL;
-
-  info("Waiting for test page to load.");
-  yield loaded;
+  yield done;
 
-  // The content doc contains a script that creates an iframe and deletes
-  // it immediately after. This is what used to make the inspector go
+  // The content doc contains a script that creates iframes and deletes them
+  // immediately after. It does this before the load event, after
+  // DOMContentLoaded and after load. This is what used to make the inspector go
   // blank when navigating to that page.
+  // At this stage, there should be no iframes in the page anymore.
+  ok(!getNode("iframe", {expectNoMatch: true}),
+    "Iframes added by the content page should have been removed");
+
+  // Create/remove an extra one now, after the load event.
   info("Creating and removing an iframe.");
-  var iframe = content.document.createElement("iframe");
+  let iframe = content.document.createElement("iframe");
   content.document.body.appendChild(iframe);
   iframe.remove();
 
-  ok(!getNode("iframe", {expectNoMatch: true}), "Iframe has been removed.");
+  ok(!getNode("iframe", {expectNoMatch: true}),
+    "The after-load iframe should have been removed.");
 
   info("Waiting for markup-view to load.");
   yield inspector.once("markuploaded");
 
   // Assert that the markup-view is displayed and works
   ok(!getNode("iframe", {expectNoMatch: true}), "Iframe has been removed.");
   is(getNode("#yay").textContent, "load", "Load event fired.");
 
--- a/browser/devtools/inspector/test/browser_inspector_search-01.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-01.js
@@ -65,18 +65,20 @@ add_task(function* () {
 
     info(index + ": Pressing key " + key + " to get id " + id);
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
     yield eventHandled;
 
     info("Got " + event + " event. Waiting for search query to complete");
     yield inspector.searchSuggestions._lastQuery;
 
-    info(inspector.selection.node.id + " is selected with text " +
+    info(inspector.selection.nodeFront.id + " is selected with text " +
          searchBox.value);
-    is(inspector.selection.node, content.document.getElementById(id),
+    let nodeFront = yield getNodeFront("#" + id, inspector);
+    is(inspector.selection.nodeFront, nodeFront,
        "Correct node is selected for state " + index);
+
     is(!searchBox.classList.contains("devtools-no-search-result"), isValid,
        "Correct searchbox result state for state " + index);
 
     index++;
   }
 });
--- a/browser/devtools/inspector/test/browser_inspector_select-last-selected.js
+++ b/browser/devtools/inspector/test/browser_inspector_select-last-selected.js
@@ -61,17 +61,18 @@ add_task(function* () {
       yield selectNode(nodeToSelect, inspector);
     }
 
     yield navigateToAndWaitForNewRoot(url);
 
     info("Waiting for inspector to update after new-root event.");
     yield inspector.once("inspector-updated");
 
-    is(inspector.selection.node, getNode(selectedNode),
+    let nodeFront = yield getNodeFront(selectedNode, inspector);
+    is(inspector.selection.nodeFront, nodeFront,
        selectedNode + " is selected after navigation.");
   }
 
   function navigateToAndWaitForNewRoot(url) {
     info("Navigating and waiting for new-root event after navigation.");
     let newRoot = inspector.once("new-root");
 
     if (url == content.location) {
--- a/browser/devtools/inspector/test/doc_frame_script.js
+++ b/browser/devtools/inspector/test/doc_frame_script.js
@@ -16,140 +16,176 @@ let {classes: Cc, interfaces: Ci, utils:
 let {LayoutHelpers} = Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", {});
 let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
             .getService(Ci.mozIJSSubScriptLoader);
 let EventUtils = {};
 loader.loadSubScript("chrome://marionette/content/EventUtils.js", EventUtils);
 
 /**
- * Given an actorID, get the corresponding actor from the first debugger-server
- * connection.
+ * If the test page creates and triggeres the custom event
+ * "test-page-processing-done", then the Test:TestPageProcessingDone message
+ * will be sent to the parent process for tests to wait for this event if needed.
+ */
+addEventListener("DOMWindowCreated", () => {
+  content.addEventListener("test-page-processing-done", () => {
+    sendAsyncMessage("Test:TestPageProcessingDone");
+  }, false);
+});
+
+/**
+ * Given an actorID and connection prefix, get the corresponding actor from the
+ * debugger-server connection.
  * @param {String} actorID
+ * @param {String} connPrefix
  */
-function getHighlighterActor(actorID) {
+function getHighlighterActor(actorID, connPrefix) {
   let {DebuggerServer} = Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
   if (!DebuggerServer.initialized) {
     return;
   }
 
-  let connID = Object.keys(DebuggerServer._connections)[0];
-  let conn = DebuggerServer._connections[connID];
+  let conn = DebuggerServer._connections[connPrefix];
+  if (!conn) {
+    return;
+  }
 
   return conn.getActor(actorID);
 }
 
 /**
  * Get the instance of CanvasFrameAnonymousContentHelper used by a given
  * highlighter actor.
  * The instance provides methods to get/set attributes/text/style on nodes of
  * the highlighter, inserted into the nsCanvasFrame.
  * @see /toolkit/devtools/server/actors/highlighter.js
  * @param {String} actorID
+ * @param {String} connPrefix
  */
-function getHighlighterCanvasFrameHelper(actorID) {
-  let actor = getHighlighterActor(actorID);
+function getHighlighterCanvasFrameHelper(actorID, connPrefix) {
+  let actor = getHighlighterActor(actorID, connPrefix);
   if (actor && actor._highlighter) {
     return actor._highlighter.markup;
   }
 }
 
 /**
  * Get a value for a given attribute name, on one of the elements of the box
  * model highlighter, given its ID.
  * @param {Object} msg The msg.data part expects the following properties
  * - {String} nodeID The full ID of the element to get the attribute for
  * - {String} name The name of the attribute to get
  * - {String} actorID The highlighter actor ID
+ * - {String} connPrefix The highlighter actor ID's connection prefix
  * @return {String} The value, if found, null otherwise
  */
 addMessageListener("Test:GetHighlighterAttribute", function(msg) {
-  let {nodeID, name, actorID} = msg.data;
+  let {nodeID, name, actorID, connPrefix} = msg.data;
 
   let value;
-  let helper = getHighlighterCanvasFrameHelper(actorID);
+  let helper = getHighlighterCanvasFrameHelper(actorID, connPrefix);
   if (helper) {
     value = helper.getAttributeForElement(nodeID, name);
   }
 
   sendAsyncMessage("Test:GetHighlighterAttribute", value);
 });
 
 /**
  * Get the textcontent of one of the elements of the box model highlighter,
  * given its ID.
  * @param {Object} msg The msg.data part expects the following properties
  * - {String} nodeID The full ID of the element to get the attribute for
  * - {String} actorID The highlighter actor ID
+ * - {String} connPrefix The highlighter connection prefix
  * @return {String} The textcontent value
  */
 addMessageListener("Test:GetHighlighterTextContent", function(msg) {
-  let {nodeID, actorID} = msg.data;
+  let {nodeID, actorID, connPrefix} = msg.data;
 
   let value;
-  let helper = getHighlighterCanvasFrameHelper(actorID);
+  let helper = getHighlighterCanvasFrameHelper(actorID, connPrefix);
   if (helper) {
     value = helper.getTextContentForElement(nodeID);
   }
 
   sendAsyncMessage("Test:GetHighlighterTextContent", value);
 });
 
 /**
  * Get the number of box-model highlighters created by the SelectorHighlighter
  * @param {Object} msg The msg.data part expects the following properties:
  * - {String} actorID The highlighter actor ID
+ * - {String} connPrefix The highlighter connection prefix
  * @return {Number} The number of box-model highlighters created, or null if the
  * SelectorHighlighter was not found.
  */
 addMessageListener("Test:GetSelectorHighlighterBoxNb", function(msg) {
-  let {actorID} = msg.data;
-  let {_highlighter: h} = getHighlighterActor(actorID);
+  let {actorID, connPrefix} = msg.data;
+  let {_highlighter: h} = getHighlighterActor(actorID, connPrefix);
   if (!h || !h._highlighters) {
     sendAsyncMessage("Test:GetSelectorHighlighterBoxNb", null);
   } else {
     sendAsyncMessage("Test:GetSelectorHighlighterBoxNb", h._highlighters.length);
   }
 });
 
 /**
  * Subscribe to the box-model highlighter's update event, modify an attribute of
  * the currently highlighted node and send a message when the highlighter has
  * updated.
  * @param {Object} msg The msg.data part expects the following properties
  * - {String} the name of the attribute to be changed
  * - {String} the new value for the attribute
  * - {String} actorID The highlighter actor ID
+ * - {String} connPrefix The highlighter connection prefix
  */
 addMessageListener("Test:ChangeHighlightedNodeWaitForUpdate", function(msg) {
   // The name and value of the attribute to be changed
-  let {name, value, actorID} = msg.data;
-  let {_highlighter: h} = getHighlighterActor(actorID);
+  let {name, value, actorID, connPrefix} = msg.data;
+  let {_highlighter: h} = getHighlighterActor(actorID, connPrefix);
 
   h.once("updated", () => {
     sendAsyncMessage("Test:ChangeHighlightedNodeWaitForUpdate");
   });
 
   h.currentNode.setAttribute(name, value);
 });
 
 /**
+ * Subscribe to a given highlighter event and respond when the event is received.
+ * @param {Object} msg The msg.data part expects the following properties
+ * - {String} event The name of the highlighter event to listen to
+ * - {String} actorID The highlighter actor ID
+ * - {String} connPrefix The highlighter connection prefix
+ */
+addMessageListener("Test:WaitForHighlighterEvent", function(msg) {
+  let {event, actorID, connPrefix} = msg.data;
+  let {_highlighter: h} = getHighlighterActor(actorID, connPrefix);
+
+  h.once(event, () => {
+    sendAsyncMessage("Test:WaitForHighlighterEvent");
+  });
+});
+
+/**
  * Change the zoom level of the page.
  * Optionally subscribe to the box-model highlighter's update event and waiting
  * for it to refresh before responding.
  * @param {Object} msg The msg.data part expects the following properties
  * - {Number} level The new zoom level
  * - {String} actorID Optional. The highlighter actor ID
+ * - {String} connPrefix Optional. The highlighter connection prefix
  */
 addMessageListener("Test:ChangeZoomLevel", function(msg) {
-  let {level, actorID} = msg.data;
+  let {level, actorID, connPrefix} = msg.data;
   dumpn("Zooming page to " + level);
 
   if (actorID) {
-    let {_highlighter: h} = getHighlighterActor(actorID);
+    let {_highlighter: h} = getHighlighterActor(actorID, connPrefix);
     h.once("updated", () => {
       sendAsyncMessage("Test:ChangeZoomLevel");
     });
   }
 
   let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIWebNavigation)
                         .QueryInterface(Ci.nsIDocShell);
@@ -198,29 +234,34 @@ addMessageListener("Test:GetAllAdjustedQ
  * Synthesize a mouse event on an element. This handler doesn't send a message
  * back. Consumers should listen to specific events on the inspector/highlighter
  * to know when the event got synthesized.
  * @param {Object} msg The msg.data part expects the following properties:
  * - {Number} x
  * - {Number} y
  * - {Boolean} center If set to true, x/y will be ignored and
  *             synthesizeMouseAtCenter will be used instead
- * - {String} type
+ * - {Object} options Other event options
  * The msg.objects part should be the element.
  * @param {Object} data Event detail properties:
  */
 addMessageListener("Test:SynthesizeMouse", function(msg) {
   let {node} = msg.objects;
-  let {x, y, center, type} = msg.data;
+  let {x, y, center, options} = msg.data;
 
   if (center) {
-    EventUtils.synthesizeMouseAtCenter(node, {type}, node.ownerDocument.defaultView);
+    EventUtils.synthesizeMouseAtCenter(node, options, node.ownerDocument.defaultView);
   } else {
-    EventUtils.synthesizeMouse(node, x, y, {type}, node.ownerDocument.defaultView);
+    EventUtils.synthesizeMouse(node, x, y, options, node.ownerDocument.defaultView);
   }
+
+  // Most consumers won't need to listen to this message, unless they want to
+  // wait for the mouse event to be synthesized and don't have another event
+  // to listen to instead.
+  sendAsyncMessage("Test:SynthesizeMouse");
 });
 
 /**
  * Check that an element currently has a pseudo-class lock.
  * @param {Object} msg The msg.data part expects the following properties:
  * - {String} pseudo The pseudoclass to check for
  * The msg.objects part should be the element.
  * @param {Object}
--- a/browser/devtools/inspector/test/doc_inspector_remove-iframe-during-load.html
+++ b/browser/devtools/inspector/test/doc_inspector_remove-iframe-during-load.html
@@ -2,34 +2,42 @@
 <html lang="en">
 <head>
   <meta charset="UTF-8">
   <title>iframe creation/deletion test</title>
 </head>
 <body>
   <div id="yay"></div>
   <script type="text/javascript">
-      var yay = document.querySelector("#yay");
-      yay.textContent = "nothing";
+    var yay = document.querySelector("#yay");
+    yay.textContent = "nothing";
+
+    // Create a custom event to let the test know when the window has finished
+    // loading.
+    var event = new Event("test-page-processing-done");
 
-      var iframe = document.createElement('iframe');
+    // Create/remove an iframe before load.
+    var iframe = document.createElement("iframe");
+    document.body.appendChild(iframe);
+    iframe.remove();
+    yay.textContent = "before events";
+
+    // Create/remove an iframe on DOMContentLoaded.
+    document.addEventListener("DOMContentLoaded", function() {
+      var iframe = document.createElement("iframe");
       document.body.appendChild(iframe);
       iframe.remove();
-
-      yay.textContent = "before events";
-
-      document.addEventListener("DOMContentLoaded", function() {
-        var iframe = document.createElement('iframe');
-        document.body.appendChild(iframe);
-        iframe.remove();
+      yay.textContent = "DOMContentLoaded";
+    });
 
-        yay.textContent = "DOMContentLoaded";
-      });
-      window.addEventListener("load", function() {
-        var iframe = document.createElement('iframe');
-        document.body.appendChild(iframe);
-        iframe.remove();
+    // Create/remove an iframe on window load.
+    window.addEventListener("load", function() {
+      var iframe = document.createElement("iframe");
+      document.body.appendChild(iframe);
+      iframe.remove();
+      yay.textContent = "load";
 
-        yay.textContent = "load";
-      });
+      // Dispatch the done event.
+      window.dispatchEvent(event);
+    });
   </script>
 </body>
 </html>
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -269,18 +269,21 @@ let getSimpleBorderRect = Task.async(fun
   return {
     top: p1.y,
     left: p1.x,
     width: p2.x - p1.x,
     height: p4.y - p1.y
   };
 });
 
-function getHighlighterActorID(toolbox) {
-  return toolbox.highlighter.actorID;
+function getHighlighterActorID(highlighter) {
+  let actorID = highlighter.actorID;
+  let connPrefix = actorID.substring(0, actorID.indexOf(highlighter.typeName));
+
+  return {actorID, connPrefix};
 }
 
 /**
  * Get the current positions and visibility of the various box-model highlighter
  * elements.
  */
 let getBoxModelStatus = Task.async(function*(toolbox) {
   let isVisible = yield isHighlighting(toolbox);
@@ -299,62 +302,40 @@ let getBoxModelStatus = Task.async(funct
   for (let guide of ["top", "right", "bottom", "left"]) {
     ret.guides[guide] = yield getGuideStatus(guide, toolbox);
   }
 
   return ret;
 });
 
 let getGuideStatus = Task.async(function*(location, toolbox) {
-  let actorID = getHighlighterActorID(toolbox);
-  let {data: hidden} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-guide-" + location,
-    name: "hidden",
-    actorID
-  });
-  let {data: x1} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-guide-" + location,
-    name: "x1",
-    actorID
-  });
-  let {data: y1} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-guide-" + location,
-    name: "y1",
-    actorID
-  });
-  let {data: x2} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-guide-" + location,
-    name: "x2",
-    actorID
-  });
-  let {data: y2} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-guide-" + location,
-    name: "y2",
-    actorID
-  });
+  let id = "box-model-guide-" + location;
+
+  let hidden = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "hidden");
+  let x1 = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "x1");
+  let y1 = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "y1");
+  let x2 = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "x2");
+  let y2 = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "y2");
 
   return {
     visible: !hidden,
     x1: x1,
     y1: y1,
     x2: x2,
     y2: y2
   };
 });
 
 /**
  * Get the coordinate (points attribute) from one of the polygon elements in the
  * box model highlighter.
  */
 let getPointsForRegion = Task.async(function*(region, toolbox) {
-  let {data: points} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-" + region,
-    name: "points",
-    actorID: getHighlighterActorID(toolbox)
-  });
+  let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
+                                                 "box-model-" + region, "points");
   points = points.split(/[, ]/);
 
   return {
     p1: {
       x: parseFloat(points[0]),
       y: parseFloat(points[1])
     },
     p2: {
@@ -372,33 +353,27 @@ let getPointsForRegion = Task.async(func
   };
 });
 
 /**
  * Is a given region polygon element of the box-model highlighter currently
  * hidden?
  */
 let isRegionHidden = Task.async(function*(region, toolbox) {
-  let {data: value} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-" + region,
-    name: "hidden",
-    actorID: getHighlighterActorID(toolbox)
-  });
+  let value = yield getHighlighterNodeAttribute(toolbox.highlighter,
+                                                "box-model-" + region, "hidden");
   return value !== null;
 });
 
 /**
  * Is the highlighter currently visible on the page?
  */
 let isHighlighting = Task.async(function*(toolbox) {
-  let {data: value} = yield executeInContent("Test:GetHighlighterAttribute", {
-    nodeID: "box-model-elements",
-    name: "hidden",
-    actorID: getHighlighterActorID(toolbox)
-  });
+  let value = yield getHighlighterNodeAttribute(toolbox.highlighter,
+                                                "box-model-elements", "hidden");
   return value === null;
 });
 
 let getHighlitNode = Task.async(function*(toolbox) {
   let {visible, content} = yield getBoxModelStatus(toolbox);
   let points = content.points;
   if (visible) {
     let x = (points.p1.x + points.p2.x + points.p3.x + points.p4.x) / 4;
@@ -553,22 +528,62 @@ let clickContainer = Task.async(function
 /**
  * Zoom the current page to a given level.
  * @param {Number} level The new zoom level.
  * @param {String} actorID Optional highlighter actor ID. If provided, the
  * returned promise will only resolve when the highlighter has updated to the
  * new zoom level.
  * @return {Promise}
  */
-let zoomPageTo = Task.async(function*(level, actorID) {
+let zoomPageTo = Task.async(function*(level, actorID, connPrefix) {
   yield executeInContent("Test:ChangeZoomLevel",
-                         {level, actorID});
+                         {level, actorID, connPrefix});
+});
+
+/**
+ * Get the value of an attribute on one of the highlighter's node.
+ * @param {Front} highlighter The front of the highlighter.
+ * @param {String} nodeID The Id of the node in the highlighter.
+ * @param {String} name The name of the attribute.
+ * @return {String} value
+ */
+let getHighlighterNodeAttribute = Task.async(function*(highlighter, nodeID, name) {
+  let {actorID, connPrefix} = getHighlighterActorID(highlighter);
+  let {data: value} = yield executeInContent("Test:GetHighlighterAttribute",
+                                             {nodeID, name, actorID, connPrefix});
+  return value;
 });
 
 /**
+ * Get the textContent value of one of the highlighter's node.
+ * @param {Front} highlighter The front of the highlighter.
+ * @param {String} nodeID The Id of the node in the highlighter.
+ * @return {String} value
+ */
+let getHighlighterNodeTextContent = Task.async(function*(highlighter, nodeID) {
+  let {actorID, connPrefix} = getHighlighterActorID(highlighter);
+  let {data: value} = yield executeInContent("Test:GetHighlighterTextContent",
+                                             {nodeID, actorID, connPrefix});
+  return value;
+});
+
+/**
+ * Subscribe to a given highlighter event and return a promise that resolves
+ * when the event is received.
+ * @param {String} event The name of the highlighter event to listen to.
+ * @param {Front} highlighter The front of the highlighter.
+ * @return {Promise}
+ */
+function waitForHighlighterEvent(event, highlighter) {
+  let {actorID, connPrefix} = getHighlighterActorID(highlighter);
+  return executeInContent("Test:WaitForHighlighterEvent",
+                          {event, actorID, connPrefix});
+}
+
+/**
  * 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) {
   info("Leaving the markup-view area");
   let def = promise.defer();
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/modules/compatibility.js
@@ -0,0 +1,109 @@
+/* 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 { Task } = require("resource://gre/modules/Task.jsm");
+loader.lazyRequireGetter(this, "EventEmitter",
+  "devtools/toolkit/event-emitter");
+
+const REQUIRED_MEMORY_ACTOR_METHODS = [
+  "attach", "detach", "startRecordingAllocations", "stopRecordingAllocations", "getAllocations"
+];
+
+/**
+ * A dummy front decorated with the provided methods.
+ *
+ * @param array blueprint
+ *        A list of [funcName, retVal] describing the class.
+ */
+function MockFront (blueprint) {
+  EventEmitter.decorate(this);
+
+  for (let [funcName, retVal] of blueprint) {
+    this[funcName] = (x => typeof x === "function" ? x() : x).bind(this, retVal);
+  }
+}
+
+function MockMemoryFront () {
+  MockFront.call(this, [
+    ["attach"],
+    ["detach"],
+    ["initialize"],
+    ["destroy"],
+    ["startRecordingAllocations", 0],
+    ["stopRecordingAllocations", 0],
+    ["getAllocations", createMockAllocations],
+  ]);
+}
+exports.MockMemoryFront = MockMemoryFront;
+
+function MockTimelineFront () {
+  MockFront.call(this, [
+    ["start", 0],
+    ["stop", 0],
+    ["initialize"],
+    ["destroy"],
+  ]);
+}
+exports.MockTimelineFront = MockTimelineFront;
+
+/**
+ * Create a fake allocations object, to be used with the MockMemoryFront
+ * so we create a fresh object each time.
+ *
+ * @return {Object}
+ */
+function createMockAllocations () {
+  return {
+    allocations: [],
+    allocationsTimestamps: [],
+    frames: [],
+    counts: []
+  };
+}
+
+/**
+ * Takes a TabTarget, and checks through all methods that are needed
+ * on the server's memory actor to determine if a mock or real MemoryActor
+ * should be used. The last of the methods added to MemoryActor
+ * landed in Gecko 35, so previous versions will fail this. Setting the `target`'s
+ * TEST_MOCK_MEMORY_ACTOR property to true will cause this function to indicate that
+ * the memory actor is not supported.
+ *
+ * @param {TabTarget} target
+ * @return {Boolean}
+ */
+function* memoryActorSupported (target) {
+  // This `target` property is used only in tests to test
+  // instances where the memory actor is not available.
+  if (target.TEST_MOCK_MEMORY_ACTOR) {
+    return false;
+  }
+
+  for (let method of REQUIRED_MEMORY_ACTOR_METHODS) {
+    if (!(yield target.actorHasMethod("memory", method))) {
+      return false;
+    }
+  }
+  return true;
+}
+exports.memoryActorSupported = Task.async(memoryActorSupported);
+
+/**
+ * Takes a TabTarget, and checks existence of a TimelineActor on
+ * the server, or if TEST_MOCK_TIMELINE_ACTOR is to be used.
+ *
+ * @param {TabTarget} target
+ * @return {Boolean}
+ */
+function* timelineActorSupported(target) {
+  // This `target` property is used only in tests to test
+  // instances where the timeline actor is not available.
+  if (target.TEST_MOCK_TIMELINE_ACTOR) {
+    return false;
+  }
+
+  return yield target.hasActor("timeline");
+}
+exports.timelineActorSupported = Task.async(timelineActorSupported);
--- a/browser/devtools/performance/modules/front.js
+++ b/browser/devtools/performance/modules/front.js
@@ -10,23 +10,23 @@ const { extend } = require("sdk/util/obj
 loader.lazyRequireGetter(this, "Services");
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 loader.lazyRequireGetter(this, "TimelineFront",
   "devtools/server/actors/timeline", true);
 loader.lazyRequireGetter(this, "MemoryFront",
   "devtools/server/actors/memory", true);
-
 loader.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/toolkit/DevToolsUtils");
+loader.lazyRequireGetter(this, "compatibility",
+  "devtools/performance/compatibility");
 
 loader.lazyImporter(this, "gDevTools",
   "resource:///modules/devtools/gDevTools.jsm");
-
 loader.lazyImporter(this, "setTimeout",
   "resource://gre/modules/Timer.jsm");
 loader.lazyImporter(this, "clearTimeout",
   "resource://gre/modules/Timer.jsm");
 
 // How often do we pull allocation sites from the memory actor.
 const DEFAULT_ALLOCATION_SITES_PULL_TIMEOUT = 200; // ms
 
@@ -51,35 +51,16 @@ SharedPerformanceActors.forTarget = func
   }
 
   let instance = new PerformanceActorsConnection(target);
   this.set(target, instance);
   return instance;
 };
 
 /**
- * A dummy front decorated with the provided methods.
- *
- * @param array blueprint
- *        A list of [funcName, retVal] describing the class.
- */
-function MockedFront(blueprint) {
-  EventEmitter.decorate(this);
-
-  for (let [funcName, retVal] of blueprint) {
-    this[funcName] = (x => x).bind(this, retVal);
-  }
-}
-
-MockedFront.prototype = {
-  initialize: function() {},
-  destroy: function() {}
-};
-
-/**
  * A connection to underlying actors (profiler, memory, framerate, etc.)
  * shared by all tools in a target.
  *
  * Use `SharedPerformanceActors.forTarget` to make sure you get the same
  * instance every time, and the `PerformanceFront` to start/stop recordings.
  *
  * @param Target target
  *        The target owning this connection.
@@ -90,16 +71,21 @@ function PerformanceActorsConnection(tar
   this._target = target;
   this._client = this._target.client;
   this._request = this._request.bind(this);
 
   Services.obs.notifyObservers(null, "performance-actors-connection-created", null);
 }
 
 PerformanceActorsConnection.prototype = {
+
+  // Properties set when mocks are being used
+  _usingMockMemory: false,
+  _usingMockTimeline: false,
+
   /**
    * Initializes a connection to the profiler and other miscellaneous actors.
    * If in the process of opening, or already open, nothing happens.
    *
    * @return object
    *         A promise that is resolved once the connection is established.
    */
   open: Task.async(function*() {
@@ -153,46 +139,39 @@ PerformanceActorsConnection.prototype = 
     // Otherwise, call `listTabs`.
     else {
       this._profiler = (yield listTabs(this._client)).profilerActor;
     }
   }),
 
   /**
    * Initializes a connection to a timeline actor.
-   * TODO: use framework level feature detection from bug 1069673
    */
   _connectTimelineActor: function() {
-    if (this._target.form && this._target.form.timelineActor) {
+    let supported = yield compatibility.timelineActorSupported(this._target);
+    if (supported) {
       this._timeline = new TimelineFront(this._target.client, this._target.form);
     } else {
-      this._timeline = new MockedFront([
-        ["start", 0],
-        ["stop", 0]
-      ]);
+      this._usingMockTimeline = true;
+      this._timeline = new compatibility.MockTimelineFront();
     }
   },
 
   /**
    * Initializes a connection to a memory actor.
-   * TODO: use framework level feature detection from bug 1069673
    */
-  _connectMemoryActor: function() {
-    if (this._target.form && this._target.form.memoryActor) {
+  _connectMemoryActor: Task.async(function* () {
+    let supported = yield compatibility.memoryActorSupported(this._target);
+    if (supported) {
       this._memory = new MemoryFront(this._target.client, this._target.form);
     } else {
-      this._memory = new MockedFront([
-        ["attach"],
-        ["detach"],
-        ["startRecordingAllocations", 0],
-        ["stopRecordingAllocations", 0],
-        ["getAllocations"]
-      ]);
+      this._usingMockMemory = true;
+      this._memory = new compatibility.MockMemoryFront();
     }
-  },
+  }),
 
   /**
    * Closes the connections to non-profiler actors.
    */
   _disconnectActors: Task.async(function* () {
     yield this._timeline.destroy();
     yield this._memory.destroy();
   }),
@@ -246,21 +225,26 @@ function PerformanceFront(connection) {
   this._request = connection._request;
 
   // Pipe events from TimelineActor to the PerformanceFront
   connection._timeline.on("markers", markers => this.emit("markers", markers));
   connection._timeline.on("frames", (delta, frames) => this.emit("frames", delta, frames));
   connection._timeline.on("memory", (delta, measurement) => this.emit("memory", delta, measurement));
   connection._timeline.on("ticks", (delta, timestamps) => this.emit("ticks", delta, timestamps));
 
+  // Set when mocks are being used
+  this._usingMockMemory = connection._usingMockMemory;
+  this._usingMockTimeline = connection._usingMockTimeline;
+
   this._pullAllocationSites = this._pullAllocationSites.bind(this);
   this._sitesPullTimeout = 0;
 }
 
 PerformanceFront.prototype = {
+
   /**
    * Manually begins a recording session.
    *
    * @param object options
    *        An options object to pass to the actors. Supported properties are
    *        `withTicks`, `withMemory` and `withAllocations`.
    * @return object
    *         A promise that is resolved once recording has started.
@@ -391,16 +375,26 @@ PerformanceFront.prototype = {
    * such as the maximum entries count, the sampling interval etc.
    *
    * Used in tests and for older backend implementations.
    */
   _customProfilerOptions: {
     entries: 1000000,
     interval: 1,
     features: ["js"]
+  },
+
+  /**
+   * Returns an object indicating if mock actors are being used or not.
+   */
+  getMocksInUse: function () {
+    return {
+      memory: this._usingMockMemory,
+      timeline: this._usingMockTimeline
+    };
   }
 };
 
 /**
  * Returns a promise resolved with a listing of all the tabs in the
  * provided thread client.
  */
 function listTabs(client) {
--- a/browser/devtools/performance/moz.build
+++ b/browser/devtools/performance/moz.build
@@ -1,14 +1,15 @@
 # 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/.
 
 EXTRA_JS_MODULES.devtools.performance += [
+    'modules/compatibility.js',
     'modules/front.js',
     'modules/io.js',
     'modules/recording-model.js',
     'modules/recording-utils.js',
     'panel.js'
 ]
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -5,28 +5,31 @@ support-files =
   doc_simple-test.html
   head.js
 
 # Commented out tests are profiler tests
 # that need to be moved over to performance tool
 
 [browser_perf-aaa-run-first-leaktest.js]
 [browser_perf-allocations-to-samples.js]
+[browser_perf-compatibility-01.js]
+[browser_perf-compatibility-02.js]
 [browser_perf-clear-01.js]
 [browser_perf-clear-02.js]
 [browser_perf-data-massaging-01.js]
 [browser_perf-data-samples.js]
 [browser_perf-details-calltree-render.js]
 [browser_perf-details-flamegraph-render.js]
 [browser_perf-details-memory-calltree-render.js]
 [browser_perf-details-memory-flamegraph-render.js]
 [browser_perf-details-waterfall-render.js]
 [browser_perf-details-01.js]
 [browser_perf-details-02.js]
 [browser_perf-details-03.js]
+[browser_perf-events-calltree.js]
 [browser_perf-front-basic-profiler-01.js]
 [browser_perf-front-basic-timeline-01.js]
 #[browser_perf-front-profiler-01.js] bug 1077464
 [browser_perf-front-profiler-02.js]
 [browser_perf-front-profiler-03.js]
 [browser_perf-front-profiler-04.js]
 #[browser_perf-front-profiler-05.js] bug 1077464
 #[browser_perf-front-profiler-06.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-compatibility-01.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test basic functionality of PerformanceFront with mock memory and timeline actors.
+ */
+
+let WAIT_TIME = 100;
+
+function spawnTest () {
+  let { target, front } = yield initBackend(SIMPLE_URL, {
+    TEST_MOCK_MEMORY_ACTOR: true,
+    TEST_MOCK_TIMELINE_ACTOR: true
+  });
+
+  let { memory, timeline } = front.getMocksInUse();
+  ok(memory, "memory should be mocked.");
+  ok(timeline, "timeline should be mocked.");
+
+  let {
+    profilerStartTime,
+    timelineStartTime,
+    memoryStartTime
+  } = yield front.startRecording({
+    withTicks: true,
+    withMemory: true,
+    withAllocations: true
+  });
+
+  ok(typeof profilerStartTime === "number",
+    "The front.startRecording() emits a profiler start time.");
+  ok(typeof timelineStartTime === "number",
+    "The front.startRecording() emits a timeline start time.");
+  ok(typeof memoryStartTime === "number",
+    "The front.startRecording() emits a memory start time.");
+
+  yield busyWait(WAIT_TIME);
+
+  let {
+    profilerEndTime,
+    timelineEndTime,
+    memoryEndTime
+  } = yield front.stopRecording({
+    withAllocations: true
+  });
+
+  ok(typeof profilerEndTime === "number",
+    "The front.stopRecording() emits a profiler end time.");
+  ok(typeof timelineEndTime === "number",
+    "The front.stopRecording() emits a timeline end time.");
+  ok(typeof memoryEndTime === "number",
+    "The front.stopRecording() emits a memory end time.");
+
+  ok(profilerEndTime > profilerStartTime,
+    "The profilerEndTime is after profilerStartTime.");
+  is(timelineEndTime, timelineStartTime,
+    "The timelineEndTime is the same as timelineStartTime.");
+  is(memoryEndTime, memoryStartTime,
+    "The memoryEndTime is the same as memoryStartTime.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-compatibility-02.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the recording model is populated correctly when using timeline
+ * and memory actor mocks.
+ */
+
+const WAIT_TIME = 1000;
+
+let test = Task.async(function*() {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL, "performance", {
+    TEST_MOCK_MEMORY_ACTOR: true,
+    TEST_MOCK_TIMELINE_ACTOR: true
+  });
+  let { EVENTS, gFront, PerformanceController, PerformanceView } = panel.panelWin;
+
+  let { memory: memoryMock, timeline: timelineMock } = gFront.getMocksInUse();
+  ok(memoryMock, "memory should be mocked.");
+  ok(timelineMock, "timeline should be mocked.");
+
+  yield startRecording(panel);
+  busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
+  yield stopRecording(panel);
+
+  let {
+    label, duration, markers, frames, memory, ticks, allocations, profile
+  } = PerformanceController.getCurrentRecording().getAllData();
+
+  is(label, "", "Empty label for mock.");
+  is(typeof duration, "number", "duration is a number");
+  ok(duration > 0, "duration is not 0");
+
+  isEmptyArray(markers, "markers");
+  isEmptyArray(frames, "frames");
+  isEmptyArray(memory, "memory");
+  isEmptyArray(ticks, "ticks");
+  isEmptyArray(allocations.sites, "allocations.sites");
+  isEmptyArray(allocations.timestamps, "allocations.timestamps");
+  isEmptyArray(allocations.frames, "allocations.frames");
+  isEmptyArray(allocations.counts, "allocations.counts");
+
+  let sampleCount = 0;
+
+  for (let thread of profile.threads) {
+    info("Checking thread: " + thread.name);
+
+    for (let sample of thread.samples) {
+      sampleCount++;
+
+      if (sample.frames[0].location != "(root)") {
+        ok(false, "The sample " + sample.toSource() + " doesn't have a root node.");
+      }
+    }
+  }
+
+  ok(sampleCount > 0,
+    "At least some samples have been iterated over, checking for root nodes.");
+
+  yield teardown(panel);
+  finish();
+});
+
+function isEmptyArray (array, name) {
+  ok(Array.isArray(array), `${name} is an array`);
+  is(array.length, 0, `${name} is empty`);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-events-calltree.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the call tree up/down events work for js calltree and memory calltree.
+ */
+let { ThreadNode } = devtools.require("devtools/profiler/tree-model");
+function spawnTest () {
+  let focus = 0;
+  let focusEvent = () => focus++;
+
+  Services.prefs.setBoolPref(MEMORY_PREF, true);
+
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, $, DetailsView, JsCallTreeView, MemoryCallTreeView } = panel.panelWin;
+
+  yield DetailsView.selectView("js-calltree");
+  ok(DetailsView.isViewSelected(JsCallTreeView), "The call tree is now selected.");
+
+  // Make a recording just so the performance tool is in the correct state
+  yield startRecording(panel);
+  let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
+  yield stopRecording(panel);
+  yield rendered;
+
+  // Mock the profile used so we can get a deterministic tree created
+  let threadNode = new ThreadNode(gSamples);
+  JsCallTreeView._populateCallTree(threadNode);
+  JsCallTreeView.emit(EVENTS.JS_CALL_TREE_RENDERED);
+
+  JsCallTreeView.on("focus", focusEvent);
+
+  click(panel.panelWin, $("#js-calltree-view .call-tree-item"));
+  fireKey("VK_DOWN");
+  fireKey("VK_DOWN");
+  fireKey("VK_DOWN");
+  fireKey("VK_DOWN");
+
+  JsCallTreeView.off("focus", focusEvent);
+
+  is(focus, 4, "several focus events are fired for the js calltree.");
+
+  yield teardown(panel);
+  finish();
+};
+
+let gSamples = [{
+  time: 5,
+  frames: [
+    { category: 8,  location: "(root)" },
+    { category: 8,  location: "A (http://foo/bar/baz:12)" },
+    { category: 16, location: "B (http://foo/bar/baz:34)" },
+    { category: 32, location: "C (http://foo/bar/baz:56)" }
+  ]
+}, {
+  time: 5 + 1,
+  frames: [
+    { category: 8,  location: "(root)" },
+    { category: 8,  location: "A (http://foo/bar/baz:12)" },
+    { category: 16, location: "B (http://foo/bar/baz:34)" },
+    { category: 64, location: "D (http://foo/bar/baz:78)" }
+  ]
+}, {
+  time: 5 + 1 + 2,
+  frames: [
+    { category: 8,  location: "(root)" },
+    { category: 8,  location: "A (http://foo/bar/baz:12)" },
+    { category: 16, location: "B (http://foo/bar/baz:34)" },
+    { category: 64, location: "D (http://foo/bar/baz:78)" }
+  ]
+}, {
+  time: 5 + 1 + 2 + 7,
+  frames: [
+    { category: 8,   location: "(root)" },
+    { category: 8,   location: "A (http://foo/bar/baz:12)" },
+    { category: 128, location: "E (http://foo/bar/baz:90)" },
+    { category: 256, location: "F (http://foo/bar/baz:99)" }
+  ]
+}];
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -7,16 +7,17 @@ const { classes: Cc, interfaces: Ci, uti
 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 
 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
 let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+let { merge } = devtools.require("sdk/util/object");
 let { getPerformanceActorsConnection, PerformanceFront } = devtools.require("devtools/performance/front");
 
 let nsIProfilerModule = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
 let TargetFactory = devtools.TargetFactory;
 let mm = null;
 
 const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-utils.js"
 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/performance/test/";
@@ -156,49 +157,63 @@ function once(aTarget, aEventName, aUseC
 function onceSpread(aTarget, aEventName, aUseCapture) {
   return once(aTarget, aEventName, aUseCapture, true);
 }
 
 function test () {
   Task.spawn(spawnTest).then(finish, handleError);
 }
 
-function initBackend(aUrl) {
+function initBackend(aUrl, targetOps={}) {
   info("Initializing a performance front.");
 
   if (!DebuggerServer.initialized) {
     DebuggerServer.init();
     DebuggerServer.addBrowserActors();
   }
 
   return Task.spawn(function*() {
     let tab = yield addTab(aUrl);
     let target = TargetFactory.forTab(tab);
 
     yield target.makeRemote();
 
+    // Attach addition options to `target`. This is used to force mock fronts
+    // to smokescreen test different servers where memory or timeline actors
+    // may not exist. Possible options that will actually work:
+    // TEST_MOCK_MEMORY_ACTOR = true
+    // TEST_MOCK_TIMELINE_ACTOR = true
+    merge(target, targetOps);
+
     yield gDevTools.showToolbox(target, "performance");
 
     let connection = getPerformanceActorsConnection(target);
     yield connection.open();
 
     let front = new PerformanceFront(connection);
     return { target, front };
   });
 }
 
-function initPerformance(aUrl, selectedTool="performance") {
+function initPerformance(aUrl, selectedTool="performance", targetOps={}) {
   info("Initializing a performance pane.");
 
   return Task.spawn(function*() {
     let tab = yield addTab(aUrl);
     let target = TargetFactory.forTab(tab);
 
     yield target.makeRemote();
 
+    // Attach addition options to `target`. This is used to force mock fronts
+    // to smokescreen test different servers where memory or timeline actors
+    // may not exist. Possible options that will actually work:
+    // TEST_MOCK_MEMORY_ACTOR = true
+    // TEST_MOCK_TIMELINE_ACTOR = true
+    merge(target, targetOps);
+
     let toolbox = yield gDevTools.showToolbox(target, selectedTool);
     let panel = toolbox.getCurrentPanel();
     return { target, panel, toolbox };
   });
 }
 
 function* teardown(panel) {
   info("Destroying the performance tool.");
@@ -386,8 +401,15 @@ function dropSelection(graph) {
   graph.dropSelection();
   graph.emit("selecting");
 }
 
 function getSourceActor(aSources, aURL) {
   let item = aSources.getItemForAttachment(a => a.source.url === aURL);
   return item && item.value;
 }
+
+/**
+ * Fires a key event, like "VK_UP", "VK_DOWN", etc.
+ */
+function fireKey (e) {
+  EventUtils.synthesizeKey(e, {});
+}
--- a/browser/devtools/performance/views/details-js-call-tree.js
+++ b/browser/devtools/performance/views/details-js-call-tree.js
@@ -90,16 +90,19 @@ let JsCallTreeView = Heritage.extend(Det
       // Call trees should only auto-expand when not inverted. Passing undefined
       // will default to the CALL_TREE_AUTO_EXPAND depth.
       autoExpandDepth: options.inverted ? 0 : undefined,
     });
 
     // Bind events.
     root.on("link", this._onLink);
 
+    // Pipe "focus" events to the view, mostly for tests
+    root.on("focus", () => this.emit("focus"));
+
     // Clear out other call trees.
     let container = $("#js-calltree-view > .call-tree-cells-container");
     container.innerHTML = "";
     root.attachTo(container);
 
     // Profiler data does not contain memory allocations information.
     root.toggleAllocations(false);
 
--- a/browser/devtools/performance/views/details-memory-call-tree.js
+++ b/browser/devtools/performance/views/details-memory-call-tree.js
@@ -89,16 +89,19 @@ let MemoryCallTreeView = Heritage.extend
       // Call trees should only auto-expand when not inverted. Passing undefined
       // will default to the CALL_TREE_AUTO_EXPAND depth.
       autoExpandDepth: options.inverted ? 0 : undefined,
     });
 
     // Bind events.
     root.on("link", this._onLink);
 
+    // Pipe "focus" events to the view, mostly for tests
+    root.on("focus", () => this.emit("focus"));
+
     // Clear out other call trees.
     let container = $("#memory-calltree-view > .call-tree-cells-container");
     container.innerHTML = "";
     root.attachTo(container);
 
     // Memory allocation samples don't contain cateogry labels.
     root.toggleCategories(false);
   },
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -11,17 +11,17 @@ let WaterfallView = Heritage.extend(Deta
   rangeChangeDebounceTime: 10, // ms
 
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
     DetailsSubview.initialize.call(this);
 
-    this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#details-pane"), TIMELINE_BLUEPRINT);
+    this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"), TIMELINE_BLUEPRINT);
     this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
 
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this._onResize = this._onResize.bind(this);
 
     this.waterfall.on("selected", this._onMarkerSelected);
     this.waterfall.on("unselected", this._onMarkerSelected);
     this.details.on("resize", this._onResize);
--- a/browser/devtools/projecteditor/lib/stores/local.js
+++ b/browser/devtools/projecteditor/lib/stores/local.js
@@ -103,31 +103,31 @@ var LocalStore = Class({
     if (this.resources.has(path)) {
       return promise.resolve(this.resources.get(path));
     }
 
     if (!this.contains(path)) {
       return promise.reject(new Error(path + " does not belong to " + this.path));
     }
 
-    return Task.spawn(function() {
+    return Task.spawn(function*() {
       let parent = yield this.resourceFor(OS.Path.dirname(path));
 
       let info;
       try {
         info = yield OS.File.stat(path);
       } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
         if (!options.create) {
           throw ex;
         }
       }
 
       let resource = this._forPath(path, info);
       parent.addChild(resource);
-      throw new Task.Result(resource);
+      return resource;
     }.bind(this));
   },
 
   refreshLoop: function() {
     // XXX: Once Bug 958280 adds a watch function, will not need to forever loop here.
     this.refresh().then(() => {
       if (SHOULD_LIVE_REFRESH) {
         this._refreshTimeout = setTimeout(this.refreshLoop,
--- a/browser/devtools/shadereditor/shadereditor.js
+++ b/browser/devtools/shadereditor/shadereditor.js
@@ -118,17 +118,17 @@ let EventsHandler = {
   },
 
   /**
    * Called for each location change in the debugged tab.
    */
   _onTabNavigated: function(event, {isFrameSwitching}) {
     switch (event) {
       case "will-navigate": {
-        Task.spawn(function() {
+        Task.spawn(function*() {
           // Make sure the backend is prepared to handle WebGL contexts.
           if (!isFrameSwitching) {
             gFront.setup({ reload: false });
           }
 
           // Reset UI.
           ShadersListView.empty();
           // When switching to an iframe, ensure displaying the reload button.
@@ -389,17 +389,17 @@ let ShadersEditorsView = {
    */
   setText: function(sources) {
     let view = this;
     function setTextAndClearHistory(editor, text) {
       editor.setText(text);
       editor.clearHistory();
     }
 
-    return Task.spawn(function() {
+    return Task.spawn(function*() {
       yield view._toggleListeners("off");
       yield promise.all([
         view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)),
         view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs))
       ]);
       yield view._toggleListeners("on");
     }).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources));
   },
@@ -485,17 +485,17 @@ let ShadersEditorsView = {
   /**
    * Recompiles the source code for the shader being edited.
    * This function is fired at a certain delay after the user stops typing.
    *
    * @param string type
    *        The corresponding shader type for the focused editor (e.g. "vs").
    */
   _doCompile: function(type) {
-    Task.spawn(function() {
+    Task.spawn(function*() {
       let editor = yield this._getEditor(type);
       let shaderActor = yield ShadersListView.selectedAttachment[type];
 
       try {
         yield shaderActor.compile(editor.getText());
         this._onSuccessfulCompilation();
       } catch (e) {
         this._onFailedCompilation(type, editor, e);
--- a/browser/devtools/timeline/widgets/waterfall.js
+++ b/browser/devtools/timeline/widgets/waterfall.js
@@ -125,17 +125,17 @@ Waterfall.prototype = {
     this._blueprint = blueprint;
   },
 
   /**
    * Keybindings.
    */
   setupKeys: function() {
     let pane = this._container;
-    pane.parentNode.parentNode.addEventListener("keydown", e => {
+    pane.addEventListener("keydown", e => {
       if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP) {
         e.preventDefault();
         this.selectNearestRow(this._selectedRowIdx - 1);
       }
       if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
         e.preventDefault();
         this.selectNearestRow(this._selectedRowIdx + 1);
       }
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -908,44 +908,57 @@ let UI = {
 
   onMessage: function(event) {
     // The custom toolbox sends a message to its parent
     // window.
     try {
       let json = JSON.parse(event.data);
       switch (json.name) {
         case "toolbox-close":
-          this.closeToolboxUI();
+          // There are many ways to close a toolbox:
+          // * Close button inside the toolbox
+          // * Toggle toolbox wrench in WebIDE
+          // * Disconnect the current runtime gracefully
+          // * Yank cord out of device
+          // We can't know for sure which one was used here, so reset the
+          // |toolboxPromise| since someone must be destroying it to reach here,
+          // and call our own close method.
+          this.toolboxPromise = null;
+          this._closeToolboxUI();
           break;
       }
     } catch(e) { console.error(e); }
   },
 
   destroyToolbox: function() {
+    // Only have a live toolbox if |this.toolboxPromise| exists
     if (this.toolboxPromise) {
-      return this.toolboxPromise.then(toolbox => {
-        toolbox.destroy();
-        this.toolboxPromise = null;
-      }, console.error);
+      let toolboxPromise = this.toolboxPromise;
+      this.toolboxPromise = null;
+      return toolboxPromise.then(toolbox => {
+        return toolbox.destroy();
+      }).catch(console.error)
+        .then(() => this._closeToolboxUI())
+        .catch(console.error);
     }
     return promise.resolve();
   },
 
   createToolbox: function() {
+    // If |this.toolboxPromise| exists, there is already a live toolbox
+    if (this.toolboxPromise) {
+      return this.toolboxPromise;
+    }
     this.toolboxPromise = AppManager.getTarget().then((target) => {
-      return this.showToolbox(target);
+      return this._showToolbox(target);
     }, console.error);
     return this.busyUntil(this.toolboxPromise, "opening toolbox");
   },
 
-  showToolbox: function(target) {
-    if (this.toolboxIframe) {
-      return;
-    }
-
+  _showToolbox: function(target) {
     let splitter = document.querySelector(".devtools-horizontal-splitter");
     splitter.removeAttribute("hidden");
 
     let iframe = document.createElement("iframe");
     iframe.id = "toolbox";
 
     document.querySelector("notificationbox").insertBefore(iframe, splitter.nextSibling);
     let host = devtools.Toolbox.HostType.CUSTOM;
@@ -959,26 +972,30 @@ let UI = {
 
     this.updateToolboxFullscreenState();
     return gDevTools.showToolbox(target, null, host, options);
   },
 
   updateToolboxFullscreenState: function() {
     let panel = document.querySelector("#deck").selectedPanel;
     let nbox = document.querySelector("#notificationbox");
-    if (panel.id == "deck-panel-details" &&
+    if (panel && panel.id == "deck-panel-details" &&
         AppManager.selectedProject.type != "packaged" &&
         this.toolboxIframe) {
       nbox.setAttribute("toolboxfullscreen", "true");
     } else {
       nbox.removeAttribute("toolboxfullscreen");
     }
   },
 
-  closeToolboxUI: function() {
+  _closeToolboxUI: function() {
+    if (!this.toolboxIframe) {
+      return;
+    }
+
     this.resetFocus();
     Services.prefs.setIntPref("devtools.toolbox.footer.height", this.toolboxIframe.height);
 
     // We have to destroy the iframe, otherwise, the keybindings of webide don't work
     // properly anymore.
     this.toolboxIframe.remove();
     this.toolboxIframe = null;
 
@@ -1227,17 +1244,21 @@ let Cmds = {
     }
     panel.addEventListener("popupshown", onPopupShown);
 
     panel.openPopup(anchor);
     return deferred.promise;
   },
 
   disconnectRuntime: function() {
-    return UI.busyUntil(AppManager.disconnectRuntime(), "disconnecting from runtime");
+    let disconnecting = Task.spawn(function*() {
+      yield UI.destroyToolbox();
+      yield AppManager.disconnectRuntime();
+    });
+    return UI.busyUntil(disconnecting, "disconnecting from runtime");
   },
 
   takeScreenshot: function() {
     return UI.busyUntil(AppManager.deviceFront.screenshotToDataURL().then(longstr => {
        return longstr.string().then(dataURL => {
          longstr.release().then(null, console.error);
          UI.openInBrowser(dataURL);
        });
--- a/browser/devtools/webide/test/head.js
+++ b/browser/devtools/webide/test/head.js
@@ -4,19 +4,19 @@
 "use strict";
 
 const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
 
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
-const {Promise: promise} = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {});
 const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const {require} = devtools;
+const promise = require("promise");
 const {AppProjects} = require("devtools/app-manager/app-projects");
 
 let TEST_BASE;
 if (window.location === "chrome://browser/content/browser.xul") {
   TEST_BASE = "chrome://mochitests/content/browser/browser/devtools/webide/test/";
 } else {
   TEST_BASE = "chrome://mochitests/content/chrome/browser/devtools/webide/test/";
 }
--- a/browser/devtools/webide/test/test_autoconnect_runtime.html
+++ b/browser/devtools/webide/test/test_autoconnect_runtime.html
@@ -66,17 +66,17 @@
 
           is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
 
           win = yield openWebIDE();
 
           win.AppManager.runtimeList.usb.push(fakeRuntime);
           win.AppManager.update("runtimelist");
 
-          yield waitForUpdate(win, "list-tabs-response");
+          yield waitForUpdate(win, "runtime-apps-found");
 
           is(Object.keys(DebuggerServer._connections).length, 1, "Automatically reconnected");
 
           yield win.Cmds.disconnectRuntime();
 
           yield closeWebIDE(win);
 
           DebuggerServer.destroy();
--- a/browser/devtools/webide/test/test_fullscreenToolbox.html
+++ b/browser/devtools/webide/test/test_fullscreenToolbox.html
@@ -30,17 +30,17 @@
 
         Task.spawn(function* () {
           Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
           win = yield openWebIDE();
           win.AppManager.update("runtimelist");
 
           yield connectToLocal(win);
 
-          yield waitForUpdate(win, "list-tabs-response");
+          yield waitForUpdate(win, "runtime-apps-found");
 
           // Select main process
           yield win.Cmds.showProjectPanel();
           SimpleTest.executeSoon(() => {
             win.document.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
           });
 
           yield waitForUpdate(win, "project");
--- a/browser/devtools/webide/test/test_runtime.html
+++ b/browser/devtools/webide/test/test_runtime.html
@@ -79,17 +79,17 @@
 
           items[0].click();
 
           ok(win.document.querySelector("window").className, "busy", "UI is busy");
           yield win.UI._busyPromise;
 
           is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
 
-          yield waitForUpdate(win, "list-tabs-response");
+          yield waitForUpdate(win, "runtime-apps-found");
 
           ok(isPlayActive(), "play button is enabled 1");
           ok(!isStopActive(), "stop button is disabled 1");
           let oldProject = win.AppManager.selectedProject;
           win.AppManager.selectedProject = null;
 
           yield nextTick();
 
@@ -109,17 +109,17 @@
           is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
 
           ok(win.AppManager.selectedProject, "A project is still selected");
           ok(!isPlayActive(), "play button is disabled 4");
           ok(!isStopActive(), "stop button is disabled 4");
 
           win.document.querySelectorAll(".runtime-panel-item-other")[1].click();
 
-          yield waitForUpdate(win, "list-tabs-response");
+          yield waitForUpdate(win, "runtime-apps-found");
 
           is(Object.keys(DebuggerServer._connections).length, 1, "Locally connected");
 
           ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
 
           // Select main process
           yield win.Cmds.showProjectPanel();
           SimpleTest.executeSoon(() => {
--- a/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/profiler.dtd
@@ -65,24 +65,16 @@
 <!ENTITY profilerUI.newtab.tooltiptext "Add new tab from selection">
 
 <!-- LOCALIZATION NOTE (profilerUI.options.tooltiptext): This is the tooltip
   -  for the options button. -->
 <!ENTITY profilerUI.options.tooltiptext "Configure performance preferences.">
 
 <!-- LOCALIZATION NOTE (profilerUI.invertTree): This is the label shown next to
   -  a checkbox that inverts and un-inverts the profiler's call tree. -->
-<!ENTITY profilerUI.invertTree "Invert Call Tree">
-
-<!-- LOCALIZATION NOTE (profilerUI.flattenTree.tooltiptext): This is the tooltip
-  -  for the tree-flattening checkbox's label.  -->
-<!ENTITY profilerUI.flattenTree.tooltiptext "Inverting the call tree displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
-
-<!-- LOCALIZATION NOTE (profilerUI.invertTree): This is the label shown next to
-  -  a checkbox that inverts and un-inverts the profiler's call tree. -->
 <!ENTITY profilerUI.invertTree             "Invert Call Tree">
 <!ENTITY profilerUI.invertTree.tooltiptext "Inverting the call tree displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
 
 <!-- LOCALIZATION NOTE (profilerUI.invertFlameGraph): This is the label shown next to
   -  a checkbox that inverts and un-inverts the profiler's flame graph. -->
 <!ENTITY profilerUI.invertFlameGraph             "Invert Flame Chart">
 <!ENTITY profilerUI.invertFlameGraph.tooltiptext "Inverting the flame chart displays the profiled call paths starting from the youngest frames and expanding out to the older frames.">
 
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -561,17 +561,17 @@ this.BrowserUITelemetry = {
       }
     }
     result.visibleTabs = visibleTabs;
     result.hiddenTabs = hiddenTabs;
 
     if (Components.isSuccessCode(searchResult)) {
       result.currentSearchEngine = Services.search.currentEngine.name;
     }
-
+    result.oneOffSearchEnabled = Services.prefs.getBoolPref("browser.search.showOneOffButtons");
     return result;
   },
 
   getToolbarMeasures: function() {
     let result = this._firstWindowMeasurements || {};
     result.countableEvents = this._countableEvents;
     result.durations = this._durations;
     return result;
--- a/browser/modules/ContentSearch.jsm
+++ b/browser/modules/ContentSearch.jsm
@@ -19,16 +19,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/FormHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SearchSuggestionController",
   "resource://gre/modules/SearchSuggestionController.jsm");
 
 const INBOUND_MESSAGE = "ContentSearch";
 const OUTBOUND_MESSAGE = INBOUND_MESSAGE;
+const MAX_LOCAL_SUGGESTIONS = 3;
+const MAX_SUGGESTIONS = 6;
 
 /**
  * ContentSearch receives messages named INBOUND_MESSAGE and sends messages
  * named OUTBOUND_MESSAGE.  The data of each message is expected to look like
  * { type, data }.  type is the message's type (or subtype if you consider the
  * type of the message itself to be INBOUND_MESSAGE), and data is data that is
  * specific to the type.
  *
@@ -263,18 +265,18 @@ this.ContentSearch = {
     let engine = Services.search.getEngineByName(data.engineName);
     if (!engine) {
       throw new Error("Unknown engine name: " + data.engineName);
     }
 
     let browserData = this._suggestionDataForBrowser(msg.target, true);
     let { controller } = browserData;
     let ok = SearchSuggestionController.engineOffersSuggestions(engine);
-    controller.maxLocalResults = ok ? 2 : 6;
-    controller.maxRemoteResults = ok ? 6 : 0;
+    controller.maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS;
+    controller.maxRemoteResults = ok ? MAX_SUGGESTIONS : 0;
     controller.remoteTimeout = data.remoteTimeout || undefined;
     let priv = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
     // fetch() rejects its promise if there's a pending request, but since we
     // process our event queue serially, there's never a pending request.
     let suggestions = yield controller.fetch(data.searchString, priv, engine);
 
     // Keep the form history result so RemoveFormHistoryEntry can remove entries
     // from it.  Keeping only one result isn't foolproof because the client may
--- a/browser/modules/CustomizationTabPreloader.jsm
+++ b/browser/modules/CustomizationTabPreloader.jsm
@@ -7,16 +7,20 @@
 this.EXPORTED_SYMBOLS = ["CustomizationTabPreloader"];
 
 const Cu = Components.utils;
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HiddenFrame",
+  "resource:///modules/HiddenFrame.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>";
 const CUSTOMIZATION_URL = "about:customizing";
 
 // The interval between swapping in a preload docShell and kicking off the
 // next preload in the background.
@@ -55,18 +59,16 @@ this.CustomizationTabPreloader = {
 };
 
 Object.freeze(CustomizationTabPreloader);
 
 this.CustomizationTabPreloaderInternal = {
   _browser: null,
 
   uninit: function () {
-    HostFrame.destroy();
-
     if (this._browser) {
       this._browser.destroy();
       this._browser = null;
     }
   },
 
   newTab: function (aTab) {
     let win = aTab.ownerDocument.defaultView;
@@ -85,16 +87,17 @@ this.CustomizationTabPreloaderInternal =
 };
 
 function HiddenBrowser() {
   this._createBrowser();
 }
 
 HiddenBrowser.prototype = {
   _timer: null,
+  _hiddenFrame: null,
 
   get isPreloaded() {
     return this._browser &&
            this._browser.contentDocument &&
            this._browser.contentDocument.readyState === "complete" &&
            this._browser.currentURI.spec === CUSTOMIZATION_URL;
   },
 
@@ -132,21 +135,29 @@ HiddenBrowser.prototype = {
     this._timer = null;
 
     // Start pre-loading the customization page.
     this._createBrowser();
   },
 
   destroy: function () {
     this._removeBrowser();
+    if (this._hiddenFrame) {
+      this._hiddenFrame.destroy();
+      this._hiddenFrame = null;
+    }
     this._timer = clearTimer(this._timer);
   },
 
   _createBrowser: function () {
-    HostFrame.get().then(aFrame => {
+    if (!this._hiddenFrame) {
+      this._hiddenFrame = new HiddenFrame();
+    }
+
+    this._hiddenFrame.get().then(aFrame => {
       let doc = aFrame.document;
       this._browser = doc.createElementNS(XUL_NS, "browser");
       this._browser.setAttribute("type", "content");
       this._browser.setAttribute("src", CUSTOMIZATION_URL);
       this._browser.style.width = "400px";
       this._browser.style.height = "400px";
       doc.getElementById("win").appendChild(this._browser);
     });
@@ -154,64 +165,8 @@ HiddenBrowser.prototype = {
 
   _removeBrowser: function () {
     if (this._browser) {
       this._browser.remove();
       this._browser = null;
     }
   }
 };
-
-let HostFrame = {
-  _frame: null,
-  _deferred: null,
-
-  get hiddenDOMDocument() {
-    return Services.appShell.hiddenDOMWindow.document;
-  },
-
-  get isReady() {
-    return this.hiddenDOMDocument.readyState === "complete";
-  },
-
-  get: function () {
-    if (!this._deferred) {
-      this._deferred = Promise.defer();
-      this._create();
-    }
-
-    return this._deferred.promise;
-  },
-
-  destroy: function () {
-    if (this._frame) {
-      if (!Cu.isDeadWrapper(this._frame)) {
-        this._frame.removeEventListener("load", this, true);
-        this._frame.remove();
-      }
-
-      this._frame = null;
-      this._deferred = null;
-    }
-  },
-
-  handleEvent: function () {
-    let contentWindow = this._frame.contentWindow;
-    if (contentWindow.location.href === XUL_PAGE) {
-      this._frame.removeEventListener("load", this, true);
-      this._deferred.resolve(contentWindow);
-    } else {
-      contentWindow.location = XUL_PAGE;
-    }
-  },
-
-  _create: function () {
-    if (this.isReady) {
-      let doc = this.hiddenDOMDocument;
-      this._frame = doc.createElementNS(HTML_NS, "iframe");
-      this._frame.addEventListener("load", this, true);
-      doc.documentElement.appendChild(this._frame);
-    } else {
-      let flags = Ci.nsIThread.DISPATCH_NORMAL;
-      Services.tm.currentThread.dispatch(() => this._create(), flags);
-    }
-  }
-};
new file mode 100644
--- /dev/null
+++ b/browser/modules/HiddenFrame.jsm
@@ -0,0 +1,86 @@
+/* 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 = ["HiddenFrame"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>";
+
+/**
+ * An hidden frame object. It takes care of creating an IFRAME and attaching it the
+ * |hiddenDOMWindow|.
+ */
+function HiddenFrame() {}
+
+HiddenFrame.prototype = {
+  _frame: null,
+  _deferred: null,
+  _retryTimerId: null,
+
+  get hiddenDOMDocument() {
+    return Services.appShell.hiddenDOMWindow.document;
+  },
+
+  get isReady() {
+    return this.hiddenDOMDocument.readyState === "complete";
+  },
+
+  /**
+   * Gets the |contentWindow| of the hidden frame. Creates the frame if needed.
+   * @returns Promise Returns a promise which is resolved when the hidden frame has finished
+   *          loading.
+   */
+  get: function () {
+    if (!this._deferred) {
+      this._deferred = PromiseUtils.defer();
+      this._create();
+    }
+
+    return this._deferred.promise;
+  },
+
+  destroy: function () {
+    clearTimeout(this._retryTimerId);
+
+    if (this._frame) {
+      if (!Cu.isDeadWrapper(this._frame)) {
+        this._frame.removeEventListener("load", this, true);
+        this._frame.remove();
+      }
+
+      this._frame = null;
+      this._deferred = null;
+    }
+  },
+
+  handleEvent: function () {
+    let contentWindow = this._frame.contentWindow;
+    if (contentWindow.location.href === XUL_PAGE) {
+      this._frame.removeEventListener("load", this, true);
+      this._deferred.resolve(contentWindow);
+    } else {
+      contentWindow.location = XUL_PAGE;
+    }
+  },
+
+  _create: function () {
+    if (this.isReady) {
+      let doc = this.hiddenDOMDocument;
+      this._frame = doc.createElementNS(HTML_NS, "iframe");
+      this._frame.addEventListener("load", this, true);
+      doc.documentElement.appendChild(this._frame);
+    } else {
+      // Check again if |hiddenDOMDocument| is ready as soon as possible.
+      this._retryTimerId = setTimeout(this._create.bind(this), 0);
+    }
+  }
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -20,16 +20,17 @@ EXTRA_JS_MODULES += [
     'ContentSearch.jsm',
     'ContentWebRTC.jsm',
     'CustomizationTabPreloader.jsm',
     'DirectoryLinksProvider.jsm',
     'E10SUtils.jsm',
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
+    'HiddenFrame.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'PanelFrame.jsm',
     'ProcessHangMonitor.jsm',
     'RemotePrompt.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TabCrashReporter.jsm',
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -4484,23 +4484,29 @@ nsGlobalWindow::GetOpenerWindow(ErrorRes
 {
   FORWARD_TO_OUTER_OR_THROW(GetOpenerWindow, (aError), aError, nullptr);
 
   nsCOMPtr<nsPIDOMWindow> opener = do_QueryReferent(mOpener);
   if (!opener) {
     return nullptr;
   }
 
+  nsGlobalWindow* win = static_cast<nsGlobalWindow*>(opener.get());
+
   // First, check if we were called from a privileged chrome script
   if (nsContentUtils::IsCallerChrome()) {
+    // Catch the case where we're chrome but the opener is not...
+    if (GetPrincipal() == nsContentUtils::GetSystemPrincipal() &&
+        win->GetPrincipal() != nsContentUtils::GetSystemPrincipal()) {
+      return nullptr;
+    }
     return opener;
   }
 
-  // First, ensure that we're not handing back a chrome window.
-  nsGlobalWindow *win = static_cast<nsGlobalWindow *>(opener.get());
+  // First, ensure that we're not handing back a chrome window to content:
   if (win->IsChromeWindow()) {
     return nullptr;
   }
 
   // We don't want to reveal the opener if the opener is a mail window,
   // because opener can be used to spoof the contents of a message (bug 105050).
   // So, we look in the opener's root docshell to see if it's a mail window.
   nsCOMPtr<nsIDocShell> openerDocShell = opener->GetDocShell();
--- a/dom/plugins/base/nsNPAPIPlugin.cpp
+++ b/dom/plugins/base/nsNPAPIPlugin.cpp
@@ -2166,29 +2166,16 @@ NPError
     *(NPBool*)result = true;
     return NPERR_NO_ERROR;
   }
 
   case NPNVcontentsScaleFactor: {
     nsNPAPIPluginInstance *inst =
       (nsNPAPIPluginInstance *) (npp ? npp->ndata : nullptr);
     double scaleFactor = inst ? inst->GetContentsScaleFactor() : 1.0;
-    // Work around a Flash ActionScript bug that causes long hangs if
-    // Flash thinks HiDPI support is available. Adobe is tracking this as
-    // ADBE 3921114. If this turns out to be Adobe's fault and they fix it,
-    // we'll no longer need this quirk. See QUIRK_FLASH_HIDE_HIDPI_SUPPORT
-    // in PluginModuleChild.h, and also bug 1118615.
-    if (inst) {
-      const char *mimeType;
-      inst->GetMIMEType(&mimeType);
-      NS_NAMED_LITERAL_CSTRING(flash, "application/x-shockwave-flash");
-      if (!PL_strncasecmp(mimeType, flash.get(), flash.Length())) {
-        scaleFactor = 1.0;
-      }
-    }
     *(double*)result = scaleFactor;
     return NPERR_NO_ERROR;
   }
 #endif
 
 #ifdef MOZ_WIDGET_ANDROID
     case kLogInterfaceV0_ANPGetValue: {
       LOG("get log interface");
--- a/dom/plugins/ipc/PluginInstanceChild.cpp
+++ b/dom/plugins/ipc/PluginInstanceChild.cpp
@@ -478,21 +478,17 @@ PluginInstanceChild::NPN_GetValue(NPNVar
 #ifndef NP_NO_QUICKDRAW
     case NPNVsupportsQuickDrawBool: {
         *((NPBool*)aValue) = false;
         return NPERR_NO_ERROR;
     }
 #endif /* NP_NO_QUICKDRAW */
 
     case NPNVcontentsScaleFactor: {
-        double scaleFactor = mContentsScaleFactor;
-        if (GetQuirks() & PluginModuleChild::QUIRK_FLASH_HIDE_HIDPI_SUPPORT) {
-          scaleFactor = 1.0;
-        }
-        *static_cast<double*>(aValue) = scaleFactor;
+        *static_cast<double*>(aValue) = mContentsScaleFactor;
         return NPERR_NO_ERROR;
     }
 #endif /* XP_MACOSX */
 
 #ifdef DEBUG
     case NPNVjavascriptEnabledBool:
     case NPNVasdEnabledBool:
     case NPNVisOfflineBool:
--- a/dom/plugins/ipc/PluginModuleChild.cpp
+++ b/dom/plugins/ipc/PluginModuleChild.cpp
@@ -2148,17 +2148,16 @@ PluginModuleChild::InitQuirksModes(const
 #endif
 
 #ifdef XP_MACOSX
     // Whitelist Flash and Quicktime to support offline renderer
     NS_NAMED_LITERAL_CSTRING(quicktime, "QuickTime Plugin.plugin");
     if (specialType == nsPluginHost::eSpecialType_Flash) {
         mQuirks |= QUIRK_FLASH_AVOID_CGMODE_CRASHES;
         mQuirks |= QUIRK_ALLOW_OFFLINE_RENDERER;
-        mQuirks |= QUIRK_FLASH_HIDE_HIDPI_SUPPORT;
     } else if (FindInReadable(quicktime, mPluginFilename)) {
         mQuirks |= QUIRK_ALLOW_OFFLINE_RENDERER;
     }
 #endif
 }
 
 bool
 PluginModuleChild::RecvPPluginInstanceConstructor(PPluginInstanceChild* aActor,
--- a/dom/plugins/ipc/PluginModuleChild.h
+++ b/dom/plugins/ipc/PluginModuleChild.h
@@ -278,21 +278,16 @@ public:
         // Mac: Allow the plugin to use offline renderer mode.
         // Use this only if the plugin is certified the support the offline renderer.
         QUIRK_ALLOW_OFFLINE_RENDERER                    = 1 << 9,
         // Mac: Work around a Flash bug that can cause plugin process crashes
         // in CoreGraphics mode:  The Flash plugin sometimes accesses the
         // CGContextRef we pass to it in NPP_HandleEvent(NPCocoaEventDrawRect)
         // outside of that call.  See bug 804606.
         QUIRK_FLASH_AVOID_CGMODE_CRASHES                = 1 << 10,
-        // Mac: Work around a Flash ActionScript bug that causes long hangs if
-        // Flash thinks HiDPI support is available. Adobe is tracking this as
-        // ADBE 3921114. If this turns out to be Adobe's fault and they fix it,
-        // we'll no longer need this quirk. See bug 1118615.
-        QUIRK_FLASH_HIDE_HIDPI_SUPPORT                  = 1 << 11,
     };
 
     int GetQuirks() { return mQuirks; }
 
     const PluginSettings& Settings() const { return mCachedSettings; }
 
 private:
     NPError DoNP_Initialize(const PluginSettings& aSettings);
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1576,31 +1576,31 @@ public abstract class GeckoApp
                                            (TextSelectionHandle) findViewById(R.id.caret_handle),
                                            (TextSelectionHandle) findViewById(R.id.focus_handle));
 
         if (ZOOMED_VIEW_ENABLED) {
             ViewStub stub = (ViewStub) findViewById(R.id.zoomed_view_stub);
             mZoomedView = (ZoomedView) stub.inflate();
         }
 
-        UpdateServiceHelper.registerForUpdates(GeckoApp.this);
-
         // Trigger the completion of the telemetry timer that wraps activity startup,
         // then grab the duration to give to FHR.
         mJavaUiStartupTimer.stop();
         final long javaDuration = mJavaUiStartupTimer.getElapsed();
 
         ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
             @Override
             public void run() {
                 final HealthRecorder rec = mHealthRecorder;
                 if (rec != null) {
                     rec.recordJavaStartupTime(javaDuration);
                 }
 
+                UpdateServiceHelper.registerForUpdates(GeckoApp.this);
+
                 // Kick off our background services. We do this by invoking the broadcast
                 // receiver, which uses the system alarm infrastructure to perform tasks at
                 // intervals.
                 GeckoPreferences.broadcastHealthReportUploadPref(GeckoApp.this);
                 if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.Launched)) {
                     return;
                 }
             }
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -877,16 +877,17 @@ sync_java_files = [
     'fxa/login/TokensAndKeysState.java',
     'fxa/receivers/FxAccountDeletedReceiver.java',
     'fxa/receivers/FxAccountDeletedService.java',
     'fxa/receivers/FxAccountUpgradeReceiver.java',
     'fxa/sync/FxAccountGlobalSession.java',
     'fxa/sync/FxAccountNotificationManager.java',
     'fxa/sync/FxAccountSchedulePolicy.java',
     'fxa/sync/FxAccountSyncAdapter.java',
+    'fxa/sync/FxAccountSyncDelegate.java',
     'fxa/sync/FxAccountSyncService.java',
     'fxa/sync/FxAccountSyncStatusHelper.java',
     'fxa/sync/SchedulePolicy.java',
     'fxa/tasks/FxAccountCodeResender.java',
     'fxa/tasks/FxAccountCreateAccountTask.java',
     'fxa/tasks/FxAccountSetupTask.java',
     'fxa/tasks/FxAccountSignInTask.java',
     'fxa/tasks/FxAccountUnlockCodeResender.java',
@@ -1144,8 +1145,12 @@ sync_java_files = [
     'sync/UnexpectedJSONException.java',
     'sync/UnknownSynchronizerConfigurationVersionException.java',
     'sync/Utils.java',
     'tokenserver/TokenServerClient.java',
     'tokenserver/TokenServerClientDelegate.java',
     'tokenserver/TokenServerException.java',
     'tokenserver/TokenServerToken.java',
 ]
+reading_list_service_java_files = [
+    'reading/ReadingListSyncAdapter.java',
+    'reading/ReadingListSyncService.java',
+]
--- a/mobile/android/base/db/LocalBrowserDB.java
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -58,33 +58,33 @@ import android.provider.Browser;
 import android.text.TextUtils;
 import android.util.Log;
 import org.mozilla.gecko.util.IOUtils;
 
 import static org.mozilla.gecko.util.IOUtils.ConsumedInputStream;
 import static org.mozilla.gecko.favicons.LoadFaviconTask.DEFAULT_FAVICON_BUFFER_SIZE;
 
 public class LocalBrowserDB implements BrowserDB {
-    // Calculate these once, at initialization. isLoggable is too expensive to
+    private static final String LOGTAG = "GeckoLocalBrowserDB";
+
+    // Calculate this once, at initialization. isLoggable is too expensive to
     // have in-line in each log call.
-    private static final String LOGTAG = "GeckoLocalBrowserDB";
+    private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
+    protected static void debug(String message) {
+        if (logDebug) {
+            Log.d(LOGTAG, message);
+        }
+    }
 
     // Sentinel value used to indicate a failure to locate an ID for a default favicon.
     private static final int FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE;
 
     // Constant used to indicate that no folder was found for particular GUID.
     private static final long FOLDER_NOT_FOUND = -1L;
 
-    private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
-    protected static void debug(String message) {
-        if (logDebug) {
-            Log.d(LOGTAG, message);
-        }
-    }
-
     private final String mProfile;
 
     // Map of folder GUIDs to IDs. Used for caching.
     private final HashMap<String, Long> mFolderIdMap;
 
     // Use wrapped Boolean so that we can have a null state
     private volatile Boolean mDesktopBookmarksExist;
 
--- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
@@ -40,17 +40,18 @@ import android.os.Bundle;
  * <p>
  * Account user data is accessible only to the Android App(s) that own the
  * Account type. Account user data is not removed when the App's private data is
  * cleared.
  */
 public class AndroidFxAccount {
   protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
 
-  public static final int CURRENT_PREFS_VERSION = 1;
+  public static final int CURRENT_SYNC_PREFS_VERSION = 1;
+  public static final int CURRENT_RL_PREFS_VERSION = 1;
 
   // When updating the account, do not forget to update AccountPickler.
   public static final int CURRENT_ACCOUNT_VERSION = 3;
   public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
   public static final String ACCOUNT_KEY_PROFILE = "profile";
   public static final String ACCOUNT_KEY_IDP_SERVER = "idpServerURI";
 
   // The audience should always be a prefix of the token server URI.
@@ -236,53 +237,66 @@ public class AndroidFxAccount {
   public String getAudience() {
     return accountManager.getUserData(account, ACCOUNT_KEY_AUDIENCE);
   }
 
   public String getTokenServerURI() {
     return accountManager.getUserData(account, ACCOUNT_KEY_TOKEN_SERVER);
   }
 
-  /**
-   * This needs to return a string because of the tortured prefs access in GlobalSession.
-   */
-  public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
+  private String constructPrefsPath(String product, long version, String extra) throws GeneralSecurityException, UnsupportedEncodingException {
     String profile = getProfile();
     String username = account.name;
 
     if (profile == null) {
       throw new IllegalStateException("Missing profile. Cannot fetch prefs.");
     }
 
     if (username == null) {
       throw new IllegalStateException("Missing username. Cannot fetch prefs.");
     }
 
-    final String tokenServerURI = getTokenServerURI();
-    if (tokenServerURI == null) {
-      throw new IllegalStateException("No token server URI. Cannot fetch prefs.");
-    }
-
     final String fxaServerURI = getAccountServerURI();
     if (fxaServerURI == null) {
       throw new IllegalStateException("No account server URI. Cannot fetch prefs.");
     }
 
-    final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
-    final long version = CURRENT_PREFS_VERSION;
+    // This is unique for each syncing 'view' of the account.
+    final String serverURLThing = fxaServerURI + "!" + extra;
+    return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
+  }
 
-    // This is unique for each syncing 'view' of the account.
-    final String serverURLThing = fxaServerURI + "!" + tokenServerURI;
-    return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
+  /**
+   * This needs to return a string because of the tortured prefs access in GlobalSession.
+   */
+  public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
+    final String tokenServerURI = getTokenServerURI();
+    if (tokenServerURI == null) {
+      throw new IllegalStateException("No token server URI. Cannot fetch prefs.");
+    }
+
+    final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
+    final long version = CURRENT_SYNC_PREFS_VERSION;
+    return constructPrefsPath(product, version, tokenServerURI);
+  }
+
+  public String getReadingListPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
+    final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".reading";
+    final long version = CURRENT_RL_PREFS_VERSION;
+    return constructPrefsPath(product, version, "");
   }
 
   public SharedPreferences getSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
     return context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
   }
 
+  public SharedPreferences getReadingListPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
+    return context.getSharedPreferences(getReadingListPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
+  }
+
   /**
    * Extract a JSON dictionary of the string values associated to this account.
    * <p>
    * <b>For debugging use only!</b> The contents of this JSON object completely
    * determine the user's Firefox Account status and yield access to whatever
    * user data the device has access to.
    *
    * @return JSON-object of Strings.
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -81,129 +81,55 @@ public class FxAccountSyncAdapter extend
   protected final FxAccountNotificationManager notificationManager;
 
   public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
     super(context, autoInitialize);
     this.executor = Executors.newSingleThreadExecutor();
     this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID);
   }
 
-  protected static class SyncDelegate {
-    protected final CountDownLatch latch;
-    protected final SyncResult syncResult;
-    protected final AndroidFxAccount fxAccount;
+  protected static class SyncDelegate extends FxAccountSyncDelegate {
+    @Override
+    public void handleSuccess() {
+      Logger.info(LOG_TAG, "Sync succeeded.");
+      super.handleSuccess();
+    }
+
+    @Override
+    public void handleError(Exception e) {
+      Logger.error(LOG_TAG, "Got exception syncing.", e);
+      super.handleError(e);
+    }
+
+    @Override
+    public void handleCannotSync(State finalState) {
+      Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
+      super.handleCannotSync(finalState);
+    }
+
+    @Override
+    public void postponeSync(long millis) {
+      if (millis <= 0) {
+        Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay.");
+      }
+      super.postponeSync(millis);
+    }
+
+    @Override
+    public void rejectSync() {
+      super.rejectSync();
+    }
+
     protected final Collection<String> stageNamesToSync;
 
     public SyncDelegate(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) {
-      if (latch == null) {
-        throw new IllegalArgumentException("latch must not be null");
-      }
-      if (syncResult == null) {
-        throw new IllegalArgumentException("syncResult must not be null");
-      }
-      if (fxAccount == null) {
-        throw new IllegalArgumentException("fxAccount must not be null");
-      }
-      this.latch = latch;
-      this.syncResult = syncResult;
-      this.fxAccount = fxAccount;
+      super(latch, syncResult, fxAccount);
       this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync);
     }
 
-    /**
-     * No error!  Say that we made progress.
-     */
-    protected void setSyncResultSuccess() {
-      syncResult.stats.numUpdates += 1;
-    }
-
-    /**
-     * Soft error. Say that we made progress, so that Android will sync us again
-     * after exponential backoff.
-     */
-    protected void setSyncResultSoftError() {
-      syncResult.stats.numUpdates += 1;
-      syncResult.stats.numIoExceptions += 1;
-    }
-
-    /**
-     * Hard error. We don't want Android to sync us again, even if we make
-     * progress, until the user intervenes.
-     */
-    protected void setSyncResultHardError() {
-      syncResult.stats.numAuthExceptions += 1;
-    }
-
-    public void handleSuccess() {
-      Logger.info(LOG_TAG, "Sync succeeded.");
-      setSyncResultSuccess();
-      latch.countDown();
-    }
-
-    public void handleError(Exception e) {
-      Logger.error(LOG_TAG, "Got exception syncing.", e);
-      setSyncResultSoftError();
-      // This is awful, but we need to propagate bad assertions back up the
-      // chain somehow, and this will do for now.
-      if (e instanceof TokenServerException) {
-        // We should only get here *after* we're locked into the married state.
-        State state = fxAccount.getState();
-        if (state.getStateLabel() == StateLabel.Married) {
-          Married married = (Married) state;
-          fxAccount.setState(married.makeCohabitingState());
-        }
-      }
-      latch.countDown();
-    }
-
-    /**
-     * When the login machine terminates, we might not be in the
-     * <code>Married</code> state, and therefore we can't sync. This method
-     * messages as much to the user.
-     * <p>
-     * To avoid stopping us syncing altogether, we set a soft error rather than
-     * a hard error. In future, we would like to set a hard error if we are in,
-     * for example, the <code>Separated</code> state, and then have some user
-     * initiated activity mark the Android account as ready to sync again. This
-     * is tricky, though, so we play it safe for now.
-     *
-     * @param finalState
-     *          that login machine ended in.
-     */
-    public void handleCannotSync(State finalState) {
-      Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
-      setSyncResultSoftError();
-      latch.countDown();
-    }
-
-    public void postponeSync(long millis) {
-      if (millis <= 0) {
-        Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay. Short-circuiting.");
-      } else {
-        // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669
-        // So we don't bother doing this. Instead, we rely on the periodic sync
-        // we schedule, and the backoff handler for the rest.
-        /*
-        Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms.");
-        syncResult.delayUntil = millis / 1000;
-         */
-      }
-      setSyncResultSoftError();
-      latch.countDown();
-    }
-
-    /**
-     * Simply don't sync, without setting any error flags.
-     * This is the appropriate behavior when a routine backoff has not yet
-     * been met.
-     */
-    public void rejectSync() {
-      latch.countDown();
-    }
-
     public Collection<String> getStageNamesToSync() {
       return this.stageNamesToSync;
     }
   }
 
   protected static class SessionCallback implements BaseGlobalSessionCallback {
     protected final SyncDelegate syncDelegate;
     protected final SchedulePolicy schedulePolicy;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountSyncDelegate.java
@@ -0,0 +1,122 @@
+/* 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/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Married;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.tokenserver.TokenServerException;
+
+import android.content.SyncResult;
+
+public class FxAccountSyncDelegate {
+  protected final CountDownLatch latch;
+  protected final SyncResult syncResult;
+  protected final AndroidFxAccount fxAccount;
+
+  public FxAccountSyncDelegate(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount) {
+    if (latch == null) {
+      throw new IllegalArgumentException("latch must not be null");
+    }
+    if (syncResult == null) {
+      throw new IllegalArgumentException("syncResult must not be null");
+    }
+    if (fxAccount == null) {
+      throw new IllegalArgumentException("fxAccount must not be null");
+    }
+    this.latch = latch;
+    this.syncResult = syncResult;
+    this.fxAccount = fxAccount;
+  }
+
+  /**
+   * No error!  Say that we made progress.
+   */
+  protected void setSyncResultSuccess() {
+    syncResult.stats.numUpdates += 1;
+  }
+
+  /**
+   * Soft error. Say that we made progress, so that Android will sync us again
+   * after exponential backoff.
+   */
+  protected void setSyncResultSoftError() {
+    syncResult.stats.numUpdates += 1;
+    syncResult.stats.numIoExceptions += 1;
+  }
+
+  /**
+   * Hard error. We don't want Android to sync us again, even if we make
+   * progress, until the user intervenes.
+   */
+  protected void setSyncResultHardError() {
+    syncResult.stats.numAuthExceptions += 1;
+  }
+
+  public void handleSuccess() {
+    setSyncResultSuccess();
+    latch.countDown();
+  }
+
+  public void handleError(Exception e) {
+    setSyncResultSoftError();
+    // This is awful, but we need to propagate bad assertions back up the
+    // chain somehow, and this will do for now.
+    if (e instanceof TokenServerException) {
+      // We should only get here *after* we're locked into the married state.
+      State state = fxAccount.getState();
+      if (state.getStateLabel() == StateLabel.Married) {
+        Married married = (Married) state;
+        fxAccount.setState(married.makeCohabitingState());
+      }
+    }
+    latch.countDown();
+  }
+
+  /**
+   * When the login machine terminates, we might not be in the
+   * <code>Married</code> state, and therefore we can't sync. This method
+   * messages as much to the user.
+   * <p>
+   * To avoid stopping us syncing altogether, we set a soft error rather than
+   * a hard error. In future, we would like to set a hard error if we are in,
+   * for example, the <code>Separated</code> state, and then have some user
+   * initiated activity mark the Android account as ready to sync again. This
+   * is tricky, though, so we play it safe for now.
+   *
+   * @param finalState
+   *          that login machine ended in.
+   */
+  public void handleCannotSync(State finalState) {
+    setSyncResultSoftError();
+    latch.countDown();
+  }
+
+  public void postponeSync(long millis) {
+    if (millis > 0) {
+      // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669
+      // So we don't bother doing this. Instead, we rely on the periodic sync
+      // we schedule, and the backoff handler for the rest.
+      /*
+      Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms.");
+      syncResult.delayUntil = millis / 1000;
+       */
+    }
+    setSyncResultSoftError();
+    latch.countDown();
+  }
+
+  /**
+   * Simply don't sync, without setting any error flags.
+   * This is the appropriate behavior when a routine backoff has not yet
+   * been met.
+   */
+  public void rejectSync() {
+    latch.countDown();
+  }
+}
\ No newline at end of file
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -556,16 +556,23 @@ just addresses the organization to follo
 
 <!ENTITY colon ":">
 
 <!-- These are only used for accessibility for the done and overflow-menu buttons in the actionbar.
      They are never shown to users -->
 <!ENTITY actionbar_menu "Menu">
 <!ENTITY actionbar_done "Done">
 
+<!-- Voice search in the awesome bar -->
+<!ENTITY voicesearch_prompt "Speak now">
+<!ENTITY voicesearch_failed_title "&brandShortName; Voice Search">
+<!ENTITY voicesearch_failed_message "There is a problem with voice search right now. Please try later.">
+<!ENTITY voicesearch_failed_message_recoverable "Sorry! We could not recognize your words. Please try again.">
+<!ENTITY voicesearch_failed_retry "Try again">
+
 <!-- Localization note (remote_tabs_last_synced): the variable is replaced by a
      "relative time span string" produced by Android.  This string describes the
      time the tabs were last synced relative to the current time; examples
      include "42 minutes ago", "4 days ago", "last week", etc. The subject of
      "Last synced" is one of the user's other Sync clients, typically Firefox on
      their desktop or laptop.-->
 <!ENTITY remote_tabs_last_synced "Last synced: &formatS;">
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -561,16 +561,20 @@ else:
 if CONFIG['MOZ_ANDROID_NEW_TABLET_UI'] and max_sdk_version >= 11:
     gbjar.sources += [
         'tabs/TabStrip.java',
         'tabs/TabStripAdapter.java',
         'tabs/TabStripItemView.java',
         'tabs/TabStripView.java'
     ]
 
+# Selectively include reading list service code.
+if CONFIG['MOZ_ANDROID_READING_LIST_SERVICE']:
+    gbjar.sources += reading_list_service_java_files
+
 gbjar.sources += sync_java_files
 gbjar.extra_jars += [
     'gecko-R.jar',
     'gecko-mozglue.jar',
     'gecko-thirdparty.jar',
     'gecko-util.jar',
     'sync-thirdparty.jar',
 ]
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListSyncAdapter.java
@@ -0,0 +1,26 @@
+/* -*- Mode: Java; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+
+import android.accounts.Account;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.SyncResult;
+import android.os.Bundle;
+
+public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter {
+    public ReadingListSyncAdapter(Context context, boolean autoInitialize) {
+      super(context, autoInitialize);
+    }
+
+    @Override
+    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
+      final AndroidFxAccount fxAccount = new AndroidFxAccount(getContext(), account);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListSyncService.java
@@ -0,0 +1,28 @@
+/* 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/. */
+
+package org.mozilla.gecko.reading;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class ReadingListSyncService extends Service {
+  private static final Object syncAdapterLock = new Object();
+  private static ReadingListSyncAdapter syncAdapter;
+
+  @Override
+  public void onCreate() {
+    synchronized (syncAdapterLock) {
+      if (syncAdapter == null) {
+        syncAdapter = new ReadingListSyncAdapter(getApplicationContext(), true);
+      }
+    }
+  }
+
+  @Override
+  public IBinder onBind(Intent intent) {
+    return syncAdapter.getSyncAdapterBinder();
+  }
+}
new file mode 100644
index 0000000000000000000000000000000000000000..b74044e6c31723a909be286150e9972af515e487
GIT binary patch
literal 874
zc$@)h1C{)VP)<h;3K|Lk000e1NJLTq002Ay002A)1^@s6I{evk0009qNkl<ZXhZFo
zOK4L;6o#7@RS*{yy3mEKxO5prtfC=Fv7sxA_&~&hg6K*?QP9>_r3+CvqMM=x1s~8Z
z(=<&<#kPoa>&AuDg@S?+MbMC#_?<G5OYI|zJJWbG1H+v=narH;{AcFQjIW{2lCREq
z8!WKb6}Jjog+1m08`2&V^dB6nuvOS&9<U+pF+uI3Wi+HSnau8Tx!i->+m!34eLi1*
zEEaodDzc0L1A7SA$AASE!2JgLD9}$$KpO!Db_1~Q09wBU)OVn_0R2Tbv=QKRI(;0V
zhs(j>knZ;O_93qT_W<0t0&p+D{hk0GA0NL8Nbd?Teerl)+G$NjLfQf^u3>#C%pC7w
zmA<Amp6R@nLi!4G___sl*#lN#mmNCaU6rf!{}<BisP?e<L<F)>C=4VLiM*7lUvc9>
zv&+ok8`3_ROg4r>p-WO`2bN_+^1>}4Tn%ZIJd0ts#W=s;-x7^R-z(s1wwA{LdnD~A
zCMGtQN~Jjkc@9V1sYzfC3HwlFR2kQWp(9eR;_&b-pU<~;c6NSMQ(!;OVJQ<a9el1*
zH3^)}X4e&q#m^YGmOKJ*SH;lfj)$~4Lfhw+wnU1+3iQulFxVW4M1IsHuu|s%&u(&w
z+y=-i2ek77piKP?S$0@c4%i)(r)5^4?hxuqU#=xr=df5cHa6DE{Cy)TWAGe#7jyYT
zH5M~>H!&rq$StBS|J??#oTFV!t^<o@tjeb__P&%A^g*CsFK0Ed+W~#1VC8>jfW7A$
zsp?1zunO$~o)^_Pa*gwZm3OeIsc9x04i}_;c6K(D%jI?w5hnmWOjYIj=O$r(!EM-i
z0d#a)>63-(G$6NlU*i}@zRe%i`$nFPj^BP!J`e0&U3O^&7Nb+C)Yd>CAU7EY$iXE*
ziqsFQ6VqE=u-X-N8qQ+Y40s2MpQQfpH7)Ohg!Cx$^^;D}O5fTQUI?*ZUv1>j@3g@J
z>kcccuz{P()eh;s7$8~FB%h#3-BGowz$W1q8_W2jWq}QQz$$Fm;l_uv3LAI1;Q_6}
zh8=EvIIFO6mm40?Ds0%{#)q>C8+W<k0qaiTKk0xoOx<+AzW@LL07*qoM6N<$f)6K_
Ao&W#<
new file mode 100644
index 0000000000000000000000000000000000000000..789eafa87033495bcac195b1708b4c6582d3db63
GIT binary patch
literal 524
zc$@(Y0`vWeP)<h;3K|Lk000e1NJLTq001Ze001Zm1^@s6jQ+T70005gNkl<ZSV!%a
zKTE?v7{;4F#V#sl6T}Z7h`5N0Lr7Agpkp=#1vkZ|n~pl^q;7%*-8w3SLMR;y4lX+A
z7ZAbGC=N<v;&XC^QX_5eLOtaUUM|Vqd-uENeeW(xM)0Bt&X<vZyb+QLN(Ffp;Psa|
zD1?HwS}hlb;T~X?DbQ)7(I^%Qg?AwUecwL>$a)*hk>fZUq5wVvWT*}11%Sh%0LDom
z0Oa0;+yf9Wg!oSa#!yfdIm8hFH3`^_0zc&7?<okNH-Og>LWgdWZ^w+HC{MO+8?@a=
zJ%{eOZWi3yhiMlB@EU`R>AF5@nr4`EP{(n6kk98gam(nqWggN00f5u}0^GtxJFrzu
zTJk(^Ae+tZCtY=bU%~ei*f#p^_6v~Q5O=UaRaG6!vaXUQu2!ovQ52EzzQQH|NK&Dv
z!W3efSF_}dhUqY(-Z22~Y1=sJHCPcAViEIL#AQ!pv@<gx@vl@WQ<|oo;c=3@lZ`=Y
z<lhJ#{}#_n$i*8LxUVr;9h-jTa#=GBV*wzG_(t-LloH-Af*?3|UH5}$4sO8K$(!e4
z8b1lmaU~#6cvHziUVu*isfM`IWGw9xkVUmtAE}^JkXHd-f0={-UcpZRf2%3HtWTl<
O0000<MNUMnLSTXc#N$)|
new file mode 100644
index 0000000000000000000000000000000000000000..2277ff082b1fb38b2e349a13c79a8c0253f7eda7
GIT binary patch
literal 1149
zc%17D@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U~%zuaSW-5dppZBLpV{QZMKQx
z%55tuF382a%Uhr%)n&FM^YR6`FHtND<Dys{^~GWe827$#yu-Kjfn&d1j$!7l8A=Cv
zMK4x~iEmi+pi509x2AsP#Fq|leD0N4P0iPPd8X#Q{r&G>{!afR?B6RR!pOf=tgx3s
z#aDrWRl&1iN;fmf)PM7anLcV3mX<f8wWjV0xGlAI-*mbD=T*76Z=V)e%u&u>x9UzP
zpNynrBxk{PMsIfWwGnrM6&_#A_~IL?DbTx<Z9!4o!hnhxzIkzrn_hhXZNB?x=Q>6U
zE2}Djf^(6E%nI^ZnVFXPs%6`fW0(c*e&_7Eo3Zi1@)yTu>l{0mHDflzs*{Qd8*kY#
zu<1hh)82$|HLN>vSJYFZ;Kd=P3w_OJHZo0EsLrSI@+N!4A&D1H=bzec5P$rD62q^b
zpMHw5D4Yyn;BuJ6pefD{rOt^r{8$#~u+DC({<gK>wr|@OXY{P^cbS=RU4rE0ge%K~
zR@UBlwrfRfan8)gGng4)*mgy)6RnV9=$plG>*l#Cj=y1(|B0OI&$zBuuz#Y-bk&`c
z8Qv=_-Y_jxh;^H^@s^zns~DtLiTf}JTl8mMKXzf?KOJucE#4v?1%|l}*KL=n&ieeb
zruW9Q|BKY~O1<Sjp5OR&zbY_Hd2by(niOdH=e(Tau1}r}c5#{_)>g+E7ICi+J}N!w
zb+h-mSutPEPxtDOoj+?<*Y>Sj_nN-sd;F)Tj+eolVZq19vKcqJN_iHX;u0|ak#WuF
z51075R_^<f#@l{J6<1VLv|fK*+h``1`EECNg>_D#LX#}(48P^h1p#4SjdY*0?~KvA
zbo1uV&6A|q*xAKdnh%<6`PH(>EOq<go3AAMWcrw{#H^U6zqjqxu}?q$tn2IRJ71<H
zCnXgn#KDp$CA0QPIOF=`e%3i_9y5fBA8lB_Yg3`*C#ky|)P*`%%q%&^?3dR*dA&lx
zRJk6`SAn&)wZ$4Dzf6;vLwO2h7Mzkb{uCrK=g|DiamkNv+4?a2{gLTzB-Xt$^6UGF
zK5Eh*FZW3#eN$3PN&3L^I8)^D%jH^|HeR_hhaqHU>gJnlJ@wZaQqpJo*fU&d+0`zw
zl_5S%?VZ>()7=}_{7hT<NQ?cfq{Cjm8rIv)0a=#8d;vQr**jft&$DmvkIw(H>SVz=
z##KohL?S!h-Q-<f`ncpT4{P7!e-5#+vGu{hm%nEnTCqeooWVqGo`*^p^E0y@ui92F
z(DRY`zOkb&)N|qHrG>H%djmdKCvE>Dbns8VK*JJK)&&eIoD+%)A=GuN32ROp?qpC2
l<aS{65K?&hh%#zFV?RS<LhhdYLSV7N;OXk;vd$@?2>@JE^1A>4
new file mode 100644
index 0000000000000000000000000000000000000000..b59c2695f260e0c7500a840befb9af084a6ad7da
GIT binary patch
literal 1843
zc%0Rh>syis0L5QOacVMeC3r!dd3#u?k5)8Xnd!(I6}e?*_)bkxQeoD>3yHVFgqdQQ
zTAA~j8j`gPDTT&jmO^Qf<_$3|Z%iXKLv!uf->?t+aL(^M=fnAW@(<#1Mh2D!000;T
z?Dr#lCjB3M0sHK`Vs`Wb0PJIc-`-F<aqh}d66$+%smAeH=vOL2hRe3o4APdjL1eSM
z*aKS7;cbBdL1Izbz|!56Xvv^t1?DgMlT4c3&Rac;{qhe#LSzK_kczO$*6-$J)^7u=
zk^Akgc4r92uf)i{{bXqWzQQw3wz95XA1UkVKH@UAr_aDVm?oVJ{G$JZogE0>!DB8g
zz5VYAKawR72z)a$*A>216sz_IvD{xXyqY^|8>J<OgoK>qGqw$FD>#y*5bxW!@B6;K
zJ}o3U+Cv{$sqXFVwc&6$k>R-~P3J`-9|zF`T`(@)x56*uSW1fKdR&J4kCOoQjjX$y
zUCR^#WYeV4n4YdWK;9yVeiHJ;f0m}h+<7dV8v;?!%nZUXf1N1Xa|@MBCXp~Dj_2wa
zA&^H%O_TBR5*unODO<EbrZfx;TPl;QW=|krtihRX^2_^r)C!<7yEQH*lxDz`cmG=y
z4DkWbv;A_s<S2~n4#7>rp}&*`Pl4Of-BWo_#X;hy;!}Kr@KK4sUZ0p)eUZ#B4Q{gC
z@>&@W$kMhx_}JrYT|}NI{D_B8@+^IkhV~#-hROYbN~>}$hZFr{Z@~RU-UUlY_f!1B
zz*NaUYw~;5IgHT94rk$YIgCd(HnY_Pl*cIw2#4|GOCs*xn;jS!2nr7mKf|{2VU^Nj
z;-IuvS^ksJ!NGKoYwfG{t%<OOiPVjx%IL-pt}f{OR3a$)uF?MO8-1oDsvUcF)k^hl
zoRgp6oxV1uN~8xb>s+hdJ&K$4jtbrIC`A_OBWh&1Cj#?$D3~veU3OSMuD{YYu4%JK
z-Y{QHpO`53R<2kd#wuSzK3rm5Kd|?y^LROzK5rwk<c=Kj{e*w6@vFJE(>SxKus>DO
z0Ass;G-17q@Vo4RIhk@L4BEnD+bwef><$*DzllE_qs7l^GEnmwzud5Nw@%q=t)bQw
z?YbZF^Jy}9h8~fFB<|MUZf-sq&0wq@p{Ay%rST+7Ew|>pdfXd$IXQb4Ot6KB7tE`6
z6pq<qBMWn!5|UmFA`nIhMAFUo48|$w!XUNPD?;+%c>m9lF<Q(tOL0k0Dv55?V|%{5
zO6j^?1pg$S2;4+8)jHkqoNa@YhU$5bczI`AV}a=~0#7U!qc6<3+{M}I(9N~qR-Y%;
zAQqWIp-|q`(C`6u8rKq{hr{72u-|+W%he&O(bn$!;ZcEV*QIuscclu2VrdLvR5Tt8
z-UutKRnefb2B2<ATBMpHSu&?2ciPd>AAc1_JN0w2T3<KYRL(toH7F>SUi$Oqh?v(k
zHa52XLkCA5rBIprbXZ)7EH-v1-$3~b@+ZSXAVs6mP2_}xPsIH3z{!nJJ{hsC1J$`(
zbm0Bn>*==fJMDzz&>|Xl=sEmeQY=NrN$}##9aD}R3NS`rL?c9afHeZAE<3TF6_`~C
zx$47O159S;$_YuEWG-j#dv{nLYBVENs;f&M>B0}4GtJc+R(2jF5<A*>JoQU2RTMI;
z;Dj=3W$2>5wu5zhQrq{s2fG6!()88mtb!&B_^)Kg^*ZxM&y7t-s-ngehE%5qx*j`!
zFH2+{<(8NYE_2xII~G{7+$wgKU46@?vYhXP6{$B@$t$PNd^bd8uKqp&`+C`;F=*ko
zY&^`9MSUquNXBiRvB&7SCBZHvi`FB>@<2xYSu1mzDtDSM^*M+%^#bc%>IMRb9L{XC
z8*0zEbm6gcQN1BK)miZ_pz4aCMjthwop5jcKKyV?#ogi2pVnt=mBZ#|0B{*-);{c2
zu9rdX`#c;rFDoy9b)H*aF<*ww3h&%lJKnFZQ_`p!)tYe<u?Bg162MhRf79qPt<$@7
zq%D7JpG%I{G2E)~2KBZ~#Rmnsr8ygUL*YHet}~9xlI*<Acu|!}&`~!?z4_O7C{Q>j
z>~WetrWUNqGoz3s1Vxa@Ro8IqY9<8Z<x;$8%C9LmNHO92x)eK_{15Y*VP~O7Y3Via
Q@#ixH0{roQJYO*TZv-lQ7ytkO
--- a/mobile/android/base/resources/layout/toolbar_edit_layout.xml
+++ b/mobile/android/base/resources/layout/toolbar_edit_layout.xml
@@ -11,11 +11,12 @@
           style="@style/UrlBar.Title"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:layout_weight="1.0"
           android:inputType="textUri|textNoSuggestions"
           android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
           android:selectAllOnFocus="true"
           android:contentDescription="@string/url_bar_default_text"
+          android:drawableRight="@drawable/ab_mic"
           gecko:autoUpdateTheme="false"/>
 
 </merge>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/xml/readinglist_syncadapter.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="@string/moz_android_shared_fxaccount_type"
+    android:contentAuthority="@string/content_authority_db_readinglist"
+    android:isAlwaysSyncable="true"
+    android:supportsUploading="true"
+    android:userVisible="true"
+/>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -479,15 +479,22 @@
   <string name="guest_browsing_notification_text">&guest_browsing_notification_text;</string>
 
   <string name="exit_guest_session_title">&exit_guest_session_title;</string>
   <string name="exit_guest_session_text">&exit_guest_session_text;</string>
 
   <string name="actionbar_menu">&actionbar_menu;</string>
   <string name="actionbar_done">&actionbar_done;</string>
 
+  <!-- Voice search from the Awesome Bar -->
+  <string name="voicesearch_prompt">&voicesearch_prompt;</string>
+  <string name="voicesearch_failed_title">&voicesearch_failed_title;</string>
+  <string name="voicesearch_failed_message">&voicesearch_failed_message;</string>
+  <string name="voicesearch_failed_message_recoverable">&voicesearch_failed_message_recoverable;</string>
+  <string name="voicesearch_failed_retry">&voicesearch_failed_retry;</string>
+
   <!-- Miscellaneous -->
   <string name="ellipsis">&ellipsis;</string>
 
   <string name="colon">&colon;</string>
 
   <string name="remote_tabs_last_synced">&remote_tabs_last_synced;</string>
 </resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/roboextender/SelectionUtils.js
@@ -0,0 +1,126 @@
+/* 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/. */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import('resource://gre/modules/Geometry.jsm');
+
+/* ============================== Utility functions ================================================
+ *
+ * Common functions available to all tests.
+ *
+ */
+function getSelectionHandler() {
+  return (!this._selectionHandler) ?
+    this._selectionHandler = Services.wm.getMostRecentWindow("navigator:browser").SelectionHandler :
+    this._selectionHandler;
+}
+
+function todo(result, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    todo: result,
+    msg: msg
+  });
+}
+
+function ok(result, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    result: result,
+    msg: msg
+  });
+}
+
+function is(one, two, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    result: one === two,
+    msg: msg + " : " + one + " === " + two
+  });
+}
+
+function isNot(one, two, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    result: one !== two,
+    msg: msg + " : " + one + " !== " + two
+  });
+}
+
+function lessThan(n1, n2, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    result: n1 < n2,
+    msg: msg + " : " + n1 + " < " + n2
+  });
+}
+
+function greaterThan(n1, n2, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    result: n1 > n2,
+    msg: msg + " : " + n1 + " > " + n2
+  });
+}
+
+// Use fuzzy logic to compare screen coords.
+function truncPoint(point) {
+  return new Point(Math.trunc(point.x), Math.trunc(point.y));
+}
+
+function pointEquals(p1, p2, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    result: truncPoint(p1).equals(truncPoint(p2)),
+    msg: msg + " : " + p1.toString() + " == " + p2.toString()
+  });
+}
+
+function pointNotEquals(p1, p2, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    result: !truncPoint(p1).equals(truncPoint(p2)),
+    msg: msg + " : " + p1.toString() + " == " + p2.toString()
+  });
+}
+
+function selectionExists(selection, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    result: !truncPoint(selection.anchorPt).equals(truncPoint(selection.focusPt)),
+    msg: msg + " : anchor:" + selection.anchorPt.toString() +
+      " focus:" + selection.focusPt.toString()
+  });
+}
+
+function selectionEquals(s1, s2, msg) {
+  return Messaging.sendRequestForResult({
+    type: TYPE_NAME,
+    result: truncPoint(s1.anchorPt).equals(truncPoint(s2.anchorPt)) &&
+      truncPoint(s1.focusPt).equals(truncPoint(s2.focusPt)),
+    msg: msg
+  });
+}
+
+/* =================================================================================================
+ *
+ * After finish of all selection tests, wrap up and go home.
+ *
+ */
+function finishTests() {
+  Messaging.sendRequest({
+    type: TYPE_NAME,
+    result: true,
+    msg: "Done!",
+    done: true
+  });
+}
+
--- a/mobile/android/base/tests/roboextender/testInputSelections.html
+++ b/mobile/android/base/tests/roboextender/testInputSelections.html
@@ -1,37 +1,36 @@
 <html>
   <head>
     <title>Automated RTL/LTR Text Selection tests for Input elements</title>
     <meta name="viewport" content="initial-scale=1.0"/>
     <script type="application/javascript"
       src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+    <script type="application/javascript" src="SelectionUtils.js"></script>
     <script type="application/javascript;version=1.8">
 
+// Name of this test.
+const TYPE_NAME = "Robocop:testInputSelections";
+
 // Used to create handle movement events for SelectionHandler.
 const ANCHOR = "ANCHOR";
 const FOCUS = "FOCUS";
 
 // Types of DOM nodes that serve as Selection Anchor/Focus nodes.
 const DIV_NODE = "DIV";
 const TEXT_NODE = "#text";
 
 // Used to specifiy midpoint selection text left/right of center.
 const EST_SEL_TEXT_BOUND_CHARS = 5;
 
 // Used to create test scenarios, and verify results.
 const LTR_INPUT_TEXT_VALUE = "This input text is one character short of it's maxmimum.";
 const RTL_INPUT_TEXT_VALUE = "טקסט קלט זה קצר תו אחד של זה גדול.";
 
 
-const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
-Cu.import("resource://gre/modules/Messaging.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import('resource://gre/modules/Geometry.jsm');
-
 /* =================================================================================
  *
  * Start of all text selection tests, check initialization state.
  */
 function startTests() {
   testLTR_selectAll().
     then(testRTL_selectAll).
 
@@ -413,133 +412,16 @@ function testRTL_dragAnchorHandleToSelf(
     selectionEquals(anchorDraggedSelection, initialSelection,
       "RTL Selection points after anchor drag " +
       "should match initial selection points."),
 
     ok(true, "testRTL_dragAnchorHandleToSelf - Test Finishes."),
   ]);
 }
 
-/* =================================================================================
- *
- * After finish of all selection tests, wrap up and go home.
- *
- */
-function finishTests() {
-  Messaging.sendRequest({
-    type: "Robocop:testInputSelections",
-    result: true,
-    msg: "Done!",
-    done: true
-  });
-}
-
-/* ============================== Utility functions ======================
- *
- * Common functions available to all tests.
- *
- */
-function getSelectionHandler() {
-  return (!this._selectionHandler) ?
-    this._selectionHandler = Services.wm.getMostRecentWindow("navigator:browser").SelectionHandler :
-    this._selectionHandler;
-}
-
-function todo(result, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    todo: result,
-    msg: msg
-  });
-}
-
-function ok(result, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    result: result,
-    msg: msg
-  });
-}
-
-function is(one, two, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    result: one === two,
-    msg: msg + " : " + one + " === " + two
-  });
-}
-
-function isNot(one, two, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    result: one !== two,
-    msg: msg + " : " + one + " !== " + two
-  });
-}
-
-function lessThan(n1, n2, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    result: n1 < n2,
-    msg: msg + " : " + n1 + " < " + n2
-  });
-}
-
-function greaterThan(n1, n2, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    result: n1 > n2,
-    msg: msg + " : " + n1 + " > " + n2
-  });
-}
-
-// Use fuzzy logic to compare screen coords.
-function truncPoint(point) {
-  return new Point(Math.trunc(point.x), Math.trunc(point.y));
-}
-
-function pointEquals(p1, p2, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    result: truncPoint(p1).equals(truncPoint(p2)),
-    msg: msg + " : " + p1.toString() + " == " + p2.toString()
-  });
-}
-
-function pointNotEquals(p1, p2, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    result: !truncPoint(p1).equals(truncPoint(p2)),
-    msg: msg + " : " + p1.toString() + " == " + p2.toString()
-  });
-}
-
-function selectionExists(selection, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    result: !truncPoint(selection.anchorPt).equals(truncPoint(selection.focusPt)),
-    msg: msg + " : anchor:" + selection.anchorPt.toString() +
-      " focus:" + selection.focusPt.toString()
-  });
-}
-
-function selectionEquals(s1, s2, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testInputSelections",
-    result: truncPoint(s1.anchorPt).equals(truncPoint(s2.anchorPt)) &&
-            truncPoint(s1.focusPt).equals(truncPoint(s2.focusPt)),
-    msg: msg
-  });
-}
-
-/* =================================================================================
- *
- * Page definition for all tests.
- *
- */
     </script>
   </head>
 
   <body onload="startTests();">
     <input id="LTRInput" dir="ltr" type="text" maxlength="57" size="57" value="">
     <br>
     <input id="RTLInput" dir="rtl" type="text" maxlength="35" size="35" value="">
   </body>
--- a/mobile/android/base/tests/roboextender/testSelectionHandler.html
+++ b/mobile/android/base/tests/roboextender/testSelectionHandler.html
@@ -1,25 +1,25 @@
 <html>
   <head>
     <title>Automated Text Selection tests for Mobile</title>
     <meta name="viewport" content="initial-scale=1.0"/>
     <script type="application/javascript"
       src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+    <script type="application/javascript" src="SelectionUtils.js"></script>
     <script type="application/javascript;version=1.8">
 
+// Name of this test.
+const TYPE_NAME = "Robocop:testSelectionHandler";
+
 const DIV_POINT_TEXT = "Under";
 const INPUT_TEXT = "Text for select all in an <input>";
 const TEXTAREA_TEXT = "Text for select all in a <textarea>";
 const READONLY_INPUT_TEXT = "readOnly text";
 
-const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
-Cu.import("resource://gre/modules/Messaging.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
 /* =================================================================================
  *
  * Start of all text selection tests, check initialization state.
  *
  */
 function startTests() {
   testSelectAllDivs().
     then(testSelectDivAtPoint).
@@ -343,63 +343,16 @@ function testAttachCaretFail() {
       is(attachCaretResult, sh.ATTACH_ERROR_INCOMPATIBLE,
         "attachCaret() should have failed predictably."),
       is(sh._activeType, sh.TYPE_NONE,
         "Selection should not be active at end of testAttachCaretFail."),
     ]);
   });
 }
 
-
-/* =================================================================================
- *
- * After finish of all selection tests, wrap up and go home.
- *
- */
-function finishTests() {
-  Messaging.sendRequest({
-    type: "Robocop:testSelectionHandler",
-    result: true,
-    msg: "Done!",
-    done: true
-  });
-}
-
-/* ============================== Utility functions ======================
- *
- * Common functions available to all tests.
- *
- */
-function getSelectionHandler() {
-  return (!this._selectionHandler) ?
-    this._selectionHandler = Services.wm.getMostRecentWindow("navigator:browser").SelectionHandler :
-    this._selectionHandler;
-}
-
-function ok(one, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testSelectionHandler",
-    result: one,
-    msg: msg
-  });
-}
-
-function is(one, two, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testSelectionHandler",
-    result: one === two,
-    msg: msg + " : " + one + " === " + two
-  });
-}
-
-/* =================================================================================
- *
- * Page definition for all tests.
- *
- */
     </script>
   </head>
 
   <body onload="startTests();">
 
     <div id="selDiv">Under sufficiently extreme conditions, quarks may become
       deconfined and exist as free particles. In the course of asymptotic freedom,
       the strong interaction becomes weaker at higher temperatures. Eventually,
--- a/mobile/android/base/tests/roboextender/testTextareaSelections.html
+++ b/mobile/android/base/tests/roboextender/testTextareaSelections.html
@@ -1,33 +1,32 @@
 <html>
   <head>
     <title>Automated RTL/LTR Text Selection tests for Textareas</title>
     <meta name="viewport" content="initial-scale=1.0"/>
     <script type="application/javascript"
       src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+    <script type="application/javascript" src="SelectionUtils.js"></script>
     <script type="application/javascript;version=1.8">
 
+// Name of this test.
+const TYPE_NAME = "Robocop:testTextareaSelections";
+
 // Used to create handle movement events for SelectionHandler.
 const ANCHOR = "ANCHOR";
 const FOCUS = "FOCUS";
 
 // Used to specifiy midpoint selection text left/right of center.
 const EST_SEL_TEXT_BOUND_CHARS = 5;
 
 // Used to ensure calculated coords for handle movement events get us
 // "into" the next/prev line vertically.
 const EST_SEL_LINE_CHG_PTS = 10;
 
 
-const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
-Cu.import("resource://gre/modules/Messaging.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import('resource://gre/modules/Geometry.jsm');
-
 // Distance between text selection lines. Reality tested, and also
 // Used to perform multi-line selection selections.
 let selectionLineHeight = 0;
 
 /* =================================================================================
  *
  * Start of all text selection tests, check initialization state.
  */
@@ -726,116 +725,16 @@ function testRTL_moveAnchorHandleDown() 
       "RTL Reversed Changed selection anchorPt.y " +
       "should be equal to Initial selection anchorPt.y"),
     greaterThan(changedSelection.focusPt.y, initialSelection.focusPt.y,
       "RTL Reversed Changed selection focusPt.y " +
       "should be greater than (below) Initial selection focusPt.y"),
   ]);
 }
 
-/* =================================================================================
- *
- * After finish of all selection tests, wrap up and go home.
- *
- */
-function finishTests() {
-  Messaging.sendRequest({
-    type: "Robocop:testTextareaSelections",
-    result: true,
-    msg: "Done!",
-    done: true
-  });
-}
-
-/* ============================== Utility functions ======================
- *
- * Common functions available to all tests.
- *
- */
-function getSelectionHandler() {
-  return (!this._selectionHandler) ?
-    this._selectionHandler = Services.wm.getMostRecentWindow("navigator:browser").SelectionHandler :
-    this._selectionHandler;
-}
-
-function todo(result, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testTextareaSelections",
-    todo: result,
-    msg: msg
-  });
-}
-
-function ok(result, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testTextareaSelections",
-    result: result,
-    msg: msg
-  });
-}
-
-function is(one, two, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testTextareaSelections",
-    result: one === two,
-    msg: msg + " : " + one + " === " + two
-  });
-}
-
-function lessThan(n1, n2, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testTextareaSelections",
-    result: n1 < n2,
-    msg: msg + " : " + n1 + " < " + n2
-  });
-}
-
-function greaterThan(n1, n2, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testTextareaSelections",
-    result: n1 > n2,
-    msg: msg + " : " + n1 + " > " + n2
-  });
-}
-
-// Use fuzzy logic to compare screen coords.
-function truncPoint(point) {
-  return new Point(Math.trunc(point.x), Math.trunc(point.y));
-}
-
-function pointEquals(p1, p2, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testTextareaSelections",
-    result: truncPoint(p1).equals(truncPoint(p2)),
-    msg: msg + " : " + p1.toString() + " == " + p2.toString()
-  });
-}
-
-function pointNotEquals(p1, p2, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testTextareaSelections",
-    result: !truncPoint(p1).equals(truncPoint(p2)),
-    msg: msg + " : " + p1.toString() + " == " + p2.toString()
-  });
-}
-
-function selectionExists(selection, msg) {
-  return Messaging.sendRequestForResult({
-    type: "Robocop:testTextareaSelections",
-    result: !truncPoint(selection.anchorPt).equals(truncPoint(selection.focusPt)),
-    msg: msg + " : anchor:" + selection.anchorPt.toString() +
-      " focus:" + selection.focusPt.toString()
-  });
-}
-
-/* =================================================================================
- *
- * Page definition for all tests.
- *
- */
     </script>
   </head>
 
   <body onload="startTests();">
     <textarea id="LTRTextarea" style="direction: ltr;" rows="10" cols="40"
       readonly="true">Under sufficiently extreme conditions, quarks may become deconfined and exist as free particles. In the course of asymptotic freedom, the strong interaction becomes weaker at higher temperatures. Eventually, color confinement would be lost and an extremely hot plasma of freely moving quarks and gluons would be formed. This theoretical phase of matter is called quark-gluon plasma.[81] The exact conditions needed to give rise to this state are unknown and have been the subject of a great deal of speculation and experimentation.</textarea>
 
     <textarea id="RTLTextarea" style="direction: rtl;" rows="10" cols="40"
--- a/mobile/android/base/toolbar/ToolbarEditText.java
+++ b/mobile/android/base/toolbar/ToolbarEditText.java
@@ -1,46 +1,60 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.toolbar;
 
+import org.mozilla.gecko.ActivityHandlerHelper;
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.CustomEditText;
+import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.InputMethods;
+import org.mozilla.gecko.R;
 import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
 import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
 import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
+import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.StringUtils;
 
+import android.app.Activity;
+import android.app.AlertDialog;
 import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.speech.RecognizerIntent;
 import android.text.Editable;
 import android.text.NoCopySpan;
 import android.text.Selection;
 import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.text.style.BackgroundColorSpan;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputConnectionWrapper;
 import android.view.inputmethod.InputMethodManager;
 import android.view.ViewParent;
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.TextView;
 
+import java.util.List;
+
 /**
 * {@code ToolbarEditText} is the text entry used when the toolbar
 * is in edit state. It handles all the necessary input method machinery.
 * It's meant to be owned by {@code ToolbarEditLayout}.
 */
 public class ToolbarEditText extends CustomEditText
                              implements AutocompleteHandler {
 
@@ -84,16 +98,17 @@ public class ToolbarEditText extends Cus
     }
 
     @Override
     public void onAttachedToWindow() {
         setOnKeyListener(new KeyListener());
         setOnKeyPreImeListener(new KeyPreImeListener());
         setOnSelectionChangedListener(new SelectionChangeListener());
         addTextChangedListener(new TextChangeListener());
+        configureCompoundDrawables();
     }
 
     @Override
     public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
 
         if (gainFocus) {
             resetAutocompleteState();
@@ -450,16 +465,108 @@ public class ToolbarEditText extends Cus
                 if (removeAutocompleteOnComposing(text)) {
                     return false;
                 }
                 return super.setComposingText(text, newCursorPosition);
             }
         };
     }
 
+    /**
+     * Detect if we are able to enable the 'buttons' made from compound drawables.
+     *
+     * Currently, only voice input.
+     */
+    private void configureCompoundDrawables() {
+        if (!AppConstants.NIGHTLY_BUILD || !supportsVoiceRecognizer()) {
+            // Remove the mic button if we can't support the voice recognizer.
+            setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+            return;
+        }
+        setOnTouchListener(new VoiceSearchOnTouchListener());
+    }
+
+    private boolean supportsVoiceRecognizer() {
+        final Intent intent = createVoiceRecognizerIntent();
+        return intent.resolveActivity(getContext().getPackageManager()) != null;
+    }
+
+    private Intent createVoiceRecognizerIntent() {
+        final Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
+        intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
+        intent.putExtra(RecognizerIntent.EXTRA_PROMPT, getResources().getString(R.string.voicesearch_prompt));
+        return intent;
+    }
+
+    private void launchVoiceRecognizer() {
+        final Intent intent = createVoiceRecognizerIntent();
+
+        Activity activity = GeckoAppShell.getGeckoInterface().getActivity();
+        ActivityHandlerHelper.startIntentForActivity(activity, intent, new ActivityResultHandler() {
+            @Override
+            public void onActivityResult(int resultCode, Intent data) {
+                switch (resultCode) {
+                    case RecognizerIntent.RESULT_CLIENT_ERROR:
+                    case RecognizerIntent.RESULT_NETWORK_ERROR:
+                    case RecognizerIntent.RESULT_SERVER_ERROR:
+                        // We have an temporarily unrecoverable error.
+                        handleVoiceSearchError(false);
+                        break;
+                    case RecognizerIntent.RESULT_AUDIO_ERROR:
+                    case RecognizerIntent.RESULT_NO_MATCH:
+                        // Maybe the user can say it differently?
+                        handleVoiceSearchError(true);
+                        break;
+                    case Activity.RESULT_CANCELED:
+                        break;
+                }
+
+                if (resultCode != Activity.RESULT_OK) {
+                    return;
+                }
+
+                // We have RESULT_OK, not RESULT_NO_MATCH so it should be safe to assume that
+                // we have at least one match. We only need one: this will be
+                // used for showing the user search engines with this search term in it.
+                List<String> voiceStrings = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
+                String text = voiceStrings.get(0);
+                setText(text);
+                setSelection(0, text.length());
+            }
+        });
+    }
+
+    private void handleVoiceSearchError(boolean offerRetry) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(getContext())
+                .setTitle(R.string.voicesearch_failed_title)
+                .setIcon(R.drawable.icon).setNeutralButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                    }
+                });
+
+        if (offerRetry) {
+            builder.setMessage(R.string.voicesearch_failed_message_recoverable)
+                   .setNegativeButton(R.string.voicesearch_failed_retry, new DialogInterface.OnClickListener() {
+                       @Override
+                       public void onClick(DialogInterface dialog, int which) {
+                           launchVoiceRecognizer();
+                       }
+                   });
+        } else {
+            builder.setMessage(R.string.voicesearch_failed_message);
+        }
+
+        AlertDialog dialog = builder.create();
+
+        dialog.show();
+    }
+
     private class SelectionChangeListener implements OnSelectionChangedListener {
         @Override
         public void onSelectionChanged(final int selStart, final int selEnd) {
             // The user has repositioned the cursor somewhere. We need to adjust
             // the autocomplete text depending on where the new cursor is.
 
             final Editable text = getText();
             final int start = text.getSpanStart(AUTOCOMPLETE_SPAN);
@@ -593,9 +700,43 @@ public class ToolbarEditText extends Cus
                 removeAutocomplete(getText())) {
                 // Delete autocomplete text when backspacing or forward deleting.
                 return true;
             }
 
             return false;
         }
     }
+
+    private class VoiceSearchOnTouchListener implements View.OnTouchListener {
+        private int mVoiceSearchIconIndex = -1;
+        private Drawable mVoiceSearchIcon;
+
+        public VoiceSearchOnTouchListener() {
+            Drawable[] drawables = getCompoundDrawables();
+            for (int i = 0; i < drawables.length; i++) {
+                if (drawables[i] != null) {
+                    mVoiceSearchIcon = drawables[i];
+                    mVoiceSearchIconIndex = i;
+                }
+            }
+        }
+
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            boolean tapped;
+            switch (mVoiceSearchIconIndex) {
+                case 0:
+                    tapped = event.getX() < (getPaddingLeft() + mVoiceSearchIcon.getIntrinsicWidth());
+                    break;
+                case 2:
+                    tapped = event.getX() > (getWidth() - getPaddingRight() - mVoiceSearchIcon.getIntrinsicWidth());
+                    break;
+                default:
+                    tapped = false;
+            }
+            if (tapped) {
+                launchVoiceRecognizer();
+            }
+            return tapped;
+        }
+    }
 }
--- a/mobile/android/base/updater/UpdateService.java
+++ b/mobile/android/base/updater/UpdateService.java
@@ -39,16 +39,17 @@ import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.Proxy;
 import java.net.ProxySelector;
+import java.net.URI;
 import java.net.URL;
 import java.net.URLConnection;
 import java.security.MessageDigest;
 import java.util.Calendar;
 import java.util.GregorianCalendar;
 import java.util.List;
 import java.util.TimeZone;
 
@@ -343,42 +344,42 @@ public class UpdateService extends Inten
             notification.setLatestEventInfo(this, getResources().getString(R.string.updater_apply_title),
                                             getResources().getString(R.string.updater_apply_select),
                                             contentIntent);
 
             mNotificationManager.notify(NOTIFICATION_ID, notification);
         }
     }
 
-    private URLConnection openConnectionWithProxy(URL url) throws java.net.URISyntaxException, java.io.IOException {
-        Log.i(LOGTAG, "opening connection with url: " + url);
+    private URLConnection openConnectionWithProxy(URI uri) throws java.net.MalformedURLException, java.io.IOException {
+        Log.i(LOGTAG, "opening connection with URI: " + uri);
 
         ProxySelector ps = ProxySelector.getDefault();
         Proxy proxy = Proxy.NO_PROXY;
         if (ps != null) {
-            List<Proxy> proxies = ps.select(url.toURI());
+            List<Proxy> proxies = ps.select(uri);
             if (proxies != null && !proxies.isEmpty()) {
                 proxy = proxies.get(0);
             }
         }
 
-        return url.openConnection(proxy);
+        return uri.toURL().openConnection(proxy);
     }
 
     private UpdateInfo findUpdate(boolean force) {
         try {
-            URL url = getUpdateUrl(force);
+            URI uri = getUpdateURI(force);
 
-            if (url == null) {
-              Log.e(LOGTAG, "failed to get update URL");
+            if (uri == null) {
+              Log.e(LOGTAG, "failed to get update URI");
               return null;
             }
 
             DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
-            Document dom = builder.parse(openConnectionWithProxy(url).getInputStream());
+            Document dom = builder.parse(openConnectionWithProxy(uri).getInputStream());
 
             NodeList nodes = dom.getElementsByTagName("update");
             if (nodes == null || nodes.getLength() == 0)
                 return null;
 
             Node updateNode = nodes.item(0);
             Node buildIdNode = updateNode.getAttributes().getNamedItem("buildID");
             if (buildIdNode == null)
@@ -396,17 +397,17 @@ public class UpdateService extends Inten
 
             if (urlNode == null || hashFunctionNode == null ||
                 hashValueNode == null || sizeNode == null) {
                 return null;
             }
 
             // Fill in UpdateInfo from the XML data
             UpdateInfo info = new UpdateInfo();
-            info.url = new URL(urlNode.getTextContent());
+            info.uri = new URI(urlNode.getTextContent());
             info.buildID = buildIdNode.getTextContent();
             info.hashFunction = hashFunctionNode.getTextContent();
             info.hashValue = hashValueNode.getTextContent();
 
             try {
                 info.size = Integer.parseInt(sizeNode.getTextContent());
             } catch (NumberFormatException e) {
                 Log.e(LOGTAG, "Failed to find APK size: ", e);
@@ -500,19 +501,27 @@ public class UpdateService extends Inten
 
         pkg.delete();
         Log.i(LOGTAG, "deleted update package: " + path);
 
         return true;
     }
 
     private File downloadUpdatePackage(UpdateInfo info, boolean overwriteExisting) {
+        URL url = null;
+        try {
+            url = info.uri.toURL();
+        } catch (java.net.MalformedURLException e) {
+            Log.e(LOGTAG, "failed to read URL: ", e);
+            return null;
+        }
+
         File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
         path.mkdirs();
-        String fileName = new File(info.url.getFile()).getName();
+        String fileName = new File(url.getFile()).getName();
         File downloadFile = new File(path, fileName);
 
         if (!overwriteExisting && info.buildID.equals(getLastBuildID()) && downloadFile.exists()) {
             // The last saved buildID is the same as the one for the current update. We also have a file
             // already downloaded, so it's probably the package we want. Verify it to be sure and just
             // return that if it matches.
 
             if (verifyDownloadedPackage(downloadFile)) {
@@ -541,17 +550,17 @@ public class UpdateService extends Inten
 
         try {
             NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo();
             if (netInfo != null && netInfo.isConnected() &&
                 netInfo.getType() == ConnectivityManager.TYPE_WIFI) {
                 mWifiLock.acquire();
             }
 
-            URLConnection conn = openConnectionWithProxy(info.url);
+            URLConnection conn = openConnectionWithProxy(info.uri);
             int length = conn.getContentLength();
 
             output = new BufferedOutputStream(new FileOutputStream(downloadFile));
             input = new BufferedInputStream(conn.getInputStream());
 
             byte[] buf = new byte[BUFSIZE];
             int len = 0;
 
@@ -704,18 +713,18 @@ public class UpdateService extends Inten
     }
 
     private void setAutoDownloadPolicy(AutoDownloadPolicy policy) {
         SharedPreferences.Editor editor = mPrefs.edit();
         editor.putInt(KEY_AUTODOWNLOAD_POLICY, policy.value);
         editor.commit();
     }
 
-    private URL getUpdateUrl(boolean force) {
-        return UpdateServiceHelper.expandUpdateUrl(this, mPrefs.getString(KEY_UPDATE_URL, null), force);
+    private URI getUpdateURI(boolean force) {
+        return UpdateServiceHelper.expandUpdateURI(this, mPrefs.getString(KEY_UPDATE_URL, null), force);
     }
 
     private void setUpdateUrl(String url) {
         SharedPreferences.Editor editor = mPrefs.edit();
         editor.putString(KEY_UPDATE_URL, url);
         editor.commit();
     }
 
@@ -724,29 +733,29 @@ public class UpdateService extends Inten
         editor.putString(KEY_LAST_BUILDID, info.buildID);
         editor.putString(KEY_LAST_HASH_FUNCTION, info.hashFunction);
         editor.putString(KEY_LAST_HASH_VALUE, info.hashValue);
         editor.putString(KEY_LAST_FILE_NAME, downloaded.toString());
         editor.commit();
     }
 
     private class UpdateInfo {
-        public URL url;
+        public URI uri;
         public String buildID;
         public String hashFunction;
         public String hashValue;
         public int size;
 
         private boolean isNonEmpty(String s) {
             return s != null && s.length() > 0;
         }
 
         public boolean isValid() {
-            return url != null && isNonEmpty(buildID) &&
+            return uri != null && isNonEmpty(buildID) &&
                 isNonEmpty(hashFunction) && isNonEmpty(hashValue) && size > 0;
         }
 
         @Override
         public String toString() {
-            return "url = " + url + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size;
+            return "uri = " + uri + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size;
         }
     }
 }
--- a/mobile/android/base/updater/UpdateServiceHelper.java
+++ b/mobile/android/base/updater/UpdateServiceHelper.java
@@ -12,17 +12,17 @@ import org.mozilla.gecko.util.GeckoJarRe
 
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ApplicationInfo;
 import android.os.Build;
 import android.util.Log;
 
-import java.net.URL;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.HashMap;
 
 public class UpdateServiceHelper {
     public static final String ACTION_REGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".REGISTER_FOR_UPDATES";
     public static final String ACTION_UNREGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".UNREGISTER_FOR_UPDATES";
     public static final String ACTION_CHECK_FOR_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_FOR_UPDATE";
     public static final String ACTION_CHECK_UPDATE_RESULT = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_UPDATE_RESULT";
@@ -82,18 +82,18 @@ public class UpdateServiceHelper {
         }
     }
 
     @RobocopTarget
     public static void setEnabled(final boolean enabled) {
         isEnabled = enabled;
     }
 
-    public static URL expandUpdateUrl(Context context, String updateUrl, boolean force) {
-        if (updateUrl == null) {
+    public static URI expandUpdateURI(Context context, String updateUri, boolean force) {
+        if (updateUri == null) {
             return null;
         }
 
         PackageManager pm = context.getPackageManager();
 
         String pkgSpecial = AppConstants.MOZ_PKG_SPECIAL != null ?
                             "-" + AppConstants.MOZ_PKG_SPECIAL :
                             "";
@@ -107,30 +107,30 @@ public class UpdateServiceHelper {
             if (jarLocale != null) {
                 locale = jarLocale.trim();
             }
         } catch (android.content.pm.PackageManager.NameNotFoundException e) {
             // Shouldn't really be possible, but fallback to default locale
             Log.i(LOGTAG, "Failed to read update locale file, falling back to " + locale);
         }
 
-        String url = updateUrl.replace("%PRODUCT%", AppConstants.MOZ_APP_BASENAME)
+        String url = updateUri.replace("%PRODUCT%", AppConstants.MOZ_APP_BASENAME)
             .replace("%VERSION%", AppConstants.MOZ_APP_VERSION)
             .replace("%BUILD_ID%", force ? "0" : AppConstants.MOZ_APP_BUILDID)
             .replace("%BUILD_TARGET%", "Android_" + AppConstants.MOZ_APP_ABI + pkgSpecial)
             .replace("%LOCALE%", locale)
             .replace("%CHANNEL%", AppConstants.MOZ_UPDATE_CHANNEL)
             .replace("%OS_VERSION%", Build.VERSION.RELEASE)
             .replace("%DISTRIBUTION%", "default")
             .replace("%DISTRIBUTION_VERSION%", "default")
             .replace("%MOZ_VERSION%", AppConstants.MOZILLA_VERSION);
 
         try {
-            return new URL(url);
-        } catch (java.net.MalformedURLException e) {
+            return new URI(url);
+        } catch (java.net.URISyntaxException e) {
             Log.e(LOGTAG, "Failed to create update url: ", e);
             return null;
         }
     }
 
     public static boolean isUpdaterEnabled() {
         return AppConstants.MOZ_UPDATER && isEnabled;
     }
@@ -163,16 +163,20 @@ public class UpdateServiceHelper {
         if (context == null) {
             return;
         }
 
         context.startService(createIntent(context, ACTION_APPLY_UPDATE));
     }
 
     public static void registerForUpdates(final Context context) {
+        if (!isUpdaterEnabled()) {
+             return;
+        }
+
         final HashMap<String, Object> prefs = new HashMap<String, Object>();
 
         PrefsHelper.getPrefs(Pref.names, new PrefsHelper.PrefHandlerBase() {
             @Override public void prefValue(String pref, String value) {
                 prefs.put(pref, value);
             }
 
             @Override public void finish() {
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -343,16 +343,20 @@ var Addons = {
               }
               box.appendChild(setting);
             }
             // Send an event so add-ons can prepopulate any non-preference based
             // settings
             let event = document.createEvent("Events");
             event.initEvent("AddonOptionsLoad", true, false);
             window.dispatchEvent(event);
+          } else {
+            // Reset the options URL to hide the options header if there are no
+            // valid settings to show.
+            detailItem.setAttribute("optionsURL", "");
           }
 
           // Also send a notification to match the behavior of desktop Firefox
           let id = aListItem.getAttribute("addonID");
           Services.obs.notifyObservers(document, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, id);
         }
       }
       xhr.send(null);
--- a/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in
+++ b/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in
@@ -4,23 +4,41 @@
             <intent-filter >
                 <action android:name="android.accounts.AccountAuthenticator" />
             </intent-filter>
 
             <meta-data
                 android:name="android.accounts.AccountAuthenticator"
                 android:resource="@xml/fxaccount_authenticator" />
         </service>
+
+        <service
+            android:exported="false"
+            android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" >
+        </service>
+
+        <!-- Firefox Sync. -->
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.fxa.sync.FxAccountSyncService" >
             <intent-filter >
                 <action android:name="android.content.SyncAdapter" />
             </intent-filter>
 
             <meta-data
                 android:name="android.content.SyncAdapter"
                 android:resource="@xml/fxaccount_syncadapter" />
         </service>
+
+        <!-- Reading List. -->
+#ifdef MOZ_ANDROID_READING_LIST_SERVICE
         <service
             android:exported="false"
-            android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" >
+            android:name="org.mozilla.gecko.reading.ReadingListSyncService" >
+            <intent-filter >
+                <action android:name="android.content.SyncAdapter" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.content.SyncAdapter"
+                android:resource="@xml/readinglist_syncadapter" />
         </service>
+#endif
--- a/toolkit/components/downloads/test/unit/head_download_manager.js
+++ b/toolkit/components/downloads/test/unit/head_download_manager.js
@@ -141,80 +141,16 @@ function getDownloadListener()
       }
     },
     onStateChange: function(a, b, c, d, e) { },
     onProgressChange: function(a, b, c, d, e, f, g) { },
     onSecurityChange: function(a, b, c, d) { }
   };
 }
 
-/**
- * Asynchronously adds visits to a page.
- *
- * @param aPlaceInfo
- *        Can be an nsIURI, in such a case a single LINK visit will be added.
- *        Otherwise can be an object describing the visit to add, or an array
- *        of these objects:
- *          { uri: nsIURI of the page,
- *            transition: one of the TRANSITION_* from nsINavHistoryService,
- *            [optional] title: title of the page,
- *            [optional] visitDate: visit date in microseconds from the epoch
- *            [optional] referrer: nsIURI of the referrer for this visit
- *          }
- *
- * @return {Promise}
- * @resolves When all visits have been added successfully.
- * @rejects JavaScript exception.
- */
-function promiseAddVisits(aPlaceInfo)
-{
-  let deferred = Promise.defer();
-  let places = [];
-  if (aPlaceInfo instanceof Ci.nsIURI) {
-    places.push({ uri: aPlaceInfo });
-  }
-  else if (Array.isArray(aPlaceInfo)) {
-    places = places.concat(aPlaceInfo);
-  } else {
-    places.push(aPlaceInfo)
-  }
-
-  // Create mozIVisitInfo for each entry.
-  let now = Date.now();
-  for (let i = 0; i < places.length; i++) {
-    if (!places[i].title) {
-      places[i].title = "test visit for " + places[i].uri.spec;
-    }
-    places[i].visits = [{
-      transitionType: places[i].transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
-                                                         : places[i].transition,
-      visitDate: places[i].visitDate || (now++) * 1000,
-      referrerURI: places[i].referrer
-    }];
-  }
-
-  PlacesUtils.asyncHistory.updatePlaces(
-    places,
-    {
-      handleError: function handleError(aResultCode, aPlaceInfo) {
-        let ex = new Components.Exception("Unexpected error in adding visits.",
-                                          aResultCode);
-        deferred.reject(ex);
-      },
-      handleResult: function () {},
-      handleCompletion: function handleCompletion() {
-        deferred.resolve();
-      }
-    }
-  );
-
-  return deferred.promise;
-}
-
-
 XPCOMUtils.defineLazyGetter(this, "Services", function() {
   Cu.import("resource://gre/modules/Services.jsm");
   return Services;
 });
 
 // Disable alert service notifications
 Services.prefs.setBoolPref("browser.download.manager.showAlertOnComplete", false);
 
--- a/toolkit/components/downloads/test/unit/test_history_expiration.js
+++ b/toolkit/components/downloads/test/unit/test_history_expiration.js
@@ -78,20 +78,20 @@ add_task(function test_execute()
     stmt.execute();
   }
   finally {
     stmt.reset();
     stmt.finalize();
   }
 
   // Add an expirable visit to this download.
-  let histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
-                getService(Ci.nsINavHistoryService);
-  yield promiseAddVisits({uri: theURI, visitDate: getExpirablePRTime(),
-                          transition: histsvc.TRANSITION_DOWNLOAD});
+  yield PlacesTestUtils.addVisits({
+    uri: theURI, visitDate: getExpirablePRTime(),
+    transition: PlacesUtils.history.TRANSITION_DOWNLOAD
+  });
 
   // Get the download manager as history observer and batch expirations
   let histobs = dm.QueryInterface(Ci.nsINavHistoryObserver);
   histobs.onBeginUpdateBatch();
 
   // Look for the removed download notification
   let obs = Cc["@mozilla.org/observer-service;1"].
             getService(Ci.nsIObserverService);
--- a/toolkit/components/passwordmgr/test/test_xhr.html
+++ b/toolkit/components/passwordmgr/test/test_xhr.html
@@ -62,22 +62,42 @@ function handleDialog(doc, testNum) {
         is(username, "xhruser1", "Checking provided username");
         is(password, "xhrpass1", "Checking provided password");
         break;
 
     case 2:
         is(username, "xhruser2", "Checking provided username");
         is(password, "xhrpass2", "Checking provided password");
 
-        // Check that the dialog has the correct parent
-        ok(doc.defaultView.opener, "dialog has opener");
-        // Not using is() because its "expected" text doesn't deal
-        // with window objects very well
-        // Disabled due to Bug 718543
-        // ok(doc.defaultView.opener == window, "dialog's opener is correct");
+        // Check that the dialog is modal, chrome and dependent;
+        // We can't just check window.opener because that'll be
+        // a content window, which therefore isn't exposed (it'll lie and
+        // be null).
+        var win = doc.defaultView;
+        var Ci = SpecialPowers.Ci;
+        var treeOwner = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+                            QueryInterface(Ci.nsIDocShellTreeItem).treeOwner;
+        treeOwner.QueryInterface(Ci.nsIInterfaceRequestor);
+        var flags = treeOwner.getInterface(Ci.nsIXULWindow).chromeFlags;
+        var wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
+        info("Flags: " + flags);
+        ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0,
+           "Dialog should be opened as chrome");
+        ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0,
+           "Dialog should be opened as a dialog");
+        ok((flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0,
+           "Dialog should be opened as dependent.");
+        ok(wbc.isWindowModal(), "Dialog should be modal");
+
+        // Check that the right tab is focused:
+        var browserWin = SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser");
+        var spec = browserWin.gBrowser.selectedBrowser.currentURI.spec;
+        ok(spec.startsWith("http://mochi.test:8888"),
+           "Tab with remote URI (rather than about:blank) should be focused (" + spec + ")");
+
 
         break;
 
     default:
         ok(false, "Uhh, unhandled switch for testNum #" + testNum);
         break;
   }
 
@@ -132,18 +152,18 @@ function xhrLoad(xmlDoc) {
 function doTest() {
   switch(++testNum) {
     case 1:
         startCallbackTimer();
         makeRequest("authenticate.sjs?user=xhruser1&pass=xhrpass1&realm=xhr");
         break;
         
     case 2:
-        // Test correct parenting, by opening another window and
-        // making sure the prompt's opener is correct
+        // Test correct parenting, by opening another tab in the foreground,
+        // and making sure the prompt re-focuses the original tab when shown:
         newWin = window.open();
         newWin.focus();
         startCallbackTimer();
         makeRequest("authenticate.sjs?user=xhruser2&pass=xhrpass2&realm=xhr2");
         break;
 
     default:
         finishTest();
--- a/toolkit/components/passwordmgr/test/test_xml_load.html
+++ b/toolkit/components/passwordmgr/test/test_xml_load.html
@@ -62,22 +62,41 @@ function handleDialog(doc, testNum) {
         is(username, "xmluser1", "Checking provided username");
         is(password, "xmlpass1", "Checking provided password");
         break;
 
     case 2:
         is(username, "xmluser2", "Checking provided username");
         is(password, "xmlpass2", "Checking provided password");
 
-        // Check that the dialog has the correct parent
-        ok(doc.defaultView.opener, "dialog has opener");
-        // Not using is() because its "expected" text doesn't deal
-        // with window objects very well 
-        // Disabled due to Bug 718543.
-        // ok(doc.defaultView.opener == window, "dialog's opener is correct");
+        // Check that the dialog is modal, chrome and dependent;
+        // We can't just check window.opener because that'll be
+        // a content window, which therefore isn't exposed (it'll lie and
+        // be null).
+        var win = doc.defaultView;
+        var Ci = SpecialPowers.Ci;
+        var treeOwner = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
+                            QueryInterface(Ci.nsIDocShellTreeItem).treeOwner;
+        treeOwner.QueryInterface(Ci.nsIInterfaceRequestor);
+        var flags = treeOwner.getInterface(Ci.nsIXULWindow).chromeFlags;
+        var wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
+        info("Flags: " + flags);
+        ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0,
+           "Dialog should be opened as chrome");
+        ok((flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0,
+           "Dialog should be opened as a dialog");
+        ok((flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0,
+           "Dialog should be opened as dependent.");
+        ok(wbc.isWindowModal(), "Dialog should be modal");
+
+        // Check that the right tab is focused:
+        var browserWin = SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser");
+        var spec = browserWin.gBrowser.selectedBrowser.currentURI.spec;
+        ok(spec.startsWith("http://mochi.test:8888"),
+           "Tab with remote URI (rather than about:blank) should be focused (" + spec + ")");
 
         break;
 
     default:
         ok(false, "Uhh, unhandled switch for testNum #" + testNum);
         break;
   }
 
@@ -131,18 +150,18 @@ function xmlLoad(responseDoc) {
 function doTest() {
   switch(++testNum) {
     case 1:
         startCallbackTimer();
         makeRequest("authenticate.sjs?user=xmluser1&pass=xmlpass1&realm=xml");
         break;
 
     case 2:
-        // Test correct parenting, by opening another window and
-        // making sure the prompt's opener is correct
+        // Test correct parenting, by opening another tab in the foreground,
+        // and making sure the prompt re-focuses the original tab when shown:
         newWin = window.open();
         newWin.focus();
         startCallbackTimer();
         makeRequest("authenticate.sjs?user=xmluser2&pass=xmlpass2&realm=xml2");
         break;
 
     default:
         finishTest();
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -41,18 +41,16 @@
  *  The following properties are only valid for URLs:
  *
  *  - url (URL, href or nsIURI)
  *      The item's URL.  Note that while input objects can contains either
  *      an URL object, an href string, or an nsIURI, output objects will always
  *      contain an URL object.
  *      An URL cannot be longer than DB_URL_LENGTH_MAX, methods will throw if a
  *      longer value is provided.
- *  - keyword (string)
- *      The associated keyword, if any.
  *
  * Each successful operation notifies through the nsINavBookmarksObserver
  * interface.  To listen to such notifications you must register using
  * nsINavBookmarksService addObserver and removeObserver methods.
  * Note that bookmark addition or order changes won't notify onItemMoved for
  * items that have their indexes changed.
  * Similarly, lastModified changes not done explicitly (like changing another
  * property) won't fire an onItemChanged notification for the lastModified
@@ -142,19 +140,16 @@ let Bookmarks = Object.freeze({
     // dateAdded may be imposed by the caller.
     let time = (info && info.dateAdded) || new Date();
     let insertInfo = validateBookmarkObject(info,
       { type: { required: true }
       , index: { defaultValue: this.DEFAULT_INDEX }
       , url: { requiredIf: b => b.type == this.TYPE_BOOKMARK
              , validIf: b => b.type == this.TYPE_BOOKMARK }
       , parentGuid: { required: true }
-      , keyword: { validIf: b => b.keyword &&
-                            b.keyword.length > 0 &&
-                            b.type == this.TYPE_BOOKMARK }
       , title: { validIf: b => [ this.TYPE_BOOKMARK
                                , this.TYPE_FOLDER ].indexOf(b.type) != -1 }
       , dateAdded: { defaultValue: time
                    , validIf: b => !b.lastModified ||
                                     b.dateAdded <= b.lastModified }
       , lastModified: { defaultValue: time,
                         validIf: b => (!b.dateAdded && b.lastModified >= time) ||
                                       (b.dateAdded && b.lastModified >= b.dateAdded) }
@@ -180,25 +175,16 @@ let Bookmarks = Object.freeze({
       // complete we may stop using it.
       let uri = item.hasOwnProperty("url") ? toURI(item.url) : null;
       let itemId = yield PlacesUtils.promiseItemId(item.guid);
       notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
                                          item.type, uri, item.title || null,
                                          toPRTime(item.dateAdded), item.guid,
                                          item.parentGuid ]);
 
-      // If a keyword is defined, notify onItemChanged for it.
-      if (item.keyword) {
-        notify(observers, "onItemChanged", [ itemId, "keyword", false,
-                                             item.keyword,
-                                             toPRTime(item.lastModified),
-                                             item.type, parent._id, item.guid,
-                                             item.parentGuid ]);
-      }
-
       // If it's a tag, notify OnItemChanged to all bookmarks for this URL.
       let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
       if (isTagging) {
         for (let entry of (yield fetchBookmarksByURL(item))) {
           notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
                                                toPRTime(entry.lastModified),
                                                entry.type, entry._parentId,
                                                entry.guid, entry.parentGuid ]);
@@ -212,18 +198,16 @@ let Bookmarks = Object.freeze({
 
   /**
    * Updates a bookmark-item.
    *
    * Only set the properties which should be changed (undefined properties
    * won't be taken into account).
    * Moreover, the item's type or dateAdded cannot be changed, since they are
    * immutable after creation.  Trying to change them will reject.
-   * Passing an empty string as keyword clears any keyword associated with
-   * this bookmark.
    *
    * Note that any known properties that don't apply to the specific item type
    * cause an exception.
    *
    * @param info
    *        object representing a bookmark-item, as defined above.
    *
    * @return {Promise} resolved when the update is complete.
@@ -263,17 +247,16 @@ let Bookmarks = Object.freeze({
       if (Object.keys(updateInfo).length < 2) {
         // Remove non-enumerable properties.
         return Object.assign({}, item);
       }
 
       let time = (updateInfo && updateInfo.dateAdded) || new Date();
       updateInfo = validateBookmarkObject(updateInfo,
         { url: { validIf: () => item.type == this.TYPE_BOOKMARK }
-        , keyword: { validIf: () => item.type == this.TYPE_BOOKMARK }
         , title: { validIf: () => [ this.TYPE_BOOKMARK
                                   , this.TYPE_FOLDER ].indexOf(item.type) != -1 }
         , lastModified: { defaultValue: new Date()
                         , validIf: b => b.lastModified >= item.dateAdded }
         });
 
       let db = yield DBConnPromised;
       let parent;
@@ -356,28 +339,16 @@ let Bookmarks = Object.freeze({
         notify(observers, "onItemChanged", [ updatedItem._id, "uri",
                                              false, updatedItem.url.href,
                                              toPRTime(updatedItem.lastModified),
                                              updatedItem.type,
                                              updatedItem._parentId,
                                              updatedItem.guid,
                                              updatedItem.parentGuid ]);
       }
-      if (updateInfo.hasOwnProperty("keyword")) {
-        // If the keyword is unset, updatedItem won't have it set.
-        let keyword = updatedItem.hasOwnProperty("keyword") ?
-                        updatedItem.keyword : "";
-        notify(observers, "onItemChanged", [ updatedItem._id, "keyword",
-                                             false, keyword,
-                                             toPRTime(updatedItem.lastModified),
-                                             updatedItem.type,
-                                             updatedItem._parentId,
-                                             updatedItem.guid,
-                                             updatedItem.parentGuid ]);
-      }
       // If the item was moved, notify onItemMoved.
       if (item.parentGuid != updatedItem.parentGuid ||
           item.index != updatedItem.index) {
         notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
                                            item.index, updatedItem._parentId,
                                            updatedItem.index, updatedItem.type,
                                            updatedItem.guid, item.parentGuid,
                                            updatedItem.parentGuid ]);
@@ -469,33 +440,29 @@ let Bookmarks = Object.freeze({
     }.bind(this));
   }),
 
   /**
    * Fetches information about a bookmark-item.
    *
    * REMARK: any successful call to this method resolves to a single
    *         bookmark-item (or null), even when multiple bookmarks may exist
-   *         (fetching by url or keyword,).  If you wish to retrieve all of the
+   *         (e.g. fetching by url).  If you wish to retrieve all of the
    *         bookmarks for a given match, use the callback instead.
    *
    * Input can be either a guid or an object with one, and only one, of these
    * filtering properties set:
    *  - guid
    *      retrieves the item with the specified guid.
    *  - parentGuid and index
    *      retrieves the item by its position.
    *  - url
    *      retrieves the most recent bookmark having the given URL.
    *      To retrieve ALL of the bookmarks for that URL, you must pass in an
    *      onResult callback, that will be invoked once for each found bookmark.
-   *  - keyword
-   *      retrieves an array of items having the given keyword.
-   *      To retrieve ALL of the bookmarks for that keyword, you must pass in an
-   *      onResult callback, that will be invoked once for each found bookmark.
    *
    * @param guidOrInfo
    *        The globally unique identifier of the item to fetch, or an
    *        object representing it, as defined above.
    * @param onResult [optional]
    *        Callback invoked for each found bookmark.
    *
    * @return {Promise} resolved when the fetch is complete.
@@ -516,43 +483,38 @@ let Bookmarks = Object.freeze({
       throw new Error("Input should be a valid object");
     if (typeof(info) != "object")
       info = { guid: guidOrInfo };
 
     // Only one condition at a time can be provided.
     let conditionsCount = [
       v => v.hasOwnProperty("guid"),
       v => v.hasOwnProperty("parentGuid") && v.hasOwnProperty("index"),
-      v => v.hasOwnProperty("url"),
-      v => v.hasOwnProperty("keyword")
+      v => v.hasOwnProperty("url")
     ].reduce((old, fn) => old + fn(info)|0, 0);
     if (conditionsCount != 1)
       throw new Error(`Unexpected number of conditions provided: ${conditionsCount}`);
 
     // Even if we ignore any other unneeded property, we still validate any
     // known property to reduce likelihood of hidden bugs.
     let fetchInfo = validateBookmarkObject(info,
       { parentGuid: { requiredIf: b => b.hasOwnProperty("index") }
       , index: { requiredIf: b => b.hasOwnProperty("parentGuid")
                , validIf: b => typeof(b.index) == "number" &&
                                b.index >= 0 || b.index == this.DEFAULT_INDEX }
-      , keyword: { validIf: b => typeof(b.keyword) == "string" &&
-                                 b.keyword.length > 0 }
       });
 
     return Task.spawn(function* () {
       let results;
       if (fetchInfo.hasOwnProperty("guid"))
         results = yield fetchBookmark(fetchInfo);
       else if (fetchInfo.hasOwnProperty("parentGuid") && fetchInfo.hasOwnProperty("index"))
         results = yield fetchBookmarkByPosition(fetchInfo);
       else if (fetchInfo.hasOwnProperty("url"))
         results = yield fetchBookmarksByURL(fetchInfo);
-      else if (fetchInfo.hasOwnProperty("keyword"))
-        results = yield fetchBookmarksByKeyword(fetchInfo);
 
       if (!results)
         return null;
 
       if (!Array.isArray(results))
         results = [results];
       // Remove non-enumerable properties.
       results = results.map(r => Object.assign({}, r));
@@ -734,27 +696,16 @@ function* updateBookmark(info, item, new
 
   let tuples = new Map();
   if (info.hasOwnProperty("lastModified"))
     tuples.set("lastModified", { value: toPRTime(info.lastModified) });
   if (info.hasOwnProperty("title"))
     tuples.set("title", { value: info.title });
 
   yield db.executeTransaction(function* () {
-    if (info.hasOwnProperty("keyword")) {
-      if (info.keyword.length > 0) {
-        yield maybeCreateKeyword(db, info.keyword);
-        tuples.set("keyword",
-                   { value: info.keyword
-                   , fragment: "keyword_id = (SELECT id FROM moz_keywords WHERE keyword = :keyword)" });
-      } else {
-        tuples.set("keyword_id", { value: null });
-      }
-    }
-
     if (info.hasOwnProperty("url")) {
       // Ensure a page exists in moz_places for this URL.
       yield db.executeCached(
         `INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid) 
          VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
         `, { url: info.url ? info.url.href : null,
              rev_host: PlacesUtils.getReversedHost(info.url),
              frecency: info.url.protocol == "place:" ? 0 : -1 });
@@ -799,38 +750,32 @@ function* updateBookmark(info, item, new
     }
 
     yield db.executeCached(
       `UPDATE moz_bookmarks
        SET ${[tuples.get(v).fragment || `${v} = :${v}` for (v of tuples.keys())].join(", ")}
        WHERE guid = :guid
       `, Object.assign({ guid: info.guid },
                        [...tuples.entries()].reduce((p, c) => { p[c[0]] = c[1].value; return p; }, {})));
-
-
-    if (info.hasOwnProperty("keyword") && info.keyword === "")
-      yield removeOrphanKeywords(db);
   });
 
   // If the parent changed, update related non-enumerable properties.
   let additionalParentInfo = {};
   if (newParent) {
     Object.defineProperty(additionalParentInfo, "_parentId",
                           { value: newParent._id, enumerable: false });
     Object.defineProperty(additionalParentInfo, "_grandParentId",
                           { value: newParent._parentId, enumerable: false });
   }
 
   let updatedItem = mergeIntoNewObject(item, info, additionalParentInfo);
 
-  // Don't return an empty title or keyword to the caller.
+  // Don't return an empty title to the caller.
   if (updatedItem.hasOwnProperty("title") && updatedItem.title === null)
     delete updatedItem.title;
-  if (updatedItem.hasOwnProperty("keyword") && updatedItem.keyword === "")
-    delete updatedItem.keyword;
 
   return updatedItem;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // Insert implementation.
 
 function* insertBookmark(item, parent) {
@@ -853,32 +798,26 @@ function* insertBookmark(item, parent) {
 
     // Adjust indices.
     yield db.executeCached(
       `UPDATE moz_bookmarks SET position = position + 1
        WHERE parent = :parent
        AND position >= :index
       `, { parent: parent._id, index: item.index });
 
-    // If a keyword was provided, add it.
-    if (item.hasOwnProperty("keyword"))
-      yield maybeCreateKeyword(db, item.keyword);
-
     // Insert the bookmark into the database.
     yield db.executeCached(
       `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
-                                  dateAdded, lastModified, guid, keyword_id)
+                                  dateAdded, lastModified, guid)
        VALUES ((SELECT id FROM moz_places WHERE url = :url), :type, :parent,
-               :index, :title, :date_added, :last_modified, :guid,
-               (SELECT id FROM moz_keywords WHERE keyword = :keyword))
+               :index, :title, :date_added, :last_modified, :guid)
       `, { url: item.hasOwnProperty("url") ? item.url.href : "nonexistent",
            type: item.type, parent: parent._id, index: item.index,
            title: item.title, date_added: toPRTime(item.dateAdded),
-           last_modified: toPRTime(item.lastModified), guid: item.guid,
-           keyword: item.keyword || "" });
+           last_modified: toPRTime(item.lastModified), guid: item.guid });
 
     yield setAncestorsLastModified(db, item.parentGuid, item.dateAdded);
   });
 
   // If not a tag recalculate frecency...
   let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
   if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
     // ...though we don't wait for the calculation.
@@ -895,106 +834,82 @@ function* insertBookmark(item, parent) {
 // Fetch implementation.
 
 function* fetchBookmark(info) {
   let db = yield DBConnPromised;
 
   let rows = yield db.executeCached(
     `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
             b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-            keyword, b.id AS _id, b.parent AS _parentId,
+            b.id AS _id, b.parent AS _parentId,
             (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
             p.parent AS _grandParentId
      FROM moz_bookmarks b
      LEFT JOIN moz_bookmarks p ON p.id = b.parent
-     LEFT JOIN moz_keywords k ON k.id = b.keyword_id
      LEFT JOIN moz_places h ON h.id = b.fk
      WHERE b.guid = :guid
     `, { guid: info.guid });
 
   return rows.length ? rowsToItemsArray(rows)[0] : null;
 }
 
 function* fetchBookmarkByPosition(info) {
   let db = yield DBConnPromised;
   let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
 
   let rows = yield db.executeCached(
     `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
             b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-            keyword, b.id AS _id, b.parent AS _parentId,
+            b.id AS _id, b.parent AS _parentId,
             (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
             p.parent AS _grandParentId
      FROM moz_bookmarks b
      LEFT JOIN moz_bookmarks p ON p.id = b.parent
-     LEFT JOIN moz_keywords k ON k.id = b.keyword_id
      LEFT JOIN moz_places h ON h.id = b.fk
      WHERE p.guid = :parentGuid
      AND b.position = IFNULL(:index, (SELECT count(*) - 1
                                       FROM moz_bookmarks
                                       WHERE parent = p.id))
     `, { parentGuid: info.parentGuid, index });
 
   return rows.length ? rowsToItemsArray(rows)[0] : null;
 }
 
 function* fetchBookmarksByURL(info) {
   let db = yield DBConnPromised;
 
   let rows = yield db.executeCached(
     `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
             b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-            keyword, b.id AS _id, b.parent AS _parentId,
+            b.id AS _id, b.parent AS _parentId,
             (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
             p.parent AS _grandParentId
      FROM moz_bookmarks b
      LEFT JOIN moz_bookmarks p ON p.id = b.parent
-     LEFT JOIN moz_keywords k ON k.id = b.keyword_id
      LEFT JOIN moz_places h ON h.id = b.fk
      WHERE h.url = :url
      AND _grandParentId <> :tags_folder
      ORDER BY b.lastModified DESC
     `, { url: info.url.href,
          tags_folder: PlacesUtils.tagsFolderId });
 
   return rows.length ? rowsToItemsArray(rows) : null;
 }
 
-function* fetchBookmarksByKeyword(info) {
-  let db = yield DBConnPromised;
-
-  let rows = yield db.executeCached(
-    `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-            b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-            keyword, b.id AS _id, b.parent AS _parentId,
-            (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
-            p.parent AS _grandParentId
-     FROM moz_bookmarks b
-     LEFT JOIN moz_bookmarks p ON p.id = b.parent
-     LEFT JOIN moz_keywords k ON k.id = b.keyword_id
-     LEFT JOIN moz_places h ON h.id = b.fk
-     WHERE keyword = :keyword
-     ORDER BY b.lastModified DESC
-    `, { keyword: info.keyword });
-
-  return rows.length ? rowsToItemsArray(rows) : null;
-}
-
 function* fetchBookmarksByParent(info) {
   let db = yield DBConnPromised;
 
   let rows = yield db.executeCached(
     `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
             b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-            keyword, b.id AS _id, b.parent AS _parentId,
+            b.id AS _id, b.parent AS _parentId,
             (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
             p.parent AS _grandParentId
      FROM moz_bookmarks b
      LEFT JOIN moz_bookmarks p ON p.id = b.parent
-     LEFT JOIN moz_keywords k ON k.id = b.keyword_id
      LEFT JOIN moz_places h ON h.id = b.fk
      WHERE p.guid = :parentGuid
      ORDER BY b.position ASC
     `, { parentGuid: info.parentGuid });
 
   return rowsToItemsArray(rows);
 }
 
@@ -1025,20 +940,16 @@ function* removeBookmark(item) {
 
     // Fix indices in the parent.
     yield db.executeCached(
       `UPDATE moz_bookmarks SET position = position - 1 WHERE
        parent = :parentId AND position > :index
       `, { parentId: item._parentId, index: item.index });
 
     yield setAncestorsLastModified(db, item.parentGuid, new Date());
-
-    // If the bookmark had a keyword, it might now be an orphan.
-    if (item.keyword)
-      removeOrphanKeywords(db);
   });
 
   // If not a tag recalculate frecency...
   if (item.type == Bookmarks.TYPE_BOOKMARK && !isUntagging) {
     // ...though we don't wait for the calculation.
     updateFrecency(db, [item.url]).then(null, Cu.reportError);
   }
 
@@ -1152,20 +1063,16 @@ function removeSameValueProperties(dest,
     switch (prop) {
       case "lastModified":
       case "dateAdded":
         remove = src.hasOwnProperty(prop) && dest[prop].getTime() == src[prop].getTime();
         break;
       case "url":
         remove = src.hasOwnProperty(prop) && dest[prop].href == src[prop].href;
         break;
-      case "keyword":
-        remove = (dest.keyword == "" && !src.hasOwnProperty("keyword")) ||
-                 dest[prop] == src[prop];
-        break;
       default:
         remove = dest[prop] == src[prop];
     }
     if (remove && prop != "guid")
       delete dest[prop];
   }
 }
 
@@ -1207,17 +1114,17 @@ function rowsToItemsArray(rows) {
   return rows.map(row => {
     let item = {};
     for (let prop of ["guid", "index", "type"]) {
       item[prop] = row.getResultByName(prop);
     }
     for (let prop of ["dateAdded", "lastModified"]) {
       item[prop] = toDate(row.getResultByName(prop));
     }
-    for (let prop of ["title", "keyword", "parentGuid", "url" ]) {
+    for (let prop of ["title", "parentGuid", "url" ]) {
       let val = row.getResultByName(prop);
       if (val)
         item[prop] = prop === "url" ? new URL(val) : val;
     }
     for (let prop of ["_id", "_parentId", "_childCount", "_grandParentId"]) {
       let val = row.getResultByName(prop);
       if (val !== null) {
         // These properties should not be returned to the API consumer, thus
@@ -1278,22 +1185,16 @@ const VALIDATORS = Object.freeze({
                               (val instanceof Ci.nsIURI && val.spec.length <= DB_URL_LENGTH_MAX) ||
                               (val instanceof URL && val.href.length <= DB_URL_LENGTH_MAX)
                       ).call(this, v);
     if (typeof(v) === "string")
       return new URL(v);
     if (v instanceof Ci.nsIURI)
       return new URL(v.spec);
     return v;
-  },
-  keyword: v => {
-    simpleValidateFunc(val => typeof(val) == "string" && /^\S*$/.test(val))
-                      .call(this, v);
-    // Keywords are handled as case-insensitive.
-    return v.toLowerCase();
   }
 });
 
 /**
  * Checks validity of a bookmark object, filling up default values for optional
  * properties.
  *
  * @param input (object)
@@ -1368,44 +1269,16 @@ let updateFrecency = Task.async(function
     `UPDATE moz_places
      SET hidden = 0
      WHERE url IN ( ${urls.map(url => JSON.stringify(url.href)).join(", ")} )
        AND frecency <> 0
     `);
 });
 
 /**
- * Creates a keyword entry, if it's missing.
- *
- * @param db
- *        the Sqlite.jsm connection handle.
- * @param keyword
- *        the keyword string to create.
- */
-let maybeCreateKeyword = Task.async(function* (db, keyword) {
-  yield db.executeCached(
-    `INSERT OR IGNORE INTO moz_keywords (keyword)
-     VALUES (:keyword)
-    `, { keyword: keyword });
-});
-
-/**
- * Removes any orphan keyword entries.
- *
- * @param db
- *        the Sqlite.jsm connection handle.
- */
-let removeOrphanKeywords = Task.async(function* (db) {
-  yield db.executeCached(
-    `DELETE FROM moz_keywords
-     WHERE NOT EXISTS(SELECT 1 FROM moz_bookmarks
-                      WHERE keyword_id = moz_keywords.id)`);
-});
-
-/**
  * Removes any orphan annotation entries.
  *
  * @param db
  *        the Sqlite.jsm connection handle.
  */
 let removeOrphanAnnotations = Task.async(function* (db) {
   yield db.executeCached(
     `DELETE FROM moz_items_annos
@@ -1491,17 +1364,17 @@ Task.async(function* (db, folderGuids) {
          WHERE p.guid = :folderGuid
          UNION ALL
          SELECT id FROM moz_bookmarks
          JOIN descendants ON parent = did
        )
        SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
               b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
               b.lastModified, b.title, p.parent AS _grandParentId,
-              NULL AS _childCount, NULL AS keyword
+              NULL AS _childCount
        FROM moz_bookmarks b
        JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON b.fk = h.id
        WHERE b.id IN descendants`, { folderGuid });
 
     itemsRemoved = itemsRemoved.concat(rowsToItemsArray(rows));
 
     yield db.executeCached(
@@ -1514,17 +1387,16 @@ Task.async(function* (db, folderGuids) {
          SELECT id FROM moz_bookmarks
          JOIN descendants ON parent = did
        )
        DELETE FROM moz_bookmarks WHERE id IN descendants`, { folderGuid });
   }
 
   // Cleanup orphans.
   yield removeOrphanAnnotations(db);
-  yield removeOrphanKeywords(db);
 
   // TODO (Bug 1087576): this may leave orphan tags behind.
 
   let urls = [for (item of itemsRemoved) if (item.url) item.url];
   updateFrecency(db, urls).then(null, Cu.reportError);
 
   // Send onItemRemoved notifications to listeners.
   // TODO (Bug 1087580): for the case of eraseEverything, this should send a
--- a/toolkit/components/places/tests/autocomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/autocomplete/head_autocomplete.js
@@ -206,17 +206,17 @@ function task_addPageBook(aURI, aTitle, 
   let title = kTitles[aTitle];
 
   let out = [aURI, aTitle, aBook, aTags, aKey];
   out.push("\nuri=" + kURIs[aURI]);
   out.push("\ntitle=" + title);
 
   // Add the page and a visit if we need to
   if (!aNoVisit) {
-    yield promiseAddVisits({
+    yield PlacesTestUtils.addVisits({
       uri: uri,
       transition: aTransitionType || TRANSITION_LINK,
       visitDate: gDate,
       title: title
     });
     out.push("\nwith visit");
   }
 
@@ -299,15 +299,15 @@ function do_removePages(aURIs)
 function markTyped(aURIs, aTitle)
 {
   gNextTestSetupTasks.push([task_markTyped, arguments]);
 }
 
 function task_markTyped(aURIs, aTitle)
 {
   for (let uri of aURIs) {
-    yield promiseAddVisits({
+    yield PlacesTestUtils.addVisits({
       uri: toURI(kURIs[uri]),
       transition: TRANSITION_TYPED,
       title: kTitles[aTitle]
     });
   }
 }
--- a/toolkit/components/places/tests/autocomplete/test_autocomplete_on_value_removed_479089.js
+++ b/toolkit/components/places/tests/autocomplete/test_autocomplete_on_value_removed_479089.js
@@ -23,17 +23,17 @@ function run_test()
 add_task(function test_autocomplete_on_value_removed()
 {
   // QI to nsIAutoCompleteSimpleResultListener
   var listener = Cc["@mozilla.org/autocomplete/search;1?name=history"].
                  getService(Components.interfaces.nsIAutoCompleteSimpleResultListener);
 
   // add history visit
   var testUri = uri("http://foo.mozilla.com/");
-  yield promiseAddVisits({
+  yield PlacesTestUtils.addVisits({
     uri: testUri,
     referrer: uri("http://mozilla.com/")
   });
   // create a query object
   var query = histsvc.getNewQuery();
   // create the options object we will never use
   var options = histsvc.getNewQueryOptions();
   // look for this uri only
--- a/toolkit/components/places/tests/autocomplete/test_tabmatches.js
+++ b/toolkit/components/places/tests/autocomplete/test_tabmatches.js
@@ -1,15 +1,19 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * vim:set ts=2 sw=2 sts=2 et:
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-let gTabRestrictChar = prefs.getCharPref("browser.urlbar.restrict.openpage");
+let gTabRestrictChar = "%";
+prefs.setCharPref("browser.urlbar.restrict.openpage", gTabRestrictChar);
+do_register_cleanup(() => {
+  prefs.clearUserPref("browser.urlbar.restrict.openpage");
+});
 
 let kSearchParam = "enable-actions";
 
 let kURIs = [
   "http://abc.com/",
   "moz-action:switchtab,http://abc.com/",
   "http://xyz.net/",
   "moz-action:switchtab,http://xyz.net/",
--- a/toolkit/components/places/tests/autocomplete/xpcshell.ini
+++ b/toolkit/components/places/tests/autocomplete/xpcshell.ini
@@ -1,12 +1,11 @@
 [DEFAULT]
 head = head_autocomplete.js
 tail = 
-firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_416211.js]
 [test_416214.js]
 [test_417798.js]
 [test_418257.js]
 [test_422277.js]
 [test_autocomplete_on_value_removed_479089.js]
--- a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js
+++ b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js
@@ -22,17 +22,17 @@ function* getForeignCountForURL(conn, ur
 function run_test() {
   run_next_test();
 }
 
 add_task(function* add_remove_change_bookmark_test() {
   let conn = yield PlacesUtils.promiseDBConnection();
 
   // Simulate a visit to the url
-  yield promiseAddVisits(T_URI);
+  yield PlacesTestUtils.addVisits(T_URI);
   Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0);
 
   // Add 1st bookmark which should increment foreign_count by 1
   let id1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                     T_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, "First Run");
   Assert.equal((yield getForeignCountForURL(conn, T_URI)), 1);
 
   // Add 2nd bookmark
@@ -58,17 +58,17 @@ add_task(function* add_remove_change_boo
   Assert.equal((yield getForeignCountForURL(conn, URI2)), 0);
 
 });
 
 add_task(function* maintenance_foreign_count_test() {
   let conn = yield PlacesUtils.promiseDBConnection();
 
   // Simulate a visit to the url
-  yield promiseAddVisits(T_URI);
+  yield PlacesTestUtils.addVisits(T_URI);
 
   // Adjust the foreign_count for the added entry to an incorrect value
   let deferred = Promise.defer();
   let stmt = DBConn().createAsyncStatement(
     "UPDATE moz_places SET foreign_count = 10 WHERE url = :t_url ");
   stmt.params.t_url = T_URI.spec;
   stmt.executeAsync({
     handleCompletion: function(){
@@ -88,17 +88,17 @@ add_task(function* maintenance_foreign_c
 
   // Check if the foreign_count has been adjusted to the correct value
   Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0);
 });
 
 add_task(function* add_remove_tags_test(){
   let conn = yield PlacesUtils.promiseDBConnection();
 
-  yield promiseAddVisits(T_URI);
+  yield PlacesTestUtils.addVisits(T_URI);
   Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0);
 
   // Check foreign count incremented by 1 for a single tag
   PlacesUtils.tagging.tagURI(T_URI, ["test tag"]);
   Assert.equal((yield getForeignCountForURL(conn, T_URI)), 1);
 
   // Check foreign count is incremented by 2 for two tags
   PlacesUtils.tagging.tagURI(T_URI, ["one", "two"]);
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks.js
@@ -612,17 +612,17 @@ add_task(function test_bookmarks() {
   // ensure that removing an item removes its annotations
   do_check_true(anno.itemHasAnnotation(newId3, "test-annotation"));
   bs.removeItem(newId3);
   do_check_false(anno.itemHasAnnotation(newId3, "test-annotation"));
 
   // bug 378820
   let uri1 = uri("http://foo.tld/a");
   bs.insertBookmark(testRoot, uri1, bs.DEFAULT_INDEX, "");
-  yield promiseAddVisits(uri1);
+  yield PlacesTestUtils.addVisits(uri1);
 
   // bug 646993 - test bookmark titles longer than the maximum allowed length
   let title15 = Array(TITLE_LENGTH_MAX + 5).join("X");
   let title15expected = title15.substring(0, TITLE_LENGTH_MAX);
   let newId15 = bs.insertBookmark(testRoot, uri("http://evil.com/"),
                                   bs.DEFAULT_INDEX, title15);
 
   do_check_eq(bs.getItemTitle(newId15).length,
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
@@ -1,57 +1,54 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 add_task(function* test_eraseEverything() {
-  yield promiseAddVisits({ uri: NetUtil.newURI("http://example.com/") });
-  yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/") });
+  yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://example.com/") });
+  yield PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/") });
   let frecencyForExample = frecencyForUrl("http://example.com/");
   let frecencyForMozilla = frecencyForUrl("http://example.com/");
   Assert.ok(frecencyForExample > 0);
   Assert.ok(frecencyForMozilla > 0);
   let unfiledFolder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                            type: PlacesUtils.bookmarks.TYPE_FOLDER });
   checkBookmarkObject(unfiledFolder);
   let unfiledBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                             url: "http://example.com/",
-                                                             keyword: "kw1" });
+                                                             url: "http://example.com/" });
   checkBookmarkObject(unfiledBookmark);
   let unfiledBookmarkInFolder =
     yield PlacesUtils.bookmarks.insert({ parentGuid: unfiledFolder.guid,
                                          type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                          url: "http://mozilla.org/" });
   checkBookmarkObject(unfiledBookmarkInFolder);
   PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(unfiledBookmarkInFolder.guid)),
                                             "testanno1", "testvalue1", 0, 0);
 
   let menuFolder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid,
                                                         type: PlacesUtils.bookmarks.TYPE_FOLDER });
   checkBookmarkObject(menuFolder);
   let menuBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid,
                                                           type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                          url: "http://example.com/",
-                                                          keyword: "kw2" });
+                                                          url: "http://example.com/" });
   checkBookmarkObject(unfiledBookmark);
   let menuBookmarkInFolder =
     yield PlacesUtils.bookmarks.insert({ parentGuid: menuFolder.guid,
                                          type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                          url: "http://mozilla.org/" });
   checkBookmarkObject(menuBookmarkInFolder);
   PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(menuBookmarkInFolder.guid)),
                                             "testanno1", "testvalue1", 0, 0);
 
   let toolbarFolder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
                                                            type: PlacesUtils.bookmarks.TYPE_FOLDER });
   checkBookmarkObject(toolbarFolder);
   let toolbarBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
                                                              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                             url: "http://example.com/",
-                                                             keyword: "kw3" });
+                                                             url: "http://example.com/" });
   checkBookmarkObject(toolbarBookmark);
   let toolbarBookmarkInFolder =
     yield PlacesUtils.bookmarks.insert({ parentGuid: toolbarFolder.guid,
                                          type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                          url: "http://mozilla.org/" });
   checkBookmarkObject(toolbarBookmarkInFolder);
   PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(toolbarBookmarkInFolder.guid)),
                                             "testanno1", "testvalue1", 0, 0);
@@ -60,21 +57,19 @@ add_task(function* test_eraseEverything(
   Assert.ok(frecencyForUrl("http://example.com/") > frecencyForExample);
   Assert.ok(frecencyForUrl("http://example.com/") > frecencyForMozilla);
 
   yield PlacesUtils.bookmarks.eraseEverything();
 
   Assert.equal(frecencyForUrl("http://example.com/"), frecencyForExample);
   Assert.equal(frecencyForUrl("http://example.com/"), frecencyForMozilla);
 
-  // Check there are no orphan keywords or annotations.
+  // Check there are no orphan annotations.
   let conn = yield PlacesUtils.promiseDBConnection();
-  let rows = yield conn.execute(`SELECT * FROM moz_keywords`);
-  Assert.equal(rows.length, 0);
-  rows = yield conn.execute(`SELECT * FROM moz_items_annos`);
+  let rows = yield conn.execute(`SELECT * FROM moz_items_annos`);
   Assert.equal(rows.length, 0);
   rows = yield conn.execute(`SELECT * FROM moz_anno_attributes`);
   Assert.equal(rows.length, 0);
 });
 
 add_task(function* test_eraseEverything_roots() {
   yield PlacesUtils.bookmarks.eraseEverything();
 
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js
@@ -26,20 +26,16 @@ add_task(function* invalid_input_throws(
   Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012",
                                                     parentGuid: "012345678901",
                                                     index: 0 }),
                 /Unexpected number of conditions provided: 2/);
   Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "123456789012",
                                                     url: "http://example.com"}),
                 /Unexpected number of conditions provided: 2/);
 
-  Assert.throws(() => PlacesUtils.bookmarks.fetch({ keyword: "test",
-                                                    url: "http://example.com"}),
-                /Unexpected number of conditions provided: 2/);
-
   Assert.throws(() => PlacesUtils.bookmarks.fetch("test"),
                 /Invalid value for property 'guid'/);
   Assert.throws(() => PlacesUtils.bookmarks.fetch(123),
                 /Invalid value for property 'guid'/);
 
   Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: "test" }),
                 /Invalid value for property 'guid'/);
   Assert.throws(() => PlacesUtils.bookmarks.fetch({ guid: null }),
@@ -69,25 +65,16 @@ add_task(function* invalid_input_throws(
 
   Assert.throws(() => PlacesUtils.bookmarks.fetch({ url: "http://te st/" }),
                 /Invalid value for property 'url'/);
   Assert.throws(() => PlacesUtils.bookmarks.fetch({ url: null }),
                 /Invalid value for property 'url'/);
   Assert.throws(() => PlacesUtils.bookmarks.fetch({ url: -10 }),
                 /Invalid value for property 'url'/);
 
-  Assert.throws(() => PlacesUtils.bookmarks.fetch({ keyword: "te st" }),
-                /Invalid value for property 'keyword'/);
-  Assert.throws(() => PlacesUtils.bookmarks.fetch({ keyword: null }),
-                /Invalid value for property 'keyword'/);
-  Assert.throws(() => PlacesUtils.bookmarks.fetch({ keyword: "" }),
-                /Invalid value for property 'keyword'/);
-  Assert.throws(() => PlacesUtils.bookmarks.fetch({ keyword: 5 }),
-                /Invalid value for property 'keyword'/);
-
   Assert.throws(() => PlacesUtils.bookmarks.fetch("123456789012", "test"),
                 /onResult callback must be a valid function/);
   Assert.throws(() => PlacesUtils.bookmarks.fetch("123456789012", {}),
                 /onResult callback must be a valid function/);
 });
 
 add_task(function* fetch_nonexistent_guid() {
   let bm = yield PlacesUtils.bookmarks.fetch({ guid: "123456789012" },
@@ -112,17 +99,16 @@ add_task(function* fetch_bookmark() {
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
   Assert.equal(bm2.url.href, "http://example.com/");
   Assert.equal(bm2.title, "a bookmark");
-  Assert.ok(!("keyword" in bm2));
 
   yield PlacesUtils.bookmarks.remove(bm1.guid);
 });
 
 add_task(function* fetch_bookmar_empty_title() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                  url: "http://example.com/",
@@ -130,17 +116,16 @@ add_task(function* fetch_bookmar_empty_t
   checkBookmarkObject(bm1);
 
   let bm2 = yield PlacesUtils.bookmarks.fetch(bm1.guid);
   checkBookmarkObject(bm2);
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.index, 0);
   Assert.ok(!("title" in bm2));
-  Assert.ok(!("keyword" in bm2));
 
   yield PlacesUtils.bookmarks.remove(bm1.guid);
 });
 
 add_task(function* fetch_folder() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                  title: "a folder" });
@@ -151,17 +136,16 @@ add_task(function* fetch_folder() {
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER);
   Assert.equal(bm2.title, "a folder");
   Assert.ok(!("url" in bm2));
-  Assert.ok(!("keyword" in bm2));
 
   yield PlacesUtils.bookmarks.remove(bm1.guid);
 });
 
 add_task(function* fetch_folder_empty_title() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                  title: "" });
@@ -187,17 +171,16 @@ add_task(function* fetch_separator() {
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
   Assert.ok(!("url" in bm2));
   Assert.ok(!("title" in bm2));
-  Assert.ok(!("keyword" in bm2));
 
   yield PlacesUtils.bookmarks.remove(bm1.guid);  
 });
 
 add_task(function* fetch_byposition_nonexisting_parentGuid() {
   let bm = yield PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012",
                                                index: 0 },
                                              gAccumulator.callback);
@@ -230,17 +213,16 @@ add_task(function* fetch_byposition() {
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
   Assert.equal(bm2.url.href, "http://example.com/");
   Assert.equal(bm2.title, "a bookmark");
-  Assert.ok(!("keyword" in bm2));
 });
 
 add_task(function* fetch_byposition_default_index() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                  url: "http://example.com/last",
                                                  title: "last child" });
   checkBookmarkObject(bm1);
@@ -255,17 +237,16 @@ add_task(function* fetch_byposition_defa
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 1);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
   Assert.equal(bm2.url.href, "http://example.com/last");
   Assert.equal(bm2.title, "last child");
-  Assert.ok(!("keyword" in bm2));
 
   yield PlacesUtils.bookmarks.remove(bm1.guid);
 });
 
 add_task(function* fetch_byurl_nonexisting() {
   let bm = yield PlacesUtils.bookmarks.fetch({ url: "http://nonexisting.com/" },
                                              gAccumulator.callback);
   Assert.equal(bm, null);
@@ -290,17 +271,16 @@ add_task(function* fetch_byurl() {
   Assert.deepEqual(gAccumulator.results[0], bm1);
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
   Assert.equal(bm2.url.href, "http://byurl.com/");
   Assert.equal(bm2.title, "a bookmark");
-  Assert.ok(!("keyword" in bm2));
 
   let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                  url: "http://byurl.com/",
                                                  title: "a bookmark" });
   let bm4 = yield PlacesUtils.bookmarks.fetch({ url: bm1.url },
                                               gAccumulator.callback);
   checkBookmarkObject(bm4);
@@ -320,75 +300,11 @@ add_task(function* fetch_byurl() {
   Assert.equal(gAccumulator.results.length, 2);
   gAccumulator.results.forEach(checkBookmarkObject);
   Assert.deepEqual(gAccumulator.results[0], bm5);
 
   // cleanup
   PlacesUtils.tagging.untagURI(uri(bm1.url.href), ["Test Tag"]);
 });
 
-add_task(function* fetch_bykeyword_nonexisting() {
-  let bm = yield PlacesUtils.bookmarks.fetch({ keyword: "nonexisting" },
-                                             gAccumulator.callback);
-  Assert.equal(bm, null);
-  Assert.equal(gAccumulator.results.length, 0);
-});
-
-add_task(function* fetch_bykeyword() {
-  let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-                                                 type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                 url: "http://bykeyword1.com/",
-                                                 keyword: "bykeyword" });
-  checkBookmarkObject(bm1);
-
-  let bm2 = yield PlacesUtils.bookmarks.fetch({ keyword: "bykeyword" },
-                                              gAccumulator.callback);
-  checkBookmarkObject(bm2);
-  Assert.equal(gAccumulator.results.length, 1);
-  checkBookmarkObject(gAccumulator.results[0]);
-  Assert.deepEqual(gAccumulator.results[0], bm1);
-
-  Assert.deepEqual(bm1, bm2);
-  Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
-  Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
-  Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
-  Assert.equal(bm2.url.href, "http://bykeyword1.com/");
-  Assert.equal(bm2.keyword, "bykeyword");
-
-  // Add a second url using the same keyword.
-  let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-                                                 type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                 url: "http://bykeyword2.com/",
-                                                 keyword: "bykeyword" });
-  let bm4 = yield PlacesUtils.bookmarks.fetch({ keyword: "bykeyword" },
-                                              gAccumulator.callback);
-  checkBookmarkObject(bm4);
-  Assert.deepEqual(bm3, bm4);
-  Assert.equal(gAccumulator.results.length, 2);
-  gAccumulator.results.forEach(checkBookmarkObject);
-  Assert.deepEqual(gAccumulator.results[0], bm4);
-
-  // After an update the returned bookmark should change.
-  yield PlacesUtils.bookmarks.update({ guid: bm1.guid, title: "new title" });
-  let bm5 = yield PlacesUtils.bookmarks.fetch({ keyword: "bykeyword" },
-                                              gAccumulator.callback);
-  checkBookmarkObject(bm5);
-  // Cannot use deepEqual cause lastModified changed.
-  Assert.equal(bm1.guid, bm5.guid);
-  Assert.ok(bm5.lastModified > bm1.lastModified);
-  Assert.equal(gAccumulator.results.length, 2);
-  gAccumulator.results.forEach(checkBookmarkObject);
-  Assert.deepEqual(gAccumulator.results[0], bm5);
-
-  // Check fetching by keyword is case-insensitive.
-  let bm6 = yield PlacesUtils.bookmarks.fetch({ keyword: "ByKeYwOrD" },
-                                              gAccumulator.callback);
-  checkBookmarkObject(bm6);
-  // Cannot use deepEqual cause lastModified changed.
-  Assert.equal(bm1.guid, bm6.guid);
-  Assert.equal(gAccumulator.results.length, 2);
-  gAccumulator.results.forEach(checkBookmarkObject);
-  Assert.deepEqual(gAccumulator.results[0], bm6);
-});
-
 function run_test() {
   run_next_test();
 }
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js
@@ -75,44 +75,28 @@ add_task(function* invalid_input_throws(
                                                      url: longurl }),
                 /Invalid value for property 'url'/);
   Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                      url: NetUtil.newURI(longurl) }),
                 /Invalid value for property 'url'/);
   Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                      url: "te st" }),
                 /Invalid value for property 'url'/);
-
-  Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                     keyword: 10 }),
-                /Invalid value for property 'keyword'/);
-  Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                     keyword: null }),
-                /Invalid value for property 'keyword'/);
-  Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                     keyword: "" }),
-                /Invalid value for property 'keyword'/);
 });
 
 add_task(function* invalid_properties_for_bookmark_type() {
   Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                      url: "http://www.moz.com/" }),
                 /Invalid value for property 'url'/);
   Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                      url: "http://www.moz.com/" }),
                 /Invalid value for property 'url'/);
   Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                      title: "test" }),
                 /Invalid value for property 'title'/);
-  Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
-                                                     keyword: "test" }),
-                /Invalid value for property 'keyword'/);
-  Assert.throws(() => PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
-                                                     keyword: "test" }),
-                /Invalid value for property 'keyword'/);
 });
 
 add_task(function* long_title_trim() {
   let longtitle = "a";
   for (let i = 0; i < 4096; i++) {
     longtitle += "a";
   }
   let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
@@ -120,17 +104,16 @@ add_task(function* long_title_trim() {
                                                 title: longtitle });
   checkBookmarkObject(bm);
   Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm.index, 0);
   Assert.equal(bm.dateAdded, bm.lastModified);
   Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER);
   Assert.equal(bm.title.length, 4096, "title should have been trimmed");
   Assert.ok(!("url" in bm), "url should not be set");
-  Assert.ok(!("keyword" in bm), "keyword should not be set");
 });
 
 add_task(function* create_separator() {
   let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                 index: PlacesUtils.bookmarks.DEFAULT_INDEX });
   checkBookmarkObject(bm);
   Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
@@ -238,38 +221,22 @@ add_task(function* create_bookmark() {
   Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
   Assert.equal(bm.url.href, "http://example.com/");
   Assert.equal(bm.title, "a bookmark");
 
   // Check parent lastModified.
   let parent = yield PlacesUtils.bookmarks.fetch({ guid: bm.parentGuid });
   Assert.deepEqual(parent.lastModified, bm.dateAdded);
 
-  // While here, also check keywords are case-insensitive.
-  bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parentGuid,
-                                            type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                            url: NetUtil.newURI("http://example.com/"),
-                                            keyword: "tEsT" });
-  checkBookmarkObject(bm);
-  Assert.equal(bm.parentGuid, parentGuid);
-  Assert.equal(bm.index, 1);
-  Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
-  Assert.equal(bm.url.href, "http://example.com/");
-  Assert.equal(bm.keyword, "test");
-
-  // Check parent lastModified.
-  parent = yield PlacesUtils.bookmarks.fetch({ guid: bm.parentGuid });
-  Assert.deepEqual(parent.lastModified, bm.dateAdded);
-
   bm = yield PlacesUtils.bookmarks.insert({ parentGuid: parentGuid,
                                             type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                             url: new URL("http://example.com/") });
   checkBookmarkObject(bm);
   Assert.equal(bm.parentGuid, parentGuid);
-  Assert.equal(bm.index, 2);
+  Assert.equal(bm.index, 1);
   Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
   Assert.equal(bm.url.href, "http://example.com/");
   Assert.ok(!("title" in bm), "title should not be set");
 });
 
 add_task(function* create_bookmark_frecency() {
   let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
@@ -65,37 +65,16 @@ add_task(function* insert_bookmark_notit
   let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
   observer.check([ { name: "onItemAdded",
                      arguments: [ itemId, parentId, bm.index, bm.type,
                                   bm.url, null, bm.dateAdded,
                                   bm.guid, bm.parentGuid ] }
                  ]);
 });
 
-add_task(function* insert_bookmark_keyword_notification() {
-  let observer = expectNotifications();
-  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-                                                url: new URL("http://example.com/"),
-                                                keyword: "Kw" });
-  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
-  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
-  // Keywords are case-insensitive.
-  Assert.equal(bm.keyword, "kw");
-  observer.check([ { name: "onItemAdded",
-                     arguments: [ itemId, parentId, bm.index, bm.type,
-                                  bm.url, null, bm.dateAdded,
-                                  bm.guid, bm.parentGuid ] },
-                   { name: "onItemChanged",
-                     arguments: [ itemId, "keyword", false, bm.keyword,
-                                  bm.lastModified, bm.type, parentId,
-                                  bm.guid, bm.parentGuid ] }
-                 ]);
-});
-
 add_task(function* insert_bookmark_tag_notification() {
   let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: new URL("http://tag.example.com/") });
   let itemId = yield PlacesUtils.promiseItemId(bm.guid);
   let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
 
   let tagFolder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
@@ -165,42 +144,16 @@ add_task(function* update_bookmark_uri()
 
   observer.check([ { name: "onItemChanged",
                      arguments: [ itemId, "uri", false, bm.url.href,
                                   bm.lastModified, bm.type, parentId, bm.guid,
                                   bm.parentGuid ] }
                  ]);
 });
 
-add_task(function* update_bookmark_keyword() {
-  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-                                                url: new URL("http://keyword.example.com/") });
-  let observer = expectNotifications();
-  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, keyword: "kW" });
-  // Keywords are case-insensitive.
-  Assert.equal(bm.keyword, "kw");
-  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
-  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
-
-  observer.check([ { name: "onItemChanged",
-                     arguments: [ itemId, "keyword", false, bm.keyword,
-                                  bm.lastModified, bm.type, parentId, bm.guid,
-                                  bm.parentGuid ] }
-                 ]);
-
-  observer = expectNotifications();
-  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid, keyword: "" });
-  observer.check([ { name: "onItemChanged",
-                     arguments: [ itemId, "keyword", false, "",
-                                  bm.lastModified, bm.type, parentId, bm.guid,
-                                  bm.parentGuid ] }
-                 ]);
-});
-
 add_task(function* update_move_same_folder() {
   // Ensure there are at least two items in place (others test do so for us,
   // but we don't have to depend on that).
   let sep = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                  parentGuid: PlacesUtils.bookmarks.unfiledGuid });
   let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: new URL("http://move.example.com/") });
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
@@ -76,61 +76,52 @@ add_task(function* remove_bookmark() {
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
   Assert.equal(bm2.url.href, "http://example.com/");
   Assert.equal(bm2.title, "a bookmark");
-  Assert.ok(!("keyword" in bm2));
 });
 
 
 add_task(function* remove_bookmark_orphans() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                  url: "http://example.com/",
-                                                 title: "a bookmark",
-                                                 keyword: "tEsT"});
+                                                 title: "a bookmark" });
   checkBookmarkObject(bm1);
   PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(bm1.guid)),
                                             "testanno", "testvalue", 0, 0);
 
   let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
   checkBookmarkObject(bm2);
-  // Keywords are case-insensitive.
-  Assert.equal(bm2.keyword, "test");
 
-  // Check there are no orphan keywords or annotations.
+  // Check there are no orphan annotations.
   let conn = yield PlacesUtils.promiseDBConnection();
-  let rows = yield conn.execute(`SELECT * FROM moz_keywords`);
+  let rows = yield conn.execute(`SELECT * FROM moz_items_annos`);
   Assert.equal(rows.length, 0);
-  rows = yield conn.execute(`SELECT * FROM moz_items_annos`);
+  rows = yield conn.execute(`SELECT * FROM moz_anno_attributes`);
   Assert.equal(rows.length, 0);
-  // removeItemAnnotations doesn't remove orphan annotations, cause it likely
-  // relies on expiration to do so.
-  //rows = yield conn.execute(`SELECT * FROM moz_anno_attributes`);
-  //Assert.equal(rows.length, 0);
 });
 
 add_task(function* remove_bookmark_empty_title() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                  url: "http://example.com/",
                                                  title: "" });
   checkBookmarkObject(bm1);
 
   let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
   checkBookmarkObject(bm2);
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.index, 0);
   Assert.ok(!("title" in bm2));
-  Assert.ok(!("keyword" in bm2));
 });
 
 add_task(function* remove_folder() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                  title: "a folder" });
   checkBookmarkObject(bm1);
 
@@ -139,17 +130,16 @@ add_task(function* remove_folder() {
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER);
   Assert.equal(bm2.title, "a folder");
   Assert.ok(!("url" in bm2));
-  Assert.ok(!("keyword" in bm2));
 });
 
 add_task(function* test_nested_contents_removed() {
   let folder1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                      type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                      title: "a folder" });
   let folder2 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder1.guid,
                                                      type: PlacesUtils.bookmarks.TYPE_FOLDER,
@@ -185,14 +175,13 @@ add_task(function* remove_separator() {
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
   Assert.ok(!("url" in bm2));
   Assert.ok(!("title" in bm2));
-  Assert.ok(!("keyword" in bm2));
 });
 
 function run_test() {
   run_next_test();
 }
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js
@@ -65,21 +65,16 @@ add_task(function* invalid_input_throws(
   Assert.throws(() => PlacesUtils.bookmarks.update({ url: "te st" }),
                 /Invalid value for property 'url'/);
 
   Assert.throws(() => PlacesUtils.bookmarks.insert({ title: -1 }),
                 /Invalid value for property 'title'/);
   Assert.throws(() => PlacesUtils.bookmarks.insert({ title: undefined }),
                 /Invalid value for property 'title'/);
 
-  Assert.throws(() => PlacesUtils.bookmarks.update({ keyword: 10 }),
-                /Invalid value for property 'keyword'/);
-  Assert.throws(() => PlacesUtils.bookmarks.update({ keyword: null }),
-                /Invalid value for property 'keyword'/);
-
   Assert.throws(() => PlacesUtils.bookmarks.update({ guid: "123456789012" }),
                 /Not enough properties to update/);
 
   Assert.throws(() => PlacesUtils.bookmarks.update({ guid: "123456789012",
                                                      parentGuid: "012345678901" }),
                 /The following properties were expected: index/);
   Assert.throws(() => PlacesUtils.bookmarks.update({ guid: "123456789012",
                                                      index: 1 }),
@@ -147,42 +142,28 @@ add_task(function* invalid_properties_fo
                                                     parentGuid: PlacesUtils.bookmarks.unfiledGuid });
   try {
     yield PlacesUtils.bookmarks.update({ guid: folder.guid,
                                          url: "http://example.com/" });
     Assert.ok(false, "Should have thrown");
   } catch (ex) {
     Assert.ok(/Invalid value for property 'url'/.test(ex));
   }
-  try {
-    yield PlacesUtils.bookmarks.update({ guid: folder.guid,
-                                         keyword: "test" });
-    Assert.ok(false, "Should have thrown");
-  } catch (ex) {
-    Assert.ok(/Invalid value for property 'keyword'/.test(ex));
-  }
 
   let separator = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                        parentGuid: PlacesUtils.bookmarks.unfiledGuid });
   try {
     yield PlacesUtils.bookmarks.update({ guid: separator.guid,
                                          url: "http://example.com/" });
     Assert.ok(false, "Should have thrown");
   } catch (ex) {
     Assert.ok(/Invalid value for property 'url'/.test(ex));
   }
   try {
     yield PlacesUtils.bookmarks.update({ guid: separator.guid,
-                                         keyword: "test" });
-    Assert.ok(false, "Should have thrown");
-  } catch (ex) {
-    Assert.ok(/Invalid value for property 'keyword'/.test(ex));
-  }
-  try {
-    yield PlacesUtils.bookmarks.update({ guid: separator.guid,
                                          title: "test" });
     Assert.ok(false, "Should have thrown");
   } catch (ex) {
     Assert.ok(/Invalid value for property 'title'/.test(ex));
   }
 });
 
 add_task(function* long_title_trim() {
@@ -233,58 +214,21 @@ add_task(function* update_lastModified()
   bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                             title: "" });
   Assert.ok(!("title" in bm));
 
   bm = yield PlacesUtils.bookmarks.fetch(bm.guid);
   Assert.ok(!("title" in bm));
 });
 
-add_task(function* update_keyword() {
-  let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-                                                type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
-                                                url: "http://example.com/",
-                                                title: "title",
-                                                keyword: "kw" });
-  checkBookmarkObject(bm);
-  let lastModified = bm.lastModified;
-
-  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
-                                            keyword: "KW2" });
-  checkBookmarkObject(bm);
-  Assert.ok(bm.lastModified >= lastModified);
-  // Keywords are case-insensitive.
-  Assert.equal(bm.keyword, "kw2");
-
-  bm = yield PlacesUtils.bookmarks.fetch(bm.guid);
-  // Keywords are case-insensitive.
-  Assert.equal(bm.keyword, "kw2");
-  lastModified = bm.lastModified;
-
-  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
-                                            keyword: "" });
-  Assert.ok(!("keyword" in bm));
-
-  bm = yield PlacesUtils.bookmarks.fetch(bm.guid);
-  Assert.ok(!("keyword" in bm));
-  Assert.ok(bm.lastModified >= lastModified);
-
-  // Check orphan keyword has been removed from the database.
-  let conn = yield PlacesUtils.promiseDBConnection();
-  let rows = yield conn.executeCached(
-    `SELECT id from moz_keywords WHERE keyword >= :keyword`, { keyword: "kw" });
-  Assert.equal(rows.length, 0);
-});
-
 add_task(function* update_url() {
   let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 url: "http://example.com/",
-                                                title: "title",
-                                                keyword: "kw" });
+                                                title: "title" });
   checkBookmarkObject(bm);
   let lastModified = bm.lastModified;
   let frecency = frecencyForUrl(bm.url);
   Assert.ok(frecency > 0, "Check frecency has been updated");
 
   bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                             url: "http://mozilla.org/" });
   checkBookmarkObject(bm);
--- a/toolkit/components/places/tests/bookmarks/test_getBookmarkedURIFor.js
+++ b/toolkit/components/places/tests/bookmarks/test_getBookmarkedURIFor.js
@@ -14,28 +14,28 @@ let bs = PlacesUtils.bookmarks;
 function run_test() {
   run_next_test();
 }
 
 add_task(function test_getBookmarkedURIFor() {
   let now = Date.now() * 1000;
   const sourceURI = uri("http://test.mozilla.org/");
   // Add a visit and a bookmark.
-  yield promiseAddVisits({ uri: sourceURI, visitDate: now });
+  yield PlacesTestUtils.addVisits({ uri: sourceURI, visitDate: now });
   do_check_eq(bs.getBookmarkedURIFor(sourceURI), null);
 
   let sourceItemId = bs.insertBookmark(bs.unfiledBookmarksFolder,
                                        sourceURI,
                                        bs.DEFAULT_INDEX,
                                        "bookmark");
   do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI));
 
   // Add a redirected visit.
   const permaURI = uri("http://perma.mozilla.org/");
-  yield promiseAddVisits({
+  yield PlacesTestUtils.addVisits({
     uri: permaURI,
     transition: TRANSITION_REDIRECT_PERMANENT,
     visitDate: now++,
     referrer: sourceURI
   });
   do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI));
   do_check_true(bs.getBookmarkedURIFor(permaURI).equals(sourceURI));
   // Add a bookmark to the destination.
@@ -47,17 +47,17 @@ add_task(function test_getBookmarkedURIF
   do_check_true(bs.getBookmarkedURIFor(permaURI).equals(permaURI));
   // Now remove the bookmark on the destination.
   bs.removeItem(permaItemId);
   // We should see the source as bookmark.
   do_check_true(bs.getBookmarkedURIFor(permaURI).equals(sourceURI));
 
   // Add another redirected visit.
   const tempURI = uri("http://perma.mozilla.org/");
-  yield promiseAddVisits({
+  yield PlacesTestUtils.addVisits({
     uri: tempURI,
     transition: TRANSITION_REDIRECT_TEMPORARY,
     visitDate: now++,
     referrer: permaURI
   });
 
   do_check_true(bs.getBookmarkedURIFor(sourceURI).equals(sourceURI));
   do_check_true(bs.getBookmarkedURIFor(tempURI).equals(sourceURI));
--- a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
+++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
@@ -268,17 +268,17 @@ add_test(function onItemMoved_bookmark()
         { name: "time", check: function (v) typeof(v) == "number" && v > 0 },
         { name: "transitionType", check: function (v) v === PlacesUtils.history.TRANSITION_TYPED },
         { name: "uri", check: function (v) v instanceof Ci.nsIURI && v.equals(uri) },
         { name: "parentId", check: function (v) v === PlacesUtils.unfiledBookmarksFolderId },
         { name: "guid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
         { name: "parentGuid", check: function (v) typeof(v) == "string" && /^[a-zA-Z0-9\-_]{12}$/.test(v) },
       ] },
   ];
-  promiseAddVisits({ uri: uri, transition: TRANSITION_TYPED });
+  PlacesTestUtils.addVisits({ uri: uri, transition: TRANSITION_TYPED });
 });
 
 add_test(function onItemRemoved_bookmark() {
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0);
   let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
   gBookmarksObserver.expected = [
     { name: "onItemChanged", // This is an unfortunate effect of bug 653910.
       args: [
--- a/toolkit/components/places/tests/bookmarks/test_savedsearches.js
+++ b/toolkit/components/places/tests/bookmarks/test_savedsearches.js
@@ -121,17 +121,17 @@ add_test(function test_savedsearches_boo
   PlacesUtils.bookmarks.removeItem(searchId);
 
   run_next_test();
 });
 
 add_task(function test_savedsearches_history() {
   // add a visit that matches the search term
   var testURI = uri("http://" + searchTerm + ".com");
-  yield promiseAddVisits({ uri: testURI, title: searchTerm });
+  yield PlacesTestUtils.addVisits({ uri: testURI, title: searchTerm });
 
   // create a saved-search that matches the visit we added
   var searchId = PlacesUtils.bookmarks.insertBookmark(testRoot,
     uri("place:terms=" + searchTerm + "&excludeQueries=1&expandQueries=1&queryType=0"),
     PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm);
 
   // query for the test root, expandQueries=1
   // the query should show up as a query container, with 1 child
@@ -161,17 +161,17 @@ add_task(function test_savedsearches_his
 
       // test that history visit shows in query results
       var item = node.getChild(0);
       do_check_eq(item.type, item.RESULT_TYPE_URI);
       do_check_eq(item.itemId, -1); // history visit
       do_check_eq(item.uri, testURI.spec); // history visit
 
       // test live-update of query results - add a history visit that matches the query
-      yield promiseAddVisits({
+      yield PlacesTestUtils.addVisits({
         uri: uri("http://foo.com"),
         title: searchTerm + "blah"
       });
       do_check_eq(node.childCount, 2);
 
       // test live-update of query results - delete a history visit that matches the query
       PlacesUtils.history.removePage(uri("http://foo.com"));
       do_check_eq(node.childCount, 1);
--- a/toolkit/components/places/tests/browser/browser_bug248970.js
+++ b/toolkit/components/places/tests/browser/browser_bug248970.js
@@ -33,17 +33,17 @@ add_task(function () {
   let bookmarksDeferred = Promise.defer();
   waitForCondition(() => {
     placeItemsCount = getPlacesItemsCount();
     return placeItemsCount > 0
   }, bookmarksDeferred.resolve, "Should have default bookmarks");
   yield bookmarksDeferred.promise;
 
   // Create a handful of history items with various visit types
-  yield promiseAddVisits([
+  yield PlacesTestUtils.addVisits([
     { uri: visitedURIs[0], transition: TRANSITION_LINK },
     { uri: visitedURIs[1], transition: TRANSITION_TYPED },
     { uri: visitedURIs[2], transition: TRANSITION_BOOKMARK },
     { uri: visitedURIs[3], transition: TRANSITION_REDIRECT_PERMANENT },
     { uri: visitedURIs[4], transition: TRANSITION_REDIRECT_TEMPORARY },
     { uri: visitedURIs[5], transition: TRANSITION_EMBED },
     { uri: visitedURIs[6], transition: TRANSITION_FRAMED_LINK },
     { uri: visitedURIs[7], transition: TRANSITION_DOWNLOAD }
--- a/toolkit/components/places/tests/browser/head.js
+++ b/toolkit/components/places/tests/browser/head.js
@@ -164,79 +164,16 @@ function addVisits(aPlaceInfo, aWindow, 
         if (aCallback)
           aCallback();
       }
     }
   );
 }
 
 /**
- * Asynchronously adds visits to a page.
- *
- * @param aPlaceInfo
- *        Can be an nsIURI, in such a case a single LINK visit will be added.
- *        Otherwise can be an object describing the visit to add, or an array
- *        of these objects:
- *          { uri: nsIURI of the page,
- *            transition: one of the TRANSITION_* from nsINavHistoryService,
- *            [optional] title: title of the page,
- *            [optional] visitDate: visit date in microseconds from the epoch
- *            [optional] referrer: nsIURI of the referrer for this visit
- *          }
- *
- * @return {Promise}
- * @resolves When all visits have been added successfully.
- * @rejects JavaScript exception.
- */
-function promiseAddVisits(aPlaceInfo)
-{
-  let deferred = Promise.defer();
-  let places = [];
-  if (aPlaceInfo instanceof Ci.nsIURI) {
-    places.push({ uri: aPlaceInfo });
-  }
-  else if (Array.isArray(aPlaceInfo)) {
-    places = places.concat(aPlaceInfo);
-  } else {
-    places.push(aPlaceInfo)
-  }
-
-  // Create mozIVisitInfo for each entry.
-  let now = Date.now();
-  for (let i = 0; i < places.length; i++) {
-    if (!places[i].title) {
-      places[i].title = "test visit for " + places[i].uri.spec;
-    }
-    places[i].visits = [{
-      transitionType: places[i].transition === undefined ? TRANSITION_LINK
-                                                         : places[i].transition,
-      visitDate: places[i].visitDate || (now++) * 1000,
-      referrerURI: places[i].referrer
-    }];
-  }
-
-  PlacesUtils.asyncHistory.updatePlaces(
-    places,
-    {
-      handleError: function AAV_handleError(aResultCode, aPlaceInfo) {
-        let ex = new Components.Exception("Unexpected error in adding visits.",
-                                          aResultCode);
-        deferred.reject(ex);
-      },
-      handleResult: function () {},
-      handleCompletion: function UP_handleCompletion() {
-        deferred.resolve();
-      }
-    }
-  );
-
-  return deferred.promise;
-}
-
-/**
  * Checks that the favicon for the given page matches the provided data.
  *
  * @param aPageURI
  *        nsIURI object for the page to check.
  * @param aExpectedMimeType
  *        Expected MIME type of the icon, for example "image/png".
  * @param aExpectedData
  *        Expected icon data, expressed as an array of byte values.
--- a/toolkit/components/places/tests/expiration/test_analyze_runs.js
+++ b/toolkit/components/places/tests/expiration/test_analyze_runs.js
@@ -59,17 +59,17 @@ function run_test()
 
 add_task(function init_tests()
 {
   const TEST_URI = NetUtil.newURI("http://mozilla.org/");
   const TEST_TITLE = "This is a test";
   let bs = PlacesUtils.bookmarks;
   bs.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI,
                     bs.DEFAULT_INDEX, TEST_TITLE);
-  yield promiseAddVisits(TEST_URI);
+  yield PlacesTestUtils.addVisits(TEST_URI);
   let thing = {
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput,
                                            Ci.nsIAutoCompletePopup,
                                            Ci.nsIAutoCompleteController]),
     get popup() { return thing; },
     get controller() { return thing; },
     popupOpen: true,
     selectedIndex: 0,
--- a/toolkit/components/places/tests/expiration/test_annos_expire_history.js
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_history.js
@@ -27,31 +27,31 @@ add_task(function test_annos_expire_hist
 
   // Expire all expirable pages.
   setMaxPages(0);
 
   // Add some visited page and a couple expire with history annotations for each.
   let now = getExpirablePRTime();
   for (let i = 0; i < 5; i++) {
     let pageURI = uri("http://page_anno." + i + ".mozilla.org/");
-    yield promiseAddVisits({ uri: pageURI, visitDate: now++ });
+    yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
     as.setPageAnnotation(pageURI, "page_expire1", "test", 0, as.EXPIRE_WITH_HISTORY);
     as.setPageAnnotation(pageURI, "page_expire2", "test", 0, as.EXPIRE_WITH_HISTORY);
   }
 
   let pages = as.getPagesWithAnnotation("page_expire1");
   do_check_eq(pages.length, 5);
   pages = as.getPagesWithAnnotation("page_expire2");
   do_check_eq(pages.length, 5);
 
   // Add some bookmarked page and a couple session annotations for each.
   for (let i = 0; i < 5; i++) {
     let pageURI = uri("http://item_anno." + i + ".mozilla.org/");
     // We also add a visit before bookmarking.
-    yield promiseAddVisits({ uri: pageURI, visitDate: now++ });
+    yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
     let id = bs.insertBookmark(bs.unfiledBookmarksFolder, pageURI,
                                bs.DEFAULT_INDEX, null);
     // Notice we use page annotations here, items annotations can't use this
     // kind of expiration policy.
     as.setPageAnnotation(pageURI, "item_persist1", "test", 0, as.EXPIRE_WITH_HISTORY);
     as.setPageAnnotation(pageURI, "item_persist2", "test", 0, as.EXPIRE_WITH_HISTORY);
   }
 
@@ -59,17 +59,17 @@ add_task(function test_annos_expire_hist
   do_check_eq(items.length, 5);
   items = as.getPagesWithAnnotation("item_persist2");
   do_check_eq(items.length, 5);
 
   // Add other visited page and a couple expire with history annotations for each.
   // We won't expire these visits, so the annotations should survive.
   for (let i = 0; i < 5; i++) {
     let pageURI = uri("http://persist_page_anno." + i + ".mozilla.org/");
-    yield promiseAddVisits({ uri: pageURI, visitDate: now++ });
+    yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
     as.setPageAnnotation(pageURI, "page_persist1", "test", 0, as.EXPIRE_WITH_HISTORY);
     as.setPageAnnotation(pageURI, "page_persist2", "test", 0, as.EXPIRE_WITH_HISTORY);
   }
 
   pages = as.getPagesWithAnnotation("page_persist1");
   do_check_eq(pages.length, 5);
   pages = as.getPagesWithAnnotation("page_persist2");
   do_check_eq(pages.length, 5);
--- a/toolkit/components/places/tests/expiration/test_annos_expire_never.js
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_never.js
@@ -30,47 +30,47 @@ add_task(function test_annos_expire_neve
 
   // Expire all expirable pages.
   setMaxPages(0);
 
   // Add some visited page and a couple expire never annotations for each.
   let now = getExpirablePRTime();
   for (let i = 0; i < 5; i++) {
     let pageURI = uri("http://page_anno." + i + ".mozilla.org/");
-    yield promiseAddVisits({ uri: pageURI, visitDate: now++ });
+    yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
     as.setPageAnnotation(pageURI, "page_expire1", "test", 0, as.EXPIRE_NEVER);
     as.setPageAnnotation(pageURI, "page_expire2", "test", 0, as.EXPIRE_NEVER);
   }
 
   let pages = as.getPagesWithAnnotation("page_expire1");
   do_check_eq(pages.length, 5);
   pages = as.getPagesWithAnnotation("page_expire2");
   do_check_eq(pages.length, 5);
 
   // Add some bookmarked page and a couple expire never annotations for each.
   for (let i = 0; i < 5; i++) {
     let pageURI = uri("http://item_anno." + i + ".mozilla.org/");
     // We also add a visit before bookmarking.
-    yield promiseAddVisits({ uri: pageURI, visitDate: now++ });
+    yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
     let id = bs.insertBookmark(bs.unfiledBookmarksFolder, pageURI,
                                bs.DEFAULT_INDEX, null);
     as.setItemAnnotation(id, "item_persist1", "test", 0, as.EXPIRE_NEVER);
     as.setItemAnnotation(id, "item_persist2", "test", 0, as.EXPIRE_NEVER);
   }
 
   let items = as.getItemsWithAnnotation("item_persist1");
   do_check_eq(items.length, 5);
   items = as.getItemsWithAnnotation("item_persist2");
   do_check_eq(items.length, 5);
 
   // Add other visited page and a couple expire never annotations for each.
   // We won't expire these visits, so the annotations should survive.
   for (let i = 0; i < 5; i++) {
     let pageURI = uri("http://persist_page_anno." + i + ".mozilla.org/");
-    yield promiseAddVisits({ uri: pageURI, visitDate: now++ });
+    yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
     as.setPageAnnotation(pageURI, "page_persist1", "test", 0, as.EXPIRE_NEVER);
     as.setPageAnnotation(pageURI, "page_persist2", "test", 0, as.EXPIRE_NEVER);
   }
 
   pages = as.getPagesWithAnnotation("page_persist1");
   do_check_eq(pages.length, 5);
   pages = as.getPagesWithAnnotation("page_persist2");
   do_check_eq(pages.length, 5);
--- a/toolkit/components/places/tests/expiration/test_annos_expire_policy.js
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_policy.js
@@ -83,17 +83,17 @@ add_task(function test_annos_expire_poli
 
   // Expire all expirable pages.
   setMaxPages(0);
 
   let now = getExpirablePRTime();
   // Add some bookmarked page and timed annotations for each.
   for (let i = 0; i < 5; i++) {
     let pageURI = uri("http://item_anno." + i + ".mozilla.org/");
-    yield promiseAddVisits({ uri: pageURI, visitDate: now++ });
+    yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
     let id = bs.insertBookmark(bs.unfiledBookmarksFolder, pageURI,
                                bs.DEFAULT_INDEX, null);
     // Add a 6 days old anno.
     add_old_anno(id, "persist_days", "test", as.EXPIRE_DAYS, 6);
     // Add a 8 days old anno, modified 5 days ago.
     add_old_anno(id, "persist_lm_days", "test", as.EXPIRE_DAYS, 8, 6);
     // Add a 8 days old anno.
     add_old_anno(id, "expire_days", "test", as.EXPIRE_DAYS, 8);
@@ -132,17 +132,17 @@ add_task(function test_annos_expire_poli
     add_old_anno(pageURI, "persist_lm_months", "test", as.EXPIRE_MONTHS, 181, 179);
     // Add a 181 days old anno.
     add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181);
   }
 
   // Add some visited page and timed annotations for each.
   for (let i = 0; i < 5; i++) {
     let pageURI = uri("http://page_anno." + i + ".mozilla.org/");
-    yield promiseAddVisits({ uri: pageURI, visitDate: now++ });
+    yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
     // Add a 6 days old anno.
     add_old_anno(pageURI, "persist_days", "test", as.EXPIRE_DAYS, 6);
     // Add a 8 days old anno, modified 5 days ago.
     add_old_anno(pageURI, "persist_lm_days", "test", as.EXPIRE_DAYS, 8, 6);
     // Add a 8 days old anno.
     add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8);
 
     // Add a 29 days old anno.
--- a/toolkit/components/places/tests/expiration/test_annos_expire_session.js
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_session.js
@@ -22,17 +22,17 @@ function run_test() {
 add_task(function test_annos_expire_session() {
   // Set interval to a large value so we don't expire on it.
   setInterval(3600); // 1h
 
   // Add some visited page and a couple session annotations for each.
   let now = Date.now() * 1000;
   for (let i = 0; i < 10; i++) {
     let pageURI = uri("http://session_page_anno." + i + ".mozilla.org/");
-    yield promiseAddVisits({ uri: pageURI, visitDate: now++ });
+    yield PlacesTestUtils.addVisits({ uri: pageURI, visitDate: now++ });
     as.setPageAnnotation(pageURI, "test1", "test", 0, as.EXPIRE_SESSION);
     as.setPageAnnotation(pageURI, "test2", "test", 0, as.EXPIRE_SESSION);
   }
 
   // Add some bookmarked page and a couple session annotations for each.
   for (let i = 0; i < 10; i++) {
     let pageURI = uri("http://session_item_anno." + i + ".mozilla.org/");
     let id = bs.insertBookmark(bs.unfiledBookmarksFolder, pageURI,
--- a/toolkit/components/places/tests/expiration/test_clearHistory.js
+++ b/toolkit/components/places/tests/expiration/test_clearHistory.js
@@ -88,17 +88,17 @@ add_task(function test_historyClear() {
 
   // Expire all expirable pages.
   setMaxPages(0);
 
   // Add some bookmarked page with visit and annotations.
   for (let i = 0; i < 5; i++) {
     let pageURI = uri("http://item_anno." + i + ".mozilla.org/");
     // This visit will be expired.
-    yield promiseAddVisits({ uri: pageURI });
+    yield PlacesTestUtils.addVisits({ uri: pageURI });
     let id = bs.insertBookmark(bs.unfiledBookmarksFolder, pageURI,
                                bs.DEFAULT_INDEX, null);
     // Will persist because it's an EXPIRE_NEVER item anno.
     as.setItemAnnotation(id, "persist", "test", 0, as.EXPIRE_NEVER);
     // Will persist because the page is bookmarked.
     as.setPageAnnotation(pageURI, "persist", "test", 0, as.EXPIRE_NEVER);
     // All EXPIRE_SESSION annotations are expected to expire on clear history.
     as.setItemAnnotation(id, "expire_session", "test", 0, as.EXPIRE_SESSION);
@@ -112,17 +112,17 @@ add_task(function test_historyClear() {
     add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181);
   }
 
   // Add some visited page and annotations for each.
   for (let i = 0; i < 5; i++) {
     // All page annotations related to these expired pages are expected to
     // expire as well.
     let pageURI = uri("http://page_anno." + i + ".mozilla.org/");
-    yield promiseAddVisits({ uri: pageURI });
+    yield PlacesTestUtils.addVisits({ uri: pageURI });
     as.setPageAnnotation(pageURI, "expire", "test", 0, as.EXPIRE_NEVER);
     as.setPageAnnotation(pageURI, "expire_session", "test", 0, as.EXPIRE_SESSION);
     add_old_anno(pageURI, "expire_days", "test", as.EXPIRE_DAYS, 8);
     add_old_anno(pageURI, "expire_weeks", "test", as.EXPIRE_WEEKS, 31);
     add_old_anno(pageURI, "expire_months", "test", as.EXPIRE_MONTHS, 181);
   }
 
   // Expire all visits for the bookmarks
--- a/toolkit/components/places/tests/expiration/test_debug_expiration.js
+++ b/toolkit/components/places/tests/expiration/test_debug_expiration.js
@@ -8,20 +8,24 @@
  * only expire orphan entries, unless -1 is passed as limit.
  */
 
 let gNow = getExpirablePRTime();
 
 add_task(function test_expire_orphans()
 {
   // Add visits to 2 pages and force a orphan expiration. Visits should survive.
-  yield promiseAddVisits({ uri: uri("http://page1.mozilla.org/"),
-                           visitDate: gNow++ });
-  yield promiseAddVisits({ uri: uri("http://page2.mozilla.org/"),
-                           visitDate: gNow++ });
+  yield PlacesTestUtils.addVisits({
+    uri: uri("http://page1.mozilla.org/"),
+    visitDate: gNow++
+  });
+  yield PlacesTestUtils.addVisits({
+    uri: uri("http://page2.mozilla.org/"),
+    visitDate: gNow++
+  });
   // Create a orphan place.
   let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                                 NetUtil.newURI("http://page3.mozilla.org/"),
                                                 PlacesUtils.bookmarks.DEFAULT_INDEX, "");
   PlacesUtils.bookmarks.removeItem(id);
 
   // Expire now.
   yield promiseForceExpirationStep(0);
@@ -33,20 +37,24 @@ add_task(function test_expire_orphans()
 
   // Clean up.
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_expire_orphans_optionalarg()
 {
   // Add visits to 2 pages and force a orphan expiration. Visits should survive.
-  yield promiseAddVisits({ uri: uri("http://page1.mozilla.org/"),
-                           visitDate: gNow++ });
-  yield promiseAddVisits({ uri: uri("http://page2.mozilla.org/"),
-                           visitDate: gNow++ });
+  yield PlacesTestUtils.addVisits({
+    uri: uri("http://page1.mozilla.org/"),
+    visitDate: gNow++
+  });
+  yield PlacesTestUtils.addVisits({
+    uri: uri("http://page2.mozilla.org/"),
+    visitDate: gNow++
+  });
   // Create a orphan place.
   let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                                 NetUtil.newURI("http://page3.mozilla.org/"),
                                                 PlacesUtils.bookmarks.DEFAULT_INDEX, "");
   PlacesUtils.bookmarks.removeItem(id);
 
   // Expire now.
   yield promiseForceExpirationStep();
@@ -59,40 +67,48 @@ add_task(function test_expire_orphans_op
   // Clean up.
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_expire_limited()
 {
   // Add visits to 2 pages and force a single expiration.
   // Only 1 page should survive.
-  yield promiseAddVisits({ uri: uri("http://page1.mozilla.org/"),
-                           visitDate: gNow++ });
-  yield promiseAddVisits({ uri: uri("http://page2.mozilla.org/"),
-                           visitDate: gNow++ });
+  yield PlacesTestUtils.addVisits({
+    uri: uri("http://page1.mozilla.org/"),
+    visitDate: gNow++
+  });
+  yield PlacesTestUtils.addVisits({
+    uri: uri("http://page2.mozilla.org/"),
+    visitDate: gNow++
+  });
 
   // Expire now.
   yield promiseForceExpirationStep(1);
 
   // Check that visits to the more recent page survived.
   do_check_false(page_in_database("http://page1.mozilla.org/"));
   do_check_eq(visits_in_database("http://page2.mozilla.org/"), 1);
 
   // Clean up.
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_expire_unlimited()
 {
   // Add visits to 2 pages and force a single expiration.
   // Only 1 page should survive.
-  yield promiseAddVisits({ uri: uri("http://page1.mozilla.org/"),
-                           visitDate: gNow++ });
-  yield promiseAddVisits({ uri: uri("http://page2.mozilla.org/"),
-                           visitDate: gNow++ });
+  yield PlacesTestUtils.addVisits({
+    uri: uri("http://page1.mozilla.org/"),
+    visitDate: gNow++
+  });
+  yield PlacesTestUtils.addVisits({
+    uri: uri("http://page2.mozilla.org/"),
+    visitDate: gNow++
+  });
 
   // Expire now.
   yield promiseForceExpirationStep(-1);
 
   // Check that visits to the more recent page survived.
   do_check_false(page_in_database("http://page1.mozilla.org/"));
   do_check_false(page_in_database("http://page2.mozilla.org/"));
 
--- a/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js
+++ b/toolkit/components/places/tests/expiration/test_notifications_onDeleteURI.js
@@ -58,17 +58,17 @@ add_task(function test_notifications_onD
     let currentTest = tests[testIndex -1];
     print("\nTEST " + testIndex + ": " + currentTest.desc);
     currentTest.receivedNotifications = 0;
 
     // Setup visits.
     let now = getExpirablePRTime();
     for (let i = 0; i < currentTest.addPages; i++) {
       let page = "http://" + testIndex + "." + i + ".mozilla.org/";
-      yield promiseAddVisits({ uri: uri(page), visitDate: now++ });
+      yield PlacesTestUtils.addVisits({ uri: uri(page), visitDate: now++ });
     }
 
     // Setup bookmarks.
     currentTest.bookmarks = [];
     for (let i = 0; i < currentTest.addBookmarks; i++) {
       let page = "http://" + testIndex + "." + i + ".mozilla.org/";
       bs.insertBookmark(bs.unfiledBookmarksFolder, uri(page),
                         bs.DEFAULT_INDEX, null);
--- a/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js
+++ b/toolkit/components/places/tests/expiration/test_notifications_onDeleteVisits.js
@@ -76,17 +76,17 @@ add_task(function test_notifications_onD
     print("\nTEST " + testIndex + ": " + currentTest.desc);
     currentTest.receivedNotifications = 0;
 
     // Setup visits.
     let now = getExpirablePRTime();
     for (let j = 0; j < currentTest.visitsPerPage; j++) {
       for (let i = 0; i < currentTest.addPages; i++) {
         let page = "http://" + testIndex + "." + i + ".mozilla.org/";
-        yield promiseAddVisits({ uri: uri(page), visitDate: now++ });
+        yield PlacesTestUtils.addVisits({ uri: uri(page), visitDate: now++ });
       }
     }
 
     // Setup bookmarks.
     currentTest.bookmarks = [];
     for (let i = 0; i < currentTest.addBookmarks; i++) {
       let page = "http://" + testIndex + "." + i + ".mozilla.org/";
       bs.insertBookmark(bs.unfiledBookmarksFolder, uri(page),
--- a/toolkit/components/places/tests/expiration/test_pref_maxpages.js
+++ b/toolkit/components/places/tests/expiration/test_pref_maxpages.js
@@ -79,17 +79,17 @@ add_task(function test_pref_maxpages() {
     let currentTest = tests[testIndex -1];
     print("\nTEST " + testIndex + ": " + currentTest.desc);
     currentTest.receivedNotifications = 0;
 
     // Setup visits.
     let now = getExpirablePRTime();
     for (let i = 0; i < currentTest.addPages; i++) {
       let page = "http://" + testIndex + "." + i + ".mozilla.org/";
-      yield promiseAddVisits({ uri: uri(page), visitDate: now++ });
+      yield PlacesTestUtils.addVisits({ uri: uri(page), visitDate: now++ });
     }
 
     // Observe history.
     historyObserver = {
       onBeginUpdateBatch: function PEX_onBeginUpdateBatch() {},
       onEndUpdateBatch: function PEX_onEndUpdateBatch() {},
       onClearHistory: function() {},
       onVisit: function() {},
--- a/toolkit/components/places/tests/favicons/test_expireAllFavicons.js
+++ b/toolkit/components/places/tests/favicons/test_expireAllFavicons.js
@@ -27,17 +27,17 @@ add_test(function test_expireAllFavicons
     checkFaviconMissingForPage(TEST_PAGE_URI, function () {
       checkFaviconMissingForPage(BOOKMARKED_PAGE_URI, function () {
         run_next_test();
       });
     });
   }, PlacesUtils.TOPIC_FAVICONS_EXPIRED, false);
 
   // Add a visited page.
-  promiseAddVisits({ uri: TEST_PAGE_URI, transition: TRANSITION_TYPED }).then(
+  PlacesTestUtils.addVisits({ uri: TEST_PAGE_URI, transition: TRANSITION_TYPED }).then(
     function () {
       PlacesUtils.favicons.setAndFetchFaviconForPage(TEST_PAGE_URI,
                                                      SMALLPNG_DATA_URI, true,
                                                      PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE);
 
       // Add a page with a bookmark.
       PlacesUtils.bookmarks.insertBookmark(
                             PlacesUtils.toolbarFolderId, BOOKMARKED_PAGE_URI,
--- a/toolkit/components/places/tests/favicons/test_favicons_conversions.js
+++ b/toolkit/components/places/tests/favicons/test_favicons_conversions.js
@@ -29,17 +29,17 @@ let isWindows = ("@mozilla.org/windows-r
  *        Windows and should not be checked on that platform.
  * @param aCallback
  *        This function is called after the check finished.
  */
 function checkFaviconDataConversion(aFileName, aFileMimeType, aFileLength,
                                     aExpectConversion, aVaryOnWindows,
                                     aCallback) {
   let pageURI = NetUtil.newURI("http://places.test/page/" + aFileName);
-  promiseAddVisits({ uri: pageURI, transition: TRANSITION_TYPED }).then(
+  PlacesTestUtils.addVisits({ uri: pageURI, transition: TRANSITION_TYPED }).then(
     function () {
       let faviconURI = NetUtil.newURI("http://places.test/icon/" + aFileName);
       let fileData = readFileOfLength(aFileName, aFileLength);
 
       PlacesUtils.favicons.replaceFaviconData(faviconURI, fileData, fileData.length,
                                               aFileMimeType);
       PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, faviconURI, true,
         PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
--- a/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js
+++ b/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js
@@ -21,17 +21,17 @@ function run_test()
   do_check_eq(FAVICON_DATA.length, 344);
   run_next_test();
 }
 
 add_test(function test_normal()
 {
   let pageURI = NetUtil.newURI("http://example.com/normal");
 
-  promiseAddVisits(pageURI).then(function () {
+  PlacesTestUtils.addVisits(pageURI).then(function () {
     PlacesUtils.favicons.setAndFetchFaviconForPage(
       pageURI, FAVICON_URI, true,
         PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
         function () {
         PlacesUtils.favicons.getFaviconDataForPage(pageURI,
           function (aURI, aDataLen, aData, aMimeType) {
             do_check_true(aURI.equals(FAVICON_URI));
             do_check_eq(FAVICON_DATA.length, aDataLen);
--- a/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js
+++ b/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js
@@ -12,17 +12,17 @@ function run_test()
 {
   run_next_test();
 }
 
 add_test(function test_normal()
 {
   let pageURI = NetUtil.newURI("http://example.com/normal");
 
-  promiseAddVisits(pageURI).then(function () {
+  PlacesTestUtils.addVisits(pageURI).then(function () {
     PlacesUtils.favicons.setAndFetchFaviconForPage(
       pageURI, SMALLPNG_DATA_URI, true,
         PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
         function () {
         PlacesUtils.favicons.getFaviconURLForPage(pageURI,
           function (aURI, aDataLen, aData, aMimeType) {
             do_check_true(aURI.equals(SMALLPNG_DATA_URI));
 
--- a/toolkit/components/places/tests/favicons/test_replaceFaviconData.js
+++ b/toolkit/components/places/tests/favicons/test_replaceFaviconData.js
@@ -57,17 +57,17 @@ function run_test() {
   do_check_eq(originalFavicon.data.length, 286);
   run_next_test();
 };
 
 add_task(function test_replaceFaviconData_validHistoryURI() {
   do_print("test replaceFaviconData for valid history uri");
 
   let pageURI = uri("http://test1.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let favicon = createFavicon("favicon1.png");
 
   iconsvc.replaceFaviconData(favicon.uri, favicon.data, favicon.data.length,
     favicon.mimetype);
 
   let deferSetAndFetchFavicon = Promise.defer();
   iconsvc.setAndFetchFaviconForPage(pageURI, favicon.uri, true,
@@ -85,17 +85,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconData_overrideDefaultFavicon() {
   do_print("test replaceFaviconData to override a later setAndFetchFaviconForPage");
 
   let pageURI = uri("http://test2.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let firstFavicon = createFavicon("favicon2.png");
   let secondFavicon = createFavicon("favicon3.png");
 
   iconsvc.replaceFaviconData(
     firstFavicon.uri, secondFavicon.data, secondFavicon.data.length,
     secondFavicon.mimetype);
 
@@ -117,17 +117,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconData_replaceExisting() {
   do_print("test replaceFaviconData to override a previous setAndFetchFaviconForPage");
 
   let pageURI = uri("http://test3.bar");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let firstFavicon = createFavicon("favicon4.png");
   let secondFavicon = createFavicon("favicon5.png");
 
   let deferSetAndFetchFavicon = Promise.defer();
   iconsvc.setAndFetchFaviconForPage(
     pageURI, firstFavicon.uri, true,
     PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
@@ -154,17 +154,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconData_unrelatedReplace() {
   do_print("test replaceFaviconData to not make unrelated changes");
 
   let pageURI = uri("http://test4.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let favicon = createFavicon("favicon6.png");
   let unrelatedFavicon = createFavicon("favicon7.png");
 
   iconsvc.replaceFaviconData(
     unrelatedFavicon.uri, unrelatedFavicon.data, unrelatedFavicon.data.length,
     unrelatedFavicon.mimetype);
 
@@ -226,17 +226,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconData_twiceReplace() {
   do_print("test replaceFaviconData on multiple replacements");
 
   let pageURI = uri("http://test5.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let firstFavicon = createFavicon("favicon9.png");
   let secondFavicon = createFavicon("favicon10.png");
 
   iconsvc.replaceFaviconData(
     firstFavicon.uri, firstFavicon.data, firstFavicon.data.length,
     firstFavicon.mimetype);
   iconsvc.replaceFaviconData(
--- a/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js
+++ b/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js
@@ -61,17 +61,17 @@ function run_test() {
   do_check_eq(originalFavicon.data.length, 286);
   run_next_test();
 };
 
 add_task(function test_replaceFaviconDataFromDataURL_validHistoryURI() {
   do_print("test replaceFaviconDataFromDataURL for valid history uri");
 
   let pageURI = uri("http://test1.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let favicon = createFavicon("favicon1.png");
   iconsvc.replaceFaviconDataFromDataURL(favicon.uri, createDataURLForFavicon(favicon));
 
   let deferSetAndFetchFavicon = Promise.defer();
   iconsvc.setAndFetchFaviconForPage(pageURI, favicon.uri, true,
     PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
     function test_replaceFaviconDataFromDataURL_validHistoryURI_check(aURI, aDataLen, aData, aMimeType) {
@@ -87,17 +87,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon() {
   do_print("test replaceFaviconDataFromDataURL to override a later setAndFetchFaviconForPage");
 
   let pageURI = uri("http://test2.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let firstFavicon = createFavicon("favicon2.png");
   let secondFavicon = createFavicon("favicon3.png");
 
   iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon));
 
   let deferSetAndFetchFavicon = Promise.defer();
   iconsvc.setAndFetchFaviconForPage(
@@ -117,17 +117,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconDataFromDataURL_replaceExisting() {
   do_print("test replaceFaviconDataFromDataURL to override a previous setAndFetchFaviconForPage");
 
   let pageURI = uri("http://test3.bar");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let firstFavicon = createFavicon("favicon4.png");
   let secondFavicon = createFavicon("favicon5.png");
 
   let deferSetAndFetchFavicon = Promise.defer();
   iconsvc.setAndFetchFaviconForPage(
     pageURI, firstFavicon.uri, true,
     PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
@@ -150,17 +150,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconDataFromDataURL_unrelatedReplace() {
   do_print("test replaceFaviconDataFromDataURL to not make unrelated changes");
 
   let pageURI = uri("http://test4.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let favicon = createFavicon("favicon6.png");
   let unrelatedFavicon = createFavicon("favicon7.png");
 
   iconsvc.replaceFaviconDataFromDataURL(unrelatedFavicon.uri, createDataURLForFavicon(unrelatedFavicon));
 
   let deferSetAndFetchFavicon = Promise.defer();
   iconsvc.setAndFetchFaviconForPage(
@@ -208,17 +208,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconDataFromDataURL_twiceReplace() {
   do_print("test replaceFaviconDataFromDataURL on multiple replacements");
 
   let pageURI = uri("http://test5.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let firstFavicon = createFavicon("favicon9.png");
   let secondFavicon = createFavicon("favicon10.png");
 
   iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(firstFavicon));
   iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon));
 
   let deferSetAndFetchFavicon = Promise.defer();
@@ -239,17 +239,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconDataFromDataURL_afterRegularAssign() {
   do_print("test replaceFaviconDataFromDataURL after replaceFaviconData");
 
   let pageURI = uri("http://test6.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let firstFavicon = createFavicon("favicon11.png");
   let secondFavicon = createFavicon("favicon12.png");
 
   iconsvc.replaceFaviconData(
     firstFavicon.uri, firstFavicon.data, firstFavicon.data.length,
     firstFavicon.mimetype);
   iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(secondFavicon));
@@ -272,17 +272,17 @@ add_task(function test_replaceFaviconDat
 
   yield PlacesTestUtils.clearHistory();
 });
 
 add_task(function test_replaceFaviconDataFromDataURL_beforeRegularAssign() {
   do_print("test replaceFaviconDataFromDataURL before replaceFaviconData");
 
   let pageURI = uri("http://test7.bar/");
-  yield promiseAddVisits(pageURI);
+  yield PlacesTestUtils.addVisits(pageURI);
 
   let firstFavicon = createFavicon("favicon13.png");
   let secondFavicon = createFavicon("favicon14.png");
 
   iconsvc.replaceFaviconDataFromDataURL(firstFavicon.uri, createDataURLForFavicon(firstFavicon));
   iconsvc.replaceFaviconData(
     firstFavicon.uri, secondFavicon.data, secondFavicon.data.length,
     secondFavicon.mimetype);
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -804,79 +804,16 @@ NavHistoryResultObserver.prototype = {
   nodeURIChanged: function () {},
   sortingChanged: function () {},
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsINavHistoryResultObserver,
   ])
 };
 
 /**
- * Asynchronously adds visits to a page.
- *
- * @param aPlaceInfo
- *        Can be an nsIURI, in such a case a single LINK visit will be added.
- *        Otherwise can be an object describing the visit to add, or an array
- *        of these objects:
- *          { uri: nsIURI of the page,
- *            transition: one of the TRANSITION_* from nsINavHistoryService,
- *            [optional] title: title of the page,
- *            [optional] visitDate: visit date in microseconds from the epoch
- *            [optional] referrer: nsIURI of the referrer for this visit
- *          }
- *
- * @return {Promise}
- * @resolves When all visits have been added successfully.
- * @rejects JavaScript exception.
- */
-function promiseAddVisits(aPlaceInfo)
-{
-  let deferred = Promise.defer();
-  let places = [];
-  if (aPlaceInfo instanceof Ci.nsIURI) {
-    places.push({ uri: aPlaceInfo });
-  }
-  else if (Array.isArray(aPlaceInfo)) {
-    places = places.concat(aPlaceInfo);
-  } else {
-    places.push(aPlaceInfo)
-  }
-
-  // Create mozIVisitInfo for each entry.
-  let now = Date.now();
-  for (let i = 0; i < places.length; i++) {
-    if (!places[i].title) {
-      places[i].title = "test visit for " + places[i].uri.spec;
-    }
-    places[i].visits = [{
-      transitionType: places[i].transition === undefined ? TRANSITION_LINK
-                                                         : places[i].transition,
-      visitDate: places[i].visitDate || (now++) * 1000,
-      referrerURI: places[i].referrer
-    }];
-  }
-
-  PlacesUtils.asyncHistory.updatePlaces(
-    places,
-    {
-      handleError: function AAV_handleError(aResultCode, aPlaceInfo) {
-        let ex = new Components.Exception("Unexpected error in adding visits.",
-                                          aResultCode);
-        deferred.reject(ex);
-      },
-      handleResult: function () {},
-      handleCompletion: function UP_handleCompletion() {
-        deferred.resolve();
-      }
-    }
-  );
-
-  return deferred.promise;
-}
-
-/**
  * Asynchronously check a url is visited.
  *
  * @param aURI The URI.
  * @return {Promise}
  * @resolves When the check has been added successfully.
  * @rejects JavaScript exception.
  */
 function promiseIsURIVisited(aURI) {
--- a/toolkit/components/places/tests/history/test_remove.js
+++ b/toolkit/components/places/tests/history/test_remove.js
@@ -9,27 +9,27 @@
 "use strict";
 
 Cu.importGlobalProperties(["URL"]);
 
 
 // Test removing a single page
 add_task(function* test_remove_single() {
   let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
-  yield promiseAddVisits(WITNESS_URI);
+  yield PlacesTestUtils.addVisits(WITNESS_URI);
   Assert.ok(page_in_database(WITNESS_URI));
 
   let remover = Task.async(function*(name, filter, options) {
     do_print(name);
     do_print(JSON.stringify(options));
     do_print("Setting up visit");
 
     let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());
     let title = "Visit " + Math.random();
-    yield promiseAddVisits({uri: uri, title: title});
+    yield PlacesTestUtils.addVisits({uri: uri, title: title});
     Assert.ok(visits_in_database(uri), "History entry created");
 
     let removeArg = yield filter(uri);
 
     if (options.addBookmark) {
       PlacesUtils.bookmarks.insertBookmark(
         PlacesUtils.unfiledBookmarksFolderId,
         uri,
@@ -136,17 +136,17 @@ add_task(function* test_remove_single() 
 });
 
 // Test removing a list of pages
 add_task(function* test_remove_many() {
   const SIZE = 10;
 
   do_print("Adding a witness page");
   let WITNESS_URI = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove/" + Math.random());;
-  yield promiseAddVisits(WITNESS_URI);
+  yield PlacesTestUtils.addVisits(WITNESS_URI);
   Assert.ok(page_in_database(WITNESS_URI), "Witness page added");
 
   do_print("Generating samples");
   let pages = [];
   for (let i = 0; i < SIZE; ++i) {
     let uri = NetUtil.newURI("http://mozilla.com/test_browserhistory/test_remove?sample=" + i + "&salt=" + Math.random());
     let title = "Visit " + i + ", " + Math.random();
     let hasBookmark = i % 3 == 0;
@@ -162,17 +162,17 @@ add_task(function* test_remove_many() {
       // `true` once `onFrecencyChangedCalled` has been called for this page
       onFrecencyChangedCalled: false,
       // `true` once `onDeleteURI` has been called for this page
       onDeleteURICalled: false,
     };
     do_print("Pushing: " + uri.spec);
     pages.push(page);
 
-    yield promiseAddVisits(page);
+    yield PlacesTestUtils.addVisits(page);
     page.guid = do_get_guid_for_uri(uri);
     if (hasBookmark) {
       PlacesUtils.bookmarks.insertBookmark(
         PlacesUtils.unfiledBookmarksFolderId,
         uri,
         PlacesUtils.bookmarks.DEFAULT_INDEX,
         "test bookmark " + i);
     }
--- a/toolkit/components/places/tests/inline/test_autocomplete_functional.js
+++ b/toolkit/components/places/tests/inline/test_autocomplete_functional.js
@@ -6,124 +6,128 @@
 
 add_autocomplete_test([
   "Check disabling autocomplete disables autofill",
   "vis",
   "vis",
   function ()
   {
     Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false);
-    promiseAddVisits({ uri: NetUtil.newURI("http://visit.mozilla.org"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://visit.mozilla.org"),
+      transition: TRANSITION_TYPED
+    });
   }
 ]);
 
 add_autocomplete_test([
   "Check disabling autofill disables autofill",
   "vis",
   "vis",
   function ()
   {
     Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
-    promiseAddVisits({ uri: NetUtil.newURI("http://visit.mozilla.org"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://visit.mozilla.org"),
+      transition: TRANSITION_TYPED
+    });
   }
 ]);
 
 add_autocomplete_test([
   "Add urls, check for correct order",
   "vis",
   "visit2.mozilla.org/",
   function ()
   {
     let places = [{ uri: NetUtil.newURI("http://visit1.mozilla.org") },
                   { uri: NetUtil.newURI("http://visit2.mozilla.org"),
                     transition: TRANSITION_TYPED }];
-    promiseAddVisits(places);
+    PlacesTestUtils.addVisits(places);
   }
 ]);
 
 add_autocomplete_test([
   "Add urls, make sure www and http are ignored",
   "visit1",
   "visit1.mozilla.org/",
   function ()
   {
-    promiseAddVisits(NetUtil.newURI("http://www.visit1.mozilla.org"));
+    PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit1.mozilla.org"));
   }
 ]);
 
 add_autocomplete_test([
   "Autocompleting after an existing host completes to the url",
   "visit3.mozilla.org/",
   "visit3.mozilla.org/",
   function ()
   {
-    promiseAddVisits(NetUtil.newURI("http://www.visit3.mozilla.org"));
+    PlacesTestUtils.addVisits(NetUtil.newURI("http://www.visit3.mozilla.org"));
   }
 ]);
 
 add_autocomplete_test([
   "Searching for www.me should yield www.me.mozilla.org/",
   "www.me",
   "www.me.mozilla.org/",
   function ()
   {
-    promiseAddVisits(NetUtil.newURI("http://www.me.mozilla.org"));
+    PlacesTestUtils.addVisits(NetUtil.newURI("http://www.me.mozilla.org"));
   }
 ]);
 
 add_autocomplete_test([
   "With a bookmark and history, the query result should be the bookmark",
   "bookmark",
   "bookmark1.mozilla.org/",
   function ()
   {
     addBookmark({ url: "http://bookmark1.mozilla.org/", });
-    promiseAddVisits(NetUtil.newURI("http://bookmark1.mozilla.org/foo"));
+    PlacesTestUtils.addVisits(NetUtil.newURI("http://bookmark1.mozilla.org/foo"));
   }
 ]);
 
 add_autocomplete_test([
   "Check to make sure we get the proper results with full paths",
   "smokey",
   "smokey.mozilla.org/",
   function ()
   {
 
     let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") },
                   { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }];
-    promiseAddVisits(places);
+    PlacesTestUtils.addVisits(places);
   }
 ]);
 
 add_autocomplete_test([
   "Check to make sure we autocomplete to the following '/'",
   "smokey.mozilla.org/fo",
   "smokey.mozilla.org/foo/",
   function ()
   {
 
     let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") },
                   { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }];
-    promiseAddVisits(places);
+    PlacesTestUtils.addVisits(places);
   }
 ]);
 
 add_autocomplete_test([
   "Check to make sure we autocomplete after ?",
   "smokey.mozilla.org/foo?",
   "smokey.mozilla.org/foo?bacon=delicious",
   function ()
   {
-    promiseAddVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious"));
+    PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious"));
   }
 ]);
 
 add_autocomplete_test([
   "Check to make sure we autocomplete after #",
   "smokey.mozilla.org/foo?bacon=delicious#bar",
   "smokey.mozilla.org/foo?bacon=delicious#bar",
   function ()
   {
-    promiseAddVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar"));
+    PlacesTestUtils.addVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar"));
   }
 ]);
--- a/toolkit/components/places/tests/inline/test_casing.js
+++ b/toolkit/components/places/tests/inline/test_casing.js
@@ -2,101 +2,101 @@
  * 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/. */
 
 add_autocomplete_test([
   "Searching for cased entry 1",
   "MOZ",
   { autoFilled: "MOZilla.org/", completed: "mozilla.org/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/test/") });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for cased entry 2",
   "mozilla.org/T",
   { autoFilled: "mozilla.org/T", completed: "mozilla.org/T" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/test/") });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for cased entry 3",
   "mozilla.org/T",
   { autoFilled: "mozilla.org/Test/", completed: "http://mozilla.org/Test/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   }
 ]);
 
 add_autocomplete_test([
   "Searching for cased entry 4",
   "mOzilla.org/t",
   { autoFilled: "mOzilla.org/t", completed: "mOzilla.org/t" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for cased entry 5",
   "mOzilla.org/T",
   { autoFilled: "mOzilla.org/Test/", completed: "http://mozilla.org/Test/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry",
   "http://mOz",
   { autoFilled: "http://mOzilla.org/", completed: "http://mozilla.org/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with www",
   "http://www.mOz",
   { autoFilled: "http://www.mOzilla.org/", completed: "http://www.mozilla.org/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with path",
   "http://mOzilla.org/t",
   { autoFilled: "http://mOzilla.org/t", completed: "http://mOzilla.org/t" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with path 2",
   "http://mOzilla.org/T",
   { autoFilled: "http://mOzilla.org/Test/", completed: "http://mozilla.org/Test/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with www and path",
   "http://www.mOzilla.org/t",
   { autoFilled: "http://www.mOzilla.org/t", completed: "http://www.mOzilla.org/t" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed cased entry with www and path 2",
   "http://www.mOzilla.org/T",
   { autoFilled: "http://www.mOzilla.org/Test/", completed: "http://www.mozilla.org/Test/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
+    PlacesTestUtils.addVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/") });
   },
 ]);
--- a/toolkit/components/places/tests/inline/test_do_not_trim.js
+++ b/toolkit/components/places/tests/inline/test_do_not_trim.js
@@ -5,63 +5,75 @@
 // Inline should never return matches shorter than the search string, since
 // that largely confuses completeDefaultIndex
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 1",
   "mozilla.org ",
   "mozilla.org ",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://mozilla.org/link/"),
+      transition: TRANSITION_TYPED
+    });
   }
 ]);
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 2",
   "mozilla.org/ ",
   "mozilla.org/ ",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://mozilla.org/link/"),
+      transition: TRANSITION_TYPED
+    });
   }
 ]);
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 3",
   "mozilla.org/link ",
   "mozilla.org/link ",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://mozilla.org/link/"),
+      transition: TRANSITION_TYPED
+    });
   }
 ]);
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 4",
   "mozilla.org/link/ ",
   "mozilla.org/link/ ",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://mozilla.org/link/"),
+      transition: TRANSITION_TYPED
+    });
   }
 ]);
 
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 5",
   "moz illa ",
   "moz illa ",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://mozilla.org/link/"),
+      transition: TRANSITION_TYPED
+    });
   }
 ]);
 
 add_autocomplete_test([
   "Do not autofill whitespaced entry 6",
   " mozilla",
   " mozilla",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://mozilla.org/link/"),
+      transition: TRANSITION_TYPED
+    });
   }
 ]);
--- a/toolkit/components/places/tests/inline/test_queryurl.js
+++ b/toolkit/components/places/tests/inline/test_queryurl.js
@@ -2,51 +2,59 @@
  * 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/. */
 
 add_autocomplete_test([
   "Searching for host match without slash should match host",
   "file",
   "file.org/",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://file.org/test/"),
-                       transition: TRANSITION_TYPED },
-                     { uri: NetUtil.newURI("file:///c:/test.html"),
-                       transition: TRANSITION_TYPED }
-                    );
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://file.org/test/"),
+      transition: TRANSITION_TYPED
+    }, {
+      uri: NetUtil.newURI("file:///c:/test.html"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
 
 add_autocomplete_test([
   "Searching match with slash at the end should do nothing",
   "file.org/",
   "file.org/",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://file.org/test/"),
-                       transition: TRANSITION_TYPED },
-                     { uri: NetUtil.newURI("file:///c:/test.html"),
-                       transition: TRANSITION_TYPED }
-                    );
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://file.org/test/"),
+      transition: TRANSITION_TYPED
+    }, {
+      uri: NetUtil.newURI("file:///c:/test.html"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
 
 add_autocomplete_test([
   "Searching match with slash in the middle should match url",
   "file.org/t",
   "file.org/test/",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://file.org/test/"),
-                       transition: TRANSITION_TYPED },
-                     { uri: NetUtil.newURI("file:///c:/test.html"),
-                       transition: TRANSITION_TYPED }
-                    );
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://file.org/test/"),
+      transition: TRANSITION_TYPED
+    }, {
+      uri: NetUtil.newURI("file:///c:/test.html"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for non-host match without slash should not match url",
   "file",
   "file",
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("file:///c:/test.html"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("file:///c:/test.html"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
--- a/toolkit/components/places/tests/inline/test_trimming.js
+++ b/toolkit/components/places/tests/inline/test_trimming.js
@@ -2,248 +2,312 @@
  * 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/. */
 
 add_autocomplete_test([
   "Searching for untrimmed https://www entry",
   "mo",
   { autoFilled: "mozilla.org/", completed: "https://www.mozilla.org/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("https://www.mozilla.org/test/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("https://www.mozilla.org/test/"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed https://www entry with path",
   "mozilla.org/t",
   { autoFilled: "mozilla.org/test/", completed: "https://www.mozilla.org/test/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("https://www.mozilla.org/test/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("https://www.mozilla.org/test/"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed https:// entry",
   "mo",
   { autoFilled: "mozilla.org/", completed: "https://mozilla.org/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("https://mozilla.org/test/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("https://mozilla.org/test/"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed https:// entry with path",
   "mozilla.org/t",
   { autoFilled: "mozilla.org/test/", completed: "https://mozilla.org/test/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("https://mozilla.org/test/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("https://mozilla.org/test/"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed http://www entry",
   "mo",
   { autoFilled: "mozilla.org/", completed: "www.mozilla.org/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/test/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://www.mozilla.org/test/"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed http://www entry with path",
   "mozilla.org/t",
   { autoFilled: "mozilla.org/test/", completed: "http://www.mozilla.org/test/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/test/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("http://www.mozilla.org/test/"),
+      transition: TRANSITION_TYPED
+    });
   },
 ]);
 
 add_autocomplete_test([
   "Searching for untrimmed ftp:// entry",
   "mo",
   { autoFilled: "mozilla.org/", completed: "ftp://mozilla.org/" },
   function () {
-    promiseAddVisits({ uri: NetUtil.newURI("ftp://mozilla.org/test/"),
-                       transition: TRANSITION_TYPED });
+    PlacesTestUtils.addVisits({
+      uri: NetUtil.newURI("ftp://mozilla.org/test/"),
+      transition: TRANSITION_TYPED