merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 07 Jan 2015 14:11:47 +0100
changeset 248241 206205dd8bd137e25298c261030184bcd058148a
parent 248207 395cc3fd41ae81bbe897c25fa32bcd5864032edc (current diff)
parent 248240 5ab0b9577eae72356c629e9d2178b740fd302306 (diff)
child 248267 d5707dca171371b2c695b7e8e9e44b7f504f1254
child 248293 3a1103c584cba23d7c09d42f00cdb16f79fd2e73
child 248318 e4a35e6a726ed3f5622bcab58e62f0e853682a97
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -269,21 +269,32 @@ let gFxAccounts = {
     if (gBrowser.currentURI.spec.split("?")[0] == "about:accounts") {
       // If the current tab is about:accounts, assume the user just completed a
       // migration step and don't bother them with a redundant notification.
       return;
     }
     let note = null;
     switch (this._migrationInfo.state) {
       case this.fxaMigrator.STATE_USER_FXA: {
-        let msg = this.strings.GetStringFromName("needUserLong");
-        let upgradeLabel =
-          this.strings.GetStringFromName("upgradeToFxA.label");
-        let upgradeAccessKey =
-          this.strings.GetStringFromName("upgradeToFxA.accessKey");
+        // There are 2 cases here - no email address means it is an offer on
+        // the first device (so the user is prompted to create an account).
+        // If there is an email address it is the "join the party" flow, so the
+        // user is prompted to sign in with the address they previously used.
+        let msg, upgradeLabel, upgradeAccessKey;
+        if (this._migrationInfo.email) {
+          msg = this.strings.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
+                                                  [this._migrationInfo.email],
+                                                  1);
+          upgradeLabel = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.label");
+          upgradeAccessKey = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.accessKey");
+        } else {
+          msg = this.strings.GetStringFromName("needUserLong");
+          upgradeLabel = this.strings.GetStringFromName("upgradeToFxA.label");
+          upgradeAccessKey = this.strings.GetStringFromName("upgradeToFxA.accessKey");
+        }
         note = new Weave.Notification(
           undefined, msg, undefined, Weave.Notifications.PRIORITY_WARNING, [
             new Weave.NotificationButton(upgradeLabel, upgradeAccessKey, () => {
               this.fxaMigrator.createFxAccount(window);
             }),
           ]
         );
         break;
@@ -316,21 +327,19 @@ let gFxAccounts = {
     switch (button.getAttribute("fxastatus")) {
     case "signedin":
       this.openPreferences();
       break;
     case "error":
       this.openSignInAgainPage("menupanel");
       break;
     case "migrate-signup":
-      this.fxaMigrator.createFxAccount(window);
-      break;
     case "migrate-verify":
-      // Instead of using the migrator module directly here the UX calls for
-      // us to open prefs which has a "resend" button.
+      // The migration flow calls for the menu item to open sync prefs rather
+      // than requesting migration start immediately.
       this.openPreferences();
       break;
     default:
       this.openAccountsPage(null, { entryPoint: "menupanel" });
       break;
     }
 
     PanelUI.hide();
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2077,17 +2077,17 @@ function getShortcutOrURIAndPostData(aUR
   let offset = aURL.indexOf(" ");
   if (offset > 0) {
     keyword = aURL.substr(0, offset);
     param = aURL.substr(offset + 1);
   }
 
   let engine = Services.search.getEngineByAlias(keyword);
   if (engine) {
-    let submission = engine.getSubmission(param);
+    let submission = engine.getSubmission(param, null, "keyword");
     postData = submission.postData;
     aCallback({ postData: submission.postData, url: submission.uri.spec,
                 mayInheritPrincipal: mayInheritPrincipal });
     return;
   }
 
   [shortcutURL, postData] =
     PlacesUtils.getURLAndPostDataForKeyword(keyword);
@@ -3563,16 +3563,21 @@ const BrowserSearch = {
       return;
     }
 
     let countId = this._getSearchEngineId(engine) + "." + source;
 
     let count = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
     count.add(countId);
   },
+
+  recordOneoffSearchInTelemetry: function (engine, source, type, where) {
+    let id = this._getSearchEngineId(engine) + "." + source;
+    BrowserUITelemetry.countOneoffSearchEvent(id, type, where);
+  }
 };
 
 const SearchHighlight = {
   eventsReady: false,
   // The pref that controls how many times to show the highlight.
   countPref: "browser.search.highlightCount",
   // The current highlight to show.
   currentPos: 0,
--- a/browser/base/content/test/newtab/browser.ini
+++ b/browser/base/content/test/newtab/browser.ini
@@ -40,9 +40,8 @@ support-files =
   searchEngine2xLogo.xml
   searchEngine1x2xLogo.xml
   ../general/searchSuggestionEngine.xml
   ../general/searchSuggestionEngine.sjs
 [browser_newtab_sponsored_icon_click.js]
 [browser_newtab_undo.js]
 [browser_newtab_unpin.js]
 [browser_newtab_update.js]
-skip-if = true # Bug 1008029
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -918,24 +918,29 @@
         <body><![CDATA[
           // Ignore all right-clicks
           if (aEvent.button == 2)
             return;
 
           var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
 
           // Check for unmodified left-click, and use default behavior
+          var searchBar = BrowserSearch.searchBar;
+          searchBar.telemetrySearchDetails = {
+            index: controller.selection.currentIndex,
+            kind: "mouse"
+          };
+
           if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
               !aEvent.altKey && !aEvent.metaKey) {
             controller.handleEnter(true);
             return;
           }
 
           // Check for middle-click or modified clicks on the search bar
-          var searchBar = BrowserSearch.searchBar;
           if (searchBar && searchBar.textbox == this.mInput) {
             // Handle search bar popup clicks
             var search = controller.getValueAt(this.selectedIndex);
 
             // close the autocomplete popup and revert the entered search term
             this.closePopup();
             controller.handleEscape();
 
@@ -991,17 +996,17 @@
         <xul:label anonid="searchbar-oneoffheader-after" flex="10000" value="&searchWith.label;"/>
       </xul:hbox>
       <xul:description anonid="search-panel-one-offs"
                        class="search-panel-one-offs"
                        xbl:inherits="hidden=showonlysettings"/>
       <xul:vbox anonid="add-engines"/>
       <xul:button anonid="search-settings"
                   xbl:inherits="showonlysettings"
-                  oncommand="openPreferences('paneSearch')"
+                  oncommand="BrowserUITelemetry.countSearchSettingsEvent('searchbar');openPreferences('paneSearch')"
                   class="search-setting-button search-panel-header"
                   label="&changeSearchSettings.button;"/>
     </content>
     <implementation>
       <method name="updateHeader">
         <body><![CDATA[
           let currentEngine = Services.search.currentEngine;
           let uri = currentEngine.iconURI;
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -189,22 +189,32 @@ loop.OTSdkDriver = (function() {
     /**
      * Handles the session event for the connection for this client being
      * destroyed.
      *
      * @param {SessionDisconnectEvent} event The event details:
      * https://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
      */
     _onSessionDisconnected: function(event) {
-      // We only need to worry about the network disconnected reason here.
-      if (event.reason === "networkDisconnected") {
-        this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
-          reason: FAILURE_REASONS.NETWORK_DISCONNECTED
-        }));
+      var reason;
+      switch (event.reason) {
+        case "networkDisconnected":
+          reason = FAILURE_REASONS.NETWORK_DISCONNECTED;
+          break;
+        case "forceDisconnected":
+          reason = FAILURE_REASONS.EXPIRED_OR_INVALID;
+          break;
+        default:
+          // Other cases don't need to be handled.
+          return;
       }
+
+      this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
+        reason: reason
+      }));
     },
 
     /**
      * Handles the connection event for a newly connecting peer.
      *
      * @param {ConnectionEvent} event The event details
      * https://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
      */
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -262,16 +262,29 @@ describe("loop.OTSdkDriver", function ()
           });
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWithMatch(dispatcher.dispatch,
             sinon.match.hasOwn("name", "connectionFailure"));
           sinon.assert.calledWithMatch(dispatcher.dispatch,
             sinon.match.hasOwn("reason", FAILURE_REASONS.NETWORK_DISCONNECTED));
         });
+
+      it("should dispatch a connectionFailure action if the session was " +
+         "forcibly disconnected", function() {
+          session.trigger("sessionDisconnected", {
+            reason: "forceDisconnected"
+          });
+
+          sinon.assert.calledOnce(dispatcher.dispatch);
+          sinon.assert.calledWithMatch(dispatcher.dispatch,
+            sinon.match.hasOwn("name", "connectionFailure"));
+          sinon.assert.calledWithMatch(dispatcher.dispatch,
+            sinon.match.hasOwn("reason", FAILURE_REASONS.EXPIRED_OR_INVALID));
+        });
     });
 
     describe("streamCreated", function() {
       var fakeStream;
 
       beforeEach(function() {
         fakeStream = {
           fakeStream: 3
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -109,22 +109,16 @@ let gSyncPane = {
 
     window.addEventListener("unload", function() {
       topics.forEach(function (topic) {
         Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this);
       }, gSyncPane);
       Weave.Svc.Obs.remove(migrateTopic, gSyncPane.updateMigrationState, gSyncPane);
     }, false);
 
-    // ask the migration module to broadcast its current state (and nothing will
-    // happen if it's not loaded - which is good, as that means no migration
-    // is pending/necessary) - we don't want to suck that module in just to
-    // find there's nothing to do.
-    Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
-
     XPCOMUtils.defineLazyGetter(this, '_stringBundle', () => {
       return Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
     }),
 
     XPCOMUtils.defineLazyGetter(this, '_accountsStringBundle', () => {
       return Services.strings.createBundle("chrome://browser/locale/accounts.properties");
     }),
 
@@ -213,26 +207,35 @@ let gSyncPane = {
       gSyncUtils.changeName(this);
     });
     setEventListener("tosPP-small-ToS", "click", gSyncPane.openToS);
     setEventListener("tosPP-small-PP", "click", gSyncPane.openPrivacyPolicy);
     setEventListener("sync-migrate-upgrade", "click", function () {
       let win = Services.wm.getMostRecentWindow("navigator:browser");
       fxaMigrator.createFxAccount(win);
     });
+    setEventListener("sync-migrate-unlink", "click", function () {
+      gSyncPane.startOverMigration();
+    });
     setEventListener("sync-migrate-forget", "click", function () {
       fxaMigrator.forgetFxAccount();
     });
     setEventListener("sync-migrate-resend", "click", function () {
       let win = Services.wm.getMostRecentWindow("navigator:browser");
       fxaMigrator.resendVerificationMail(win);
     });
   },
 
   updateWeavePrefs: function () {
+    // ask the migration module to broadcast its current state (and nothing will
+    // happen if it's not loaded - which is good, as that means no migration
+    // is pending/necessary) - we don't want to suck that module in just to
+    // find there's nothing to do.
+    Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
+
     let service = Components.classes["@mozilla.org/weave/service;1"]
                   .getService(Components.interfaces.nsISupports)
                   .wrappedJSObject;
     // service.fxAccountsEnabled is false iff sync is already configured for
     // the legacy provider.
     if (service.fxAccountsEnabled) {
       // determine the fxa status...
       this.page = PAGE_PLEASE_WAIT;
@@ -294,19 +297,45 @@ let gSyncPane = {
     }
   },
 
   updateMigrationState: function(subject, state) {
     let selIndex;
     switch (state) {
       case fxaMigrator.STATE_USER_FXA: {
         let sb = this._accountsStringBundle;
+        // There are 2 cases here - no email address means it is an offer on
+        // the first device (so the user is prompted to create an account).
+        // If there is an email address it is the "join the party" flow, so the
+        // user is prompted to sign in with the address they previously used.
+        let email = subject ? subject.QueryInterface(Components.interfaces.nsISupportsString).data : null;
+        let elt = document.getElementById("sync-migrate-upgrade-description");
+        elt.textContent = email ?
+                          sb.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
+                                                  [email], 1) :
+                          sb.GetStringFromName("needUserLong");
+
+        // The "upgrade" button.
         let button = document.getElementById("sync-migrate-upgrade");
-        button.setAttribute("label", sb.GetStringFromName("upgradeToFxA.label"));
-        button.setAttribute("accesskey", sb.GetStringFromName("upgradeToFxA.accessKey"));
+        button.setAttribute("label",
+                            sb.GetStringFromName(email
+                                                 ? "signInAfterUpgradeOnOtherDevice.label"
+                                                 : "upgradeToFxA.label"));
+        button.setAttribute("accesskey",
+                            sb.GetStringFromName(email
+                                                 ? "signInAfterUpgradeOnOtherDevice.accessKey"
+                                                 : "upgradeToFxA.accessKey"));
+        // The "unlink" button - this is only shown for first migration
+        button = document.getElementById("sync-migrate-unlink");
+        if (email) {
+          button.hidden = true;
+        } else {
+          button.setAttribute("label", sb.GetStringFromName("unlinkMigration.label"));
+          button.setAttribute("accesskey", sb.GetStringFromName("unlinkMigration.accessKey"));
+        }
         selIndex = 0;
         break;
       }
       case fxaMigrator.STATE_USER_FXA_VERIFIED: {
         let sb = this._accountsStringBundle;
         let email = subject.QueryInterface(Components.interfaces.nsISupportsString).data;
         let label = sb.formatStringFromName("needVerifiedUserLong", [email], 1);
         let elt = document.getElementById("sync-migrate-verify-label");
@@ -350,16 +379,39 @@ let gSyncPane = {
       if (buttonChoice == 1)
         return;
     }
 
     Weave.Service.startOver();
     this.updateWeavePrefs();
   },
 
+  // When the "Unlink" button in the migration header is selected we display
+  // a slightly different message.
+  startOverMigration: function () {
+    let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+                Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL +
+                Services.prompt.BUTTON_POS_1_DEFAULT;
+    let sb = this._accountsStringBundle;
+    let buttonChoice =
+      Services.prompt.confirmEx(window,
+                                sb.GetStringFromName("unlinkVerificationTitle"),
+                                sb.GetStringFromName("unlinkVerificationDescription"),
+                                flags,
+                                sb.GetStringFromName("unlinkVerificationConfirm"),
+                                null, null, null, {});
+
+    // If the user selects cancel, just bail
+    if (buttonChoice == 1)
+      return;
+
+    Weave.Service.startOver();
+    this.updateWeavePrefs();
+  },
+
   updatePass: function () {
     if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED)
       gSyncUtils.changePassword();
     else
       gSyncUtils.updatePassphrase();
   },
 
   resetPass: function () {
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -41,18 +41,19 @@
        data-category="paneSync"
        hidden="true">
 
   <vbox id="sync-migration" flex="1" hidden="true">
 
     <deck id="sync-migration-deck">
       <!-- When we are in the "need FxA user" state -->
       <hbox align="center">
-        <label>&migrate.upgradeNeeded;</label>
+        <description id="sync-migrate-upgrade-description" flex="1"/>
         <spacer flex="1"/>
+        <button id="sync-migrate-unlink"/>
         <button id="sync-migrate-upgrade"/>
       </hbox>
 
       <!-- When we are in the "need the user to be verified" state -->
       <hbox align="center">
         <label id="sync-migrate-verify-label"/>
         <spacer flex="1"/>
         <button id="sync-migrate-forget"/>
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -496,43 +496,72 @@
 #ifdef XP_MACOSX
                                                    aEvent.metaKey))
 #else
                                                    aEvent.ctrlKey))
 #endif
               where = "tab-background";
           }
 
+          let selection = this.telemetrySearchDetails;
           this.doSearch(textValue, where, aEngine);
+
+          if (!selection || (selection.index == -1)) {
+            let target = aEvent.originalTarget;
+            let source = "unknown";
+            let type = "unknown";
+            if (aEvent instanceof KeyboardEvent) {
+              type = "key";
+              if (this._textbox.getSelectedOneOff()) {
+                source = "oneoff";
+              }
+            } else if (aEvent instanceof MouseEvent) {
+              type = "mouse";
+              if (target.classList.contains("searchbar-engine-one-off-item")) {
+                source = "oneoff";
+              } else if (target.classList.contains("search-panel-header") ||
+                         target.parentNode.classList.contains("search-panel-header")) {
+                source = "header";
+              }
+            }
+
+            BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type, where);
+          }
+
           if (where == "tab-background")
             this.focus();
         ]]></body>
       </method>
 
       <method name="doSearch">
         <parameter name="aData"/>
         <parameter name="aWhere"/>
         <parameter name="aEngine"/>
         <body><![CDATA[
           var textBox = this._textbox;
-        
+
           // Save the current value in the form history
           if (aData && !PrivateBrowsingUtils.isWindowPrivate(window)) {
             this.FormHistory.update(
               { op : "bump",
                 fieldname : textBox.getAttribute("autocompletesearchparam"),
                 value : aData },
               { handleError : function(aError) {
                   Components.utils.reportError("Saving search to form history failed: " + aError.message);
               }});
           }
 
           let engine = aEngine || this.currentEngine;
           var submission = engine.getSubmission(aData, null, "searchbar");
-          BrowserSearch.recordSearchInHealthReport(engine, "searchbar");
+          let telemetrySearchDetails = this.telemetrySearchDetails;
+          this.telemetrySearchDetails = null;
+          if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
+            telemetrySearchDetails = null;
+          }
+          BrowserSearch.recordSearchInHealthReport(engine, "searchbar", telemetrySearchDetails);
           // null parameter below specifies HTML response for search
           let params = {
             postData: submission.postData,
             inBackground: aWhere == "tab-background"
           };
           openUILinkIn(submission.uri.spec,
                        aWhere == "tab-background" ? "tab" : aWhere,
                        params);
@@ -937,16 +966,21 @@
         <parameter name="aEvent"/>
         <body><![CDATA[
           var evt = aEvent || this.mEnterEvent;
 
           let engine;
           let oneOff = this.getSelectedOneOff();
           if (oneOff)
             engine = oneOff.engine;
+          if (this.mEnterEvent && this._selectionDetails &&
+              this._selectionDetails.currentIndex != -1) {
+            BrowserSearch.searchBar.telemetrySearchDetails = this._selectionDetails;
+            this._selectionDetails = null;
+          }
           document.getBindingParent(this).handleSearchCommand(evt, engine);
 
           this.mEnterEvent = null;
         ]]></body>
       </method>
 
       <method name="getSelectedOneOff">
         <body><![CDATA[
--- a/browser/components/search/test/browser_amazon_behavior.js
+++ b/browser/components/search/test/browser_amazon_behavior.js
@@ -11,16 +11,17 @@ const BROWSER_SEARCH_PREF = "browser.sea
 
 
 function test() {
   let engine = Services.search.getEngineByName("Amazon.com");
   ok(engine, "Amazon is installed");
 
   let previouslySelectedEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
+  engine.alias = "a";
 
   let base = "http://www.amazon.com/exec/obidos/external-search/?field-keywords=foo&mode=blended&tag=mozilla-20&sourceid=Mozilla-search";
   let url;
 
   // Test search URLs (including purposes).
   url = engine.getSubmission("foo").uri.spec;
   is(url, base, "Check search URL for 'foo'");
 
@@ -42,16 +43,25 @@ function test() {
       searchURL: base,
       run: function () {
         gURLBar.value = "? foo";
         gURLBar.focus();
         EventUtils.synthesizeKey("VK_RETURN", {});
       }
     },
     {
+      name: "keyword search",
+      searchURL: base,
+      run: function () {
+        gURLBar.value = "a foo";
+        gURLBar.focus();
+        EventUtils.synthesizeKey("VK_RETURN", {});
+      }
+    },
+    {
       name: "search bar search",
       searchURL: base,
       run: function () {
         let sb = BrowserSearch.searchBar;
         sb.focus();
         sb.value = "foo";
         registerCleanupFunction(function () {
           sb.value = "";
@@ -134,16 +144,17 @@ function test() {
 
       req.cancel(Components.results.NS_ERROR_FAILURE);
 
       executeSoon(nextTest);
     }
   }
 
   registerCleanupFunction(function () {
+    engine.alias = undefined;
     gBrowser.removeProgressListener(listener);
     gBrowser.removeTab(tab);
     Services.search.currentEngine = previouslySelectedEngine;
   });
 
   tab.linkedBrowser.addEventListener("load", function load() {
     tab.linkedBrowser.removeEventListener("load", load, true);
     gBrowser.addProgressListener(listener);
--- a/browser/components/search/test/browser_bing_behavior.js
+++ b/browser/components/search/test/browser_bing_behavior.js
@@ -11,17 +11,18 @@ const BROWSER_SEARCH_PREF = "browser.sea
 
 
 function test() {
   let engine = Services.search.getEngineByName("Bing");
   ok(engine, "Bing is installed");
 
   let previouslySelectedEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
-
+  engine.alias = "b";
+  
   let base = "http://www.bing.com/search?q=foo&pc=MOZI";
   let url;
 
   // Test search URLs (including purposes).
   url = engine.getSubmission("foo").uri.spec;
   is(url, base, "Check search URL for 'foo'");
 
   waitForExplicitFinish();
@@ -42,16 +43,25 @@ function test() {
       searchURL: base + "&form=MOZLBR",
       run: function () {
         gURLBar.value = "? foo";
         gURLBar.focus();
         EventUtils.synthesizeKey("VK_RETURN", {});
       }
     },
     {
+      name: "keyword search with alias",
+      searchURL: base + "&form=MOZLBR",
+      run: function () {
+        gURLBar.value = "b foo";
+        gURLBar.focus();
+        EventUtils.synthesizeKey("VK_RETURN", {});
+      }
+    },
+    {
       name: "search bar search",
       searchURL: base + "&form=MOZSBR",
       run: function () {
         let sb = BrowserSearch.searchBar;
         sb.focus();
         sb.value = "foo";
         registerCleanupFunction(function () {
           sb.value = "";
@@ -134,16 +144,17 @@ function test() {
 
       req.cancel(Components.results.NS_ERROR_FAILURE);
 
       executeSoon(nextTest);
     }
   }
 
   registerCleanupFunction(function () {
+    engine.alias = undefined;
     gBrowser.removeProgressListener(listener);
     gBrowser.removeTab(tab);
     Services.search.currentEngine = previouslySelectedEngine;
   });
 
   tab.linkedBrowser.addEventListener("load", function load() {
     tab.linkedBrowser.removeEventListener("load", load, true);
     gBrowser.addProgressListener(listener);
--- a/browser/components/search/test/browser_eBay_behavior.js
+++ b/browser/components/search/test/browser_eBay_behavior.js
@@ -11,16 +11,17 @@ const BROWSER_SEARCH_PREF = "browser.sea
 
 
 function test() {
   let engine = Services.search.getEngineByName("eBay");
   ok(engine, "eBay is installed");
 
   let previouslySelectedEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
+  engine.alias = 'e';
 
   let base = "http://rover.ebay.com/rover/1/711-47294-18009-3/4?mfe=search&mpre=http://www.ebay.com/sch/i.html?_nkw=foo";
   let url;
 
   // Test search URLs (including purposes).
   url = engine.getSubmission("foo").uri.spec;
   is(url, base, "Check search URL for 'foo'");
 
@@ -42,16 +43,25 @@ function test() {
       searchURL: base,
       run: function () {
         gURLBar.value = "? foo";
         gURLBar.focus();
         EventUtils.synthesizeKey("VK_RETURN", {});
       }
     },
     {
+      name: "keyword search",
+      searchURL: base,
+      run: function () {
+        gURLBar.value = "e foo";
+        gURLBar.focus();
+        EventUtils.synthesizeKey("VK_RETURN", {});
+      }
+    },
+    {
       name: "search bar search",
       searchURL: base,
       run: function () {
         let sb = BrowserSearch.searchBar;
         sb.focus();
         sb.value = "foo";
         registerCleanupFunction(function () {
           sb.value = "";
@@ -134,16 +144,17 @@ function test() {
 
       req.cancel(Components.results.NS_ERROR_FAILURE);
 
       executeSoon(nextTest);
     }
   }
 
   registerCleanupFunction(function () {
+    engine.alias = undefined;
     gBrowser.removeProgressListener(listener);
     gBrowser.removeTab(tab);
     Services.search.currentEngine = previouslySelectedEngine;
   });
 
   tab.linkedBrowser.addEventListener("load", function load() {
     tab.linkedBrowser.removeEventListener("load", load, true);
     gBrowser.addProgressListener(listener);
--- a/browser/components/search/test/browser_google_behavior.js
+++ b/browser/components/search/test/browser_google_behavior.js
@@ -8,16 +8,17 @@
 "use strict";
 
 function test() {
   let engine = Services.search.getEngineByName("Google");
   ok(engine, "Google is installed");
 
   let previouslySelectedEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
+  engine.alias = "g";
 
   let base = "https://www.google.com/search?q=foo&ie=utf-8&oe=utf-8";
 
   let url;
 
   // Test search URLs (including purposes).
   url = engine.getSubmission("foo").uri.spec;
   is(url, base, "Check search URL for 'foo'");
@@ -40,16 +41,25 @@ function test() {
       searchURL: base,
       run: function () {
         gURLBar.value = "? foo";
         gURLBar.focus();
         EventUtils.synthesizeKey("VK_RETURN", {});
       }
     },
     {
+      name: "keyword search",
+      searchURL: base,
+      run: function () {
+        gURLBar.value = "g foo";
+        gURLBar.focus();
+        EventUtils.synthesizeKey("VK_RETURN", {});
+      }
+    },
+    {
       name: "search bar search",
       searchURL: base,
       run: function () {
         let sb = BrowserSearch.searchBar;
         sb.focus();
         sb.value = "foo";
         registerCleanupFunction(function () {
           sb.value = "";
@@ -132,16 +142,17 @@ function test() {
 
       req.cancel(Components.results.NS_ERROR_FAILURE);
 
       executeSoon(nextTest);
     }
   }
 
   registerCleanupFunction(function () {
+    engine.alias = undefined;
     gBrowser.removeProgressListener(listener);
     gBrowser.removeTab(tab);
     Services.search.currentEngine = previouslySelectedEngine;
   });
 
   tab.linkedBrowser.addEventListener("load", function load() {
     tab.linkedBrowser.removeEventListener("load", load, true);
     gBrowser.addProgressListener(listener);
--- a/browser/components/search/test/browser_yahoo_behavior.js
+++ b/browser/components/search/test/browser_yahoo_behavior.js
@@ -11,16 +11,17 @@ const BROWSER_SEARCH_PREF = "browser.sea
 
 
 function test() {
   let engine = Services.search.getEngineByName("Yahoo");
   ok(engine, "Yahoo is installed");
 
   let previouslySelectedEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
+  engine.alias = "y";
 
   let base = "https://search.yahoo.com/yhs/search?p=foo&ei=UTF-8&hspart=mozilla&hsimp=yhs-001";
   let url;
 
   // Test search URLs (including purposes).
   url = engine.getSubmission("foo").uri.spec;
   is(url, base, "Check search URL for 'foo'");
 
@@ -42,16 +43,25 @@ function test() {
       searchURL: base,
       run: function () {
         gURLBar.value = "? foo";
         gURLBar.focus();
         EventUtils.synthesizeKey("VK_RETURN", {});
       }
     },
     {
+      name: "keyword search",
+      searchURL: base,
+      run: function () {
+        gURLBar.value = "y foo";
+        gURLBar.focus();
+        EventUtils.synthesizeKey("VK_RETURN", {});
+      }
+    },
+    {
       name: "search bar search",
       searchURL: base,
       run: function () {
         let sb = BrowserSearch.searchBar;
         sb.focus();
         sb.value = "foo";
         registerCleanupFunction(function () {
           sb.value = "";
@@ -134,16 +144,17 @@ function test() {
 
       req.cancel(Components.results.NS_ERROR_FAILURE);
 
       executeSoon(nextTest);
     }
   }
 
   registerCleanupFunction(function () {
+    engine.alias = undefined;
     gBrowser.removeProgressListener(listener);
     gBrowser.removeTab(tab);
     Services.search.currentEngine = previouslySelectedEngine;
   });
 
   tab.linkedBrowser.addEventListener("load", function load() {
     tab.linkedBrowser.removeEventListener("load", load, true);
     gBrowser.addProgressListener(listener);
--- a/browser/devtools/netmonitor/test/browser_net_details-no-duplicated-content.js
+++ b/browser/devtools/netmonitor/test/browser_net_details-no-duplicated-content.js
@@ -97,20 +97,59 @@ let test = Task.async(function* () {
 
     ok(true, "Received NETWORK_EVENT. Selecting the item.");
     let item = RequestsMenu.getItemAtIndex(0);
     RequestsMenu.selectedItem = item;
 
     info("Item selected. Waiting for NETWORKDETAILSVIEW_POPULATED");
     yield onDetailsPopulated;
 
-    info("Selecting tab at index " + tabIndex);
+    info("Received populated event. Selecting tab at index " + tabIndex);
     NetworkDetails.widget.selectedIndex = tabIndex;
 
-    ok(true, "Received NETWORKDETAILSVIEW_POPULATED. Waiting for request to finish");
+    info("Waiting for request to finish.");
     yield onRequestFinished;
 
-    ok(true, "Request finished. Waiting for tab update to complete");
-    let onDetailsUpdateFinished = waitFor(panel, EVENTS.TAB_UPDATED);
-    yield onDetailsUpdateFinished;
-    ok(true, "Details were updated");
+    ok(true, "Request finished.");
+
+    /**
+     * Because this test uses lazy updates there's four scenarios to consider:
+     * #1: Everything is updated and test is ready to continue.
+     * #2: There's updates that are waiting to be flushed.
+     * #3: Updates are flushed but the tab update is still running.
+     * #4: There's pending updates and a tab update is still running.
+     *
+     * For case #1 there's not going to be a TAB_UPDATED event so don't wait for
+     * it (bug 1106181).
+     *
+     * For cases #2 and #3 it's enough to wait for one TAB_UPDATED event as for
+     * - case #2 the next flush will perform the final update and single
+     *   TAB_UPDATED event is emitted.
+     * - case #3 the running update is the final update that'll emit one
+     *   TAB_UPDATED event.
+     *
+     * For case #4 we must wait for the updates to be flushed before we can
+     * start waiting for TAB_UPDATED event or we'll continue the test right
+     * after the pending update finishes.
+     */
+    let hasQueuedUpdates = RequestsMenu._updateQueue.length !== 0;
+    let hasRunningTabUpdate = NetworkDetails._viewState.updating[tabIndex];
+
+    if (hasQueuedUpdates || hasRunningTabUpdate) {
+      info("There's pending updates - waiting for them to finish.");
+      info("  hasQueuedUpdates: " + hasQueuedUpdates);
+      info("  hasRunningTabUpdate: " + hasRunningTabUpdate);
+
+      if (hasQueuedUpdates && hasRunningTabUpdate) {
+        info("Waiting for updates to be flushed.");
+        // _flushRequests calls .populate which emits the following event
+        yield waitFor(panel, EVENTS.NETWORKDETAILSVIEW_POPULATED);
+
+        info("Requests flushed.");
+      }
+
+      info("Waiting for final tab update.");
+      yield waitFor(panel, EVENTS.TAB_UPDATED);
+    }
+
+    info("All updates completed.");
   }
 });
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -64,17 +64,19 @@ let OverviewView = {
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
   },
 
   /**
    * Sets up the framerate graph.
    */
   _showFramerateGraph: Task.async(function *() {
-    this.framerateGraph = new LineGraphWidget($("#time-framerate"), L10N.getStr("graphs.fps"));
+    this.framerateGraph = new LineGraphWidget($("#time-framerate"), {
+      metric: L10N.getStr("graphs.fps")
+    });
     this.framerateGraph.fixedHeight = FRAMERATE_GRAPH_HEIGHT;
     yield this.framerateGraph.ready();
   }),
 
   /**
    * Sets up the markers overivew graph.
    */
   _showMarkersGraph: Task.async(function *() {
--- a/browser/devtools/profiler/ui-profile.js
+++ b/browser/devtools/profiler/ui-profile.js
@@ -387,17 +387,19 @@ let ProfileView = {
       oldGraph.destroy();
     }
 
     // Don't create a graph if there's not enough data to show.
     if (!framerateData || framerateData.length < 2) {
       return null;
     }
 
-    let graph = new LineGraphWidget($(".framerate", panel), L10N.getStr("graphs.fps"));
+    let graph = new LineGraphWidget($(".framerate", panel), {
+      metric: L10N.getStr("graphs.fps")
+    });
     graph.fixedHeight = FRAMERATE_GRAPH_HEIGHT;
     graph.dataOffsetX = beginAt;
 
     yield graph.setDataWhenReady(framerateData);
 
     graph.on("mouseup", this._onGraphMouseUp);
     graph.on("scroll", this._onGraphScroll);
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/lib/plugins/rename/rename.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { Class } = require("sdk/core/heritage");
+const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
+const { getLocalizedString } = require("projecteditor/helpers/l10n");
+
+var RenamePlugin = Class({
+  extends: Plugin,
+
+  init: function(host) {
+    this.host.addCommand(this, {
+      id: "cmd-rename"
+    });
+    this.contextMenuItem = this.host.createMenuItem({
+      parent: this.host.contextMenuPopup,
+      label: getLocalizedString("projecteditor.renameLabel"),
+      command: "cmd-rename"
+    });
+  },
+
+  onCommand: function(cmd) {
+    if (cmd === "cmd-rename") {
+      let tree = this.host.projectTree;
+      let resource = tree.getSelectedResource();
+      let parent = resource.parent;
+      let oldName = resource.basename;
+
+      tree.promptEdit(oldName, resource).then(name => {
+        if (name === oldName) {
+          return resource;
+        }
+        if (resource.hasChild(parent, name)) {
+          let matches = name.match(/([^\d.]*)(\d*)([^.]*)(.*)/);
+          let template = matches[1] + "{1}" + matches[3] + matches[4];
+          name = this.suggestName(resource, template, parseInt(matches[2]) || 2);
+        }
+        return parent.rename(oldName,name);
+      }).then(resource => {
+        this.host.project.refresh();
+        tree.selectResource(resource);
+        if (!resource.isDir) {
+          this.host.currentEditor.focus();
+        }
+      }).then(null, console.error);
+    }
+  },
+
+  suggestName: function(resource, template, start=1) {
+    let i = start;
+    let name;
+    let parent = resource.parent;
+    do {
+      name = template.replace("\{1\}", i === 1 ? "" : i);
+      i++;
+    } while (resource.hasChild(parent, name));
+
+    return name;
+  }
+});
+
+exports.RenamePlugin = RenamePlugin;
+registerPlugin(RenamePlugin);
--- a/browser/devtools/projecteditor/lib/projecteditor.js
+++ b/browser/devtools/projecteditor/lib/projecteditor.js
@@ -22,16 +22,17 @@ const { Services } = Cu.import("resource
 const ITCHPAD_URL = "chrome://browser/content/devtools/projecteditor.xul";
 const { confirm } = require("projecteditor/helpers/prompts");
 const { getLocalizedString } = require("projecteditor/helpers/l10n");
 
 // Enabled Plugins
 require("projecteditor/plugins/dirty/dirty");
 require("projecteditor/plugins/delete/delete");
 require("projecteditor/plugins/new/new");
+require("projecteditor/plugins/rename/rename");
 require("projecteditor/plugins/save/save");
 require("projecteditor/plugins/image-view/plugin");
 require("projecteditor/plugins/app-manager/plugin");
 require("projecteditor/plugins/status-bar/plugin");
 
 // Uncomment to enable logging.
 // require("projecteditor/plugins/logging/logging");
 
--- a/browser/devtools/projecteditor/lib/stores/resource.js
+++ b/browser/devtools/projecteditor/lib/stores/resource.js
@@ -122,16 +122,31 @@ var Resource = Class({
     resource.parent = this;
     this.children.add(resource);
     this.store.notifyAdd(resource);
     emit(this, "children-changed", this);
     return resource;
   },
 
   /**
+   * Checks a resource has child with specific name.
+   *
+   * @param Resource resource
+   * @param string name
+   */
+  hasChild: function(resource, name) {
+    for (let child of resource.children) {
+      if (child.basename === name) {
+        return true;
+      }
+    }
+    return false;
+  },
+
+  /**
    * Remove a resource to children set and notify of the change.
    *
    * @param Resource resource
    */
   removeChild: function(resource) {
     resource.parent = null;
     this.children.remove(resource);
     this.store.notifyRemove(resource);
@@ -299,16 +314,37 @@ var FileResource = Class({
       if (!resource) {
         throw new Error("Error creating " + newPath);
       }
       return resource;
     });
   },
 
   /**
+   * Rename the file from the filesystem
+   *
+   * @returns Promise
+   *          Resolves with the renamed FileResource.
+   */
+  rename: function(oldName, newName) {
+    let oldPath = OS.Path.join(this.path, oldName);
+    let newPath = OS.Path.join(this.path, newName);
+
+    return OS.File.move(oldPath, newPath).then(() => {
+      return this.store.refresh();
+    }).then(() => {
+      let resource = this.store.resources.get(newPath);
+      if (!resource) {
+        throw new Error("Error creating " + newPath);
+      }
+      return resource;
+    });
+  },
+
+  /**
    * Write a string to this file.
    *
    * @param string content
    * @returns Promise
    *          Resolves once it has been written to disk.
    *          Rejected if there is an error
    */
   save: function(content) {
--- a/browser/devtools/projecteditor/lib/tree.js
+++ b/browser/devtools/projecteditor/lib/tree.js
@@ -272,16 +272,49 @@ var TreeView = Class({
         item.parentNode.removeChild(item);
       },
     });
 
     return deferred.promise;
   },
 
   /**
+   * Prompt the user to rename file in the tree.
+   *
+   * @param string initial
+   *               The suggested starting file name
+   * @param resource
+   *
+   * @returns Promise
+   *          Resolves once the prompt has been successful,
+   *          Rejected if it is cancelled
+   */
+  promptEdit: function(initial, resource) {
+    let deferred = promise.defer();
+    let placeholder = this._containers.get(resource).elt;
+
+    new InplaceEditor({
+      element: placeholder,
+      initial: initial,
+      start: editor => {
+        editor.input.select();
+      },
+      done: function(val, commit) {
+        if (commit) {
+          deferred.resolve(val);
+        } else {
+          deferred.reject(val);
+        }
+      },
+    });
+
+    return deferred.promise;
+  },
+
+  /**
    * Add a new Store into the TreeView
    *
    * @param Store model
    */
   addModel: function(model) {
     if (this.models.has(model)) {
       // Requesting to add a model that already exists
       return;
--- a/browser/devtools/projecteditor/moz.build
+++ b/browser/devtools/projecteditor/moz.build
@@ -47,16 +47,20 @@ EXTRA_JS_MODULES.devtools.projecteditor.
 EXTRA_JS_MODULES.devtools.projecteditor.plugins.logging += [
     'lib/plugins/logging/logging.js',
 ]
 
 EXTRA_JS_MODULES.devtools.projecteditor.plugins.new += [
     'lib/plugins/new/new.js',
 ]
 
+EXTRA_JS_MODULES.devtools.projecteditor.plugins.rename += [
+    'lib/plugins/rename/rename.js',
+]
+
 EXTRA_JS_MODULES.devtools.projecteditor.plugins.save += [
     'lib/plugins/save/save.js',
 ]
 
 EXTRA_JS_MODULES.devtools.projecteditor.plugins['status-bar'] += [
     'lib/plugins/status-bar/plugin.js',
 ]
 
--- a/browser/devtools/projecteditor/test/browser.ini
+++ b/browser/devtools/projecteditor/test/browser.ini
@@ -7,16 +7,17 @@ support-files =
   helper_edits.js
 
 [browser_projecteditor_app_options.js]
 skip-if = buildapp == 'mulet'
 [browser_projecteditor_confirm_unsaved.js]
 [browser_projecteditor_contextmenu_01.js]
 [browser_projecteditor_contextmenu_02.js]
 [browser_projecteditor_delete_file.js]
+[browser_projecteditor_rename_file.js]
 skip-if = buildapp == 'mulet'
 [browser_projecteditor_editing_01.js]
 skip-if = buildapp == 'mulet'
 [browser_projecteditor_editors_image.js]
 [browser_projecteditor_external_change.js]
 [browser_projecteditor_immediate_destroy.js]
 [browser_projecteditor_init.js]
 [browser_projecteditor_menubar_01.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_rename_file.js
@@ -0,0 +1,59 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test file rename functionality
+
+add_task(function*() {
+  let projecteditor = yield addProjectEditorTabForTempDirectory();
+  ok(true, "ProjectEditor has loaded");
+
+  let root = [...projecteditor.project.allStores()][0].root;
+  is(root.path, TEMP_PATH, "The root store is set to the correct temp path.");
+  for (let child of root.children) {
+    yield renameWithContextMenu(projecteditor, projecteditor.projectTree.getViewContainer(child));
+  }
+});
+
+function openContextMenuOn(node) {
+  EventUtils.synthesizeMouseAtCenter(
+    node,
+    {button: 2, type: "contextmenu"},
+    node.ownerDocument.defaultView
+  );
+}
+
+function renameWithContextMenu(projecteditor, container) {
+  let defer = promise.defer();
+  let popup = projecteditor.contextMenuPopup;
+  let resource = container.resource;
+  info ("Going to attempt renaming for: " + resource.path);
+
+  onPopupShow(popup).then(function () {
+    let renameCommand = popup.querySelector("[command=cmd-rename]");
+    ok (renameCommand, "Rename command exists in popup");
+    is (renameCommand.getAttribute("hidden"), "", "Rename command is visible");
+    is (renameCommand.getAttribute("disabled"), "", "Rename command is enabled");
+
+    projecteditor.project.on("refresh-complete", function refreshComplete() {
+      projecteditor.project.off("refresh-complete", refreshComplete);
+      OS.File.stat(resource.path + ".renamed").then(() => {
+        ok (true, "File is renamed");
+        defer.resolve();
+      }, (ex) => {
+        ok (false, "Failed to rename file");
+        defer.resolve();
+      });
+    });
+
+    renameCommand.click();
+    popup.hidePopup();
+    EventUtils.sendString(resource.basename + ".renamed", projecteditor.window);
+    EventUtils.synthesizeKey("VK_RETURN", {}, projecteditor.window);
+  });
+
+  openContextMenuOn(container.label);
+  return defer.promise;
+}
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -24,16 +24,17 @@ support-files =
 [browser_graphs-07a.js]
 [browser_graphs-07b.js]
 [browser_graphs-08.js]
 [browser_graphs-09a.js]
 [browser_graphs-09b.js]
 [browser_graphs-09c.js]
 [browser_graphs-09d.js]
 [browser_graphs-09e.js]
+[browser_graphs-09f.js]
 [browser_graphs-10a.js]
 [browser_graphs-10b.js]
 [browser_graphs-11a.js]
 [browser_graphs-11b.js]
 [browser_graphs-12.js]
 [browser_graphs-13.js]
 [browser_graphs-14.js]
 [browser_inplace-editor.js]
--- a/browser/devtools/shared/test/browser_css_color.js
+++ b/browser/devtools/shared/test/browser_css_color.js
@@ -48,16 +48,17 @@ function testColorUtils() {
     is(color.hex, hex, "color.hex === hex");
     is(color.hsl, hsl, "color.hsl === hsl");
     is(color.rgb, rgb, "color.rgb === rgb");
 
     testToString(color, name, hex, hsl, rgb);
     testColorMatch(name, hex, hsl, rgb, color.rgba);
   }
   testProcessCSSString();
+  testSetAlpha();
   finishUp();
 }
 
 function testToString(color, name, hex, hsl, rgb) {
   switchColorUnit(colorUtils.CssColor.COLORUNIT.name);
   is(color.toString(), name, "toString() with authored type");
 
   switchColorUnit(colorUtils.CssColor.COLORUNIT.hex);
@@ -133,16 +134,39 @@ function testProcessCSSString() {
                  "color #0F0; font-weight: bold; " +
                  "background-color: transparent; " +
                  "border-top-color: rgba(0, 0, 255, 0.5);";
   let after = colorUtils.processCSSString(before);
 
   is(after, expected, "CSS string processed correctly");
 }
 
+function testSetAlpha() {
+  let values = [
+    ["longhex", "#ff0000", 0.5, "rgba(255, 0, 0, 0.5)"],
+    ["hex", "#f0f", 0.2, "rgba(255, 0, 255, 0.2)"],
+    ["rgba", "rgba(120, 34, 23, 1)", 0.25, "rgba(120, 34, 23, 0.25)"],
+    ["rgb", "rgb(120, 34, 23)", 0.25, "rgba(120, 34, 23, 0.25)"],
+    ["hsl", "hsl(208, 100%, 97%)", 0.75, "rgba(239, 247, 255, 0.75)"],
+    ["hsla", "hsla(208, 100%, 97%, 1)", 0.75, "rgba(239, 247, 255, 0.75)"]
+  ];
+  values.forEach(([type, value, alpha, expected]) => {
+    is(colorUtils.setAlpha(value, alpha), expected, "correctly sets alpha value for " + type);
+  });
+
+  try {
+    colorUtils.setAlpha("rgb(24, 25, 45, 1)", 1);
+    ok(false, "Should fail when passing in an invalid color.");
+  } catch (e) {
+    ok(true, "Fails when setAlpha receives an invalid color.");
+  }
+
+  is(colorUtils.setAlpha("#fff"), "rgba(255, 255, 255, 1)", "sets alpha to 1 if invalid.");
+}
+
 function finishUp() {
   Services = colorUtils = Loader = null;
   gBrowser.removeCurrentTab();
   finish();
 }
 
 function getTestData() {
   return [
--- a/browser/devtools/shared/test/browser_graphs-09a.js
+++ b/browser/devtools/shared/test/browser_graphs-09a.js
@@ -13,17 +13,17 @@ let test = Task.async(function*() {
   yield promiseTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
   finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
-  let graph = new LineGraphWidget(doc.body, "fps");
+  let graph = new LineGraphWidget(doc.body, { metric: "fps" });
 
   yield testGraph(graph);
 
   graph.destroy();
   host.destroy();
 }
 
 function* testGraph(graph) {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_graphs-09f.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the constructor options for `min`, `max` and `avg` on displaying the
+// gutter/tooltips and lines.
+
+const TEST_DATA = [{ delta: 100, value: 60 }, { delta: 200, value: 1 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+
+  yield testGraph(doc.body, { avg: false });
+  yield testGraph(doc.body, { min: false });
+  yield testGraph(doc.body, { max: false });
+  yield testGraph(doc.body, { min: false, max: false, avg: false });
+  yield testGraph(doc.body, {});
+
+  host.destroy();
+}
+
+function* testGraph (parent, options) {
+  options.metric = "fps";
+  let graph = new LineGraphWidget(parent, options);
+  yield graph.setDataWhenReady(TEST_DATA);
+  let shouldGutterShow = options.min === false && options.max === false;
+
+  is(graph._gutter.hidden, shouldGutterShow,
+    `The gutter should ${shouldGutterShow ? "" : "not "}be shown`);
+
+  is(graph._maxTooltip.hidden, options.max === false,
+    `The max tooltip should ${options.max === false ? "not " : ""}be shown`);
+  is(graph._maxGutterLine.hidden, options.max === false,
+    `The max gutter should ${options.max === false ? "not " : ""}be shown`);
+  is(graph._minTooltip.hidden, options.min === false,
+    `The min tooltip should ${options.min === false ? "not " : ""}be shown`);
+  is(graph._minGutterLine.hidden, options.min === false,
+    `The min gutter should ${options.min === false ? "not " : ""}be shown`);
+  is(graph._avgTooltip.hidden, options.avg === false,
+    `The avg tooltip should ${options.avg === false ? "not " : ""}be shown`);
+  is(graph._avgGutterLine.hidden, options.avg === false,
+    `The avg gutter should ${options.avg === false ? "not " : ""}be shown`);
+
+  graph.destroy();
+}
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -1164,30 +1164,42 @@ AbstractCanvasGraph.prototype = {
  *     { delta: x2, value: y2 },
  *     ...
  *     { delta: xn, value: yn }
  *   ]
  * where each item in the array represents a point in the graph.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the graph.
- * @param string metric [optional]
- *        The metric displayed in the graph, e.g. "fps" or "bananas".
+ * @param object options [optional]
+ *        `metric`: The metric displayed in the graph, e.g. "fps" or "bananas".
+ *        `min`: Boolean whether to show the min tooltip/gutter/line (default: true)
+ *        `max`: Boolean whether to show the max tooltip/gutter/line (default: true)
+ *        `avg`: Boolean whether to show the avg tooltip/gutter/line (default: true)
  */
-this.LineGraphWidget = function(parent, metric, ...args) {
+this.LineGraphWidget = function(parent, options, ...args) {
+  options = options || {};
+  let metric = options.metric;
+
+  this._showMin = options.min !== false;
+  this._showMax = options.max !== false;
+  this._showAvg = options.avg !== false;
   AbstractCanvasGraph.apply(this, [parent, "line-graph", ...args]);
 
   this.once("ready", () => {
+    // Create all gutters and tooltips incase the showing of min/max/avg
+    // are changed later
     this._gutter = this._createGutter();
+
     this._maxGutterLine = this._createGutterLine("maximum");
-    this._avgGutterLine = this._createGutterLine("average");
+    this._maxTooltip = this._createTooltip("maximum", "start", "max", metric);
     this._minGutterLine = this._createGutterLine("minimum");
-    this._maxTooltip = this._createTooltip("maximum", "start", "max", metric);
+    this._minTooltip = this._createTooltip("minimum", "start", "min", metric);
+    this._avgGutterLine = this._createGutterLine("average");
     this._avgTooltip = this._createTooltip("average", "end", "avg", metric);
-    this._minTooltip = this._createTooltip("minimum", "start", "min", metric);
   });
 };
 
 LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
   backgroundColor: LINE_GRAPH_BACKGROUND_COLOR,
   backgroundGradientStart: LINE_GRAPH_BACKGROUND_GRADIENT_START,
   backgroundGradientEnd: LINE_GRAPH_BACKGROUND_GRADIENT_END,
   strokeColor: LINE_GRAPH_STROKE_COLOR,
@@ -1357,47 +1369,50 @@ LineGraphWidget.prototype = Heritage.ext
    * @param number dataScaleY
    */
   _drawOverlays: function(ctx, minValue, maxValue, avgValue, dataScaleY) {
     let width = this._width;
     let height = this._height;
     let totalTicks = this._data.length;
 
     // Draw the maximum value horizontal line.
-
-    ctx.strokeStyle = this.maximumLineColor;
-    ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
-    ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
-    ctx.beginPath();
-    let maximumY = height - maxValue * dataScaleY;
-    ctx.moveTo(0, maximumY);
-    ctx.lineTo(width, maximumY);
-    ctx.stroke();
+    if (this._showMax) {
+      ctx.strokeStyle = this.maximumLineColor;
+      ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
+      ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
+      ctx.beginPath();
+      let maximumY = height - maxValue * dataScaleY;
+      ctx.moveTo(0, maximumY);
+      ctx.lineTo(width, maximumY);
+      ctx.stroke();
+    }
 
     // Draw the average value horizontal line.
-
-    ctx.strokeStyle = this.averageLineColor;
-    ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
-    ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
-    ctx.beginPath();
-    let averageY = height - avgValue * dataScaleY;
-    ctx.moveTo(0, averageY);
-    ctx.lineTo(width, averageY);
-    ctx.stroke();
+    if (this._showAvg) {
+      ctx.strokeStyle = this.averageLineColor;
+      ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
+      ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
+      ctx.beginPath();
+      let averageY = height - avgValue * dataScaleY;
+      ctx.moveTo(0, averageY);
+      ctx.lineTo(width, averageY);
+      ctx.stroke();
+    }
 
     // Draw the minimum value horizontal line.
-
-    ctx.strokeStyle = this.minimumLineColor;
-    ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
-    ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
-    ctx.beginPath();
-    let minimumY = height - minValue * dataScaleY;
-    ctx.moveTo(0, minimumY);
-    ctx.lineTo(width, minimumY);
-    ctx.stroke();
+    if (this._showMin) {
+      ctx.strokeStyle = this.minimumLineColor;
+      ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
+      ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
+      ctx.beginPath();
+      let minimumY = height - minValue * dataScaleY;
+      ctx.moveTo(0, minimumY);
+      ctx.lineTo(width, minimumY);
+      ctx.stroke();
+    }
 
     // Update the tooltips text and gutter lines.
 
     this._maxTooltip.querySelector("[text=value]").textContent =
       L10N.numberWithDecimals(maxValue, 2);
     this._avgTooltip.querySelector("[text=value]").textContent =
       L10N.numberWithDecimals(avgValue, 2);
     this._minTooltip.querySelector("[text=value]").textContent =
@@ -1426,20 +1441,24 @@ LineGraphWidget.prototype = Heritage.ext
     this._avgGutterLine.style.top = avgPosY + "px";
     this._minGutterLine.style.top = minPosY + "px";
 
     this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
     this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
     this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
 
     let distanceMinMax = Math.abs(maxTooltipTop - minTooltipTop);
-    this._maxTooltip.hidden = !totalTicks || distanceMinMax < LINE_GRAPH_MIN_MAX_TOOLTIP_DISTANCE;
-    this._avgTooltip.hidden = !totalTicks;
-    this._minTooltip.hidden = !totalTicks;
-    this._gutter.hidden = !totalTicks || !this.withTooltipArrows;
+    this._maxTooltip.hidden = this._showMax === false || !totalTicks || distanceMinMax < LINE_GRAPH_MIN_MAX_TOOLTIP_DISTANCE;
+    this._avgTooltip.hidden = this._showAvg === false || !totalTicks;
+    this._minTooltip.hidden = this._showMin === false || !totalTicks;
+    this._gutter.hidden = (this._showMin === false && this._showMax === false) || !totalTicks || !this.withTooltipArrows;
+
+    this._maxGutterLine.hidden = this._showMax === false;
+    this._avgGutterLine.hidden = this._showAvg === false;
+    this._minGutterLine.hidden = this._showMin === false;
   },
 
   /**
    * Creates the gutter node when constructing this graph.
    * @return nsIDOMNode
    */
   _createGutter: function() {
     let gutter = this._document.createElementNS(HTML_NS, "div");
--- a/browser/devtools/timeline/test/browser.ini
+++ b/browser/devtools/timeline/test/browser.ini
@@ -5,15 +5,16 @@ support-files =
   head.js
 
 [browser_timeline_aaa_run_first_leaktest.js]
 [browser_timeline_blueprint.js]
 [browser_timeline_filters.js]
 [browser_timeline_overview-initial-selection-01.js]
 [browser_timeline_overview-initial-selection-02.js]
 [browser_timeline_overview-update.js]
+[browser_timeline_overview-theme.js]
 [browser_timeline_panels.js]
 [browser_timeline_recording-without-memory.js]
 [browser_timeline_recording.js]
 [browser_timeline_waterfall-background.js]
 [browser_timeline_waterfall-generic.js]
 [browser_timeline_waterfall-styles.js]
 [browser_timeline_waterfall-sidebar.js]
--- a/browser/devtools/timeline/test/browser_timeline_filters.js
+++ b/browser/devtools/timeline/test/browser_timeline_filters.js
@@ -43,39 +43,39 @@ add_task(function*() {
   ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (1)");
 
   let heightBefore = overview.fixedHeight;
   EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin);
   yield once(menuItem1, "command");
 
   yield waitUntil(() => !waterfall._outstandingMarkers.length);
 
-  // A row is 11px. See markers-overview.js
-  is(overview.fixedHeight, heightBefore - 11, "Overview is smaller");
+  is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
   ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (2)");
   ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (2)");
   ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (2)");
 
   heightBefore = overview.fixedHeight;
   EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin);
   yield once(menuItem2, "command");
 
   yield waitUntil(() => !waterfall._outstandingMarkers.length);
 
-  is(overview.fixedHeight, heightBefore - 11, "Overview is smaller");
+  is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
   ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (3)");
   ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (3)");
   ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (3)");
 
   heightBefore = overview.fixedHeight;
   EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin);
   yield once(menuItem3, "command");
 
   yield waitUntil(() => !waterfall._outstandingMarkers.length);
 
+  // A row is 11px. See markers-overview.js
   is(overview.fixedHeight, heightBefore - 11, "Overview is smaller");
   ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)");
   ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (4)");
   ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (4)");
 
   for (let item of [menuItem1, menuItem2, menuItem3]) {
     EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin);
     yield once(item, "command");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_overview-theme.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the markers and memory overviews render with the correct
+ * theme on load, and rerenders when changed.
+ */
+
+const LIGHT_BG = "#fcfcfc";
+const DARK_BG = "#14171a";
+
+add_task(function*() {
+  let { target, panel } = yield initTimelinePanel("about:blank");
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+  $("#memory-checkbox").checked = true;
+
+  setTheme("dark");
+
+  yield TimelineController.updateMemoryRecording();
+  is(TimelineView.markersOverview.backgroundColor, DARK_BG,
+    "correct theme on load for markers.");
+  is(TimelineView.memoryOverview.backgroundColor, DARK_BG,
+    "correct theme on load for memory.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has ended.");
+
+  let refreshed = Promise.all([
+    once(TimelineView.markersOverview, "refresh"),
+    once(TimelineView.memoryOverview, "refresh"),
+  ]);
+
+  setTheme("light");
+  yield refreshed;
+
+  ok(true, "Both memory and markers were rerendered after theme change.");
+  is(TimelineView.markersOverview.backgroundColor, LIGHT_BG,
+    "correct theme on after toggle for markers.");
+  is(TimelineView.memoryOverview.backgroundColor, LIGHT_BG,
+    "correct theme on after toggle for memory.");
+
+  refreshed = Promise.all([
+    once(TimelineView.markersOverview, "refresh"),
+    once(TimelineView.memoryOverview, "refresh"),
+  ]);
+
+  setTheme("dark");
+  yield refreshed;
+
+  ok(true, "Both memory and markers were rerendered after theme change.");
+  is(TimelineView.markersOverview.backgroundColor, DARK_BG,
+    "correct theme on after toggle for markers once more.");
+  is(TimelineView.memoryOverview.backgroundColor, DARK_BG,
+    "correct theme on after toggle for memory once more.");
+
+  refreshed = Promise.all([
+    once(TimelineView.markersOverview, "refresh"),
+    once(TimelineView.memoryOverview, "refresh"),
+  ]);
+
+  // Set theme back to light
+  setTheme("light");
+  yield refreshed;
+});
+
+/**
+ * Mimics selecting the theme selector in the toolbox;
+ * sets the preference and emits an event on gDevTools to trigger
+ * the themeing.
+ */
+function setTheme (newTheme) {
+  let oldTheme = Services.prefs.getCharPref("devtools.theme");
+  info("Setting `devtools.theme` to \"" + newTheme + "\"");
+  Services.prefs.setCharPref("devtools.theme", newTheme);
+  gDevTools.emit("pref-changed", {
+    pref: "devtools.theme",
+    newValue: newTheme,
+    oldValue: oldTheme
+  });
+}
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-sidebar.js
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-sidebar.js
@@ -2,17 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the sidebar is properly updated when a marker is selected.
  */
 
 add_task(function*() {
   let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, $$, EVENTS, TimelineController, TimelineView } = panel.panelWin;
+  let { $, $$, EVENTS, TimelineController, TimelineView, TIMELINE_BLUEPRINT} = panel.panelWin;
   let { L10N } = devtools.require("devtools/timeline/global");
 
   yield TimelineController.toggleRecording();
   ok(true, "Recording has started.");
 
   yield waitUntil(() => {
     // Wait until we get 3 different markers.
     let markers = TimelineController.getMarkers();
@@ -34,17 +34,19 @@ add_task(function*() {
   ok(bars.length > 2, "got at least 3 markers");
 
   let sidebar = $("#timeline-waterfall-details");
   for (let i = 0; i < bars.length; i++) {
     let bar = bars[i];
     bar.click();
     let m = markers[i];
 
-    is($("#timeline-waterfall-details .marker-details-type").getAttribute("value"), m.name,
+    let name = TIMELINE_BLUEPRINT[m.name].label;
+
+    is($("#timeline-waterfall-details .marker-details-type").getAttribute("value"), name,
       "sidebar title matches markers name");
 
     let printedStartTime = $(".marker-details-start .marker-details-labelvalue").getAttribute("value");
     let printedEndTime = $(".marker-details-end .marker-details-labelvalue").getAttribute("value");
     let printedDuration= $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
 
     let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
 
--- a/browser/devtools/timeline/timeline.js
+++ b/browser/devtools/timeline/timeline.js
@@ -1,18 +1,20 @@
 /* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
 
 devtools.lazyRequireGetter(this, "promise");
 devtools.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
 devtools.lazyRequireGetter(this, "MarkersOverview",
   "devtools/timeline/markers-overview", true);
 devtools.lazyRequireGetter(this, "MemoryOverview",
@@ -93,16 +95,17 @@ let TimelineController = {
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function() {
     this._onRecordingTick = this._onRecordingTick.bind(this);
     this._onMarkers = this._onMarkers.bind(this);
     this._onMemory = this._onMemory.bind(this);
     this._onFrames = this._onFrames.bind(this);
+
     gFront.on("markers", this._onMarkers);
     gFront.on("memory", this._onMemory);
     gFront.on("frames", this._onFrames);
   },
 
   /**
    * Destruction function, called when the tool is closed.
    */
@@ -288,37 +291,44 @@ let TimelineView = {
    * Initialization function, called when the tool is started.
    */
   initialize: Task.async(function*() {
     let blueprint = this._getFilteredBluePrint();
     this.markersOverview = new MarkersOverview($("#markers-overview"), blueprint);
     this.waterfall = new Waterfall($("#timeline-waterfall"), $("#timeline-pane"), blueprint);
     this.markerDetails = new MarkerDetails($("#timeline-waterfall-details"), $("#timeline-waterfall-container > splitter"));
 
+    this._onThemeChange = this._onThemeChange.bind(this);
     this._onSelecting = this._onSelecting.bind(this);
     this._onRefresh = this._onRefresh.bind(this);
+
+    gDevTools.on("pref-changed", this._onThemeChange);
     this.markersOverview.on("selecting", this._onSelecting);
     this.markersOverview.on("refresh", this._onRefresh);
     this.markerDetails.on("resize", this._onRefresh);
 
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this.waterfall.on("selected", this._onMarkerSelected);
     this.waterfall.on("unselected", this._onMarkerSelected);
 
+    let theme = Services.prefs.getCharPref("devtools.theme");
+    this.markersOverview.setTheme(theme);
+
     yield this.markersOverview.ready();
 
     yield this.waterfall.recalculateBounds();
 
     this._buildFilterPopup();
   }),
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function() {
+    gDevTools.off("pref-changed", this._onThemeChange);
     this.markerDetails.off("resize", this._onRefresh);
     this.markerDetails.destroy();
     this.waterfall.off("selected", this._onMarkerSelected);
     this.waterfall.off("unselected", this._onMarkerSelected);
     this.waterfall.destroy();
     this.markersOverview.off("selecting", this._onSelecting);
     this.markersOverview.off("refresh", this._onRefresh);
     this.markersOverview.destroy();
@@ -328,17 +338,20 @@ let TimelineView = {
       this.memoryOverview.destroy();
     }
   },
 
   /**
    * Shows the memory overview graph.
    */
   showMemoryOverview: Task.async(function*() {
+    let theme = Services.prefs.getCharPref("devtools.theme");
+
     this.memoryOverview = new MemoryOverview($("#memory-overview"));
+    this.memoryOverview.setTheme(theme);
     yield this.memoryOverview.ready();
 
     let interval = TimelineController.getInterval();
     let memory = TimelineController.getMemory();
     this.memoryOverview.setData({ interval, memory });
 
     CanvasGraphUtils.linkAnimation(this.markersOverview, this.memoryOverview);
     CanvasGraphUtils.linkSelection(this.markersOverview, this.memoryOverview);
@@ -570,16 +583,30 @@ let TimelineView = {
       // Style used by pseudo element ::before in timeline.css.in
       let bulletStyle = `--bullet-bg: ${markerDetails.fill};`
       bulletStyle += `--bullet-border: ${markerDetails.stroke}`;
       menuitem.setAttribute("style", bulletStyle);
 
       popup.appendChild(menuitem);
     }
   },
+
+  /*
+   * Called when the developer tools theme changes. Redraws
+   * the graphs with the new theme setting.
+   */
+  _onThemeChange: function (_, theme) {
+    if (this.memoryOverview) {
+      this.memoryOverview.setTheme(theme.newValue);
+      this.memoryOverview.refresh({ force: true });
+    }
+
+    this.markersOverview.setTheme(theme.newValue);
+    this.markersOverview.refresh({ force: true });
+  }
 };
 
 /**
  * Convenient way of emitting events from the panel window.
  */
 EventEmitter.decorate(this);
 
 /**
--- a/browser/devtools/timeline/widgets/global.js
+++ b/browser/devtools/timeline/widgets/global.js
@@ -25,45 +25,45 @@ const L10N = new ViewHelpers.L10N(STRING
  * Whenever this is changed, browser_timeline_waterfall-styles.js *must* be
  * updated as well.
  */
 const TIMELINE_BLUEPRINT = {
   "Styles": {
     group: 0,
     fill: "hsl(285,50%,68%)",
     stroke: "hsl(285,50%,48%)",
-    label: L10N.getStr("timeline.label.styles")
+    label: L10N.getStr("timeline.label.styles2")
   },
   "Reflow": {
-    group: 2,
+    group: 0,
+    fill: "hsl(285,50%,68%)",
+    stroke: "hsl(285,50%,48%)",
+    label: L10N.getStr("timeline.label.reflow2")
+  },
+  "Paint": {
+    group: 0,
     fill: "hsl(104,57%,71%)",
     stroke: "hsl(104,57%,51%)",
-    label: L10N.getStr("timeline.label.reflow")
-  },
-  "Paint": {
-    group: 1,
-    fill: "hsl(39,82%,69%)",
-    stroke: "hsl(39,82%,49%)",
     label: L10N.getStr("timeline.label.paint")
   },
   "DOMEvent": {
-    group: 3,
-    fill: "hsl(219,82%,69%)",
-    stroke: "hsl(219,82%,69%)",
+    group: 1,
+    fill: "hsl(39,82%,69%)",
+    stroke: "hsl(39,82%,49%)",
     label: L10N.getStr("timeline.label.domevent")
   },
+  "Javascript": {
+    group: 1,
+    fill: "hsl(39,82%,69%)",
+    stroke: "hsl(39,82%,49%)",
+    label: L10N.getStr("timeline.label.javascript2")
+  },
   "ConsoleTime": {
-    group: 4,
+    group: 2,
     fill: "hsl(0,0%,80%)",
     stroke: "hsl(0,0%,60%)",
     label: L10N.getStr("timeline.label.consoleTime")
   },
-  "Javascript": {
-    group: 4,
-    fill: "hsl(0,0%,80%)",
-    stroke: "hsl(0,0%,60%)",
-    label: L10N.getStr("timeline.label.javascript")
-  },
 };
 
 // Exported symbols.
 exports.L10N = L10N;
 exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;
--- a/browser/devtools/timeline/widgets/markers-overview.js
+++ b/browser/devtools/timeline/widgets/markers-overview.js
@@ -9,70 +9,65 @@
  * markers are visible in the "waterfall".
  */
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource:///modules/devtools/Graphs.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
+const { colorUtils: { setAlpha }} = require("devtools/css-color");
+const { getColor } = require("devtools/shared/theme");
+
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
 
-const HTML_NS = "http://www.w3.org/1999/xhtml";
-
 const OVERVIEW_HEADER_HEIGHT = 14; // px
 const OVERVIEW_ROW_HEIGHT = 11; // row height
 
-const OVERVIEW_BACKGROUND_COLOR = "#fff";
-const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
-const OVERVIEW_SELECTION_LINE_COLOR = "#555";
-const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
-const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const OVERVIEW_BODY_HEIGHT = 55; // 11px * 5 groups
+const OVERVIEW_SELECTION_LINE_COLOR = "#666";
+const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
 
 const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
 const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
 const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
-const OVERVIEW_HEADER_BACKGROUND = "#fff";
-const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
 const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
 const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
 const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
 const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px
-const OVERVIEW_TIMELINE_STROKES = "#ccc";
 const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1];
 const OVERVIEW_MARKER_DURATION_MIN = 4; // ms
 const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
-const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)";
 
 /**
  * An overview for the markers data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
  * @param Object blueprint
  *        List of names and colors defining markers.
  */
 function MarkersOverview(parent, blueprint, ...args) {
   AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
 
+  this.setTheme();
+
   // Set the list of names, properties and colors used to paint this overview.
   this.setBlueprint(blueprint);
 
   this.once("ready", () => {
     // Populate this overview with some dummy initial data.
     this.setData({ interval: { startTime: 0, endTime: 1000 }, markers: [] });
   });
 }
 
 MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
   clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
   selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
-  selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
-  selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
   headerHeight: OVERVIEW_HEADER_HEIGHT,
   rowHeight: OVERVIEW_ROW_HEIGHT,
   groupPadding: OVERVIEW_GROUP_VERTICAL_PADDING,
 
   /**
    * Compute the height of the overview.
    */
   get fixedHeight() {
@@ -133,25 +128,25 @@ MarkersOverview.prototype = Heritage.ext
     let groupHeight = this.rowHeight * this._pixelRatio;
     let groupPadding = this.groupPadding * this._pixelRatio;
 
     let totalTime = (endTime - startTime) || 0;
     let dataScale = this.dataScaleX = availableWidth / totalTime;
 
     // Draw the header and overview background.
 
-    ctx.fillStyle = OVERVIEW_HEADER_BACKGROUND;
+    ctx.fillStyle = this.headerBackgroundColor;
     ctx.fillRect(0, 0, canvasWidth, headerHeight);
 
-    ctx.fillStyle = OVERVIEW_BACKGROUND_COLOR;
+    ctx.fillStyle = this.backgroundColor;
     ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
 
     // Draw the alternating odd/even group backgrounds.
 
-    ctx.fillStyle = OVERVIEW_GROUP_ALTERNATING_BACKGROUND;
+    ctx.fillStyle = this.alternatingBackgroundColor;
     ctx.beginPath();
 
     for (let i = 0; i < totalGroups; i += 2) {
       let top = headerHeight + i * groupHeight;
       ctx.rect(0, top, canvasWidth, groupHeight);
     }
 
     ctx.fill();
@@ -161,18 +156,18 @@ MarkersOverview.prototype = Heritage.ext
     let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
     let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
     let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
     let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
     let tickInterval = this._findOptimalTickInterval(dataScale);
 
     ctx.textBaseline = "middle";
     ctx.font = fontSize + "px " + fontFamily;
-    ctx.fillStyle = OVERVIEW_HEADER_TEXT_COLOR;
-    ctx.strokeStyle = OVERVIEW_TIMELINE_STROKES;
+    ctx.fillStyle = this.headerTextColor;
+    ctx.strokeStyle = this.headerTimelineStrokeColor;
     ctx.beginPath();
 
     for (let x = 0; x < availableWidth; x += tickInterval) {
       let lineLeft = x;
       let textLeft = lineLeft + textPaddingLeft;
       let time = Math.round(x / dataScale);
       let label = L10N.getFormatStr("timeline.tick", time);
       ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
@@ -227,12 +222,28 @@ MarkersOverview.prototype = Heritage.ext
     while (true) {
       let scaledStep = dataScale * timingStep;
       if (scaledStep < spacingMin) {
         timingStep <<= 1;
         continue;
       }
       return scaledStep;
     }
+  },
+
+  /**
+   * Sets the theme via `theme` to either "light" or "dark",
+   * and updates the internal styling to match. Requires a redraw
+   * to see the effects.
+   */
+  setTheme: function (theme) {
+    theme = theme || "light";
+    this.backgroundColor = getColor("body-background", theme);
+    this.selectionBackgroundColor = setAlpha(getColor("selection-background", theme), 0.25);
+    this.selectionStripesColor = setAlpha("#fff", 0.1);
+    this.headerBackgroundColor = getColor("body-background", theme);
+    this.headerTextColor = getColor("body-color", theme);
+    this.headerTimelineStrokeColor = setAlpha(getColor("body-color-alt", theme), 0.1);
+    this.alternatingBackgroundColor = setAlpha(getColor("body-color", theme), 0.05);
   }
 });
 
 exports.MarkersOverview = MarkersOverview;
--- a/browser/devtools/timeline/widgets/memory-overview.js
+++ b/browser/devtools/timeline/widgets/memory-overview.js
@@ -8,67 +8,56 @@
  * of all the memory measurements taken while streaming the timeline data.
  */
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource:///modules/devtools/Graphs.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
+const { colorUtils: { setAlpha }} = require("devtools/css-color");
+const { getColor } = require("devtools/shared/theme");
+
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const OVERVIEW_HEIGHT = 30; // px
-
-const OVERVIEW_BACKGROUND_COLOR = "#fff";
-const OVERVIEW_BACKGROUND_GRADIENT_START = "rgba(0,136,204,0.1)";
-const OVERVIEW_BACKGROUND_GRADIENT_END = "rgba(0,136,204,0.0)";
 const OVERVIEW_STROKE_WIDTH = 1; // px
-const OVERVIEW_STROKE_COLOR = "rgba(0,136,204,1)";
-const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
-const OVERVIEW_SELECTION_LINE_COLOR = "#555";
 const OVERVIEW_MAXIMUM_LINE_COLOR = "rgba(0,136,204,0.4)";
 const OVERVIEW_AVERAGE_LINE_COLOR = "rgba(0,136,204,0.7)";
 const OVERVIEW_MINIMUM_LINE_COLOR = "rgba(0,136,204,0.9)";
-
-const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
-const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
+const OVERVIEW_SELECTION_LINE_COLOR = "#555";
 
 /**
  * An overview for the memory data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
  */
 function MemoryOverview(parent) {
-  LineGraphWidget.call(this, parent, L10N.getStr("graphs.memory"));
-
+  LineGraphWidget.call(this, parent, { metric: L10N.getStr("graphs.memory") });
+  this.setTheme();
   this.once("ready", () => {
     // Populate this overview with some dummy initial data.
     this.setData({ interval: { startTime: 0, endTime: 1000 }, memory: [] });
   });
 }
 
 MemoryOverview.prototype = Heritage.extend(LineGraphWidget.prototype, {
   dampenValuesFactor: 0.95,
   fixedHeight: OVERVIEW_HEIGHT,
-  backgroundColor: OVERVIEW_BACKGROUND_COLOR,
-  backgroundGradientStart: OVERVIEW_BACKGROUND_GRADIENT_START,
-  backgroundGradientEnd: OVERVIEW_BACKGROUND_GRADIENT_END,
-  strokeColor: OVERVIEW_STROKE_COLOR,
   strokeWidth: OVERVIEW_STROKE_WIDTH,
   maximumLineColor: OVERVIEW_MAXIMUM_LINE_COLOR,
   averageLineColor: OVERVIEW_AVERAGE_LINE_COLOR,
   minimumLineColor: OVERVIEW_MINIMUM_LINE_COLOR,
   clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
   selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
-  selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
-  selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
   withTooltipArrows: false,
   withFixedTooltipPositions: true,
 
   /**
    * Disables selection and empties this graph.
    */
   clearView: function() {
     this.selectionEnabled = false;
@@ -77,12 +66,27 @@ MemoryOverview.prototype = Heritage.exte
   },
 
   /**
    * Sets the data source for this graph.
    */
   setData: function({ interval, memory }) {
     this.dataOffsetX = interval.startTime;
     LineGraphWidget.prototype.setData.call(this, memory);
+  },
+
+  /**
+   * Sets the theme via `theme` to either "light" or "dark",
+   * and updates the internal styling to match. Requires a redraw
+   * to see the effects.
+   */
+  setTheme: function (theme) {
+    theme = theme || "light";
+    this.backgroundColor = getColor("body-background", theme);
+    this.backgroundGradientStart = setAlpha(getColor("highlight-blue", theme), 0.1);
+    this.backgroundGradientEnd = setAlpha(getColor("highlight-blue", theme), 0);
+    this.strokeColor = getColor("highlight-blue", theme);
+    this.selectionBackgroundColor = setAlpha(getColor("selection-background", theme), 0.25);
+    this.selectionStripesColor = "rgba(255, 255, 255, 0.1)";
   }
 });
 
 exports.MemoryOverview = MemoryOverview;
--- a/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-param-flags.js
+++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-param-flags.js
@@ -4,25 +4,26 @@
 /**
  * Test AudioNode#getParamFlags()
  */
 
 add_task(function*() {
   let { target, front } = yield initBackend(SIMPLE_NODES_URL);
   let [_, nodes] = yield Promise.all([
     front.setup({ reload: true }),
-    getN(front, "create-node", 14)
+    getN(front, "create-node", 15)
   ]);
 
   let allNodeParams = yield Promise.all(nodes.map(node => node.getParams()));
   let nodeTypes = [
     "AudioDestinationNode",
     "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode",
     "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode",
-    "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode"
+    "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode",
+    "StereoPannerNode"
   ];
 
   // For some reason nodeTypes.forEach and params.forEach fail here so we use
   // simple for loops.
   for (let i = 0; i < nodeTypes.length; i++) {
     let type = nodeTypes[i];
     let params = allNodeParams[i];
 
--- a/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-params-01.js
+++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-params-01.js
@@ -4,25 +4,26 @@
 /**
  * Test AudioNode#getParams()
  */
 
 add_task(function*() {
   let { target, front } = yield initBackend(SIMPLE_NODES_URL);
   let [_, nodes] = yield Promise.all([
     front.setup({ reload: true }),
-    getN(front, "create-node", 14)
+    getN(front, "create-node", 15)
   ]);
 
   let allNodeParams = yield Promise.all(nodes.map(node => node.getParams()));
   let nodeTypes = [
     "AudioDestinationNode",
     "AudioBufferSourceNode", "ScriptProcessorNode", "AnalyserNode", "GainNode",
     "DelayNode", "BiquadFilterNode", "WaveShaperNode", "PannerNode", "ConvolverNode",
-    "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode"
+    "ChannelSplitterNode", "ChannelMergerNode", "DynamicsCompressorNode", "OscillatorNode",
+    "StereoPannerNode"
   ];
 
   nodeTypes.forEach((type, i) => {
     let params = allNodeParams[i];
     params.forEach(({param, value, flags}) => {
       ok(param in NODE_DEFAULT_VALUES[type], "expected parameter for " + type);
 
       ok(typeof flags === "object", type + " has a flags object");
--- a/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-params-02.js
+++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-params-02.js
@@ -5,25 +5,25 @@
  * Tests that default properties are returned with the correct type
  * from the AudioNode actors.
  */
 
 add_task(function*() {
   let { target, front } = yield initBackend(SIMPLE_NODES_URL);
   let [_, nodes] = yield Promise.all([
     front.setup({ reload: true }),
-    getN(front, "create-node", 14)
+    getN(front, "create-node", 15)
   ]);
 
   let allParams = yield Promise.all(nodes.map(node => node.getParams()));
   let types = [
     "AudioDestinationNode", "AudioBufferSourceNode", "ScriptProcessorNode",
     "AnalyserNode", "GainNode", "DelayNode", "BiquadFilterNode", "WaveShaperNode",
     "PannerNode", "ConvolverNode", "ChannelSplitterNode", "ChannelMergerNode",
-    "DynamicsCompressorNode", "OscillatorNode"
+    "DynamicsCompressorNode", "OscillatorNode", "StereoPannerNode"
   ];
 
   allParams.forEach((params, i) => {
     compare(params, NODE_DEFAULT_VALUES[types[i]], types[i]);
   });
 
   yield removeTab(target.tab);
 });
--- a/browser/devtools/webaudioeditor/test/browser_wa_navigate.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_navigate.js
@@ -20,25 +20,25 @@ add_task(function*() {
 
   var { nodes, edges } = countGraphObjects(panelWin);
   ise(nodes, 3, "should only be 3 nodes.");
   ise(edges, 2, "should only be 2 edges.");
 
   navigate(target, SIMPLE_NODES_URL);
 
   var [actors] = yield Promise.all([
-    getN(gFront, "create-node", 14),
-    waitForGraphRendered(panelWin, 14, 0)
+    getN(gFront, "create-node", 15),
+    waitForGraphRendered(panelWin, 15, 0)
   ]);
 
   is($("#reload-notice").hidden, true,
     "The 'reload this page' notice should be hidden after context found after navigation.");
   is($("#waiting-notice").hidden, true,
     "The 'waiting for an audio context' notice should be hidden after context found after navigation.");
   is($("#content").hidden, false,
     "The tool's content should reappear without closing and reopening the toolbox.");
 
   var { nodes, edges } = countGraphObjects(panelWin);
-  ise(nodes, 14, "after navigation, should have 14 nodes");
+  ise(nodes, 15, "after navigation, should have 15 nodes");
   ise(edges, 0, "after navigation, should have 0 edges.");
 
   yield teardown(target);
 });
--- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params.js
@@ -12,18 +12,18 @@ add_task(function*() {
   let { gFront, $, $$, EVENTS, InspectorView } = panelWin;
   let gVars = InspectorView._propsView;
 
   let started = once(gFront, "start-context");
 
   reload(target);
 
   let [actors] = yield Promise.all([
-    getN(gFront, "create-node", 14),
-    waitForGraphRendered(panelWin, 14, 0)
+    getN(gFront, "create-node", 15),
+    waitForGraphRendered(panelWin, 15, 0)
   ]);
   let nodeIds = actors.map(actor => actor.actorID);
   let types = [
     "AudioDestinationNode", "AudioBufferSourceNode", "ScriptProcessorNode",
     "AnalyserNode", "GainNode", "DelayNode", "BiquadFilterNode", "WaveShaperNode",
     "PannerNode", "ConvolverNode", "ChannelSplitterNode", "ChannelMergerNode",
     "DynamicsCompressorNode", "OscillatorNode"
   ];
--- a/browser/devtools/webaudioeditor/test/doc_simple-node-creation.html
+++ b/browser/devtools/webaudioeditor/test/doc_simple-node-creation.html
@@ -13,16 +13,16 @@
     <script type="text/javascript;version=1.8">
       "use strict";
 
       let ctx = new AudioContext();
       let NODE_CREATION_METHODS = [
           "createBufferSource", "createScriptProcessor", "createAnalyser",
           "createGain", "createDelay", "createBiquadFilter", "createWaveShaper",
           "createPanner", "createConvolver", "createChannelSplitter", "createChannelMerger",
-          "createDynamicsCompressor", "createOscillator"
+          "createDynamicsCompressor", "createOscillator", "createStereoPanner"
       ];
       let nodes = NODE_CREATION_METHODS.map(method => ctx[method]());
 
     </script>
   </body>
 
 </html>
--- a/browser/devtools/webaudioeditor/test/head.js
+++ b/browser/devtools/webaudioeditor/test/head.js
@@ -492,10 +492,13 @@ const NODE_DEFAULT_VALUES = {
     "reduction": 0,
     "attack": 0.003000000026077032,
     "release": 0.25
   },
   "OscillatorNode": {
     "type": "sine",
     "frequency": 440,
     "detune": 0
+  },
+  "StereoPannerNode": {
+    "pan": 0
   }
 };
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -5,27 +5,40 @@
 # LOCALIZATION NOTE (needUserShort)
 # %S = Firefox Accounts brand name from syncBrand.dtd
 needUserShort = %S required for sync
 needUserLong = We've rebuilt Sync to make it easier for everyone. Please upgrade to a Firefox Account to continue syncing.
 
 upgradeToFxA.label = Upgrade
 upgradeToFxA.accessKey = U
 
+# LOCALIZATION NOTE (signInAfterUpgradeOnOtherDevice.description)
+# %S = Email address of user's Firefox Account
+signInAfterUpgradeOnOtherDevice.description = Sync was upgraded on another device by %S. Resume syncing?
+signInAfterUpgradeOnOtherDevice.label = Sign In
+signInAfterUpgradeOnOtherDevice.accessKey = S
+
 # LOCALIZATION NOTE (needVerifiedUserShort, needVerifiedUserLong)
 # %S = Email address of user's Firefox Account
 needVerifiedUserShort = %S not verified
 needVerifiedUserLong = Please click the verification link in the email sent to %S
 
 resendVerificationEmail.label = Resend
 resendVerificationEmail.accessKey = R
 
 forgetMigration.label = Forget
 forgetMigration.accessKey = F
 
+unlinkMigration.label = Unlink Sync
+unlinkMigration.accessKey = L
+
+unlinkVerificationTitle = Unlink old version of Sync?
+unlinkVerificationDescription = If you no longer want to be reminded about upgrading Sync, you can unlink your old Sync account to remove it.
+unlinkVerificationConfirm = Unlink
+
 # These strings are used in a dialog we display after the user requests we resend
 # a verification email.
 verificationSentTitle = Verification Sent
 # LOCALIZATION NOTE (verificationSentHeading) - %S = Email address of user's Firefox Account
 verificationSentHeading = A verification link has been sent to %S
 verificationSentDescription = Please check your email and click the link to begin syncing.
 
 verificationNotSentTitle = Unable to Send Verification
--- a/browser/locales/en-US/chrome/browser/devtools/projecteditor.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/projecteditor.properties
@@ -41,16 +41,21 @@ projecteditor.deleteFolderPromptMessage=
 # to make sure if a file should be removed.
 projecteditor.deleteFilePromptMessage=Are you sure you want to delete this file?
 
 # LOCALIZATION NOTE (projecteditor.newLabel):
 # This string is displayed as a menu item for adding a new file to
 # the directory.
 projecteditor.newLabel=New…
 
+# LOCALIZATION NOTE (projecteditor.renameLabel):
+# This string is displayed as a menu item for renaming a file in
+# the directory.
+projecteditor.renameLabel=Rename
+
 # LOCALIZATION NOTE (projecteditor.saveLabel):
 # This string is displayed as a menu item for saving the current file.
 projecteditor.saveLabel=Save
 
 # LOCALIZATION NOTE (projecteditor.saveAsLabel):
 # This string is displayed as a menu item for saving the current file
 # with a new name.
 projecteditor.saveAsLabel=Save As…
--- a/browser/locales/en-US/chrome/browser/devtools/timeline.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties
@@ -30,20 +30,21 @@ timeline.tooltip=Performance Timeline
 timeline.tick=%S ms
 
 # LOCALIZATION NOTE (timeline.records):
 # This string is displayed in the timeline waterfall, as a title for the menu.
 timeline.records=RECORDS
 
 # LOCALIZATION NOTE (timeline.label.*):
 # These strings are displayed in the timeline waterfall, identifying markers.
-timeline.label.styles=Styles
-timeline.label.reflow=Reflow
+# We want to use the same wording as Google Chrome
+timeline.label.styles2=Recalculate Style
+timeline.label.reflow2=Layout
 timeline.label.paint=Paint
-timeline.label.javascript=Javascript
+timeline.label.javascript2=Function Call
 timeline.label.domevent=DOM Event
 timeline.label.consoleTime=Console
 
 # LOCALIZATION NOTE (graphs.memory):
 # This string is displayed in the memory graph of the Performance tool,
 # as the unit used to memory consumption. This label should be kept
 # AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
 graphs.memory=MB
--- a/browser/locales/en-US/chrome/browser/preferences/sync.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
@@ -73,11 +73,8 @@ both, to better adapt this sentence to t
 <!ENTITY verify.label                "Verify Email">
 <!ENTITY forget.label                "Forget this Email">
 
 <!ENTITY welcome.description "Access your tabs, bookmarks, passwords and more wherever you use &brandShortName;.">
 <!ENTITY welcome.signIn.label "Sign In">
 <!ENTITY welcome.createAccount.label "Create Account">
 
 <!ENTITY welcome.useOldSync.label "Using an older version of Sync?">
-
-<!-- Sync Migration -->
-<!ENTITY migrate.upgradeNeeded      "The sync account system is being discontinued. A new Firefox Account is required to sync.">
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -280,19 +280,21 @@ this.BrowserUITelemetry = {
     // our measurements because at that point all browser windows have
     // probably been closed, since the vast majority of saved-session
     // pings are gathered during shutdown.
     let win = RecentWindow.getMostRecentBrowserWindow({
       private: false,
       allowPopups: false,
     });
 
-    // If there are no such windows, we're out of luck. :(
-    this._firstWindowMeasurements = win ? this._getWindowMeasurements(win)
-                                        : {};
+    Services.search.init(rv => {
+      // If there are no such windows, we're out of luck. :(
+      this._firstWindowMeasurements = win ? this._getWindowMeasurements(win, rv)
+                                          : {};
+    });
   },
 
   _registerWindow: function(aWindow) {
     aWindow.addEventListener("unload", this);
     let document = aWindow.document;
 
     for (let areaID of CustomizableUI.areas) {
       let areaNode = document.getElementById(areaID);
@@ -459,17 +461,17 @@ this.BrowserUITelemetry = {
     // If not, we need to check if one of the ancestors of the clicked
     // item is in our list of built-in items to check.
     let candidate = getIDBasedOnFirstIDedAncestor(item);
     if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) {
       this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button);
     }
   },
 
-  _getWindowMeasurements: function(aWindow) {
+  _getWindowMeasurements: function(aWindow, searchResult) {
     let document = aWindow.document;
     let result = {};
 
     // Determine if the window is in the maximized, normal or
     // fullscreen state.
     result.sizemode = document.documentElement.getAttribute("sizemode");
 
     // Determine if the Bookmarks bar is currently visible
@@ -548,16 +550,20 @@ this.BrowserUITelemetry = {
         let visibleTabsNum = someWin.gBrowser.visibleTabs.length;
         visibleTabs.push(visibleTabsNum);
         hiddenTabs.push(someWin.gBrowser.tabs.length - visibleTabsNum);
       }
     }
     result.visibleTabs = visibleTabs;
     result.hiddenTabs = hiddenTabs;
 
+    if (Components.isSuccessCode(searchResult)) {
+      result.currentSearchEngine = Services.search.currentEngine;
+    }
+
     return result;
   },
 
   getToolbarMeasures: function() {
     let result = this._firstWindowMeasurements || {};
     result.countableEvents = this._countableEvents;
     result.durations = this._durations;
     return result;
@@ -572,16 +578,24 @@ this.BrowserUITelemetry = {
     if ((/^[a-zA-Z]+:[^\/\\]/).test(query)) {
       this._countEvent(["search", "urlbar-keyword"]);
     }
     if (selection) {
       this._countEvent(["search", "selection", source, selection.index, selection.kind]);
     }
   },
 
+  countOneoffSearchEvent: function(id, type, where) {
+    this._countEvent(["search-oneoff", id, type, where]);
+  },
+
+  countSearchSettingsEvent: function(source) {
+    this._countEvent(["click-builtin-item", source, "search-settings"]);
+  },
+
   _logAwesomeBarSearchResult: function (url) {
     let spec = Services.search.parseSubmissionURL(url);
     if (spec.engine) {
       let matchedEngine = "default";
       if (spec.engine.name !== Services.search.currentEngine.name) {
         matchedEngine = "other";
       }
       this.countSearchEvent("autocomplete-" + matchedEngine);
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -642,40 +642,42 @@ toolbarpaletteitem[place="palette"] > to
   box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
 }
 
 #PanelUI-fxa-status:not([disabled]):hover,
 #PanelUI-fxa-status:not([disabled]):hover:active {
   outline: none;
 }
 
+#PanelUI-update-status {
+  color: black;
+}
+
 #PanelUI-update-status[update-status="succeeded"] {
-  background-color: hsla(96, 65%, 75%, 0.1);
+  background-color: hsla(96, 65%, 75%, 0.5);
 }
 
 #PanelUI-update-status[update-status="succeeded"]:not([disabled]):hover {
-  background-color: hsla(96, 65%, 75%, 0.4);
+  background-color: hsla(96, 65%, 75%, 0.8);
 }
 
 #PanelUI-update-status[update-status="succeeded"]:not([disabled]):hover:active {
-  background-color: hsla(96, 65%, 75%, 0.6);
-  box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
+  background-color: hsl(96, 65%, 75%);
 }
 
 #PanelUI-update-status[update-status="failed"] {
-  background-color: hsla(359, 69%, 84%, 0.1);
+  background-color: hsla(359, 69%, 84%, 0.5);
 }
 
 #PanelUI-update-status[update-status="failed"]:not([disabled]):hover {
-  background-color: hsla(359, 69%, 84%, 0.4);
+  background-color: hsla(359, 69%, 84%, 0.8);
 }
 
 #PanelUI-update-status[update-status="failed"]:not([disabled]):hover:active {
-  background-color: hsla(359, 69%, 84%, 0.6);
-  box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
+  background-color: hsl(359, 69%, 84%);
 }
 
 #PanelUI-quit:not([disabled]):hover {
   background-color: #d94141;
   outline-color: #c23a3a;
 }
 
 #PanelUI-quit:not([disabled]):hover:active {
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -286,16 +286,21 @@ description > html|a {
 
 /**
  * End Dialog
  */
 
 /**
  * Sync migration
  */
+#sync-migrate-upgrade-description {
+  /* description elts need a min-width to wrap correctly - bug 630864? */
+  min-width: 100px
+}
+
 #sync-migration {
   border: 1px solid rgba(0, 0, 0, 0.32);
   background-color: InfoBackground;
   color: InfoText;
   text-shadow: none;
   margin: 5px 0 0 0;
   animation: fadein 3000ms;
 }
--- a/docshell/base/TimelineMarker.h
+++ b/docshell/base/TimelineMarker.h
@@ -98,17 +98,16 @@ protected:
 
 private:
 
   const char* mName;
   TracingMetadata mMetaData;
   DOMHighResTimeStamp mTime;
   nsString mCause;
 
-  // While normally it is not a good idea to make a persistent
-  // root, in this case changing nsDocShell to participate in
-  // cycle collection was deemed too invasive, the stack trace
-  // can't actually cause a cycle, and the markers are only held
+  // While normally it is not a good idea to make a persistent root,
+  // in this case changing nsDocShell to participate in cycle
+  // collection was deemed too invasive, and the markers are only held
   // here temporarily to boot.
   mozilla::Maybe<JS::PersistentRooted<JSObject*>> mStackTrace;
 };
 
 #endif /* TimelineMarker_h__ */
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -658,16 +658,20 @@ pref("ui.scrolling.min_scrollable_distan
 // The axis lock mode for panning behaviour - set between standard, free and sticky
 pref("ui.scrolling.axis_lock_mode", "standard");
 // Negate scrollY, true will make the mouse scroll wheel move the screen the same direction as with most desktops or laptops.
 pref("ui.scrolling.negate_wheel_scrollY", true);
 // Determine the dead zone for gamepad joysticks. Higher values result in larger dead zones; use a negative value to
 // auto-detect based on reported hardware values
 pref("ui.scrolling.gamepad_dead_zone", 115);
 
+// Prefs for fling acceleration
+pref("ui.scrolling.fling_accel_interval", 500);
+pref("ui.scrolling.fling_accel_base_multiplier", "1.0");
+pref("ui.scrolling.fling_accel_supplemental_multiplier", "1.0");
 
 // Enable accessibility mode if platform accessibility is enabled.
 pref("accessibility.accessfu.activate", 2);
 pref("accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement,Landmark,ListItem");
 // Active quicknav mode, index value of list from quicknav_modes
 pref("accessibility.accessfu.quicknav_index", 0);
 // Setting for an utterance order (0 - description first, 1 - description last).
 pref("accessibility.accessfu.utterance", 1);
--- a/mobile/android/base/home/ReadingListRow.java
+++ b/mobile/android/base/home/ReadingListRow.java
@@ -5,20 +5,22 @@
 
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.home.TwoLinePageRow;
+import org.mozilla.gecko.util.StringUtils;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class ReadingListRow extends LinearLayout {
 
     private final Resources resources;
@@ -50,21 +52,23 @@ public class ReadingListRow extends Line
         readTime = (TextView) findViewById(R.id.read_time);
     }
 
     public void updateFromCursor(Cursor cursor) {
         if (cursor == null) {
             return;
         }
 
-        final int titleIndex = cursor.getColumnIndexOrThrow(ReadingListItems.TITLE);
-        title.setText(cursor.getString(titleIndex));
+        final String url = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.URL));
 
-        final int excerptIndex = cursor.getColumnIndexOrThrow(ReadingListItems.EXCERPT);
-        excerpt.setText(cursor.getString(excerptIndex));
+        final String titleText = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.TITLE));
+        title.setText(TextUtils.isEmpty(titleText) ? StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url)) : titleText);
+
+        final String excerptText = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.EXCERPT));
+        excerpt.setText(TextUtils.isEmpty(excerptText) ? url : excerptText);
 
         /* Disabled until UX issues are fixed (see bug 1110461).
         final int lengthIndex = cursor.getColumnIndexOrThrow(ReadingListItems.LENGTH);
         final int minutes = getEstimatedReadTime(cursor.getInt(lengthIndex));
         if (minutes <= 60) {
             readTime.setText(resources.getString(R.string.reading_list_time_minutes, minutes));
         } else {
             readTime.setText(resources.getString(R.string.reading_list_time_over_an_hour));
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -113,17 +113,17 @@
 <!ENTITY overlay_share_send_tab_btn_label "Send to another device">
 <!ENTITY overlay_share_no_url "No link found in this share">
 <!ENTITY overlay_share_retry "Try again">
 <!ENTITY overlay_share_select_device "Select device">
 
 <!ENTITY pref_category_search3 "Search">
 <!ENTITY pref_category_search_summary "Customize your search providers">
 <!ENTITY pref_category_display "Display">
-<!ENTITY pref_category_display_summary "Text, title bar, full-screen browsing">
+<!ENTITY pref_category_display_summary2 "Text, full-screen browsing">
 <!ENTITY pref_category_privacy_short "Privacy">
 <!ENTITY pref_category_privacy_summary "Control passwords, cookies, tracking, data">
 <!ENTITY pref_category_vendor "&vendorShortName;">
 <!ENTITY pref_category_vendor_summary "About &brandShortName;, FAQs, data choices">
 <!ENTITY pref_category_datareporting "Data choices">
 <!ENTITY pref_learn_more "Learn more">
 <!ENTITY pref_category_installed_search_engines "Installed search engines">
 <!ENTITY pref_category_add_search_providers "Add more search providers">
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -121,17 +121,17 @@
   <string name="settings">&settings;</string>
   <string name="settings_title">&settings_title;</string>
   <string name="pref_category_advanced">&pref_category_advanced;</string>
   <string name="pref_category_customize">&pref_category_customize;</string>
   <string name="pref_category_customize_summary">&pref_category_customize_summary;</string>
   <string name="pref_category_search">&pref_category_search3;</string>
   <string name="pref_category_search_summary">&pref_category_search_summary;</string>
   <string name="pref_category_display">&pref_category_display;</string>
-  <string name="pref_category_display_summary">&pref_category_display_summary;</string>
+  <string name="pref_category_display_summary">&pref_category_display_summary2;</string>
   <string name="pref_category_privacy_short">&pref_category_privacy_short;</string>
   <string name="pref_category_privacy_summary">&pref_category_privacy_summary;</string>
   <string name="pref_category_vendor">&pref_category_vendor;</string>
   <string name="pref_category_vendor_summary">&pref_category_vendor_summary;</string>
   <string name="pref_category_datareporting">&pref_category_datareporting;</string>
   <string name="pref_category_installed_search_engines">&pref_category_installed_search_engines;</string>
   <string name="pref_category_add_search_providers">&pref_category_add_search_providers;</string>
   <string name="pref_category_search_restore_defaults">&pref_category_search_restore_defaults;</string>
--- a/mobile/android/base/tests/StringHelper.java
+++ b/mobile/android/base/tests/StringHelper.java
@@ -1,8 +1,13 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.tests;
 
 
 public class StringHelper {
     private StringHelper() {}
 
     public static final String OK = "OK";
 
--- a/mobile/android/base/widget/FadedMultiColorTextView.java
+++ b/mobile/android/base/widget/FadedMultiColorTextView.java
@@ -50,18 +50,20 @@ public class FadedMultiColorTextView ext
         final boolean needsEllipsis = needsEllipsis();
         if (needsEllipsis) {
             final int right = getWidth() - getCompoundPaddingRight();
             final float left = right - fadeWidth;
 
             updateGradientShader(needsEllipsis, right);
 
             final float center = getHeight() / 2;
-            final float top = center - getTextSize();
-            final float bottom = center + getTextSize();
+
+            // Shrink height of gradient to prevent it overlaying parent view border.
+            final float top = center - getTextSize() + 1;
+            final float bottom = center + getTextSize() - 1;
 
             canvas.drawRect(left, top, right, bottom, fadePaint);
         }
     }
 
     private void updateGradientShader(final boolean needsEllipsis, final int gradientEndRight) {
         final int backgroundColor =
                 fadeBackgroundColorList.getColorForState(getDrawableState(), Color.RED);
--- a/services/sync/modules/FxaMigrator.jsm
+++ b/services/sync/modules/FxaMigrator.jsm
@@ -133,21 +133,24 @@ Migrator.prototype = {
         this.log.error(msg);
         return this._promiseCurrentUserState(forceObserver)
       }
     );
   },
 
   _promiseCurrentUserState: Task.async(function* (forceObserver) {
     this.log.trace("starting _promiseCurrentUserState");
-    let update = (newState, subject=null) => {
+    let update = (newState, email=null) => {
       this.log.info("Migration state: '${state}' => '${newState}'",
                     {state: this._state, newState: newState});
       if (forceObserver || newState !== this._state) {
         this._state = newState;
+        let subject = Cc["@mozilla.org/supports-string;1"]
+                      .createInstance(Ci.nsISupportsString);
+        subject.data = email || "";
         Services.obs.notifyObservers(subject, OBSERVER_STATE_CHANGE_TOPIC, newState);
       }
       return newState;
     }
 
     // If we have no sync user, or are already using an FxA account we must
     // be done.
     if (WeaveService.fxAccountsEnabled) {
@@ -168,23 +171,24 @@ Migrator.prototype = {
 
     if (!isEOL) {
       return update(null);
     }
 
     // So we are in EOL mode - have we a user?
     let fxauser = yield fxAccounts.getSignedInUser();
     if (!fxauser) {
-      return update(this.STATE_USER_FXA);
+      // See if there is a migration sentinel so we can send the email
+      // address that was used on a different device for this account (ie, if
+      // this is a "join the party" migration rather than the first)
+      let sentinel = yield this._getSyncMigrationSentinel();
+      return update(this.STATE_USER_FXA, sentinel && sentinel.email);
     }
     if (!fxauser.verified) {
-      let email = Cc["@mozilla.org/supports-string;1"].
-                  createInstance(Ci.nsISupportsString);
-      email.data = fxauser.email || "";
-      return update(this.STATE_USER_FXA_VERIFIED, email);
+      return update(this.STATE_USER_FXA_VERIFIED, fxauser.email);
     }
 
     // So we just have housekeeping to do - we aren't blocked on a user, so
     // reflect that.
     this.log.info("No next user state - doing some housekeeping");
     update(null);
 
     // We need to disable sync from automatically starting,
@@ -196,24 +200,42 @@ Migrator.prototype = {
       // our observers will kick us further along when complete.
       this.log.info("waiting for sync to complete")
       return null;
     }
 
     // Write the migration sentinel if necessary.
     yield this._setMigrationSentinelIfNecessary();
 
+    // Get the list of enabled engines to we can restore that state.
+    let enginePrefs = this._getEngineEnabledPrefs();
+
     // Must be ready to perform the actual migration.
     this.log.info("Performing final sync migration steps");
     // Do the actual migration.
     let startOverComplete = new Promise((resolve, reject) => {
       let observe;
       Services.obs.addObserver(observe = () => {
         this.log.info("observed that startOver is complete");
         Services.obs.removeObserver(observe, "weave:service:start-over:finish");
+        // We've now reset all sync prefs - set the engine related prefs back to
+        // what they were.
+        for (let [prefName, prefType, prefVal] of enginePrefs) {
+          switch (prefType) {
+            case Services.prefs.PREF_BOOL:
+              Services.prefs.setBoolPref(prefName, prefVal);
+              break;
+            case Services.prefs.PREF_STRING:
+              Services.prefs.setCharPref(prefName, prefVal);
+              break;
+            default:
+              // _getEngineEnabledPrefs doesn't return any other type...
+              Cu.reportError("unknown engine pref type for " + prefName + ": " + prefType);
+          }
+        }
         resolve();
       }, "weave:service:start-over:finish", false);
     });
 
     Weave.Service.startOver();
     // need to wait for an observer.
     yield startOverComplete;
     // observer fired, now kick things off with the FxA user.
@@ -306,16 +328,43 @@ Migrator.prototype = {
   _blockSync() {
     Weave.Service.scheduler.blockSync();
   },
 
   _unblockSync() {
     Weave.Service.scheduler.unblockSync();
   },
 
+  /* Return a list of [prefName, prefType, prefVal] for all engine related
+     preferences.
+  */
+  _getEngineEnabledPrefs() {
+    let result = [];
+    for (let engine of Weave.Service.engineManager.getAll()) {
+      let prefName = "services.sync.engine." + engine.prefName;
+      let prefVal;
+      try {
+        prefVal = Services.prefs.getBoolPref(prefName);
+        result.push([prefName, Services.prefs.PREF_BOOL, prefVal]);
+      } catch (ex) {} /* just skip this pref */
+    }
+    // and the declined list.
+    try {
+      let prefName = "services.sync.declinedEngines";
+      let prefVal = Services.prefs.getCharPref(prefName);
+      result.push([prefName, Services.prefs.PREF_STRING, prefVal]);
+    } catch (ex) {}
+    return result;
+  },
+
+  /* return true if all engines are enabled, false otherwise. */
+  _allEnginesEnabled() {
+    return Weave.Service.engineManager.getAll().every(e => e.enabled);
+  },
+
   /*
    * Some helpers for the UI to try and move to the next state.
    */
 
   // Open a UI for the user to create a Firefox Account.  This should only be
   // called while we are in the STATE_USER_FXA state.  When the user completes
   // the creation we'll see an ONLOGIN_NOTIFICATION notification from FxA and
   // we'll move to either the STATE_USER_FXA_VERIFIED state or we'll just
@@ -333,17 +382,22 @@ Migrator.prototype = {
       this._applySentinelPrefs(sentinel.prefs);
     }
     // If we already have a sentinel then we assume the user has previously
     // created the specified account, so just ask to sign-in.
     let action = sentinel ? "signin" : "signup";
     // See if we can find a default account name to use.
     let email = yield this._getDefaultAccountName(sentinel);
     let tail = email ? "&email=" + encodeURIComponent(email) : "";
-    win.switchToTabHavingURI("about:accounts?" + action + tail, true,
+    // We want to ask FxA to offer a "Customize Sync" checkbox iff any engines
+    // are disabled.
+    let customize = !this._allEnginesEnabled();
+    tail += "&customizeSync=" + customize;
+
+    win.switchToTabHavingURI("about:accounts?action=" + action + tail, true,
                              {ignoreFragment: true, replaceQueryString: true});
     // An FxA observer will fire when the user completes this, which will
     // cause us to move to the next "user blocked" state and notify via our
     // observer notification.
   }),
 
   // Ask the FxA servers to re-send a verification mail for the currently
   // logged in user. This should only be called while we are in the
--- a/services/sync/tests/unit/test_fxa_migration.js
+++ b/services/sync/tests/unit/test_fxa_migration.js
@@ -85,27 +85,38 @@ function configureFxa() {
 }
 
 add_task(function *testMigration() {
   configureFxa();
 
   // when we do a .startOver we want the new provider.
   let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
   Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false);
+
+  // disable the addons engine - this engine choice is arbitrary, but we
+  // want to check it remains disabled after migration.
+  Services.prefs.setBoolPref("services.sync.engine.addons", false);
+
   do_register_cleanup(() => {
     Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue)
+    Services.prefs.setBoolPref("services.sync.engine.addons", true);
   });
 
   // No sync user - that should report no user-action necessary.
   Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null,
                    "no user state when complete");
 
   // Arrange for a legacy sync user and manually bump the migrator
   let [engine, server] = configureLegacySync();
 
+  // Check our disabling of the "addons" engine worked, and for good measure,
+  // that the "passwords" engine is enabled.
+  Assert.ok(!Service.engineManager.get("addons").enabled, "addons is disabled");
+  Assert.ok(Service.engineManager.get("passwords").enabled, "passwords is enabled");
+
   // monkey-patch the migration sentinel code so we know it was called.
   let haveStartedSentinel = false;
   let origSetFxAMigrationSentinel = Service.setFxAMigrationSentinel;
   let promiseSentinelWritten = new Promise((resolve, reject) => {
     Service.setFxAMigrationSentinel = function(arg) {
       haveStartedSentinel = true;
       return origSetFxAMigrationSentinel.call(Service, arg).then(result => {
         Service.setFxAMigrationSentinel = origSetFxAMigrationSentinel;
@@ -244,16 +255,19 @@ add_task(function *testMigration() {
   Assert.ok(Service.identity instanceof BrowserIDManager,
             "sync is configured with the browserid_identity provider.");
   Assert.equal(Service.identity.username, config.username, "correct user configured")
   Assert.ok(!Service.scheduler.isBlocked, "sync is not blocked.")
   // and the user state should remain null.
   Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()),
                    null,
                    "still no user action necessary");
+  // and our engines should be in the same enabled/disabled state as before.
+  Assert.ok(!Service.engineManager.get("addons").enabled, "addons is still disabled");
+  Assert.ok(Service.engineManager.get("passwords").enabled, "passwords is still enabled");
 
   // aaaand, we are done - clean up.
   yield promiseStopServer(server);
 });
 
 
 function run_test() {
   initTestLogging();
--- a/toolkit/content/aboutwebrtc/aboutWebrtc.js
+++ b/toolkit/content/aboutwebrtc/aboutWebrtc.js
@@ -1,16 +1,19 @@
 /** @jsx React.DOM */
 /* 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/. */
 
+/* jshint newcap:false */
+/* global React, WebrtcGlobalInformation, document */
+
 "use strict";
 
-var Tabs = React.createClass({displayName: 'Tabs',
+var Tabs = React.createClass({displayName: "Tabs",
   getDefaultProps: function() {
     return {selectedIndex: 0};
   },
 
   getInitialState: function() {
     return {selectedIndex: this.props.selectedIndex};
   },
 
@@ -21,123 +24,107 @@ var Tabs = React.createClass({displayNam
     }.bind(this);
   },
 
   _findSelectedTabContent: function() {
     // Using map() to filter children…
     // https://github.com/facebook/react/issues/1644#issuecomment-45138113
     return React.Children.map(this.props.children, function(tab, i) {
       return i === this.state.selectedIndex ? tab : null;
-    }.bind(this))
+    }.bind(this));
   },
 
   render: function() {
     var cx = React.addons.classSet;
     return (
-      React.DOM.div({className: "tabs"}, 
-        React.DOM.ul(null, 
+      React.createElement("div", {className: "tabs"}, 
+        React.createElement("ul", null, 
           React.Children.map(this.props.children, function(tab, i) {
             return (
-              React.DOM.li({className: cx({active: i === this.state.selectedIndex})}, 
-                React.DOM.a({href: "#", key: i, onClick: this.selectTab(i)}, 
+              React.createElement("li", {className: cx({active: i === this.state.selectedIndex})}, 
+                React.createElement("a", {href: "#", key: i, onClick: this.selectTab(i)}, 
                   tab.props.title
                 )
               )
             );
           }.bind(this))
         ), 
         this._findSelectedTabContent()
       )
     );
   }
 });
 
-var Tab = React.createClass({displayName: 'Tab',
+var Tab = React.createClass({displayName: "Tab",
   render: function() {
-    return React.DOM.section(null, this.props.children);
+    return React.createElement("section", null, this.props.children);
   }
 });
 
-var AboutWebRTC = React.createClass({displayName: 'AboutWebRTC',
+var AboutWebRTC = React.createClass({displayName: "AboutWebRTC",
   getInitialState: function() {
     return {logs: null, reports: this.props.reports};
   },
 
   displayLogs: function() {
     WebrtcGlobalInformation.getLogging('', function(logs) {
       this.setState({logs: logs, reports: this.state.reports});
-    }.bind(this))
+    }.bind(this));
   },
 
   render: function() {
     return (
-      React.DOM.div(null, 
-        React.DOM.div({id: "stats"}, 
-          PeerConnections({reports: this.state.reports})
+      React.createElement("div", null, 
+        React.createElement("div", {id: "stats"}, 
+          React.createElement(PeerConnections, {reports: this.state.reports})
         ), 
-        React.DOM.div({id: "buttons"}, 
-          LogsButton({handler: this.displayLogs}), 
-          DebugButton(null), 
-          AECButton(null)
+        React.createElement("div", {id: "buttons"}, 
+          React.createElement(LogsButton, {handler: this.displayLogs}), 
+          React.createElement(DebugButton, null), 
+          React.createElement(AECButton, null)
         ), 
-        React.DOM.div({id: "logs"}, 
-          this.state.logs ? Logs({logs: this.state.logs}) : null
+        React.createElement("div", {id: "logs"}, 
+          this.state.logs ? React.createElement(Logs, {logs: this.state.logs}) : null
         )
       )
     );
   }
 });
 
-var PeerConnections = React.createClass({displayName: 'PeerConnections',
+var PeerConnections = React.createClass({displayName: "PeerConnections",
   getInitialState: function() {
     // Sort the reports to have the more recent at the top
     var reports = this.props.reports.slice().sort(function(r1, r2) {
       return r2.timestamp - r1.timestamp;
     });
 
     return {reports: reports};
   },
 
   render: function() {
     return (
-      React.DOM.div({className: "peer-connections"}, 
+      React.createElement("div", {className: "peer-connections"}, 
         
           this.state.reports.map(function(report, i) {
-            return PeerConnection({key: i, report: report});
+            return React.createElement(PeerConnection, {key: i, report: report});
           })
         
       )
     );
   }
 });
 
-var PeerConnection = React.createClass({displayName: 'PeerConnection',
+var PeerConnection = React.createClass({displayName: "PeerConnection",
   getPCInfo: function(report) {
-    // Extract id and url from pcid (varies as it is settable from chrome):
-    //   000 (id=00 url=http)
-    //   000 (session=000 call=000 id=00 url=http)
-    var idmatch = report.pcid.match(/id=(\S+)/);
-    var urlmatch = report.pcid.match(/url=([^)]+)/);
     return {
-      id: (idmatch? idmatch[1] : report.pcid),
-      url: (urlmatch? urlmatch[1] : ""),
+      id: report.pcid.match(/id=(\S+)/)[1],
+      url: report.pcid.match(/url=([^)]+)/)[1],
       closed: report.closed
     };
   },
-  getPCId: function(report) {
-    // Erase extracted id and url from pcid to avoid showing duplicate info
-    // - 000 (session=000 call=000 id=00 url=http)
-    // + 000 (session=000 call=000)
-    // - 000 (id=00 url=http)
-    // + 000
-
-    // regexp's match those in getPCInfo above, plus tidying.
-    return report.pcid.replace(/id=\S+/, "").replace(/url=[^)]+/, "")
-        .replace(/\s+[)]/, ")").replace(" ()", "");
-  },
 
   getIceCandidatePairs: function(report) {
     var candidates =
       report.iceCandidateStats.reduce(function(candidates, candidate) {
         candidates[candidate.id] = candidate;
 
         return candidates;
       }, {});
@@ -155,17 +142,17 @@ var PeerConnection = React.createClass({
         selected: pair.selected
       };
     });
 
     var pairedCandidates = pairs.reduce(function(paired, pair) {
       paired.add(pair.localCandidate.id);
       paired.add(pair.remoteCandidate.id);
 
-      return paired
+      return paired;
     }, new Set());
 
     var unifiedPairs =
       report.iceCandidateStats.reduce(function(pairs, candidate) {
         if (pairedCandidates.has(candidate)) {
             return pairs;
         }
 
@@ -193,52 +180,52 @@ var PeerConnection = React.createClass({
   },
 
   onFold: function() {
     this.setState({unfolded: !this.state.unfolded});
   },
 
   body: function(report, pairs) {
     return (
-      React.DOM.div(null, 
-        React.DOM.p({className: "pcid"}, "PeerConnection ID: ", this.getPCId(report)),
-        Tabs(null, 
-          Tab({title: "Ice Stats"}, 
-            IceStats({pairs: pairs})
+      React.createElement("div", null, 
+        React.createElement("p", {className: "pcid"}, "PeerConnection ID: ", report.pcid), 
+        React.createElement(Tabs, null, 
+          React.createElement(Tab, {title: "Ice Stats"}, 
+            React.createElement(IceStats, {pairs: pairs})
           ), 
-          Tab({title: "SDP"}, 
-            SDP({type: "local", sdp: report.localSdp}), 
-            SDP({type: "remote", sdp: report.remoteSdp})
+          React.createElement(Tab, {title: "SDP"}, 
+            React.createElement(SDP, {type: "local", sdp: report.localSdp}), 
+            React.createElement(SDP, {type: "remote", sdp: report.remoteSdp})
           ), 
-          Tab({title: "RTP Stats"}, 
-            RTPStats({report: report})
+          React.createElement(Tab, {title: "RTP Stats"}, 
+            React.createElement(RTPStats, {report: report})
           )
         )
       )
     );
   },
 
   render: function() {
     var report = this.props.report;
     var pcInfo = this.getPCInfo(report);
     var pairs  = this.getIceCandidatePairs(report);
 
     return (
-      React.DOM.div({className: "peer-connection"}, 
-        React.DOM.h3({onClick: this.onFold}, 
+      React.createElement("div", {className: "peer-connection"}, 
+        React.createElement("h3", {onClick: this.onFold}, 
           "[", pcInfo.id, "] ", pcInfo.url, " ", pcInfo.closed ? "(closed)" : null, 
           new Date(report.timestamp).toTimeString()
         ), 
         this.state.unfolded ? this.body(report, pairs) : undefined
       )
     );
   }
 });
 
-var IceStats = React.createClass({displayName: 'IceStats',
+var IceStats = React.createClass({displayName: "IceStats",
   sortHeadings: {
     "Local candidate":  "localCandidate",
     "Remote candidate": "remoteCandidate",
     "Ice State":        "state",
     "Priority":         "priority",
     "Nominated":        "nominated",
     "Selected":         "selected"
   },
@@ -260,75 +247,75 @@ var IceStats = React.createClass({displa
     sorting = (key === sorting) ? null : key;
     this.setState({pairs: pairs, sorting: sorting});
   },
 
   generateSortHeadings: function() {
     return Object.keys(this.sortHeadings).map(function(heading, i) {
       var sortKey = this.sortHeadings[heading];
       return (
-        React.DOM.th(null, 
-          React.DOM.a({href: "#", onClick: this.sort.bind(this, sortKey)}, 
+        React.createElement("th", null, 
+          React.createElement("a", {href: "#", onClick: this.sort.bind(this, sortKey)}, 
             heading
           )
         )
       );
     }.bind(this));
   },
 
   getInitialState: function() {
     return {pairs: this.props.pairs, sorting: null};
   },
 
   render: function() {
     return (
-      React.DOM.table(null, 
-        React.DOM.tbody(null, 
-          React.DOM.tr(null, this.generateSortHeadings()), 
+      React.createElement("table", null, 
+        React.createElement("tbody", null, 
+          React.createElement("tr", null, this.generateSortHeadings()), 
           this.state.pairs.map(function(pair, i) {
-            return IceCandidatePair({key: i, pair: pair});
+            return React.createElement(IceCandidatePair, {key: i, pair: pair});
           })
         )
       )
     );
   }
 });
 
-var IceCandidatePair = React.createClass({displayName: 'IceCandidatePair',
+var IceCandidatePair = React.createClass({displayName: "IceCandidatePair",
   render: function() {
     var pair = this.props.pair;
     return (
-      React.DOM.tr(null, 
-        React.DOM.td(null, pair.localCandidate), 
-        React.DOM.td(null, pair.remoteCandidate), 
-        React.DOM.td(null, pair.state), 
-        React.DOM.td(null, pair.priority), 
-        React.DOM.td(null, pair.nominated ? '✔' : null), 
-        React.DOM.td(null, pair.selected ? '✔' : null)
+      React.createElement("tr", null, 
+        React.createElement("td", null, pair.localCandidate), 
+        React.createElement("td", null, pair.remoteCandidate), 
+        React.createElement("td", null, pair.state), 
+        React.createElement("td", null, pair.priority), 
+        React.createElement("td", null, pair.nominated ? '✔' : null), 
+        React.createElement("td", null, pair.selected ? '✔' : null)
       )
     );
   }
 });
 
-var SDP = React.createClass({displayName: 'SDP',
+var SDP = React.createClass({displayName: "SDP",
   render: function() {
     var type = labelize(this.props.type);
 
     return (
-      React.DOM.div(null, 
-        React.DOM.h4(null, type, " SDP"), 
-        React.DOM.pre(null, 
+      React.createElement("div", null, 
+        React.createElement("h4", null, type, " SDP"), 
+        React.createElement("pre", null, 
           this.props.sdp
         )
       )
     );
   }
 });
 
-var RTPStats = React.createClass({displayName: 'RTPStats',
+var RTPStats = React.createClass({displayName: "RTPStats",
   getRtpStats: function(report) {
     var remoteRtpStats = {};
     var rtpStats = [];
 
     rtpStats = rtpStats.concat(report.inboundRTPStreamStats  || []);
     rtpStats = rtpStats.concat(report.outboundRTPStreamStats || []);
 
     rtpStats.forEach(function(stats) {
@@ -350,17 +337,17 @@ var RTPStats = React.createClass({displa
     var statsString = "";
     if (stats.mozAvSyncDelay) {
       statsString += `A/V sync: ${stats.mozAvSyncDelay} ms `;
     }
     if (stats.mozJitterBufferDelay) {
       statsString += `Jitter-buffer delay: ${stats.mozJitterBufferDelay} ms`;
     }
 
-    return React.DOM.div(null, statsString);
+    return React.createElement("div", null, statsString);
   },
 
   dumpCoderStats: function(stats) {
     var statsString = "";
     var label;
 
     if (stats.bitrateMean) {
       statsString += ` Avg. bitrate: ${(stats.bitrateMean/1000000).toFixed(2)} Mbps`;
@@ -383,17 +370,17 @@ var RTPStats = React.createClass({displa
       statsString += ` Discarded packets: ${stats.discardedPackets}`;
     }
 
     if (statsString) {
       label = (stats.packetsReceived)? " Decoder:" : " Encoder:";
       statsString = label + statsString;
     }
 
-    return React.DOM.div(null, statsString);
+    return React.createElement("div", null, statsString);
   },
 
   dumpRtpStats: function(stats, type) {
     var label = labelize(type);
     var time  = new Date(stats.timestamp).toTimeString();
 
     var statsString = `${label}: ${time} ${stats.type} SSRC: ${stats.ssrc}`;
 
@@ -411,100 +398,100 @@ var RTPStats = React.createClass({displa
       }
     } else if (stats.packetsSent) {
       statsString += ` Sent: ${stats.packetsSent} packets`;
       if (stats.bytesSent) {
         statsString += ` (${round00(stats.bytesSent/1024)} Kb)`;
       }
     }
 
-    return React.DOM.div(null, statsString);
+    return React.createElement("div", null, statsString);
   },
 
   render: function() {
     var rtpStats = this.getRtpStats(this.props.report);
 
     return (
-      React.DOM.div(null, 
+      React.createElement("div", null, 
         rtpStats.map(function(stats) {
           var isAvStats = (stats.mozAvSyncDelay || stats.mozJitterBufferDelay);
           var remoteRtpStats = stats.remoteId ? stats.remoteRtpStats : null;
 
           return [
-            React.DOM.h5(null, stats.id),
+            React.createElement("h5", null, stats.id),
             isAvStats ? this.dumpAvStats(stats) : null,
             this.dumpCoderStats(stats),
             this.dumpRtpStats(stats, "local"),
             remoteRtpStats ? this.dumpRtpStats(remoteRtpStats, "remote") : null
-          ]
+          ];
         }.bind(this))
       )
     );
   }
 });
 
-var LogsButton = React.createClass({displayName: 'LogsButton',
+var LogsButton = React.createClass({displayName: "LogsButton",
   render: function() {
     return (
-      React.DOM.button({onClick: this.props.handler}, "Connection log")
+      React.createElement("button", {onClick: this.props.handler}, "Connection log")
     );
   }
 });
 
-var DebugButton = React.createClass({displayName: 'DebugButton',
+var DebugButton = React.createClass({displayName: "DebugButton",
   getInitialState: function() {
     var on = (WebrtcGlobalInformation.debugLevel > 0);
     return {on: on};
   },
 
   onClick: function() {
     if (this.state.on)
       WebrtcGlobalInformation.debugLevel = 0;
     else
       WebrtcGlobalInformation.debugLevel = 65535;
 
     this.setState({on: !this.state.on});
   },
 
   render: function() {
     return (
-      React.DOM.button({onClick: this.onClick}, 
+      React.createElement("button", {onClick: this.onClick}, 
         this.state.on ? "Stop debug mode" : "Start debug mode"
       )
     );
   }
 });
 
-var AECButton = React.createClass({displayName: 'AECButton',
+var AECButton = React.createClass({displayName: "AECButton",
   getInitialState: function() {
     return {on: WebrtcGlobalInformation.aecDebug};
   },
 
   onClick: function() {
     WebrtcGlobalInformation.aecDebug = !this.state.on;
     this.setState({on: WebrtcGlobalInformation.aecDebug});
   },
 
   render: function() {
     return (
-      React.DOM.button({onClick: this.onClick}, 
+      React.createElement("button", {onClick: this.onClick}, 
         this.state.on ? "Stop AEC logging" : "Start AEC logging"
       )
     );
   }
 });
 
-var Logs = React.createClass({displayName: 'Logs',
+var Logs = React.createClass({displayName: "Logs",
   render: function() {
     return (
-      React.DOM.div(null, 
-        React.DOM.h3(null, "Logging:"), 
-        React.DOM.div(null, 
+      React.createElement("div", null, 
+        React.createElement("h3", null, "Logging:"), 
+        React.createElement("div", null, 
           this.props.logs.map(function(line, i) {
-            return React.DOM.div({key: i}, line);
+            return React.createElement("div", {key: i}, line);
           })
         )
       )
     );
   }
 });
 
 function iceCandidateMapping(iceCandidates) {
@@ -528,18 +515,18 @@ function labelize(label) {
 
 function candidateToString(c) {
   var type = c.candidateType;
 
   if (c.type == "localcandidate" && c.candidateType == "relayed") {
     type = `${c.candidateType}-${c.mozLocalTransport}`;
   }
 
-  return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`
+  return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`;
 }
 
 function onLoad() {
   WebrtcGlobalInformation.getAllStats(function(globalReport) {
     var reports = globalReport.reports;
-    React.renderComponent(AboutWebRTC({reports: reports}),
-                          document.querySelector("#body"));
+    React.render(React.createElement(AboutWebRTC, {reports: reports}),
+                 document.querySelector("#body"));
   });
 }
--- a/toolkit/content/aboutwebrtc/aboutWebrtc.jsx
+++ b/toolkit/content/aboutwebrtc/aboutWebrtc.jsx
@@ -1,13 +1,16 @@
 /** @jsx React.DOM */
 /* 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/. */
 
+/* jshint newcap:false */
+/* global React, WebrtcGlobalInformation, document */
+
 "use strict";
 
 var Tabs = React.createClass({
   getDefaultProps: function() {
     return {selectedIndex: 0};
   },
 
   getInitialState: function() {
@@ -21,17 +24,17 @@ var Tabs = React.createClass({
     }.bind(this);
   },
 
   _findSelectedTabContent: function() {
     // Using map() to filter children…
     // https://github.com/facebook/react/issues/1644#issuecomment-45138113
     return React.Children.map(this.props.children, function(tab, i) {
       return i === this.state.selectedIndex ? tab : null;
-    }.bind(this))
+    }.bind(this));
   },
 
   render: function() {
     var cx = React.addons.classSet;
     return (
       <div className="tabs">
         <ul>{
           React.Children.map(this.props.children, function(tab, i) {
@@ -59,17 +62,17 @@ var Tab = React.createClass({
 var AboutWebRTC = React.createClass({
   getInitialState: function() {
     return {logs: null, reports: this.props.reports};
   },
 
   displayLogs: function() {
     WebrtcGlobalInformation.getLogging('', function(logs) {
       this.setState({logs: logs, reports: this.state.reports});
-    }.bind(this))
+    }.bind(this));
   },
 
   render: function() {
     return (
       <div>
         <div id="stats">
           <PeerConnections reports={this.state.reports}/>
         </div>
@@ -139,17 +142,17 @@ var PeerConnection = React.createClass({
         selected: pair.selected
       };
     });
 
     var pairedCandidates = pairs.reduce(function(paired, pair) {
       paired.add(pair.localCandidate.id);
       paired.add(pair.remoteCandidate.id);
 
-      return paired
+      return paired;
     }, new Set());
 
     var unifiedPairs =
       report.iceCandidateStats.reduce(function(pairs, candidate) {
         if (pairedCandidates.has(candidate)) {
             return pairs;
         }
 
@@ -413,17 +416,17 @@ var RTPStats = React.createClass({
           var remoteRtpStats = stats.remoteId ? stats.remoteRtpStats : null;
 
           return [
             <h5>{stats.id}</h5>,
             isAvStats ? this.dumpAvStats(stats) : null,
             this.dumpCoderStats(stats),
             this.dumpRtpStats(stats, "local"),
             remoteRtpStats ? this.dumpRtpStats(remoteRtpStats, "remote") : null
-          ]
+          ];
         }.bind(this))
       }</div>
     );
   }
 });
 
 var LogsButton = React.createClass({
   render: function() {
@@ -512,18 +515,18 @@ function labelize(label) {
 
 function candidateToString(c) {
   var type = c.candidateType;
 
   if (c.type == "localcandidate" && c.candidateType == "relayed") {
     type = `${c.candidateType}-${c.mozLocalTransport}`;
   }
 
-  return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`
+  return `${c.ipAddress}:${c.portNumber}/${c.transport}(${type})`;
 }
 
 function onLoad() {
   WebrtcGlobalInformation.getAllStats(function(globalReport) {
     var reports = globalReport.reports;
-    React.renderComponent(<AboutWebRTC reports={reports}/>,
-                          document.querySelector("#body"));
+    React.render(<AboutWebRTC reports={reports}/>,
+                 document.querySelector("#body"));
   });
 }
--- a/toolkit/content/aboutwebrtc/aboutWebrtc.xhtml
+++ b/toolkit/content/aboutwebrtc/aboutWebrtc.xhtml
@@ -11,12 +11,12 @@
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <title>Webrtc Internals</title>
     <link rel="stylesheet" type="text/css" media="all" href="chrome://global/content/aboutwebrtc/aboutWebrtc.css"/>
   </head>
   <body id="body" onload="onLoad()">
   </body>
 
-  <script type="text/javascript" src="chrome://browser/content/loop/shared/libs/react-0.11.2.js"></script>
+  <script type="text/javascript" src="chrome://browser/content/loop/shared/libs/react-0.12.2.js"></script>
   <script type="text/javascript;version=1.8" src="chrome://global/content/aboutwebrtc/aboutWebrtc.js"/>
 </html>
 
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -417,16 +417,17 @@
             return (new Function("eventType", "param", handlerString)).bind(this, aEventType);
           }
           return null;
         ]]></body>
       </method>
 
       <!-- ::::::::::::: key handling ::::::::::::: -->
 
+      <field name="_selectionDetails">null</field>
       <method name="onKeyPress">
         <parameter name="aEvent"/>
         <body><![CDATA[
           if (aEvent.target.localName != "textbox")
             return true; // Let child buttons of autocomplete take input
 
           //XXXpch this is so bogus...
           if (aEvent.defaultPrevented)
@@ -480,16 +481,22 @@
               break;
             case KeyEvent.DOM_VK_RETURN:
 #ifdef XP_MACOSX
               // Prevent the default action, since it will beep on Mac
               if (aEvent.metaKey)
                 aEvent.preventDefault();
 #endif
               this.mEnterEvent = aEvent;
+              if (this.mController.selection) {
+                this._selectionDetails = {
+                  index: this.mController.selection.currentIndex,
+                  kind: "key"
+                };
+              }
               cancel = this.mController.handleEnter(false);
               break;
             case KeyEvent.DOM_VK_DELETE:
 #ifdef XP_MACOSX
             case KeyEvent.DOM_VK_BACK_SPACE:
               if (aEvent.shiftKey)
 #endif
               cancel = this.mController.handleDelete();
--- a/toolkit/devtools/css-color.js
+++ b/toolkit/devtools/css-color.js
@@ -74,17 +74,18 @@ const SPECIALVALUES = new Set([
 
 function CssColor(colorValue) {
   this.newColor(colorValue);
 }
 
 module.exports.colorUtils = {
   CssColor: CssColor,
   processCSSString: processCSSString,
-  rgbToHsl: rgbToHsl
+  rgbToHsl: rgbToHsl,
+  setAlpha: setAlpha
 };
 
 /**
  * Values used in COLOR_UNIT_PREF
  */
 CssColor.COLORUNIT = {
   "authored": "authored",
   "hex": "hex",
@@ -398,11 +399,40 @@ function rgbToHsl([r,g,b]) {
     if (h < 0) {
       h += 360;
     }
   }
 
   return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
 }
 
+/**
+ * Takes a color value of any type (hex, hsl, hsla, rgb, rgba)
+ * and an alpha value to generate an rgba string with the correct
+ * alpha value.
+ *
+ * @param  {String} colorValue
+ *         Color in the form of hex, hsl, hsla, rgb, rgba.
+ * @param  {Number} alpha
+ *         Alpha value for the color, between 0 and 1.
+ * @return {String}
+ *         Converted color with `alpha` value in rgba form.
+ */
+function setAlpha(colorValue, alpha) {
+  let color = new CssColor(colorValue);
+
+  // Throw if the color supplied is not valid.
+  if (!color.valid) {
+    throw new Error("Invalid color.");
+  }
+
+  // If an invalid alpha valid, just set to 1.
+  if (!(alpha >= 0 && alpha <= 1)) {
+    alpha = 1;
+  }
+
+  let { r, g, b } = color._getRGBATuple();
+  return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
+}
+
 loader.lazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
--- a/toolkit/devtools/server/actors/actor-registry.js
+++ b/toolkit/devtools/server/actors/actor-registry.js
@@ -6,40 +6,36 @@
 
 const protocol = require("devtools/server/protocol");
 const { method, custom, Arg, Option, RetVal } = protocol;
 
 const { Cu, CC, components } = require("chrome");
 const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 const Services = require("Services");
 const { DebuggerServer } = require("devtools/server/main");
-const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+const ActorRegistryUtils = require("devtools/server/actors/utils/actor-registry-utils");
+const { registerActor, unregisterActor } = ActorRegistryUtils;
+
+loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
 
 /**
  * The ActorActor gives you a handle to an actor you've dynamically
  * registered and allows you to unregister it.
  */
 const ActorActor = protocol.ActorClass({
   typeName: "actorActor",
 
   initialize: function (conn, options) {
     protocol.Actor.prototype.initialize.call(this, conn);
 
     this.options = options;
   },
 
   unregister: method(function () {
-    if (this.options.tab) {
-      DebuggerServer.removeTabActor(this.options);
-    }
-
-    if (this.options.global) {
-      DebuggerServer.removeGlobalActor(this.options);
-    }
+    unregisterActor(this.options);
   }, {
     request: {},
     response: {}
   })
 });
 
 const ActorActorFront = protocol.FrontClass(ActorActor, {
   initialize: function (client, form) {
@@ -56,38 +52,19 @@ exports.ActorActorFront = ActorActorFron
 const ActorRegistryActor = protocol.ActorClass({
   typeName: "actorRegistry",
 
   initialize: function (conn) {
     protocol.Actor.prototype.initialize.call(this, conn);
   },
 
   registerActor: method(function (sourceText, fileName, options) {
-    const principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
-    const sandbox = Cu.Sandbox(principal);
-    const exports = sandbox.exports = {};
-    sandbox.require = require;
-
-    Cu.evalInSandbox(sourceText, sandbox, "1.8", fileName, 1);
-
-    let { prefix, constructor, type } = options;
+    registerActor(sourceText, fileName, options);
 
-    if (type.global) {
-      DebuggerServer.addGlobalActor({
-        constructorName: constructor,
-        constructorFun: sandbox[constructor]
-      }, prefix);
-    }
-
-    if (type.tab) {
-      DebuggerServer.addTabActor({
-        constructorName: constructor,
-        constructorFun: sandbox[constructor]
-      }, prefix);
-    }
+    let { constructor, type } = options;
 
     return ActorActor(this.conn, {
       name: constructor,
       tab: type.tab,
       global: type.global
     });
   }, {
     request: {
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/utils/actor-registry-utils.js
@@ -0,0 +1,74 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let { Cu, CC, Ci, Cc } = require("chrome");
+
+const { DebuggerServer } = require("devtools/server/main");
+
+/**
+ * Support for actor registration. Main used by ActorRegistryActor
+ * for dynamic registration of new actors.
+ *
+ * @param sourceText {String} Source of the actor implementation
+ * @param fileName {String} URL of the actor module (for proper stack traces)
+ * @param options {Object} Configuration object
+ */
+exports.registerActor = function(sourceText, fileName, options) {
+  const principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
+  const sandbox = Cu.Sandbox(principal);
+  const exports = sandbox.exports = {};
+  sandbox.require = require;
+
+  Cu.evalInSandbox(sourceText, sandbox, "1.8", fileName, 1);
+
+  let { prefix, constructor, type } = options;
+
+  if (type.global && !DebuggerServer.globalActorFactories.hasOwnProperty(prefix)) {
+    DebuggerServer.addGlobalActor({
+      constructorName: constructor,
+      constructorFun: sandbox[constructor]
+    }, prefix);
+  }
+
+  if (type.tab && !DebuggerServer.tabActorFactories.hasOwnProperty(prefix)) {
+    DebuggerServer.addTabActor({
+      constructorName: constructor,
+      constructorFun: sandbox[constructor]
+    }, prefix);
+  }
+
+  // Also register in all child processes in case the current scope
+  // is chrome parent process.
+  if (!DebuggerServer.isInChildProcess) {
+    DebuggerServer.setupInChild({
+      module: "devtools/server/actors/utils/actor-registry-utils",
+      setupChild: "registerActor",
+      args: [sourceText, fileName, options]
+    });
+  }
+}
+
+exports.unregisterActor = function(options) {
+  if (options.tab) {
+    DebuggerServer.removeTabActor(options);
+  }
+
+  if (options.global) {
+    DebuggerServer.removeGlobalActor(options);
+  }
+
+  // Also unregister it from all child processes in case the current
+  // scope is chrome parent process.
+  if (!DebuggerServer.isInChildProcess) {
+    DebuggerServer.setupInChild({
+      module: "devtools/server/actors/utils/actor-registry-utils",
+      setupChild: "unregisterActor",
+      args: [options]
+    });
+  }
+}
--- a/toolkit/devtools/server/actors/webaudio.js
+++ b/toolkit/devtools/server/actors/webaudio.js
@@ -25,17 +25,17 @@ const AUDIO_GLOBALS = [
   "AudioContext", "AudioNode", "AudioParam"
 ];
 
 const NODE_CREATION_METHODS = [
   "createBufferSource", "createMediaElementSource", "createMediaStreamSource",
   "createMediaStreamDestination", "createScriptProcessor", "createAnalyser",
   "createGain", "createDelay", "createBiquadFilter", "createWaveShaper",
   "createPanner", "createConvolver", "createChannelSplitter", "createChannelMerger",
-  "createDynamicsCompressor", "createOscillator"
+  "createDynamicsCompressor", "createOscillator", "createStereoPanner"
 ];
 
 const AUTOMATION_METHODS = [
   "setValueAtTime", "linearRampToValueAtTime", "exponentialRampToValueAtTime",
   "setTargetAtTime", "setValueCurveAtTime"
 ];
 
 const NODE_ROUTING_METHODS = [
@@ -106,16 +106,19 @@ const NODE_PROPERTIES = {
   },
   "AudioDestinationNode": {},
   "ChannelSplitterNode": {},
   "ChannelMergerNode": {},
   "MediaElementAudioSourceNode": {},
   "MediaStreamAudioSourceNode": {},
   "MediaStreamAudioDestinationNode": {
     "stream": { "MediaStream": true }
+  },
+  "StereoPannerNode": {
+    "pan": {}
   }
 };
 
 /**
  * An Audio Node actor allowing communication to a specific audio node in the
  * Audio Context graph.
  */
 types.addActorType("audionode");
--- a/toolkit/devtools/server/child.js
+++ b/toolkit/devtools/server/child.js
@@ -9,17 +9,19 @@ try {
 let chromeGlobal = this;
 
 // Encapsulate in its own scope to allows loading this frame script
 // more than once.
 (function () {
   let Cu = Components.utils;
   let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
   const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
-  const {DebuggerServer, ActorPool} = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+  const { dumpn } = DevToolsUtils;
+  const { DebuggerServer, ActorPool } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+
   if (!DebuggerServer.childID) {
     DebuggerServer.childID = 1;
   }
 
   if (!DebuggerServer.initialized) {
     DebuggerServer.init();
 
     // message manager helpers provided for actor module parent/child message exchange
@@ -52,16 +54,46 @@ let chromeGlobal = this;
     actorPool.addActor(actor);
     conn.addActorPool(actorPool);
 
     sendAsyncMessage("debug:actor", {actor: actor.form(), childID: id});
   });
 
   addMessageListener("debug:connect", onConnect);
 
+  // Allows executing module setup helper from the parent process.
+  // See also: DebuggerServer.setupInChild()
+  let onSetupInChild = DevToolsUtils.makeInfallible(msg => {
+    let { module, setupChild, args } = msg.data;
+    let m, fn;
+
+    try {
+      m = devtools.require(module);
+
+      if (!setupChild in m) {
+        dumpn("ERROR: module '" + module + "' does not export '" +
+              setupChild + "'");
+        return false;
+      }
+
+      m[setupChild].apply(m, args);
+
+      return true;
+    } catch(e) {
+      let error_msg = "exception during actor module setup running in the child process: ";
+      DevToolsUtils.reportException(error_msg + e);
+      dumpn("ERROR: " + error_msg + " \n\t module: '" + module +
+            "' \n\t setupChild: '" + setupChild + "'\n" +
+            DevToolsUtils.safeErrorString(e));
+      return false;
+    }
+  });
+
+  addMessageListener("debug:setup-in-child", onSetupInChild);
+
   let onDisconnect = DevToolsUtils.makeInfallible(function (msg) {
     removeMessageListener("debug:disconnect", onDisconnect);
 
     // Call DebuggerServerConnection.close to destroy all child actors
     // (It should end up calling DebuggerServerConnection.onClosed
     // that would actually cleanup all actor pools)
     let childID = msg.data.childID;
     let conn = connections.get(childID);
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -726,16 +726,41 @@ var DebuggerServer = {
    * Check if the caller is running in a content child process.
    *
    * @return boolean
    *         true if the caller is running in a content
    */
   get isInChildProcess() !!this.parentMessageManager,
 
   /**
+   * In a chrome parent process, ask all content child processes
+   * to execute a given module setup helper.
+   *
+   * @param module
+   *        The module to be required
+   * @param setupChild
+   *        The name of the setup helper exported by the above module
+   *        (setup helper signature: function ({mm}) { ... })
+   */
+  setupInChild: function({ module, setupChild, args }) {
+    if (this.isInChildProcess) {
+      return;
+    }
+
+    const gMessageManager = Cc["@mozilla.org/globalmessagemanager;1"].
+      getService(Ci.nsIMessageListenerManager);
+
+    gMessageManager.broadcastAsyncMessage("debug:setup-in-child", {
+      module: module,
+      setupChild: setupChild,
+      args: args,
+    });
+  },
+
+  /**
    * In a content child process, ask the DebuggerServer in the parent process
    * to execute a given module setup helper.
    *
    * @param module
    *        The module to be required
    * @param setupParent
    *        The name of the setup helper exported by the above module
    *        (setup helper signature: function ({mm}) { ... })
@@ -824,16 +849,18 @@ var DebuggerServer = {
 
       dumpn("establishing forwarding for app with prefix " + prefix);
 
       actor = msg.json.actor;
 
       let { NetworkMonitorManager } = require("devtools/toolkit/webconsole/network-monitor");
       netMonitor = new NetworkMonitorManager(aFrame, actor.actor);
 
+      events.emit(DebuggerServer, "new-child-process", { mm: mm });
+
       deferred.resolve(actor);
     }).bind(this);
     mm.addMessageListener("debug:actor", onActorCreated);
 
     let onMessageManagerDisconnect = DevToolsUtils.makeInfallible(function (subject, topic, data) {
       if (subject == mm) {
         Services.obs.removeObserver(onMessageManagerDisconnect, topic);
 
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -65,16 +65,17 @@ EXTRA_JS_MODULES.devtools.server.actors 
     'actors/webapps.js',
     'actors/webaudio.js',
     'actors/webbrowser.js',
     'actors/webconsole.js',
     'actors/webgl.js',
 ]
 
 EXTRA_JS_MODULES.devtools.server.actors.utils += [
+    'actors/utils/actor-registry-utils.js',
     'actors/utils/automation-timeline.js',
     'actors/utils/make-debugger.js',
     'actors/utils/map-uri-to-addon-id.js',
     'actors/utils/ScriptStore.js',
     'actors/utils/stack.js',
 ]
 
 FAIL_ON_WARNINGS = True
--- a/toolkit/devtools/server/tests/unit/test_actor-registry-actor.js
+++ b/toolkit/devtools/server/tests/unit/test_actor-registry-actor.js
@@ -60,17 +60,17 @@ function talkToNewActor() {
   });
 }
 
 function unregisterNewActor() {
   gActorFront
     .unregister()
     .then(testActorIsUnregistered)
     .then(null, e => {
-      DevToolsUtils.reportException("registerNewActor", e)
+      DevToolsUtils.reportException("unregisterNewActor", e)
       do_check_true(false);
     });
 }
 
 function testActorIsUnregistered() {
   gClient.listTabs(({ helloActor }) => {
     do_check_true(!helloActor);
 
--- a/toolkit/modules/NewTabUtils.jsm
+++ b/toolkit/modules/NewTabUtils.jsm
@@ -272,21 +272,21 @@ let AllPages = {
   },
 
   /**
    * Updates all currently active pages but the given one.
    * @param aExceptPage The page to exclude from updating.
    * @param aReason The reason for updating all pages.
    */
   update(aExceptPage, aReason = "") {
-    this._pages.forEach(function (aPage) {
-      if (aExceptPage != aPage) {
-        aPage.update(aReason);
+    for (let page of this._pages.slice()) {
+      if (aExceptPage != page) {
+        page.update(aReason);
       }
-    });
+    }
   },
 
   /**
    * Implements the nsIObserver interface to get notified when the preference
    * value changes or when a new copy of a page thumbnail is available.
    */
   observe: function AllPages_observe(aSubject, aTopic, aData) {
     if (aTopic == "nsPref:changed") {
--- a/webapprt/WebappManager.jsm
+++ b/webapprt/WebappManager.jsm
@@ -37,17 +37,17 @@ this.WebappManager = {
         if (chromeWin) {
           this.doUninstall(data, chromeWin);
         }
         break;
       case "webapps-launch":
         WebappOSUtils.launch(data);
         break;
       case "webapps-uninstall":
-        WebappOSUtils.uninstall(data);
+        WebappOSUtils.uninstall(data).then(null, Cu.reportError);
         break;
     }
   },
 
   update: function(aApp, aManifest, aZipPath) {
     let nativeApp = new NativeApp(aApp, aManifest,
                                   WebappRT.config.app.categories,
                                   WebappRT.config.registryDir);
@@ -117,17 +117,17 @@ this.WebappManager = {
       bundle.GetStringFromName("webapps.uninstall.dontuninstall"),
       null,
       null,
       {});
 
     // Perform the uninstall if the user allows it
     if (choice == 0) {
       DOMApplicationRegistry.confirmUninstall(aData).then((aApp) => {
-        WebappOSUtils.uninstall(aApp);
+        WebappOSUtils.uninstall(aApp).then(null, Cu.reportError);
       });
     } else {
       DOMApplicationRegistry.denyUninstall(aData);
     }
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference])
--- a/webapprt/test/chrome/browser_getUserMedia.js
+++ b/webapprt/test/chrome/browser_getUserMedia.js
@@ -22,17 +22,17 @@ function test() {
 
   Services.ww.registerNotification(winObserver);
 
   let mutObserver = null;
 
   loadWebapp("getUserMedia.webapp", undefined, function onLoad() {
     let msg = gAppBrowser.contentDocument.getElementById("msg");
     mutObserver = new MutationObserver(function(mutations) {
-      is(msg.textContent, "PermissionDeniedError",
+      is(msg.textContent, "PermissionDeniedError: The user did not grant permission for the operation.",
          "getUserMedia permission denied.");
       ok(getUserMediaDialogOpened, "Prompt shown.");
       finish();
     });
     mutObserver.observe(msg, { childList: true });
   });
 
   registerCleanupFunction(function() {
--- a/webapprt/test/chrome/browser_webperm.js
+++ b/webapprt/test/chrome/browser_webperm.js
@@ -1,36 +1,66 @@
 Cu.import("resource://webapprt/modules/WebappRT.jsm");
 let { AppsUtils } = Cu.import("resource://gre/modules/AppsUtils.jsm", {});
 let { DOMApplicationRegistry } =
   Cu.import("resource://gre/modules/Webapps.jsm", {});
-let { PermissionsTable, PermissionsReverseTable } =
+let { PermissionsTable } =
   Cu.import("resource://gre/modules/PermissionsTable.jsm", {});
 
 function test() {
   waitForExplicitFinish();
 
   loadWebapp("webperm.webapp", undefined, function onLoad() {
     let app = WebappRT.config.app;
 
     // Check that the app is non privileged.
     is(AppsUtils.getAppManifestStatus(app.manifest), Ci.nsIPrincipal.APP_STATUS_INSTALLED, "The app is not privileged");
 
     // Check that the app principal has the correct appId.
     let principal = document.getElementById("content").contentDocument.defaultView.document.nodePrincipal;
     is(DOMApplicationRegistry.getAppLocalIdByManifestURL(app.manifestURL), principal.appId, "Principal app ID correct");
 
-    let perms = [ "indexedDB-unlimited", "geolocation",
-                  "camera", "alarms", "tcp-socket", "network-events",
-                  "webapps-manage", "desktop-notification" ];
+    let perms = [
+    {
+      manifestName: "storage",
+      permName: "indexedDB",
+    },
+    {
+      manifestName: "geolocation",
+      permName: "geolocation",
+    },
+    {
+      manifestName: "camera",
+      permName: "camera",
+    },
+    {
+      manifestName: "alarms",
+      permName: "alarms",
+    },
+    {
+      manifestName: "tcp-socket",
+      permName: "tcp-socket",
+    },
+    {
+      manifestName: "network-events",
+      permName: "network-events",
+    },
+    {
+      manifestName: "webapps-manage",
+      permName: "webapps-manage",
+    },
+    {
+      manifestName: "desktop-notification",
+      permName: "desktop-notification",
+    },
+    ];
 
-    for (let permName of perms) {
+    for (let perm of perms) {
       // Get the values for all the permission.
-      let permValue = Services.perms.testExactPermissionFromPrincipal(principal, permName);
+      let permValue = Services.perms.testExactPermissionFromPrincipal(principal, perm.permName);
 
       // Check if the app has the permission as specified in the PermissionsTable.jsm file.
-      let realPerm = PermissionsReverseTable[permName];
-      is(permValue, PermissionsTable[realPerm]["app"], "Permission " + permName + " correctly set.");
+      is(permValue, PermissionsTable[perm.manifestName]["app"], "Permission " + perm.permName + " correctly set.");
     }
 
     finish();
   });
 }