Merge m-c to oak
authorRobert Strong <robert.bugzilla@gmail.com>
Tue, 07 Jun 2016 00:33:19 -0700
changeset 491556 73b7044518b539d720b540bfa0f6674a63bab3b7
parent 491555 62ec8878edcb6e1b7c409bac4966ae4e28112a28 (current diff)
parent 375957 1828937da9493b2cd54862b9c520b2ba5c7db92b (diff)
child 491557 cd3002d531a817f79337e8075eb158b860053960
push id47343
push userbmo:dothayer@mozilla.com
push dateWed, 01 Mar 2017 22:58:58 +0000
milestone50.0a1
Merge m-c to oak
CLOBBER
browser/app/nsBrowserApp.cpp
browser/app/profile/firefox.js
browser/base/content/browser.js
browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
devtools/client/responsive.html/events.js
devtools/client/shared/css-properties-db.js
devtools/client/themes/images/commandline-icon.png
devtools/client/themes/images/commandline-icon@2x.png
devtools/server/actors/memprof.js
devtools/server/tests/mochitest/memprof-helpers.js
devtools/server/tests/mochitest/test_memprof.html
devtools/server/tests/unit/test_addons_actor/web-extension/manifest.json
devtools/shared/promise_defer.js
devtools/shared/tests/unit/test_promise_defer.js
ipc/glue/GeckoChildProcessHost.cpp
js/xpconnect/src/XPCShellImpl.cpp
media/gmp-clearkey/0.1/Endian.h
mobile/android/base/java/org/mozilla/gecko/NetworkInfoService.java
mobile/android/base/java/org/mozilla/gecko/mozglue/SafeIntentUtils.java
mobile/android/base/resources/values-large-v11/dimens.xml
mobile/android/base/resources/values-large-v11/integers.xml
mobile/android/base/resources/values-large-v11/styles.xml
mobile/android/base/resources/values/xml.xml
netwerk/base/NetworkInfoServiceAndroid.jsm
netwerk/base/nsIHttpAuthenticatorCallback.idl
netwerk/base/nsNetworkInfoService.js
netwerk/base/nsNetworkInfoService.manifest
old-configure.in
security/nss/external_tests/der_gtest/der_gtest.cc
security/nss/external_tests/pk11_gtest/pk11_gtest.cc
security/nss/external_tests/util_gtest/util_gtest.cc
security/nss/tests/der_gtests/der_gtests.sh
security/nss/tests/pk11_gtests/pk11_gtests.sh
security/nss/tests/util_gtests/util_gtests.sh
services/common/blocklist-clients.js
services/common/tests/unit/test_blocklist_certificates.js
testing/talos/treeherder-schemas/performance-artifact.json
testing/taskcluster/tasks/branches/base_job_flags.yml.orig
toolkit/components/telemetry/Histograms.json
toolkit/xre/nsAppRunner.cpp
--- a/.eslintignore
+++ b/.eslintignore
@@ -105,19 +105,20 @@ devtools/client/webaudioeditor/**
 devtools/client/webconsole/**
 !devtools/client/webconsole/panel.js
 !devtools/client/webconsole/jsterm.js
 !devtools/client/webconsole/console-commands.js
 devtools/client/webide/**
 devtools/server/**
 !devtools/server/actors/webbrowser.js
 !devtools/server/actors/styles.js
+!devtools/server/actors/string.js
 devtools/shared/*.js
 !devtools/shared/css-lexer.js
-!devtools/shared/promise_defer.js
+!devtools/shared/defer.js
 !devtools/shared/task.js
 devtools/shared/*.jsm
 devtools/shared/apps/**
 devtools/shared/client/**
 devtools/shared/discovery/**
 devtools/shared/gcli/**
 !devtools/shared/gcli/templater.js
 devtools/shared/heapsnapshot/**
--- a/.gdbinit
+++ b/.gdbinit
@@ -174,8 +174,10 @@ end
 
 def js
   call DumpJSStack()
 end
 
 def ft
   call $arg0->DumpFrameTree()
 end
+
+source .gdbinit_python
new file mode 100644
--- /dev/null
+++ b/.gdbinit_python
@@ -0,0 +1,5 @@
+python
+import sys
+sys.path.append('python/gdbpp/')
+import gdbpp
+end
--- a/.gitignore
+++ b/.gitignore
@@ -111,8 +111,11 @@ testing/eslint/node_modules/
 # the status command, so we ignore them.
 testing/talos/.Python
 testing/talos/bin/
 testing/talos/include/
 testing/talos/lib/
 testing/talos/talos/tests/tp5n.zip
 testing/talos/talos/tests/tp5n
 testing/talos/talos/tests/devtools/damp.manifest.develop
+
+# Ignore files created when running a reftest.
+lextab.py
--- a/.hgignore
+++ b/.hgignore
@@ -128,8 +128,11 @@ GPATH
 # the status command, so we ignore them.
 ^testing/talos/.Python
 ^testing/talos/bin/
 ^testing/talos/include/
 ^testing/talos/lib/
 ^testing/talos/talos/tests/tp5n.zip
 ^testing/talos/talos/tests/tp5n
 ^testing/talos/talos/tests/devtools/damp.manifest.develop
+
+# Ignore files created when running a reftest.
+^lextab.py$
--- a/.hgtags
+++ b/.hgtags
@@ -121,8 +121,9 @@ 66a95a483d2c77dfc387019336d18093acd6aac2
 312c68b16549de9cea1557f461d5d234bd5e0a7d FIREFOX_AURORA_41_BASE
 7a19194812eb767bee7cdf8fc36ba9a383c1bead FIREFOX_AURORA_42_BASE
 fcef8ded82219c89298b4e376cfbdfba79a1d35a FIREFOX_AURORA_43_BASE
 67a788db9f07822cfef52351bbbe3745dff8bd7f FIREFOX_AURORA_44_BASE
 99137d6d4061f408ae0869122649d8bdf489cc30 FIREFOX_AURORA_45_BASE
 67c66c2878aed17ae3096d7db483ddbb2293c503 FIREFOX_AURORA_46_BASE
 68d3781deda0d4d58ec9877862830db89669b3a5 FIREFOX_AURORA_47_BASE
 1c6385ae1fe7e37d8f23f958ce14582f07af729e FIREFOX_AURORA_48_BASE
+d98f20c25feeac4dd7ebbd1c022957df1ef58af4 FIREFOX_AURORA_49_BASE
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1247047 - Update Android dependencies (Google Play Services)
+Merge day clobber
\ No newline at end of file
--- a/accessible/ipc/ProxyAccessible.h
+++ b/accessible/ipc/ProxyAccessible.h
@@ -420,22 +420,22 @@ private:
   role mRole : 30;
   bool mOuterDoc : 1;
   const bool mIsDoc: 1;
 };
 
 enum Interfaces
 {
   HYPERTEXT = 1,
-  HYPERLINK = 2,
-  IMAGE = 4,
-  VALUE = 8,
-  TABLE = 16,
-  TABLECELL = 32,
-  DOCUMENT = 64,
-  SELECTION = 128,
-  ACTION = 256,
+  HYPERLINK = 1 << 1,
+  IMAGE = 1 << 2,
+  VALUE = 1 << 3,
+  TABLE = 1 << 4,
+  TABLECELL = 1 << 5,
+  DOCUMENT = 1 << 6,
+  SELECTION = 1 << 7,
+  ACTION = 1 << 8,
 };
 
 }
 }
 
 #endif
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
-<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1464091834000">
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1464967719000">
   <emItems>
       <emItem  blockID="i58" id="webmaster@buzzzzvideos.info">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i71" id="youtube@2youtube.com">
@@ -359,16 +359,25 @@
               </prefs>
     </emItem>
       <emItem  blockID="i482" id="brasilescapeeight@facebook.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i1227" id="{A34CAF42-A3E3-11E5-945F-18C31D5D46B0}">
+                        <versionRange  minVersion="0" maxVersion="*" severity="1">
+                    </versionRange>
+                    <prefs>
+                  <pref>security.csp.enable</pref>
+                  <pref>security.fileuri.strict_origin_policy</pref>
+                  <pref>security.mixed_content.block_active_content</pref>
+              </prefs>
+    </emItem>
       <emItem  blockID="i1129" id="youtubeunblocker__web@unblocker.yt">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i17" id="{3252b9ae-c69a-4eaf-9502-dc9c1f6c009e}">
                         <versionRange  minVersion="2.2" maxVersion="2.2">
@@ -480,16 +489,22 @@
               </prefs>
     </emItem>
       <emItem  blockID="i748" id="{32da2f20-827d-40aa-a3b4-2fc4a294352e}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i1229" id="/^.*@unblocker\.yt$/">
+                        <versionRange  minVersion="0" maxVersion="*" severity="3">
+                    </versionRange>
+                    <prefs>
+              </prefs>
+    </emItem>
       <emItem  blockID="i404" id="{a9bb9fa0-4122-4c75-bd9a-bc27db3f9155}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i83" id="flash@adobee.com">
                         <versionRange  minVersion="0" maxVersion="*">
@@ -973,17 +988,19 @@
     </emItem>
       <emItem  blockID="i550" id="colmer@yopmail.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i1222" id="tmbepff@trendmicro.com">
-                        <versionRange  minVersion="0" maxVersion="9.2.0.1024" severity="1">
+                        <versionRange  minVersion="0" maxVersion="9.1.0.1035" severity="1">
+                    </versionRange>
+                                <versionRange  minVersion="9.2" maxVersion="9.2.0.1023" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i46" id="{841468a1-d7f4-4bd3-84e6-bb0f13a06c64}">
                         <versionRange  minVersion="0.1" maxVersion="*">
                       <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
                               <versionRange  minVersion="9.0a1" maxVersion="9.0" />
@@ -1648,16 +1665,22 @@
               </prefs>
     </emItem>
       <emItem  blockID="i1012" id="wxtui502n2xce9j@no14">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i1228" id="unblocker30__web@unblocker.yt">
+                        <versionRange  minVersion="0" maxVersion="*" severity="3">
+                    </versionRange>
+                    <prefs>
+              </prefs>
+    </emItem>
       <emItem  blockID="i692" id="/^(j003-lqgrmgpcekslhg|SupraSavings|j003-dkqonnnthqjnkq|j003-kaggrpmirxjpzh)@jetpack$/">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i525" id="/^({65f9f6b7-2dae-46fc-bfaf-f88e4af1beca}|{9ed31f84-c8b3-4926-b950-dff74047ff79}|{0134af61-7a0c-4649-aeca-90d776060cb3}|{02edb56b-9b33-435b-b7df-b2843273a694}|{da51d4f6-3e7e-4ef8-b400-9198e0874606}|{b24577db-155e-4077-bb37-3fdd3c302bb5})$/">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
@@ -3473,16 +3496,28 @@
       <pluginItem  os="Linux" blockID="p1150">
                   <match name="filename" exp="libflashplayer\.so" />                      <versionRange  minVersion="11.2.202.569" maxVersion="11.2.202.577" severity="0" vulnerabilitystatus="1"></versionRange>
                             <infoURL>https://get.adobe.com/flashplayer/</infoURL>
           </pluginItem>
       <pluginItem  blockID="p1151">
                   <match name="filename" exp="npqtplugin\.dll" />                      <versionRange  minVersion="0" maxVersion="*" severity="0" vulnerabilitystatus="2"></versionRange>
                             <infoURL>https://support.apple.com/en-us/HT205771</infoURL>
           </pluginItem>
+      <pluginItem  os="Linux" blockID="p1224">
+                  <match name="filename" exp="libflashplayer\.so" />                      <versionRange  minVersion="11.2.202.577" maxVersion="11.2.202.616" severity="0" vulnerabilitystatus="1"></versionRange>
+                            <infoURL>https://get.adobe.com/flashplayer/</infoURL>
+          </pluginItem>
+      <pluginItem  blockID="p1225">
+                  <match name="filename" exp="(NPSWF32.*\.dll)|(NPSWF64.*\.dll)|(Flash\ Player\.plugin)" />                      <versionRange  minVersion="18.0.0.333" maxVersion="18.0.0.343" severity="0" vulnerabilitystatus="1"></versionRange>
+                            <infoURL>https://get.adobe.com/flashplayer/</infoURL>
+          </pluginItem>
+      <pluginItem  blockID="p1226">
+                  <match name="filename" exp="(NPSWF32.*\.dll)|(NPSWF64.*\.dll)|(Flash\ Player\.plugin)" />                      <versionRange  minVersion="21.0.0.197" maxVersion="21.0.0.226" severity="0" vulnerabilitystatus="1"></versionRange>
+                            <infoURL>https://get.adobe.com/flashplayer/</infoURL>
+          </pluginItem>
     </pluginItems>
 
   <gfxItems>
     <gfxBlacklistEntry  blockID="g35">      <os>WINNT 6.1</os>      <vendor>0x10de</vendor>              <devices>
                       <device>0x0a6c</device>
                   </devices>
             <feature>DIRECT2D</feature>      <featureStatus>BLOCKED_DRIVER_VERSION</featureStatus>      <driverVersion>8.17.12.5896</driverVersion>      <driverVersionComparator>LESS_THAN_OR_EQUAL</driverVersionComparator>    </gfxBlacklistEntry>
     <gfxBlacklistEntry  blockID="g36">      <os>WINNT 6.1</os>      <vendor>0x10de</vendor>              <devices>
--- a/browser/app/nsBrowserApp.cpp
+++ b/browser/app/nsBrowserApp.cpp
@@ -25,39 +25,43 @@
 #include <stdarg.h>
 #include <time.h>
 
 #include "nsCOMPtr.h"
 #include "nsIFile.h"
 #include "nsStringGlue.h"
 
 #ifdef XP_WIN
-// we want a wmain entry point
 #ifdef MOZ_ASAN
 // ASAN requires firefox.exe to be built with -MD, and it's OK if we don't
 // support Windows XP SP2 in ASAN builds.
 #define XRE_DONT_SUPPORT_XPSP2
 #endif
 #define XRE_WANT_ENVIRON
-#include "nsWindowsWMain.cpp"
 #if defined(_MSC_VER) && (_MSC_VER < 1900)
 #define snprintf _snprintf
 #endif
 #define strcasecmp _stricmp
 #ifdef MOZ_SANDBOX
 #include "mozilla/sandboxing/SandboxInitialization.h"
 #endif
 #endif
 #include "BinaryPath.h"
 
 #include "nsXPCOMPrivate.h" // for MAXPATHLEN and XPCOM_DLL
 
 #include "mozilla/Telemetry.h"
 #include "mozilla/WindowsDllBlocklist.h"
 
+#if !defined(MOZ_WIDGET_COCOA) && !defined(MOZ_WIDGET_ANDROID) \
+  && !(defined(XP_LINUX) && defined(MOZ_SANDBOX))
+#define MOZ_BROWSER_CAN_BE_CONTENTPROC
+#include "../../ipc/contentproc/plugin-container.cpp"
+#endif
+
 using namespace mozilla;
 
 #ifdef XP_MACOSX
 #define kOSXResourcesFolder "Resources"
 #endif
 #define kDesktopFolder "browser"
 
 static void Output(const char *fmt, ... )
@@ -123,26 +127,34 @@ static bool IsArg(const char* arg, const
 XRE_GetFileFromPathType XRE_GetFileFromPath;
 XRE_CreateAppDataType XRE_CreateAppData;
 XRE_FreeAppDataType XRE_FreeAppData;
 XRE_TelemetryAccumulateType XRE_TelemetryAccumulate;
 XRE_StartupTimelineRecordType XRE_StartupTimelineRecord;
 XRE_mainType XRE_main;
 XRE_StopLateWriteChecksType XRE_StopLateWriteChecks;
 XRE_XPCShellMainType XRE_XPCShellMain;
+XRE_GetProcessTypeType XRE_GetProcessType;
+XRE_SetProcessTypeType XRE_SetProcessType;
+XRE_InitChildProcessType XRE_InitChildProcess;
+XRE_EnableSameExecutableForContentProcType XRE_EnableSameExecutableForContentProc;
 
 static const nsDynamicFunctionLoad kXULFuncs[] = {
     { "XRE_GetFileFromPath", (NSFuncPtr*) &XRE_GetFileFromPath },
     { "XRE_CreateAppData", (NSFuncPtr*) &XRE_CreateAppData },
     { "XRE_FreeAppData", (NSFuncPtr*) &XRE_FreeAppData },
     { "XRE_TelemetryAccumulate", (NSFuncPtr*) &XRE_TelemetryAccumulate },
     { "XRE_StartupTimelineRecord", (NSFuncPtr*) &XRE_StartupTimelineRecord },
     { "XRE_main", (NSFuncPtr*) &XRE_main },
     { "XRE_StopLateWriteChecks", (NSFuncPtr*) &XRE_StopLateWriteChecks },
     { "XRE_XPCShellMain", (NSFuncPtr*) &XRE_XPCShellMain },
+    { "XRE_GetProcessType", (NSFuncPtr*) &XRE_GetProcessType },
+    { "XRE_SetProcessType", (NSFuncPtr*) &XRE_SetProcessType },
+    { "XRE_InitChildProcess", (NSFuncPtr*) &XRE_InitChildProcess },
+    { "XRE_EnableSameExecutableForContentProc", (NSFuncPtr*) &XRE_EnableSameExecutableForContentProc },
     { nullptr, nullptr }
 };
 
 static int do_main(int argc, char* argv[], char* envp[], nsIFile *xreDirectory)
 {
   nsCOMPtr<nsIFile> appini;
   nsresult rv;
   uint32_t mainFlags = 0;
@@ -291,35 +303,64 @@ sizeof(XPCOM_DLL) - 1))
   if (NS_FAILED(rv)) {
     Output("Couldn't load XRE functions.\n");
     return rv;
   }
 
   // This will set this thread as the main thread.
   NS_LogInit();
 
-  // chop XPCOM_DLL off exePath
-  *lastSlash = '\0';
+  if (xreDirectory) {
+    // chop XPCOM_DLL off exePath
+    *lastSlash = '\0';
 #ifdef XP_MACOSX
-  lastSlash = strrchr(exePath, XPCOM_FILE_PATH_SEPARATOR[0]);
-  strcpy(lastSlash + 1, kOSXResourcesFolder);
+    lastSlash = strrchr(exePath, XPCOM_FILE_PATH_SEPARATOR[0]);
+    strcpy(lastSlash + 1, kOSXResourcesFolder);
 #endif
 #ifdef XP_WIN
-  rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(exePath), false,
-                       xreDirectory);
+    rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(exePath), false,
+                         xreDirectory);
 #else
-  rv = NS_NewNativeLocalFile(nsDependentCString(exePath), false,
-                             xreDirectory);
+    rv = NS_NewNativeLocalFile(nsDependentCString(exePath), false,
+                               xreDirectory);
 #endif
+  }
 
   return rv;
 }
 
 int main(int argc, char* argv[], char* envp[])
 {
+#ifdef MOZ_BROWSER_CAN_BE_CONTENTPROC
+  // We are launching as a content process, delegate to the appropriate
+  // main
+  if (argc > 1 && IsArg(argv[1], "contentproc")) {
+#if defined(XP_WIN) && defined(MOZ_SANDBOX)
+    // We need to initialize the sandbox TargetServices before InitXPCOMGlue
+    // because we might need the sandbox broker to give access to some files.
+    if (!sandboxing::GetInitializedTargetServices()) {
+      Output("Failed to initialize the sandbox target services.");
+      return 255;
+    }
+#endif
+
+    nsresult rv = InitXPCOMGlue(argv[0], nullptr);
+    if (NS_FAILED(rv)) {
+      return 255;
+    }
+
+    int result = content_process_main(argc, argv);
+
+    // InitXPCOMGlue calls NS_LogInit, so we need to balance it here.
+    NS_LogTerm();
+
+    return result;
+  }
+#endif
+
   mozilla::TimeStamp start = mozilla::TimeStamp::Now();
 
 #ifdef XP_MACOSX
   TriggerQuirks();
 #endif
 
   int gotCounters;
 #if defined(XP_UNIX)
@@ -374,16 +415,20 @@ int main(int argc, char* argv[], char* e
       XRE_TelemetryAccumulate(mozilla::Telemetry::GLUESTARTUP_HARD_FAULTS,
                               int(newRUsage.ru_majflt - initialRUsage.ru_majflt));
     }
 #else
   #error "Unknown platform"  // having this here keeps cppcheck happy
 #endif
   }
 
+#ifdef MOZ_BROWSER_CAN_BE_CONTENTPROC
+  XRE_EnableSameExecutableForContentProc();
+#endif
+
   int result = do_main(argc, argv, envp, xreDirectory);
 
   NS_LogTerm();
 
 #ifdef XP_MACOSX
   // Allow writes again. While we would like to catch writes from static
   // destructors to allow early exits to use _exit, we know that there is
   // at least one such write that we don't control (see bug 826029). For
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1396,20 +1396,18 @@ pref("dom.webnotifications.serviceworker
 pref("dom.push.enabled", true);
 
 // These are the thumbnail width/height set in about:newtab.
 // If you change this, ENSURE IT IS THE SAME SIZE SET
 // by about:newtab. These values are in CSS pixels.
 pref("toolkit.pageThumbs.minWidth", 280);
 pref("toolkit.pageThumbs.minHeight", 190);
 
-#ifdef NIGHTLY_BUILD
-// Enable speech synthesis, only Nightly for now
+// Enable speech synthesis
 pref("media.webspeech.synth.enabled", true);
-#endif
 
 pref("browser.esedbreader.loglevel", "Error");
 
 pref("browser.laterrun.enabled", false);
 
 // Enable browser frames for use on desktop.  Only exposed to chrome callers.
 pref("dom.mozBrowserFramesEnabled", true);
 
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -1,16 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var gFxAccounts = {
 
-  PREF_SYNC_START_DOORHANGER: "services.sync.ui.showSyncStartDoorhanger",
-  DOORHANGER_ACTIVATE_DELAY_MS: 5000,
   SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration",
 
   _initialized: false,
   _inCustomizationMode: false,
 
   get weave() {
     delete this.weave;
     return this.weave = Cc["@mozilla.org/weave/service;1"]
@@ -18,23 +16,21 @@ var gFxAccounts = {
                           .wrappedJSObject;
   },
 
   get topics() {
     // Do all this dance to lazy-load FxAccountsCommon.
     delete this.topics;
     return this.topics = [
       "weave:service:ready",
-      "weave:service:sync:start",
       "weave:service:login:error",
       "weave:service:setup-complete",
       "weave:ui:login:error",
       "fxa-migration:state-changed",
       this.FxAccountsCommon.ONLOGIN_NOTIFICATION,
-      this.FxAccountsCommon.ONVERIFIED_NOTIFICATION,
       this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
       this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
     ];
   },
 
   get panelUIFooter() {
     delete this.panelUIFooter;
     return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
@@ -77,32 +73,26 @@ var gFxAccounts = {
       return false;
     }
     // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
     // All other login failures are assumed to be transient and should go
     // away by themselves, so aren't reflected here.
     return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
   },
 
-  get isActiveWindow() {
-    let fm = Services.focus;
-    return fm.activeWindow == window;
-  },
-
   init: function () {
     // Bail out if we're already initialized and for pop-up windows.
     if (this._initialized || !window.toolbar.visible) {
       return;
     }
 
     for (let topic of this.topics) {
       Services.obs.addObserver(this, topic, false);
     }
 
-    addEventListener("activate", this);
     gNavToolbox.addEventListener("customizationstarting", this);
     gNavToolbox.addEventListener("customizationending", this);
 
     EnsureFxAccountsWebChannel();
     this._initialized = true;
 
     this.updateUI();
   },
@@ -116,51 +106,28 @@ var gFxAccounts = {
       Services.obs.removeObserver(this, topic);
     }
 
     this._initialized = false;
   },
 
   observe: function (subject, topic, data) {
     switch (topic) {
-      case this.FxAccountsCommon.ONVERIFIED_NOTIFICATION:
-        Services.prefs.setBoolPref(this.PREF_SYNC_START_DOORHANGER, true);
-        break;
-      case "weave:service:sync:start":
-        this.onSyncStart();
-        break;
       case "fxa-migration:state-changed":
         this.onMigrationStateChanged(data, subject);
         break;
       case this.FxAccountsCommon.ONPROFILE_IMAGE_CHANGE_NOTIFICATION:
         this.updateUI();
         break;
       default:
         this.updateUI();
         break;
     }
   },
 
-  onSyncStart: function () {
-    if (!this.isActiveWindow) {
-      return;
-    }
-
-    let showDoorhanger = false;
-
-    try {
-      showDoorhanger = Services.prefs.getBoolPref(this.PREF_SYNC_START_DOORHANGER);
-    } catch (e) { /* The pref might not exist. */ }
-
-    if (showDoorhanger) {
-      Services.prefs.clearUserPref(this.PREF_SYNC_START_DOORHANGER);
-      this.showSyncStartedDoorhanger();
-    }
-  },
-
   onMigrationStateChanged: function () {
     // Since we nuked most of the migration code, this notification will fire
     // once after legacy Sync has been disconnected (and should never fire
     // again)
     let nb = window.document.getElementById("global-notificationbox");
 
     let msg = this.strings.GetStringFromName("autoDisconnectDescription")
     let signInLabel = this.strings.GetStringFromName("autoDisconnectSignIn.label");
@@ -194,43 +161,18 @@ var gFxAccounts = {
                           nb.PRIORITY_WARNING_LOW,
                           buttons);
 
     // ensure the hamburger menu reflects the newly disconnected state.
     this.updateAppMenuItem();
   },
 
   handleEvent: function (event) {
-    if (event.type == "activate") {
-      // Our window might have been in the background while we received the
-      // sync:start notification. If still needed, show the doorhanger after
-      // a short delay. Without this delay the doorhanger would not show up
-      // or with a too small delay show up while we're still animating the
-      // window.
-      setTimeout(() => this.onSyncStart(), this.DOORHANGER_ACTIVATE_DELAY_MS);
-    } else {
-      this._inCustomizationMode = event.type == "customizationstarting";
-      this.updateAppMenuItem();
-    }
-  },
-
-  showDoorhanger: function (id) {
-    let panel = document.getElementById(id);
-    let anchor = document.getElementById("PanelUI-menu-button");
-
-    let iconAnchor =
-      document.getAnonymousElementByAttribute(anchor, "class",
-                                              "toolbarbutton-icon");
-
-    panel.hidden = false;
-    panel.openPopup(iconAnchor || anchor, "bottomcenter topright");
-  },
-
-  showSyncStartedDoorhanger: function () {
-    this.showDoorhanger("sync-start-panel");
+    this._inCustomizationMode = event.type == "customizationstarting";
+    this.updateAppMenuItem();
   },
 
   updateUI: function () {
     // It's possible someone signed in to FxA after seeing our notification
     // about "Legacy Sync migration" (which now is actually "Legacy Sync
     // auto-disconnect") so kill that notification if it still exists.
     let nb = window.document.getElementById("global-notificationbox");
     let n = nb.getNotificationWithValue(this.SYNC_MIGRATION_NOTIFICATION_TITLE);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3225,45 +3225,59 @@ function getPEMString(cert)
   return "-----BEGIN CERTIFICATE-----\r\n"
          + wrapped
          + "\r\n-----END CERTIFICATE-----\r\n";
 }
 
 var PrintPreviewListener = {
   _printPreviewTab: null,
   _tabBeforePrintPreview: null,
+  _simplifyPageTab: null,
 
   getPrintPreviewBrowser: function () {
     if (!this._printPreviewTab) {
       let browser = gBrowser.selectedTab.linkedBrowser;
       let forceNotRemote = gMultiProcessBrowser && !browser.isRemoteBrowser;
       this._tabBeforePrintPreview = gBrowser.selectedTab;
       this._printPreviewTab = gBrowser.loadOneTab("about:blank",
                                                   { inBackground: false,
                                                     forceNotRemote });
       gBrowser.selectedTab = this._printPreviewTab;
     }
     return gBrowser.getBrowserForTab(this._printPreviewTab);
   },
+  createSimplifiedBrowser: function () {
+    this._simplifyPageTab = gBrowser.loadOneTab("about:blank",
+                                                { inBackground: true });
+    return this.getSimplifiedSourceBrowser();
+  },
   getSourceBrowser: function () {
     return this._tabBeforePrintPreview ?
       this._tabBeforePrintPreview.linkedBrowser : gBrowser.selectedBrowser;
   },
+  getSimplifiedSourceBrowser: function () {
+    return this._simplifyPageTab ?
+      gBrowser.getBrowserForTab(this._simplifyPageTab) : null;
+  },
   getNavToolbox: function () {
     return gNavToolbox;
   },
   onEnter: function () {
     gInPrintPreviewMode = true;
     this._toggleAffectedChrome();
   },
   onExit: function () {
     gBrowser.selectedTab = this._tabBeforePrintPreview;
     this._tabBeforePrintPreview = null;
     gInPrintPreviewMode = false;
     this._toggleAffectedChrome();
+    if (this._simplifyPageTab) {
+      gBrowser.removeTab(this._simplifyPageTab);
+      this._simplifyPageTab = null;
+    }
     gBrowser.removeTab(this._printPreviewTab);
     this._printPreviewTab = null;
   },
   _toggleAffectedChrome: function () {
     gNavToolbox.collapsed = gInPrintPreviewMode;
 
     if (gInPrintPreviewMode)
       this._hideChrome();
@@ -7159,54 +7173,49 @@ var gIdentityHandler = {
   },
 
   updateSitePermissions: function () {
     while (this._permissionList.hasChildNodes())
       this._permissionList.removeChild(this._permissionList.lastChild);
 
     let uri = gBrowser.currentURI;
 
-    for (let permission of SitePermissions.listPermissions()) {
-      let state = SitePermissions.get(uri, permission);
-
-      if (state == SitePermissions.UNKNOWN)
-        continue;
-
-      let item = this._createPermissionItem(permission, state);
+    for (let permission of SitePermissions.getPermissionsByURI(uri)) {
+      let item = this._createPermissionItem(permission);
       this._permissionList.appendChild(item);
     }
   },
 
   setPermission: function (aPermission, aState) {
     if (aState == SitePermissions.getDefault(aPermission))
       SitePermissions.remove(gBrowser.currentURI, aPermission);
     else
       SitePermissions.set(gBrowser.currentURI, aPermission, aState);
   },
 
-  _createPermissionItem: function (aPermission, aState) {
+  _createPermissionItem: function (aPermission) {
     let menulist = document.createElement("menulist");
     let menupopup = document.createElement("menupopup");
-    for (let state of SitePermissions.getAvailableStates(aPermission)) {
+    for (let state of aPermission.availableStates) {
       let menuitem = document.createElement("menuitem");
-      menuitem.setAttribute("value", state);
-      menuitem.setAttribute("label", SitePermissions.getStateLabel(aPermission, state));
+      menuitem.setAttribute("value", state.id);
+      menuitem.setAttribute("label", state.label);
       menupopup.appendChild(menuitem);
     }
     menulist.appendChild(menupopup);
-    menulist.setAttribute("value", aState);
+    menulist.setAttribute("value", aPermission.state);
     menulist.setAttribute("oncommand", "gIdentityHandler.setPermission('" +
-                                       aPermission + "', this.value)");
-    menulist.setAttribute("id", "identity-popup-permission:" + aPermission);
+                                       aPermission.id + "', this.value)");
+    menulist.setAttribute("id", "identity-popup-permission:" + aPermission.id);
 
     let label = document.createElement("label");
     label.setAttribute("flex", "1");
     label.setAttribute("class", "identity-popup-permission-label");
     label.setAttribute("control", menulist.getAttribute("id"));
-    label.textContent = SitePermissions.getPermissionLabel(aPermission);
+    label.textContent = aPermission.label;
 
     let container = document.createElement("hbox");
     container.setAttribute("align", "center");
     container.appendChild(label);
     container.appendChild(menulist);
 
     // The menuitem text can be long and we don't want the dropdown
     // to expand to the width of unselected labels.
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -416,31 +416,16 @@
         <button class="ctrlTab-preview" flex="1"/>
         <button class="ctrlTab-preview" flex="1"/>
       </hbox>
       <hbox pack="center">
         <button id="ctrlTab-showAll" class="ctrlTab-preview" noicon="true"/>
       </hbox>
     </panel>
 
-    <!-- Sync Panel -->
-    <panel id="sync-start-panel" class="sync-panel" type="arrow" hidden="true"
-           noautofocus="true" onclick="this.hidePopup();"
-           flip="slide">
-      <hbox class="sync-panel-outer">
-        <image class="sync-panel-icon"/>
-        <vbox class="sync-panel-inner">
-          <description id="sync-start-panel-title"
-                       value="&syncStartPanel2.heading;"/>
-          <description id="sync-start-panel-subtitle"
-                       value="&syncStartPanel2.subTitle;"/>
-        </vbox>
-      </hbox>
-    </panel>
-
     <!-- Bookmarks and history tooltip -->
     <tooltip id="bhTooltip"/>
 
     <tooltip id="tabbrowser-tab-tooltip" onpopupshowing="gBrowser.createTooltip(event);"/>
 
     <tooltip id="back-button-tooltip">
       <label class="tooltip-label" value="&backButton.tooltip;"/>
 #ifdef XP_MACOSX
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -571,25 +571,30 @@ Sanitizer.prototype = {
                       .getService(Ci.nsISiteSecurityService);
           sss.clearAll();
         } catch (ex) {
           seenException = ex;
         }
 
         // Clear all push notification subscriptions
         try {
-          let push = Cc["@mozilla.org/push/Service;1"]
-                       .getService(Ci.nsIPushService);
-          push.clearForDomain("*", status => {
-            if (!Components.isSuccessCode(status)) {
-              dump("Error clearing Web Push data: " + status + "\n");
-            }
+          yield new Promise((resolve, reject) => {
+            let push = Cc["@mozilla.org/push/Service;1"]
+                         .getService(Ci.nsIPushService);
+            push.clearForDomain("*", status => {
+              if (Components.isSuccessCode(status)) {
+                resolve();
+              } else {
+                reject(new Error("Error clearing push subscriptions: " +
+                                 status));
+              }
+            });
           });
-        } catch (e) {
-          dump("Web Push may not be available.\n");
+        } catch (ex) {
+          seenException = ex;
         }
 
         TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj);
         if (seenException) {
           throw seenException;
         }
       })
     },
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2886,21 +2886,27 @@
       <method name="selectTabAtIndex">
         <parameter name="aIndex"/>
         <parameter name="aEvent"/>
         <body>
         <![CDATA[
           let tabs = this.visibleTabs;
 
           // count backwards for aIndex < 0
-          if (aIndex < 0)
+          if (aIndex < 0) {
             aIndex += tabs.length;
-
-          if (aIndex >= 0 && aIndex < tabs.length)
-            this.selectedTab = tabs[aIndex];
+            // clamp at index 0 if still negative.
+            if (aIndex < 0)
+              aIndex = 0;
+          } else if (aIndex >= tabs.length) {
+            // clamp at right-most tab if out of range.
+            aIndex = tabs.length - 1;
+          }
+
+          this.selectedTab = tabs[aIndex];
 
           if (aEvent) {
             aEvent.preventDefault();
             aEvent.stopPropagation();
           }
         ]]>
         </body>
       </method>
--- a/browser/base/content/test/general/browser_bookmark_popup.js
+++ b/browser/base/content/test/general/browser_bookmark_popup.js
@@ -37,17 +37,16 @@ function* test_bookmarks_popup({isNewBoo
 
     if (popupEditFn) {
       yield popupEditFn();
     }
     let bookmarks = [];
     yield PlacesUtils.bookmarks.fetch({url: "about:home"}, bm => bookmarks.push(bm));
     is(bookmarks.length, 1, "Only one bookmark should exist");
     is(bookmarkStar.getAttribute("starred"), "true", "Page is starred");
-    is(bookmarkPanel.state, "open", "Check that panel state is 'open'");
     is(bookmarkPanelTitle.value,
       isNewBookmark ?
         gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
         gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"),
       "title should match isEditingBookmark state");
 
     if (!shouldAutoClose) {
       yield new Promise(resolve => setTimeout(resolve, 400));
--- a/browser/base/content/test/general/browser_selectTabAtIndex.js
+++ b/browser/base/content/test/general/browser_selectTabAtIndex.js
@@ -1,22 +1,81 @@
+"use strict";
+
 function test() {
-  for (let i = 0; i < 9; i++)
-    gBrowser.addTab();
+  const isLinux = navigator.platform.indexOf("Linux") == 0;
 
-  var isLinux = navigator.platform.indexOf("Linux") == 0;
-  for (let i = 9; i >= 1; i--) {
+  function assertTab(expectedTab) {
+    is(gBrowser.tabContainer.selectedIndex, expectedTab,
+       `tab index ${expectedTab} should be selected`);
+  }
+
+  function sendAccelKey(key) {
     // Make sure the keystroke goes to chrome.
     document.activeElement.blur();
+    EventUtils.synthesizeKey(key.toString(), { altKey: isLinux, accelKey: !isLinux });
+  }
 
-    EventUtils.synthesizeKey(i.toString(), { altKey: isLinux, accelKey: !isLinux });
+  function createTabs(count) {
+    for (let n = 0; n < count; n++)
+      gBrowser.addTab();
+  }
 
-    is(gBrowser.tabContainer.selectedIndex, (i == 9 ? gBrowser.tabs.length : i) - 1,
-       (isLinux ? "Alt" : "Accel") + "+" + i + " selects expected tab");
+  function testKey(key, expectedTab) {
+    sendAccelKey(key);
+    assertTab(expectedTab);
+  }
+
+  function testIndex(index, expectedTab) {
+    gBrowser.selectTabAtIndex(index);
+    assertTab(expectedTab);
   }
 
-  gBrowser.selectTabAtIndex(-3);
-  is(gBrowser.tabContainer.selectedIndex, gBrowser.tabs.length - 3,
-     "gBrowser.selectTabAtIndex(-3) selects expected tab");
+  // Create fewer tabs than our 9 number keys.
+  is(gBrowser.tabs.length, 1, "should have 1 tab");
+  createTabs(4);
+  is(gBrowser.tabs.length, 5, "should have 5 tabs");
+
+  // Test keyboard shortcuts. Order tests so that no two test cases have the
+  // same expected tab in a row. This ensures that tab selection actually
+  // changed the selected tab.
+  testKey(8, 4);
+  testKey(1, 0);
+  testKey(2, 1);
+  testKey(4, 3);
+  testKey(9, 4);
+
+  // Test index selection.
+  testIndex(0, 0);
+  testIndex(4, 4);
+  testIndex(-5, 0);
+  testIndex(5, 4);
+  testIndex(-4, 1);
+  testIndex(1, 1);
+  testIndex(-1, 4);
+  testIndex(9, 4);
 
-  for (let i = 0; i < 9; i++)
+  // Create more tabs than our 9 number keys.
+  createTabs(10);
+  is(gBrowser.tabs.length, 15, "should have 15 tabs");
+
+  // Test keyboard shortcuts.
+  testKey(2, 1);
+  testKey(1, 0);
+  testKey(4, 3);
+  testKey(8, 7);
+  testKey(9, 14);
+
+  // Test index selection.
+  testIndex(-15, 0);
+  testIndex(14, 14);
+  testIndex(-14, 1);
+  testIndex(15, 14);
+  testIndex(-1, 14);
+  testIndex(0, 0);
+  testIndex(1, 1);
+  testIndex(9, 9);
+
+  // Clean up tabs.
+  for (let n = 15; n > 1; n--)
     gBrowser.removeTab(gBrowser.selectedTab, {skipPermitUnload: true});
+  is(gBrowser.tabs.length, 1, "should have 1 tab");
 }
--- a/browser/base/content/test/general/browser_visibleTabs.js
+++ b/browser/base/content/test/general/browser_visibleTabs.js
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+"use strict";
+
 add_task(function* () {
   // There should be one tab when we start the test
   let [origTab] = gBrowser.visibleTabs;
 
   // Add a tab that will get pinned
   let pinned = gBrowser.addTab();
   gBrowser.pinTab(pinned);
 
@@ -28,26 +30,28 @@ add_task(function* () {
   gBrowser.showOnlyTheseTabs([testTab]);
 
   visible = gBrowser.visibleTabs;
   is(visible.length, 2, "2 tabs should be visible including the pinned");
   is(visible[0], pinned, "first is pinned");
   is(visible[1], testTab, "next is the test tab");
   is(gBrowser.tabs.length, 3, "3 tabs should still be open");
 
+  gBrowser.selectTabAtIndex(1);
+  is(gBrowser.selectedTab, testTab, "second tab is the test tab");
   gBrowser.selectTabAtIndex(0);
   is(gBrowser.selectedTab, pinned, "first tab is pinned");
-  gBrowser.selectTabAtIndex(1);
-  is(gBrowser.selectedTab, testTab, "second tab is the test tab");
   gBrowser.selectTabAtIndex(2);
   is(gBrowser.selectedTab, testTab, "no third tab, so no change");
   gBrowser.selectTabAtIndex(0);
   is(gBrowser.selectedTab, pinned, "switch back to the pinned");
   gBrowser.selectTabAtIndex(2);
-  is(gBrowser.selectedTab, pinned, "no third tab, so no change");
+  is(gBrowser.selectedTab, testTab, "no third tab, so select last tab");
+  gBrowser.selectTabAtIndex(-2);
+  is(gBrowser.selectedTab, pinned, "pinned tab is second from left (when orig tab is hidden)");
   gBrowser.selectTabAtIndex(-1);
   is(gBrowser.selectedTab, testTab, "last tab is the test tab");
 
   gBrowser.tabContainer.advanceSelectedTab(1, true);
   is(gBrowser.selectedTab, pinned, "wrapped around the end to pinned");
   gBrowser.tabContainer.advanceSelectedTab(1, true);
   is(gBrowser.selectedTab, testTab, "next to test tab");
   gBrowser.tabContainer.advanceSelectedTab(1, true);
@@ -86,9 +90,8 @@ add_task(function* () {
 
   // Close the last visible tab and make sure we still get a visible tab
   gBrowser.removeTab(testTab);
   is(gBrowser.visibleTabs.length, 1, "only orig is left and visible");
   is(gBrowser.tabs.length, 1, "sanity check that it matches");
   is(gBrowser.selectedTab, origTab, "got the orig tab");
   is(origTab.hidden, false, "and it's not hidden -- visible!");
 });
-
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -417,22 +417,23 @@ function createUserContextMenu(event, ad
   let bundle = document.getElementById("bundle_browser");
   let docfrag = document.createDocumentFragment();
 
   ContextualIdentityService.getIdentities().forEach(identity => {
     let menuitem = document.createElement("menuitem");
     menuitem.setAttribute("usercontextid", identity.userContextId);
     menuitem.setAttribute("label", bundle.getString(identity.label));
     menuitem.setAttribute("accesskey", bundle.getString(identity.accessKey));
+    menuitem.classList.add("menuitem-iconic");
 
     if (addCommandAttribute) {
       menuitem.setAttribute("command", "Browser:NewUserContextTab");
     }
 
-    menuitem.style.listStyleImage = "url(" + identity.icon + ")";
+    menuitem.setAttribute("image", identity.icon);
 
     docfrag.appendChild(menuitem);
   });
 
   event.target.appendChild(docfrag);
   return true;
 }
 
--- a/browser/components/contextualidentity/test/browser/browser.ini
+++ b/browser/components/contextualidentity/test/browser/browser.ini
@@ -6,12 +6,14 @@ support-files =
   serviceworker.html
   worker.js
 
 [browser_aboutURLs.js]
 [browser_usercontext.js]
 [browser_usercontextid_tabdrop.js]
 skip-if = os == "mac" || os == "win" # Intermittent failure - bug 1268276
 [browser_windowName.js]
+tags = openwindow
 [browser_windowOpen.js]
+tags = openwindow
 [browser_serviceworkers.js]
 [browser_broadcastchannel.js]
 [browser_blobUrl.js]
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -9,27 +9,46 @@ XPCOMUtils.defineLazyGetter(this, "Histo
   return PlacesUtils.history;
 });
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 const {
   normalizeTime,
 } = ExtensionUtils;
 
+let historySvc = Ci.nsINavHistoryService;
+const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
+  ["link", historySvc.TRANSITION_LINK],
+  ["typed", historySvc.TRANSITION_TYPED],
+  ["auto_bookmark", historySvc.TRANSITION_BOOKMARK],
+  ["auto_subframe", historySvc.TRANSITION_EMBED],
+  ["manual_subframe", historySvc.TRANSITION_FRAMED_LINK],
+]);
+
+function getTransitionType(transition) {
+  // cannot set a default value for the transition argument as the framework sets it to null
+  transition = transition || "link";
+  let transitionType = TRANSITION_TO_TRANSITION_TYPES_MAP.get(transition);
+  if (!transitionType) {
+    throw new Error(`|${transition}| is not a supported transition for history`);
+  }
+  return transitionType;
+}
+
 /*
  * Converts a nsINavHistoryResultNode into a plain object
  *
  * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
  */
 function convertNavHistoryResultNode(node) {
   return {
     id: node.pageGuid,
     url: node.uri,
     title: node.title,
-    lastVisitTime: PlacesUtils.toTime(node.time),
+    lastVisitTime: PlacesUtils.toDate(node.time).getTime(),
     visitCount: node.accessCount,
   };
 }
 
 /*
  * Converts a nsINavHistoryContainerResultNode into an array of objects
  *
  * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryContainerResultNode
@@ -43,23 +62,49 @@ function convertNavHistoryContainerResul
   }
   container.containerOpen = false;
   return results;
 }
 
 extensions.registerSchemaAPI("history", "history", (extension, context) => {
   return {
     history: {
+      addUrl: function(details) {
+        let transition, date;
+        try {
+          transition = getTransitionType(details.transition);
+        } catch (error) {
+          return Promise.reject({message: error.message});
+        }
+        if (details.visitTime) {
+          date = normalizeTime(details.visitTime);
+        }
+        let pageInfo = {
+          title: details.title,
+          url: details.url,
+          visits: [
+            {
+              transition,
+              date,
+            },
+          ],
+        };
+        try {
+          return History.insert(pageInfo).then(() => undefined);
+        } catch (error) {
+          return Promise.reject({message: error.message});
+        }
+      },
       deleteAll: function() {
         return History.clear();
       },
       deleteRange: function(filter) {
         let newFilter = {
-          beginDate: new Date(filter.startTime),
-          endDate: new Date(filter.endTime),
+          beginDate: normalizeTime(filter.startTime),
+          endDate: normalizeTime(filter.endTime),
         };
         // History.removeVisitsByFilter returns a boolean, but our API should return nothing
         return History.removeVisitsByFilter(newFilter).then(() => undefined);
       },
       deleteUrl: function(details) {
         let url = details.url;
         // History.remove returns a boolean, but our API should return nothing
         return History.remove(url).then(() => undefined);
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -225,16 +225,46 @@ let tabListener = {
   },
 
   emitRemoved(tab, isWindowClosing) {
     let windowId = WindowManager.getId(tab.ownerDocument.defaultView);
     let tabId = TabManager.getId(tab);
 
     this.emit("tab-removed", {tab, tabId, windowId, isWindowClosing});
   },
+
+  tabReadyInitialized: false,
+  tabReadyPromises: new WeakMap(),
+
+  initTabReady() {
+    if (!this.tabReadyInitialized) {
+      AllWindowEvents.addListener("progress", this);
+
+      this.tabReadyInitialized = true;
+    }
+  },
+
+  onLocationChange(browser, webProgress, request, locationURI, flags) {
+    if (webProgress.isTopLevel) {
+      let gBrowser = browser.ownerDocument.defaultView.gBrowser;
+      let tab = gBrowser.getTabForBrowser(browser);
+
+      let deferred = this.tabReadyPromises.get(tab);
+      if (deferred) {
+        deferred.resolve(tab);
+        this.tabReadyPromises.delete(tab);
+      }
+    }
+  },
+
+  awaitTabReady(tab) {
+    return new Promise((resolve, reject) => {
+      this.tabReadyPromises.set(tab, {resolve, reject});
+    });
+  },
 };
 
 extensions.registerSchemaAPI("tabs", null, (extension, context) => {
   let self = {
     tabs: {
       onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
         let tab = event.originalTarget;
         let tabId = TabManager.getId(tab);
@@ -448,64 +478,74 @@ extensions.registerSchemaAPI("tabs", nul
           AllWindowEvents.removeListener("TabAttrModified", listener);
           AllWindowEvents.removeListener("TabPinned", listener);
           AllWindowEvents.removeListener("TabUnpinned", listener);
         };
       }).api(),
 
       create: function(createProperties) {
         return new Promise((resolve, reject) => {
-          function createInWindow(window) {
-            let url;
-
-            if (createProperties.url !== null) {
-              url = context.uri.resolve(createProperties.url);
-
-              if (!context.checkLoadURL(url, {dontReportErrors: true})) {
-                reject({message: `URL not allowed: ${url}`});
-                return;
-              }
-            }
-
-            let tab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL);
-
-            let active = true;
-            if (createProperties.active !== null) {
-              active = createProperties.active;
-            }
-            if (active) {
-              window.gBrowser.selectedTab = tab;
-            }
-
-            if (createProperties.index !== null) {
-              window.gBrowser.moveTabTo(tab, createProperties.index);
-            }
-
-            if (createProperties.pinned) {
-              window.gBrowser.pinTab(tab);
-            }
-
-            resolve(TabManager.convert(extension, tab));
-          }
-
           let window = createProperties.windowId !== null ?
             WindowManager.getWindow(createProperties.windowId, context) :
             WindowManager.topWindow;
           if (!window.gBrowser) {
             let obs = (finishedWindow, topic, data) => {
               if (finishedWindow != window) {
                 return;
               }
               Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
-              createInWindow(window);
+              resolve(window);
             };
             Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
           } else {
-            createInWindow(window);
+            resolve(window);
+          }
+        }).then(window => {
+          let url;
+
+          if (createProperties.url !== null) {
+            url = context.uri.resolve(createProperties.url);
+
+            if (!context.checkLoadURL(url, {dontReportErrors: true})) {
+              return Promise.reject({message: `Illegal URL: ${url}`});
+            }
+          }
+
+          tabListener.initTabReady();
+          let tab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL);
+
+          let active = true;
+          if (createProperties.active !== null) {
+            active = createProperties.active;
+          }
+          if (active) {
+            window.gBrowser.selectedTab = tab;
           }
+
+          if (createProperties.index !== null) {
+            window.gBrowser.moveTabTo(tab, createProperties.index);
+          }
+
+          if (createProperties.pinned) {
+            window.gBrowser.pinTab(tab);
+          }
+
+          if (!createProperties.url || createProperties.url.startsWith("about:")) {
+            // We can't wait for a location change event for about:newtab,
+            // since it may be pre-rendered, in which case its initial
+            // location change event has already fired.
+            return tab;
+          }
+
+          // Wait for the first location change event, so that operations
+          // like `executeScript` are dispatched to the inner window that
+          // contains the URL we're attempting to load.
+          return tabListener.awaitTabReady(tab);
+        }).then(tab => {
+          return TabManager.convert(extension, tab);
         });
       },
 
       remove: function(tabs) {
         if (!Array.isArray(tabs)) {
           tabs = [tabs];
         }
 
@@ -525,17 +565,17 @@ extensions.registerSchemaAPI("tabs", nul
         }
 
         let tabbrowser = tab.ownerDocument.defaultView.gBrowser;
 
         if (updateProperties.url !== null) {
           let url = context.uri.resolve(updateProperties.url);
 
           if (!context.checkLoadURL(url, {dontReportErrors: true})) {
-            return Promise.reject({message: `URL not allowed: ${url}`});
+            return Promise.reject({message: `Illegal URL: ${url}`});
           }
 
           tab.linkedBrowser.loadURI(url);
         }
 
         if (updateProperties.active !== null) {
           if (updateProperties.active) {
             tabbrowser.selectedTab = tab;
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -553,17 +553,17 @@ global.TabManager = {
         // of a new window, and did not have an `adoptedTab` detail when it was
         // opened.
         this._tabs.set(adoptedBy, this.getId(event.target));
       }
     }
   },
 
   handleWindowOpen(window) {
-    if (window.arguments[0] instanceof window.XULElement) {
+    if (window.arguments && window.arguments[0] instanceof window.XULElement) {
       // If the first window argument is a XUL element, it means the
       // window is about to adopt a tab from another window to replace its
       // initial tab.
       let adoptedTab = window.arguments[0];
 
       this._tabs.set(window.gBrowser.tabs[0], this.getId(adoptedTab));
     }
   },
--- a/browser/components/extensions/schemas/commands.json
+++ b/browser/components/extensions/schemas/commands.json
@@ -6,17 +6,17 @@
   {
     "namespace": "manifest",
     "types": [
      {
         "id": "KeyName",
         "choices": [
           {
             "type": "string",
-            "pattern": "^\\s*(Alt|Ctrl|Command|MacCtr)\\s*\\+\\s*(Shift\\s*\\+\\s*)?([A-Z0-9]|Comma|Period|Home|End|PageUp|PageDown|Space|Insert|Delete|Up|Down|Left|Right)\\s*$"
+            "pattern": "^\\s*(Alt|Ctrl|Command|MacCtrl)\\s*\\+\\s*(Shift\\s*\\+\\s*)?([A-Z0-9]|Comma|Period|Home|End|PageUp|PageDown|Space|Insert|Delete|Up|Down|Left|Right)\\s*$"
           },
           {
             "type": "string",
             "pattern": "^(MediaNextTrack|MediaPlayPause|MediaPrevTrack|MediaStop)$"
           }
         ]
       },
       {
--- a/browser/components/extensions/schemas/history.json
+++ b/browser/components/extensions/schemas/history.json
@@ -182,28 +182,42 @@
                 }
               }
             ]
           }
         ]
       },
       {
         "name": "addUrl",
-        "unsupported": true,
         "type": "function",
-        "description": "Adds a URL to the history at the current time with a $(topic:transition-types)[transition type] of \"link\".",
+        "description": "Adds a URL to the history with a default visitTime of the current time and a default $(topic:transition-types)[transition type] of \"link\".",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "url": {
                 "type": "string",
-                "description": "The URL to add."
+                "description": "The URL to add. Must be a valid URL that can be added to history."
+              },
+              "title": {
+                "type": "string",
+                "optional": true,
+                "description": "The title of the page."
+              },
+              "transition": {
+                "$ref": "TransitionType",
+                "optional": true,
+                "description": "The $(topic:transition-types)[transition type] for this visit from its referrer."
+              },
+              "visitTime": {
+                "$ref": "HistoryTime",
+                "optional": true,
+                "description": "The date when this visit occurred."
               }
             }
           },
           {
             "name": "callback",
             "type": "function",
             "optional": true,
             "parameters": []
@@ -240,22 +254,22 @@
         "description": "Removes all items within the specified date range from the history.  Pages will not be removed from the history unless all visits fall within the range.",
         "async": "callback",
         "parameters": [
           {
             "name": "range",
             "type": "object",
             "properties": {
               "startTime": {
-                "type": "number",
-                "description": "Items added to history after this date, represented in milliseconds since the epoch."
+                "$ref": "HistoryTime",
+                "description": "Items added to history after this date."
               },
               "endTime": {
-                "type": "number",
-                "description": "Items added to history before this date, represented in milliseconds since the epoch."
+                "$ref": "HistoryTime",
+                "description": "Items added to history before this date."
               }
             }
           },
           {
             "name": "callback",
             "type": "function",
             "parameters": []
           }
--- a/browser/components/extensions/test/browser/browser_ext_history.js
+++ b/browser/components/extensions/test/browser/browser_ext_history.js
@@ -1,14 +1,18 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
                                   "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
+                                  "resource://gre/modules/ExtensionUtils.jsm");
 
 add_task(function* test_delete() {
   function background() {
     browser.test.onMessage.addListener((msg, arg) => {
       if (msg === "delete-url") {
         browser.history.deleteUrl({url: arg}).then(result => {
           browser.test.assertEq(undefined, result, "browser.history.deleteUrl returns nothing");
           browser.test.sendMessage("url-deleted");
@@ -25,17 +29,16 @@ add_task(function* test_delete() {
         });
       }
     });
 
     browser.test.sendMessage("ready");
   }
 
   const REFERENCE_DATE = new Date(1999, 9, 9, 9, 9);
-  let {PlacesUtils} = Cu.import("resource://gre/modules/PlacesUtils.jsm", {});
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["history"],
     },
     background: `(${background})()`,
   });
 
@@ -66,30 +69,30 @@ add_task(function* test_delete() {
   let testUrl = visits[6].uri.spec;
   ok(yield PlacesTestUtils.isPageInDB(testUrl), "expected url found in history database");
 
   extension.sendMessage("delete-url", testUrl);
   yield extension.awaitMessage("url-deleted");
   is(yield PlacesTestUtils.isPageInDB(testUrl), false, "expected url not found in history database");
 
   let filter = {
-    startTime: PlacesUtils.toTime(visits[1].visitDate),
-    endTime: PlacesUtils.toTime(visits[3].visitDate),
+    startTime: PlacesUtils.toDate(visits[1].visitDate),
+    endTime: PlacesUtils.toDate(visits[3].visitDate),
   };
 
   extension.sendMessage("delete-range", filter);
   yield extension.awaitMessage("range-deleted");
 
   ok(yield PlacesTestUtils.isPageInDB(visits[0].uri), "expected uri found in history database");
   is(yield PlacesTestUtils.visitsInDB(visits[0].uri), 2, "2 visits for uri found in history database");
   ok(yield PlacesTestUtils.isPageInDB(visits[5].uri), "expected uri found in history database");
   is(yield PlacesTestUtils.visitsInDB(visits[5].uri), 1, "1 visit for uri found in history database");
 
-  filter.startTime = PlacesUtils.toTime(visits[0].visitDate);
-  filter.endTime = PlacesUtils.toTime(visits[5].visitDate);
+  filter.startTime = PlacesUtils.toDate(visits[0].visitDate);
+  filter.endTime = PlacesUtils.toDate(visits[5].visitDate);
 
   extension.sendMessage("delete-range", filter);
   yield extension.awaitMessage("range-deleted");
 
   is(yield PlacesTestUtils.isPageInDB(visits[0].uri), false, "expected uri not found in history database");
   is(yield PlacesTestUtils.visitsInDB(visits[0].uri), 0, "0 visits for uri found in history database");
   is(yield PlacesTestUtils.isPageInDB(visits[5].uri), false, "expected uri not found in history database");
   is(yield PlacesTestUtils.visitsInDB(visits[5].uri), 0, "0 visits for uri found in history database");
@@ -186,8 +189,91 @@ add_task(function* test_search() {
   results = yield extension.awaitMessage("max-results-search");
   is(results.length, 1, "history.search returned 1 result");
   checkResult(results, DOUBLE_VISIT_URL, 2);
 
   yield extension.awaitFinish("search");
   yield extension.unload();
   yield PlacesTestUtils.clearHistory();
 });
+
+add_task(function* test_add_url() {
+  function background() {
+    const TEST_DOMAIN = "http://example.com/";
+
+    browser.test.onMessage.addListener((msg, testData) => {
+      let [details, type] = testData;
+      details.url = details.url || `${TEST_DOMAIN}${type}`;
+      if (msg === "add-url") {
+        details.title = `Title for ${type}`;
+        browser.history.addUrl(details).then(() => {
+          return browser.history.search({text: details.url});
+        }).then(results => {
+          browser.test.assertEq(1, results.length, "1 result found when searching for added URL");
+          browser.test.sendMessage("url-added", {details, result: results[0]});
+        });
+      } else if (msg === "expect-failure") {
+        let expectedMsg = testData[2];
+        browser.history.addUrl(details).then(() => {
+          browser.test.fail(`Expected error thrown for ${type}`);
+        }, error => {
+          browser.test.assertTrue(
+            error.message.includes(expectedMsg),
+            `"Expected error thrown when trying to add a URL with  ${type}`
+          );
+          browser.test.sendMessage("add-failed");
+        });
+      }
+    });
+
+    browser.test.sendMessage("ready");
+  }
+
+  let addTestData = [
+    [{}, "default"],
+    [{visitTime: new Date()}, "with_date"],
+    [{visitTime: Date.now()}, "with_ms_number"],
+    [{visitTime: Date.now().toString()}, "with_ms_string"],
+    [{visitTime: new Date().toISOString()}, "with_iso_string"],
+    [{transition: "typed"}, "valid_transition"],
+  ];
+
+  let failTestData = [
+    [{transition: "generated"}, "an invalid transition", "|generated| is not a supported transition for history"],
+    [{visitTime: Date.now() + 1000000}, "a future date", "cannot be a future date"],
+    [{url: "about.config"}, "an invalid url", "about.config is not a valid URL"],
+  ];
+
+  function* checkUrl(results) {
+    ok(yield PlacesTestUtils.isPageInDB(results.details.url), `${results.details.url} found in history database`);
+    ok(PlacesUtils.isValidGuid(results.result.id), "URL was added with a valid id");
+    is(results.result.title, results.details.title, "URL was added with the correct title");
+    if (results.details.visitTime) {
+      is(results.result.lastVisitTime,
+         Number(ExtensionUtils.normalizeTime(results.details.visitTime)),
+         "URL was added with the correct date");
+    }
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["history"],
+    },
+    background: `(${background})()`,
+  });
+
+  yield PlacesTestUtils.clearHistory();
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+
+  for (let data of addTestData) {
+    extension.sendMessage("add-url", data);
+    let results = yield extension.awaitMessage("url-added");
+    yield checkUrl(results);
+  }
+
+  for (let data of failTestData) {
+    extension.sendMessage("expect-failure", data);
+    yield extension.awaitMessage("add-failed");
+  }
+
+  yield extension.unload();
+});
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -200,18 +200,16 @@ add_task(function* testTabSwitchContext(
           browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
             if (tabId == details.id && changed.url == details.url) {
               browser.tabs.onUpdated.removeListener(listener);
               resolve();
             }
           });
         });
       };
-      let tabLoadPromise;
-
       return [
         expect => {
           browser.test.log("Initial state. No icon visible.");
           expect(null);
         },
         expect => {
           browser.test.log("Show the icon on the first tab, expect default properties.");
           browser.pageAction.show(tabs[0]);
@@ -220,26 +218,23 @@ add_task(function* testTabSwitchContext(
         expect => {
           browser.test.log("Change the icon. Expect default properties excluding the icon.");
           browser.pageAction.setIcon({tabId: tabs[0], path: "1.png"});
           expect(details[1]);
         },
         expect => {
           browser.test.log("Create a new tab. No icon visible.");
           browser.tabs.create({active: true, url: "about:blank?0"}, tab => {
-            tabLoadPromise = promiseTabLoad({url: "about:blank?0", id: tab.id});
             tabs.push(tab.id);
             expect(null);
           });
         },
         expect => {
           browser.test.log("Await tab load. No icon visible.");
-          tabLoadPromise.then(() => {
-            expect(null);
-          });
+          expect(null);
         },
         expect => {
           browser.test.log("Change properties. Expect new properties.");
           let tabId = tabs[1];
           browser.pageAction.show(tabId);
           browser.pageAction.setIcon({tabId, path: "2.png"});
           browser.pageAction.setPopup({tabId, popup: "2.html"});
           browser.pageAction.setTitle({tabId, title: "Title 2"});
--- a/browser/components/extensions/test/browser/browser_ext_tabs_create.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create.js
@@ -96,53 +96,54 @@ add_task(function* () {
               return;
             }
 
             let test = tests.shift();
             let expected = Object.assign({}, DEFAULTS, test.result);
 
             browser.test.log(`Testing tabs.create(${JSON.stringify(test.create)}), expecting ${JSON.stringify(test.result)}`);
 
-            let tabId;
             let updatedPromise = new Promise(resolve => {
               let onUpdated = (changedTabId, changed) => {
-                if (changedTabId === tabId && changed.url) {
+                if (changed.url) {
                   browser.tabs.onUpdated.removeListener(onUpdated);
-                  resolve(changed.url);
+                  resolve({tabId: changedTabId, url: changed.url});
                 }
               };
               browser.tabs.onUpdated.addListener(onUpdated);
             });
 
             let createdPromise = new Promise(resolve => {
               let onCreated = tab => {
                 browser.test.assertTrue("id" in tab, `Expected tabs.onCreated callback to receive tab object`);
                 resolve();
               };
               browser.tabs.onCreated.addListener(onCreated);
             });
 
+            let tabId;
             Promise.all([
               browser.tabs.create(test.create),
               createdPromise,
             ]).then(([tab]) => {
               tabId = tab.id;
 
               for (let key of Object.keys(expected)) {
                 if (key === "url") {
                   // FIXME: This doesn't get updated until later in the load cycle.
                   continue;
                 }
 
                 browser.test.assertEq(expected[key], tab[key], `Expected value for tab.${key}`);
               }
 
               return updatedPromise;
-            }).then(url => {
-              browser.test.assertEq(expected.url, url, `Expected value for tab.url`);
+            }).then(updated => {
+              browser.test.assertEq(tabId, updated.tabId, `Expected value for tab.id`);
+              browser.test.assertEq(expected.url, updated.url, `Expected value for tab.url`);
 
               return browser.tabs.remove(tabId);
             }).then(() => {
               return browser.tabs.update(activeTab, {active: true});
             }).then(() => {
               nextTest();
             });
           }
--- a/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js
@@ -8,17 +8,17 @@ function* testTabsCreateInvalidURL(tabsC
       "permissions": ["tabs"],
     },
 
     background: function() {
       browser.test.sendMessage("ready");
       browser.test.onMessage.addListener((msg, tabsCreateURL) => {
         browser.tabs.create({url: tabsCreateURL}, (tab) => {
           browser.test.assertEq(undefined, tab, "on error tab should be undefined");
-          browser.test.assertTrue(/URL not allowed/.test(browser.runtime.lastError.message),
+          browser.test.assertTrue(/Illegal URL/.test(browser.runtime.lastError.message),
                                   "runtime.lastError should report the expected error message");
 
           // Remove the opened tab is any.
           if (tab) {
             browser.tabs.remove(tab.id);
           }
           browser.test.sendMessage("done");
         });
--- a/browser/components/extensions/test/browser/browser_ext_tabs_detectLanguage.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_detectLanguage.js
@@ -7,30 +7,17 @@ add_task(function* testDetectLanguage() 
     manifest: {
       "permissions": ["tabs"],
     },
 
     background() {
       const BASE_PATH = "browser/browser/components/extensions/test/browser";
 
       function loadTab(url) {
-        let tabId;
-        let awaitUpdated = new Promise(resolve => {
-          browser.tabs.onUpdated.addListener(function onUpdated(changedTabId, changed, tab) {
-            if (changedTabId === tabId && changed.url) {
-              browser.tabs.onUpdated.removeListener(onUpdated);
-              resolve(tab);
-            }
-          });
-        });
-
-        return browser.tabs.create({url}).then(tab => {
-          tabId = tab.id;
-          return awaitUpdated;
-        });
+        return browser.tabs.create({url});
       }
 
       loadTab(`http://example.co.jp/${BASE_PATH}/file_language_ja.html`).then(tab => {
         return browser.tabs.detectLanguage(tab.id).then(lang => {
           browser.test.assertEq("ja", lang, "Japanese document should be detected as Japanese");
           return browser.tabs.remove(tab.id);
         });
       }).then(() => {
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
@@ -113,16 +113,24 @@ add_task(function* testExecuteScript() {
 
         browser.tabs.executeScript({
           code: "location.href;",
           frameId: frames[1].frameId,
         }).then(result => {
           browser.test.assertEq("http://mochi.test:8888/", result, "Result for frameId[1] is correct");
         }),
 
+        browser.tabs.create({url: "http://example.com/"}).then(tab => {
+          return browser.tabs.executeScript(tab.id, {code: "location.href"}).then(result => {
+            browser.test.assertEq("http://example.com/", result, "Script executed correctly in new tab");
+
+            return browser.tabs.remove(tab.id);
+          });
+        }),
+
         new Promise(resolve => {
           browser.runtime.onMessage.addListener(message => {
             browser.test.assertEq("script ran", message, "Expected runtime message");
             resolve();
           });
         }),
       ]);
     }).then(() => {
@@ -130,17 +138,17 @@ add_task(function* testExecuteScript() {
     }).catch(e => {
       browser.test.fail(`Error: ${e} :: ${e.stack}`);
       browser.test.notifyFail("executeScript");
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      "permissions": ["http://mochi.test/", "webNavigation"],
+      "permissions": ["http://mochi.test/", "http://example.com/", "webNavigation"],
     },
 
     background,
 
     files: {
       "script.js": function() {
         browser.runtime.sendMessage("script ran");
       },
--- a/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js
@@ -32,17 +32,17 @@ function* testTabsUpdateURL(existentTabU
             browser.test.assertTrue(tab, "on success the tab should be defined");
           }
         };
 
         let onTabsUpdateError = (error) => {
           if (!isErrorExpected) {
             browser.test.fails(`tabs.update with URL ${tabsUpdateURL} should not be rejected`);
           } else {
-            browser.test.assertTrue(/^URL not allowed/.test(error.message),
+            browser.test.assertTrue(/^Illegal URL/.test(error.message),
                                     "tabs.update should be rejected with the expected error message");
           }
         };
 
         let onTabsUpdateDone = () => browser.test.sendMessage("done");
 
         browser.tabs.query({lastFocusedWindow: true}, (tabs) => {
           browser.tabs.update(tabs[1].id, {url: tabsUpdateURL})
--- a/browser/components/extensions/test/browser/browser_ext_windows_events.js
+++ b/browser/components/extensions/test/browser/browser_ext_windows_events.js
@@ -1,70 +1,101 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+SimpleTest.requestCompleteLog();
+
 add_task(function* testWindowsEvents() {
   function background() {
     browser.windows.onCreated.addListener(function listener(window) {
+      browser.test.log(`onCreated: windowId=${window.id}`);
+
       browser.test.assertTrue(Number.isInteger(window.id),
                               "Window object's id is an integer");
       browser.test.assertEq("normal", window.type,
                             "Window object returned with the correct type");
       browser.test.sendMessage("window-created", window.id);
     });
 
     let lastWindowId;
     browser.windows.onFocusChanged.addListener(function listener(windowId) {
+      browser.test.log(`onFocusChange: windowId=${windowId} lastWindowId=${lastWindowId}`);
+
       browser.test.assertTrue(lastWindowId !== windowId,
                               "onFocusChanged fired once for the given window");
       lastWindowId = windowId;
+
       browser.test.assertTrue(Number.isInteger(windowId),
                               "windowId is an integer");
+
       browser.windows.getLastFocused().then(window => {
         browser.test.assertEq(windowId, window.id,
                               "Last focused window has the correct id");
-        browser.test.sendMessage(`window-focus-changed-${windowId}`);
+        browser.test.sendMessage(`window-focus-changed`, window.id);
       });
     });
 
     browser.windows.onRemoved.addListener(function listener(windowId) {
+      browser.test.log(`onRemoved: windowId=${windowId}`);
+
       browser.test.assertTrue(Number.isInteger(windowId),
                               "windowId is an integer");
-      browser.test.sendMessage(`window-removed-${windowId}`);
+      browser.test.sendMessage(`window-removed`, windowId);
       browser.test.notifyPass("windows.events");
     });
 
     browser.test.sendMessage("ready");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${background})()`,
   });
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
 
   let {WindowManager} = Cu.import("resource://gre/modules/Extension.jsm", {});
+
   let currentWindow = window;
   let currentWindowId = WindowManager.getId(currentWindow);
+  info(`Current window ID: ${currentWindowId}`);
 
+  info(`Create browser window 1`);
   let win1 = yield BrowserTestUtils.openNewBrowserWindow();
   let win1Id = yield extension.awaitMessage("window-created");
-  yield extension.awaitMessage(`window-focus-changed-${win1Id}`);
+  info(`Window 1 ID: ${win1Id}`);
 
+  let winId = yield extension.awaitMessage(`window-focus-changed`);
+  is(winId, win1Id, "Got focus change event for the correct window ID.");
+
+  info(`Create browser window 2`);
   let win2 = yield BrowserTestUtils.openNewBrowserWindow();
   let win2Id = yield extension.awaitMessage("window-created");
-  yield extension.awaitMessage(`window-focus-changed-${win2Id}`);
+  info(`Window 2 ID: ${win2Id}`);
 
+  winId = yield extension.awaitMessage(`window-focus-changed`);
+  is(winId, win2Id, "Got focus change event for the correct window ID.");
+
+  info(`Focus browser window 1`);
   yield focusWindow(win1);
-  yield extension.awaitMessage(`window-focus-changed-${win1Id}`);
 
+  winId = yield extension.awaitMessage(`window-focus-changed`);
+  is(winId, win1Id, "Got focus change event for the correct window ID.");
+
+  info(`Close browser window 2`);
   yield BrowserTestUtils.closeWindow(win2);
-  yield extension.awaitMessage(`window-removed-${win2Id}`);
 
+  winId = yield extension.awaitMessage(`window-removed`);
+  is(winId, win2Id, "Got removed event for the correct window ID.");
+
+  info(`Close browser window 1`);
   yield BrowserTestUtils.closeWindow(win1);
-  yield extension.awaitMessage(`window-removed-${win1Id}`);
+
+  winId = yield extension.awaitMessage(`window-removed`);
+  is(winId, win1Id, "Got removed event for the correct window ID.");
 
-  yield extension.awaitMessage(`window-focus-changed-${currentWindowId}`);
+  winId = yield extension.awaitMessage(`window-focus-changed`);
+  is(winId, currentWindowId, "Got focus change event for the correct window ID.");
+
   yield extension.awaitFinish("windows.events");
   yield extension.unload();
 });
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -304,16 +304,19 @@ BrowserGlue.prototype = {
       case "browser-lastwindow-close-granted":
         if (OBSERVE_LASTWINDOW_CLOSE_TOPICS) {
           this._setPrefToSaveSession();
         }
         break;
       case "weave:service:ready":
         this._setSyncAutoconnectDelay();
         break;
+      case "fxaccounts:onverified":
+        this._showSyncStartedDoorhanger();
+        break;
       case "weave:engine:clients:display-uri":
         this._onDisplaySyncURI(subject);
         break;
       case "session-save":
         this._setPrefToSaveSession(true);
         subject.QueryInterface(Ci.nsISupportsPRBool);
         subject.data = true;
         break;
@@ -521,16 +524,17 @@ BrowserGlue.prototype = {
     os.addObserver(this, "browser:purge-session-history", false);
     os.addObserver(this, "quit-application-requested", false);
     os.addObserver(this, "quit-application-granted", false);
     if (OBSERVE_LASTWINDOW_CLOSE_TOPICS) {
       os.addObserver(this, "browser-lastwindow-close-requested", false);
       os.addObserver(this, "browser-lastwindow-close-granted", false);
     }
     os.addObserver(this, "weave:service:ready", false);
+    os.addObserver(this, "fxaccounts:onverified", false);
     os.addObserver(this, "weave:engine:clients:display-uri", false);
     os.addObserver(this, "session-save", false);
     os.addObserver(this, "places-init-complete", false);
     this._isPlacesInitObserver = true;
     os.addObserver(this, "places-database-locked", false);
     this._isPlacesLockedObserver = true;
     os.addObserver(this, "distribution-customization-complete", false);
     os.addObserver(this, "places-shutdown", false);
@@ -586,16 +590,17 @@ BrowserGlue.prototype = {
     os.removeObserver(this, "quit-application-requested");
     os.removeObserver(this, "quit-application-granted");
     os.removeObserver(this, "restart-in-safe-mode");
     if (OBSERVE_LASTWINDOW_CLOSE_TOPICS) {
       os.removeObserver(this, "browser-lastwindow-close-requested");
       os.removeObserver(this, "browser-lastwindow-close-granted");
     }
     os.removeObserver(this, "weave:service:ready");
+    os.removeObserver(this, "fxaccounts:onverified");
     os.removeObserver(this, "weave:engine:clients:display-uri");
     os.removeObserver(this, "session-save");
     if (this._bookmarksBackupIdleTime) {
       this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
       delete this._bookmarksBackupIdleTime;
     }
     if (this._isPlacesInitObserver)
       os.removeObserver(this, "places-init-complete");
@@ -1890,16 +1895,29 @@ BrowserGlue.prototype = {
 
     var notifyBox = win.gBrowser.getNotificationBox();
     var notification = notifyBox.appendNotification(text, title, null,
                                                     notifyBox.PRIORITY_CRITICAL_MEDIUM,
                                                     buttons);
     notification.persistence = -1; // Until user closes it
   },
 
+  _showSyncStartedDoorhanger: function () {
+    let bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
+    let title = bundle.GetStringFromName("syncStartNotification.title");
+    let body = bundle.GetStringFromName("syncStartNotification.body");
+
+    let clickCallback = (subject, topic, data) => {
+      if (topic != "alertclickcallback")
+        return;
+      this._openPreferences("sync");
+    }
+    AlertsService.showAlertNotification(null, title, body, true, null, clickCallback);
+  },
+
   _migrateUI: function BG__migrateUI() {
     const UI_VERSION = 38;
     const BROWSER_DOCURL = "chrome://browser/content/browser.xul";
 
     let currentUIVersion;
     if (Services.prefs.prefHasUserValue("browser.migration.version")) {
       currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
     } else {
--- a/browser/components/preferences/in-content/security.js
+++ b/browser/components/preferences/in-content/security.js
@@ -184,20 +184,20 @@ var gSecurityPane = {
     });
 
     blockUncommonUnwanted.addEventListener("command", function() {
       blockUnwantedPref.value = blockUncommonUnwanted.checked;
       blockUncommonPref.value = blockUncommonUnwanted.checked;
 
       let malware = malwareTable.value
         .split(",")
-        .filter(x => x !== "goog-unwanted-simple" && x !== "test-unwanted-simple");
+        .filter(x => x !== "goog-unwanted-shavar" && x !== "test-unwanted-simple");
 
       if (blockUncommonUnwanted.checked) {
-        malware.push("goog-unwanted-simple");
+        malware.push("goog-unwanted-shavar");
         malware.push("test-unwanted-simple");
       }
 
       // sort alphabetically to keep the pref consistent
       malware.sort();
 
       malwareTable.value = malware.join(",");
     });
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -179,32 +179,23 @@ var gSyncPane = {
   _updateComputerNameValue: function(save) {
     if (save) {
       let textbox = document.getElementById("fxaSyncComputerName");
       Weave.Service.clientsEngine.localName = textbox.value;
     }
     this._populateComputerName(Weave.Service.clientsEngine.localName);
   },
 
-  _closeSyncStatusMessageBox: function() {
-    document.getElementById("syncStatusMessage").removeAttribute("message-type");
-    document.getElementById("syncStatusMessageTitle").textContent = "";
-    document.getElementById("syncStatusMessageDescription").textContent = "";
-  },
-
   _setupEventListeners: function() {
     function setEventListener(aId, aEventType, aCallback)
     {
       document.getElementById(aId)
               .addEventListener(aEventType, aCallback.bind(gSyncPane));
     }
 
-    setEventListener("syncStatusMessageClose", "command", function () {
-      gSyncPane._closeSyncStatusMessageBox();
-    });
     setEventListener("noAccountSetup", "click", function (aEvent) {
       aEvent.stopPropagation();
       gSyncPane.openSetup(null);
     });
     setEventListener("noAccountPair", "click", function (aEvent) {
       aEvent.stopPropagation();
       gSyncPane.openSetup('pair');
     });
@@ -588,41 +579,33 @@ var gSyncPane = {
       .then(url => {
         this.openContentInBrowser(url, {
           replaceQueryString: true
         });
       });
   },
 
   verifyFirefoxAccount: function() {
-    this._closeSyncStatusMessageBox();
-    let changesyncStatusMessage = (data) => {
+    let showVerifyNotification = (data) => {
       let isError = !data;
-      let syncStatusMessage = document.getElementById("syncStatusMessage");
-      let syncStatusMessageTitle = document.getElementById("syncStatusMessageTitle");
-      let syncStatusMessageDescription = document.getElementById("syncStatusMessageDescription");
       let maybeNot = isError ? "Not" : "";
       let sb = this._accountsStringBundle;
       let title = sb.GetStringFromName("verification" + maybeNot + "SentTitle");
       let email = !isError && data ? data.email : "";
-      let description = sb.formatStringFromName("verification" + maybeNot + "SentFull", [email], 1)
-
-      syncStatusMessageTitle.textContent = title;
-      syncStatusMessageDescription.textContent = description;
-      let messageType = isError ? "verify-error" : "verify-success";
-      syncStatusMessage.setAttribute("message-type", messageType);
+      let body = sb.formatStringFromName("verification" + maybeNot + "SentBody", [email], 1);
+      new Notification(title, { body })
     }
 
     let onError = () => {
-      changesyncStatusMessage();
+      showVerifyNotification();
     };
 
     let onSuccess = data => {
       if (data) {
-        changesyncStatusMessage(data);
+        showVerifyNotification(data);
       } else {
         onError();
       }
     };
 
     fxAccounts.resendVerificationEmail()
       .then(fxAccounts.getSignedInUser, onError)
       .then(onSuccess, onError);
@@ -632,37 +615,34 @@ var gSyncPane = {
     let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync";
     this.openContentInBrowser(url);
   },
 
   unlinkFirefoxAccount: function(confirm) {
     if (confirm) {
       // We use a string bundle shared with aboutAccounts.
       let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
-      let continueLabel = sb.GetStringFromName("continue.label");
+      let disconnectLabel = sb.GetStringFromName("disconnect.label");
       let title = sb.GetStringFromName("disconnect.verify.title");
-      let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
-      let brandShortName = brandBundle.GetStringFromName("brandShortName");
-      let body = sb.GetStringFromName("disconnect.verify.heading") +
+      let body = sb.GetStringFromName("disconnect.verify.bodyHeading") +
                  "\n\n" +
-                 sb.formatStringFromName("disconnect.verify.description",
-                                         [brandShortName], 1);
+                 sb.GetStringFromName("disconnect.verify.bodyText");
       let ps = Services.prompt;
       let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) +
                         (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) +
                         ps.BUTTON_POS_1_DEFAULT;
 
       let factory = Cc["@mozilla.org/prompter;1"]
                       .getService(Ci.nsIPromptFactory);
       let prompt = factory.getPrompt(window, Ci.nsIPrompt);
       let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
       bag.setPropertyAsBool("allowTabModal", true);
 
       let pressed = prompt.confirmEx(title, body, buttonFlags,
-                                     continueLabel, null, null, null, {});
+                                     disconnectLabel, null, null, null, {});
 
       if (pressed != 0) { // 0 is the "continue" button
         return;
       }
     }
     fxAccounts.signOut().then(() => {
       this.updateWeavePrefs();
     });
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -34,28 +34,16 @@
       class="header"
       hidden="true"
       data-category="paneSync">
   <label class="header-name" flex="1">&paneSync.title;</label>
   <button class="help-button"
           aria-label="&helpButton.label;"/>
 </hbox>
 
-<vbox id="syncStatusMessage-container" data-category="paneSync" hidden="true">
-  <hbox id="syncStatusMessage">
-    <vbox id="syncStatusMessageWrapper">
-      <label id="syncStatusMessageTitle"></label>
-      <description id="syncStatusMessageDescription"></description>
-    </vbox>
-    <vbox>
-      <button id="syncStatusMessageClose" class="close-icon"/>
-    </vbox>
-  </hbox>
-</vbox>
-
 <deck id="weavePrefsDeck" data-category="paneSync" hidden="true">
   <!-- These panels are for the "legacy" sync provider -->
   <vbox id="noAccount" align="center">
     <spacer flex="1"/>
     <description id="syncDesc">
       &weaveDesc.label;
     </description>
     <separator/>
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -18,16 +18,17 @@ skip-if = os != "win" # This test tests 
 [browser_connection_bug388287.js]
 [browser_cookies_exceptions.js]
 [browser_defaultbrowser_alwayscheck.js]
 [browser_healthreport.js]
 skip-if = true || !healthreport # Bug 1185403 for the "true"
 [browser_homepages_filter_aboutpreferences.js]
 [browser_notifications_do_not_disturb.js]
 [browser_permissions_urlFieldHidden.js]
+skip-if = true # Bug 1278388
 [browser_proxy_backup.js]
 [browser_privacypane_1.js]
 [browser_privacypane_3.js]
 [browser_privacypane_4.js]
 [browser_privacypane_5.js]
 [browser_privacypane_8.js]
 [browser_sanitizeOnShutdown_prefLocked.js]
 [browser_searchsuggestions.js]
--- a/browser/components/preferences/in-content/tests/browser_security.js
+++ b/browser/components/preferences/in-content/tests/browser_security.js
@@ -107,18 +107,18 @@ add_task(function*() {
     // check that both settings are now turned on or off
     is(Services.prefs.getBoolPref("browser.safebrowsing.downloads.remote.block_potentially_unwanted"), !checked,
        "block_potentially_unwanted is set correctly");
     is(Services.prefs.getBoolPref("browser.safebrowsing.downloads.remote.block_uncommon"), !checked,
        "block_uncommon is set correctly");
 
     // when the preference is on, the malware table should include these ids
     let malwareTable = Services.prefs.getCharPref("urlclassifier.malwareTable").split(",");
-    is(malwareTable.includes("goog-unwanted-simple"), !checked,
-       "malware table doesn't include goog-unwanted-simple");
+    is(malwareTable.includes("goog-unwanted-shavar"), !checked,
+       "malware table doesn't include goog-unwanted-shavar");
     is(malwareTable.includes("test-unwanted-simple"), !checked,
        "malware table doesn't include test-unwanted-simple");
     let sortedMalware = malwareTable.slice(0);
     sortedMalware.sort();
     Assert.deepEqual(malwareTable, sortedMalware, "malware table has been sorted");
 
     yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
   }
--- a/browser/components/privatebrowsing/test/browser/browser.ini
+++ b/browser/components/privatebrowsing/test/browser/browser.ini
@@ -1,9 +1,10 @@
 [DEFAULT]
+tags = openwindow
 skip-if = buildapp == "mulet"
 support-files =
   browser_privatebrowsing_concurrent_page.html
   browser_privatebrowsing_geoprompt_page.html
   browser_privatebrowsing_localStorage_before_after_page.html
   browser_privatebrowsing_localStorage_before_after_page2.html
   browser_privatebrowsing_localStorage_page1.html
   browser_privatebrowsing_localStorage_page2.html
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -28,16 +28,21 @@ const NOTIFY_RESTORING_ON_STARTUP = "ses
 const NOTIFY_INITIATING_MANUAL_RESTORE = "sessionstore-initiating-manual-restore";
 
 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
 
 // Maximum number of tabs to restore simultaneously. Previously controlled by
 // the browser.sessionstore.max_concurrent_tabs pref.
 const MAX_CONCURRENT_TAB_RESTORES = 3;
 
+// Amount (in CSS px) by which we allow window edges to be off-screen
+// when restoring a window, before we override the saved position to
+// pull the window back within the available screen area.
+const SCREEN_EDGE_SLOP = 8;
+
 // global notifications observed
 const OBSERVING = [
   "browser-window-before-show", "domwindowclosed",
   "quit-application-granted", "browser-lastwindow-close-granted",
   "quit-application", "browser:purge-session-history",
   "browser:purge-domain-data",
   "idle-daily",
 ];
@@ -2160,17 +2165,17 @@ var SessionStoreInternal = {
       throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
     }
 
     // fetch the data of closed tab, while removing it from the array
     let {state, pos} = this.removeClosedTabData(closedTabs, aIndex);
 
     // create a new tab
     let tabbrowser = aWindow.gBrowser;
-    let tab = tabbrowser.selectedTab = tabbrowser.addTab();
+    let tab = tabbrowser.selectedTab = tabbrowser.addTab(null, state);
 
     // restore tab content
     this.restoreTab(tab, state);
 
     // restore the tab's position
     tabbrowser.moveTabTo(tab, pos);
 
     // focus the tab's content area (bug 342432)
@@ -2918,22 +2923,36 @@ var SessionStoreInternal = {
     // make sure that the selected tab won't be closed in order to
     // prevent unnecessary flickering
     if (overwriteTabs && tabbrowser.selectedTab._tPos >= newTabCount)
       tabbrowser.moveTabTo(tabbrowser.selectedTab, newTabCount - 1);
 
     let numVisibleTabs = 0;
 
     for (var t = 0; t < newTabCount; t++) {
-      tabs.push(t < openTabCount ?
-                tabbrowser.tabs[t] :
-                tabbrowser.addTab("about:blank", {
-                  skipAnimation: true,
-                  forceNotRemote: true,
-                }));
+      // When trying to restore into existing tab, we also take the userContextId
+      // into account if present.
+      let userContextId = winData.tabs[t].userContextId;
+      let reuseExisting = t < openTabCount &&
+                          (tabbrowser.tabs[t].getAttribute("usercontextid") == (userContextId || ""));
+      let tab = reuseExisting ? tabbrowser.tabs[t] :
+                                tabbrowser.addTab("about:blank",
+                                                  {skipAnimation: true,
+                                                   forceNotRemote: true,
+                                                   userContextId});
+
+      // If we inserted a new tab because the userContextId didn't match with the
+      // open tab, even though `t < openTabCount`, we need to remove that open tab
+      // and put the newly added tab in its place.
+      if (!reuseExisting && t < openTabCount) {
+        tabbrowser.removeTab(tabbrowser.tabs[t]);
+        tabbrowser.moveTabTo(tab, t);
+      }
+
+      tabs.push(tab);
 
       if (winData.tabs[t].pinned)
         tabbrowser.pinTab(tabs[t]);
 
       if (winData.tabs[t].hidden) {
         tabbrowser.hideTab(tabs[t]);
       }
       else {
@@ -3466,36 +3485,50 @@ var SessionStoreInternal = {
       let screenLeft = {}, screenTop = {}, screenWidth = {}, screenHeight = {};
       screen.GetAvailRectDisplayPix(screenLeft, screenTop, screenWidth, screenHeight);
       // screenX/Y are based on the origin of the screen's desktop-pixel coordinate space
       let screenLeftCss = screenLeft.value;
       let screenTopCss = screenTop.value;
       // convert screen's device pixel dimensions to CSS px dimensions
       screen.GetAvailRect(screenLeft, screenTop, screenWidth, screenHeight);
       let cssToDevScale = screen.defaultCSSScaleFactor;
-      let screenWidthCss = screenWidth.value / cssToDevScale;
-      let screenHeightCss = screenHeight.value / cssToDevScale;
-      // constrain the dimensions to the actual space available
-      if (aWidth > screenWidthCss) {
-        aWidth = screenWidthCss;
-      }
-      if (aHeight > screenHeightCss) {
-        aHeight = screenHeightCss;
+      let screenRightCss = screenLeftCss + screenWidth.value / cssToDevScale;
+      let screenBottomCss = screenTopCss + screenHeight.value / cssToDevScale;
+
+      // Pull the window within the screen's bounds (allowing a little slop
+      // for windows that may be deliberately placed with their border off-screen
+      // as when Win10 "snaps" a window to the left/right edge -- bug 1276516).
+      // First, ensure the left edge is large enough...
+      if (aLeft < screenLeftCss - SCREEN_EDGE_SLOP) {
+        aLeft = screenLeftCss;
       }
-      // and then pull the window within the screen's bounds
-      if (aLeft < screenLeftCss) {
-        aLeft = screenLeftCss;
-      } else if (aLeft + aWidth > screenLeftCss + screenWidthCss) {
-        aLeft = screenLeftCss + screenWidthCss - aWidth;
+      // Then check the resulting right edge, and reduce it if necessary.
+      let right = aLeft + aWidth;
+      if (right > screenRightCss + SCREEN_EDGE_SLOP) {
+        right = screenRightCss;
+        // See if we can move the left edge leftwards to maintain width.
+        if (aLeft > screenLeftCss) {
+          aLeft = Math.max(right - aWidth, screenLeftCss);
+        }
       }
-      if (aTop < screenTopCss) {
+      // Finally, update aWidth to account for the adjusted left and right edges.
+      aWidth = right - aLeft;
+
+      // And do the same in the vertical dimension.
+      if (aTop < screenTopCss - SCREEN_EDGE_SLOP) {
         aTop = screenTopCss;
-      } else if (aTop + aHeight > screenTopCss + screenHeightCss) {
-        aTop = screenTopCss + screenHeightCss - aHeight;
       }
+      let bottom = aTop + aHeight;
+      if (bottom > screenBottomCss + SCREEN_EDGE_SLOP) {
+        bottom = screenBottomCss;
+        if (aTop > screenTopCss) {
+          aTop = Math.max(bottom - aHeight, screenTopCss);
+        }
+      }
+      aHeight = bottom - aTop;
     }
 
     // only modify those aspects which aren't correct yet
     if (!isNaN(aLeft) && !isNaN(aTop) && (aLeft != win_("screenX") || aTop != win_("screenY"))) {
       aWindow.moveTo(aLeft, aTop);
     }
     if (aWidth && aHeight && (aWidth != win_("width") || aHeight != win_("height"))) {
       // Don't resize the window if it's currently maximized and we would
--- a/browser/components/sessionstore/test/browser_sessionStoreContainer.js
+++ b/browser/components/sessionstore/test/browser_sessionStoreContainer.js
@@ -1,15 +1,17 @@
 /* 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/. */
 
+requestLongerTimeout(3);
+
 add_task(function* () {
   for (let i = 0; i < 3; ++i) {
-    let tab = gBrowser.addTab("http://example.com/", {userContextId: i});
+    let tab = gBrowser.addTab("http://example.com/", { userContextId: i });
     let browser = tab.linkedBrowser;
 
     yield promiseBrowserLoaded(browser);
 
     let tab2 = gBrowser.duplicateTab(tab);
     Assert.equal(tab2.getAttribute("usercontextid"), i);
     let browser2 = tab2.linkedBrowser;
     yield promiseTabRestored(tab2)
@@ -21,29 +23,164 @@ add_task(function* () {
     });
 
     yield promiseRemoveTab(tab);
     yield promiseRemoveTab(tab2);
   }
 });
 
 add_task(function* () {
-  let tab = gBrowser.addTab("http://example.com/", {userContextId: 1});
+  let tab = gBrowser.addTab("http://example.com/", { userContextId: 1 });
   let browser = tab.linkedBrowser;
 
   yield promiseBrowserLoaded(browser);
 
   gBrowser.selectedTab = tab;
 
   let tab2 = gBrowser.duplicateTab(tab);
   let browser2 = tab2.linkedBrowser;
   yield promiseTabRestored(tab2)
 
-  yield ContentTask.spawn(browser2, { expectedId: 1}, function* (args) {
+  yield ContentTask.spawn(browser2, { expectedId: 1 }, function* (args) {
     Assert.equal(docShell.getOriginAttributes().userContextId,
                  args.expectedId,
                  "The docShell has the correct userContextId");
   });
 
   yield promiseRemoveTab(tab);
   yield promiseRemoveTab(tab2);
 });
 
+add_task(function* () {
+  let tab = gBrowser.addTab("http://example.com/", { userContextId: 1 });
+  let browser = tab.linkedBrowser;
+
+  yield promiseBrowserLoaded(browser);
+
+  gBrowser.removeTab(tab);
+
+  let tab2 = ss.undoCloseTab(window, 0);
+  Assert.equal(tab2.getAttribute("usercontextid"), 1);
+  yield promiseTabRestored(tab2);
+  yield ContentTask.spawn(tab2.linkedBrowser, { expectedId: 1 }, function* (args) {
+    Assert.equal(docShell.getOriginAttributes().userContextId,
+                 args.expectedId,
+                 "The docShell has the correct userContextId");
+  });
+
+  yield promiseRemoveTab(tab2);
+});
+
+add_task(function* () {
+  let win = window.openDialog(location, "_blank", "chrome,all,dialog=no");
+  yield promiseWindowLoaded(win);
+
+  // Create 4 tabs with different userContextId.
+  for (let userContextId = 1; userContextId < 5; userContextId++) {
+    let tab = win.gBrowser.addTab("http://example.com/", {userContextId});
+    yield promiseBrowserLoaded(tab.linkedBrowser);
+    yield TabStateFlusher.flush(tab.linkedBrowser);
+  }
+
+  // Move the default tab of window to the end.
+  // We want the 1st tab to have non-default userContextId, so later when we
+  // restore into win2 we can test restore into an existing tab with different
+  // userContextId.
+  win.gBrowser.moveTabTo(win.gBrowser.tabs[0], win.gBrowser.tabs.length - 1);
+
+  let winState = JSON.parse(ss.getWindowState(win));
+
+  for (let i = 0; i < 4; i++) {
+    Assert.equal(winState.windows[0].tabs[i].userContextId, i + 1,
+                 "1st Window: tabs[" + i + "].userContextId should exist.");
+  }
+
+  let win2 = window.openDialog(location, "_blank", "chrome,all,dialog=no");
+  yield promiseWindowLoaded(win2);
+
+  // Create tabs with different userContextId, but this time we create them with
+  // fewer tabs and with different order with win.
+  for (let userContextId = 3; userContextId > 0; userContextId--) {
+    let tab = win2.gBrowser.addTab("http://example.com/", {userContextId});
+    yield promiseBrowserLoaded(tab.linkedBrowser);
+    yield TabStateFlusher.flush(tab.linkedBrowser);
+  }
+
+  ss.setWindowState(win2, JSON.stringify(winState), true);
+
+  for (let i = 0; i < 4; i++) {
+    let browser = win2.gBrowser.tabs[i].linkedBrowser;
+    yield ContentTask.spawn(browser, { expectedId: i + 1 }, function* (args) {
+      Assert.equal(docShell.getOriginAttributes().userContextId,
+                   args.expectedId,
+                   "The docShell has the correct userContextId");
+
+      Assert.equal(content.document.nodePrincipal.originAttributes.userContextId,
+                   args.expectedId,
+                   "The document has the correct userContextId");
+    });
+  }
+
+  // Test the last tab, which doesn't have userContextId.
+  let browser = win2.gBrowser.tabs[4].linkedBrowser;
+  yield ContentTask.spawn(browser, { expectedId: 0 }, function* (args) {
+    Assert.equal(docShell.getOriginAttributes().userContextId,
+                 args.expectedId,
+                 "The docShell has the correct userContextId");
+
+    Assert.equal(content.document.nodePrincipal.originAttributes.userContextId,
+                 args.expectedId,
+                 "The document has the correct userContextId");
+  });
+
+  yield BrowserTestUtils.closeWindow(win);
+  yield BrowserTestUtils.closeWindow(win2);
+});
+
+add_task(function* () {
+  let win = window.openDialog(location, "_blank", "chrome,all,dialog=no");
+  yield promiseWindowLoaded(win);
+
+  let tab = win.gBrowser.addTab("http://example.com/", { userContextId: 1 });
+  yield promiseBrowserLoaded(tab.linkedBrowser);
+  yield TabStateFlusher.flush(tab.linkedBrowser);
+
+  // win should have 1 default tab, and 1 container tab.
+  Assert.equal(win.gBrowser.tabs.length, 2, "win should have 2 tabs");
+
+  let winState = JSON.parse(ss.getWindowState(win));
+
+  for (let i = 0; i < 2; i++) {
+    Assert.equal(winState.windows[0].tabs[i].userContextId, i,
+                 "1st Window: tabs[" + i + "].userContextId should be " + i);
+  }
+
+  let win2 = window.openDialog(location, "_blank", "chrome,all,dialog=no");
+  yield promiseWindowLoaded(win2);
+
+  let tab2 = win2.gBrowser.addTab("http://example.com/", { userContextId : 1 });
+  yield promiseBrowserLoaded(tab2.linkedBrowser);
+  yield TabStateFlusher.flush(tab2.linkedBrowser);
+
+  // Move the first normal tab to end, so the first tab of win2 will be a
+  // container tab.
+  win2.gBrowser.moveTabTo(win2.gBrowser.tabs[0], win2.gBrowser.tabs.length - 1);
+  yield TabStateFlusher.flush(win2.gBrowser.tabs[0].linkedBrowser);
+
+  ss.setWindowState(win2, JSON.stringify(winState), true);
+
+  for (let i = 0; i < 2; i++) {
+    let browser = win2.gBrowser.tabs[i].linkedBrowser;
+    yield ContentTask.spawn(browser, { expectedId: i }, function* (args) {
+      Assert.equal(docShell.getOriginAttributes().userContextId,
+                   args.expectedId,
+                   "The docShell has the correct userContextId");
+
+      Assert.equal(content.document.nodePrincipal.originAttributes.userContextId,
+                   args.expectedId,
+                   "The document has the correct userContextId");
+    });
+  }
+
+  yield BrowserTestUtils.closeWindow(win);
+  yield BrowserTestUtils.closeWindow(win2);
+});
+
--- a/browser/config/version.txt
+++ b/browser/config/version.txt
@@ -1,1 +1,1 @@
-49.0a1
+50.0a1
--- a/browser/config/version_display.txt
+++ b/browser/config/version_display.txt
@@ -1,1 +1,1 @@
-49.0a1
+50.0a1
--- a/browser/extensions/loop/chrome/test/mochitest/browser.ini
+++ b/browser/extensions/loop/chrome/test/mochitest/browser.ini
@@ -1,24 +1,26 @@
 [DEFAULT]
 support-files =
     head.js
     loop_fxa.sjs
     test_loopLinkClicker_channel.html
     ../../../../../base/content/test/general/browser_fxa_oauth_with_keys.html
 
 [browser_copypanel.js]
+skip-if = os == "linux" # Bug 1266041
+
 [browser_fxa_login.js]
 [browser_loop_fxa_server.js]
 [browser_LoopRooms_channel.js]
 [browser_menuitem.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_chat.js]
 [browser_mozLoop_context.js]
 [browser_mozLoop_infobar.js]
 [browser_mozLoop_socialShare.js]
 [browser_panel_privateBrowsing.js]
 [browser_mozLoop_sharingListeners.js]
 [browser_mozLoop_telemetry.js]
-skip-if = os == win && !debug # Bug 1267562 zombiecheck | child process 1228 still alive after shutdown (on win7-vm specifically)
+skip-if = os == (win && !debug) || true # Bug 1267562 zombiecheck | child process 1228 still alive after shutdown (on win7-vm specifically) | Bug 1278389 for the true
 [browser_sharingTitleListeners.js]
 [browser_throttler.js]
 [browser_toolbarbutton.js]
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.5.276
+Current extension version is: 1.5.281
--- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
+++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
@@ -155,22 +155,20 @@ function getLocalizedString(strings, id,
   return id;
 }
 
 function makeContentReadable(obj, window) {
   /* jshint -W027 */
   return Cu.cloneInto(obj, window);
 }
 
-function createNewChannel(uri, node, principal) {
+function createNewChannel(uri) {
   return NetUtil.newChannel({
     uri: uri,
-    loadingNode: node,
-    loadingPrincipal: principal,
-    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+    loadUsingSystemPrincipal: true
   });
 }
 
 function asyncFetchChannel(channel, callback) {
   return NetUtil.asyncFetch(channel, callback);
 }
 
 // PDF data storage
@@ -261,17 +259,17 @@ ChromeActions.prototype = {
       filename = 'document.pdf';
     }
     var blobUri = NetUtil.newURI(blobUrl);
     var extHelperAppSvc =
           Cc['@mozilla.org/uriloader/external-helper-app-service;1'].
              getService(Ci.nsIExternalHelperAppService);
 
     var docIsPrivate = this.isInPrivateBrowsing();
-    var netChannel = createNewChannel(blobUri, this.domWindow.document, null);
+    var netChannel = createNewChannel(blobUri);
     if ('nsIPrivateBrowsingChannel' in Ci &&
         netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
       netChannel.setPrivate(docIsPrivate);
     }
     asyncFetchChannel(netChannel, function(aInputStream, aResult) {
       if (!Components.isSuccessCode(aResult)) {
         if (sendResponse) {
           sendResponse(true);
@@ -288,16 +286,17 @@ ChromeActions.prototype = {
         channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
         if (self.contentDispositionFilename && !data.isAttachment) {
           channel.contentDispositionFilename = self.contentDispositionFilename;
         } else {
           channel.contentDispositionFilename = filename;
         }
       } catch (e) {}
       channel.setURI(originalUri);
+      channel.loadInfo = netChannel.loadInfo;
       channel.contentStream = aInputStream;
       if ('nsIPrivateBrowsingChannel' in Ci &&
           channel instanceof Ci.nsIPrivateBrowsingChannel) {
         channel.setPrivate(docIsPrivate);
       }
 
       var listener = {
         extListener: null,
@@ -323,17 +322,17 @@ ChromeActions.prototype = {
         },
         onDataAvailable: function(aRequest, aContext, aInputStream, aOffset,
                                   aCount) {
           this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
                                            aOffset, aCount);
         }
       };
 
-      channel.asyncOpen(listener, null);
+      channel.asyncOpen2(listener);
     });
   },
   getLocale: function() {
     return getStringPref('general.useragent.locale', 'en-US');
   },
   getStrings: function(data) {
     try {
       // Lazy initialization of localizedStrings
@@ -968,31 +967,31 @@ PdfStreamConverter.prototype = {
 
     // Creating storage for PDF data
     var contentLength = aRequest.contentLength;
     this.dataListener = new PdfDataListener(contentLength);
     this.binaryStream = Cc['@mozilla.org/binaryinputstream;1']
                         .createInstance(Ci.nsIBinaryInputStream);
 
     // Create a new channel that is viewer loaded as a resource.
-    var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
-    var channel = createNewChannel(PDF_VIEWER_WEB_PAGE, null, systemPrincipal);
+    var channel = createNewChannel(PDF_VIEWER_WEB_PAGE);
 
     var listener = this.listener;
     var dataListener = this.dataListener;
     // Proxy all the request observer calls, when it gets to onStopRequest
     // we can get the dom window.  We also intentionally pass on the original
     // request(aRequest) below so we don't overwrite the original channel and
     // trigger an assertion.
     var proxy = {
       onStartRequest: function(request, context) {
-        listener.onStartRequest(aRequest, context);
+        listener.onStartRequest(aRequest, aContext);
       },
       onDataAvailable: function(request, context, inputStream, offset, count) {
-        listener.onDataAvailable(aRequest, context, inputStream, offset, count);
+        listener.onDataAvailable(aRequest, aContext, inputStream,
+                                 offset, count);
       },
       onStopRequest: function(request, context, statusCode) {
         // We get the DOM window here instead of before the request since it
         // may have changed during a redirect.
         var domWindow = getDOMWindow(channel);
         var actions;
         if (rangeRequest || streamRequest) {
           actions = new RangedChromeActions(
@@ -1005,17 +1004,17 @@ PdfStreamConverter.prototype = {
         var requestListener = new RequestListener(actions);
         domWindow.addEventListener(PDFJS_EVENT_ID, function(event) {
           requestListener.receive(event);
         }, false, true);
         if (actions.supportsIntegratedFind()) {
           var findEventManager = new FindEventManager(domWindow);
           findEventManager.bind();
         }
-        listener.onStopRequest(aRequest, context, statusCode);
+        listener.onStopRequest(aRequest, aContext, statusCode);
 
         if (domWindow.frameElement) {
           var isObjectEmbed = domWindow.frameElement.tagName !== 'IFRAME' ||
             domWindow.frameElement.className === 'previewPluginContentFrame';
           PdfJsTelemetry.onEmbed(isObjectEmbed);
         }
       }
     };
@@ -1031,17 +1030,17 @@ PdfStreamConverter.prototype = {
     // e.g. useful for NoScript
     var ssm = Cc['@mozilla.org/scriptsecuritymanager;1']
                 .getService(Ci.nsIScriptSecurityManager);
     var uri = NetUtil.newURI(PDF_VIEWER_WEB_PAGE, null, null);
     var attrs = aRequest.loadInfo.originAttributes;
     var resourcePrincipal;
     resourcePrincipal = ssm.createCodebasePrincipal(uri, attrs);
     aRequest.owner = resourcePrincipal;
-    channel.asyncOpen(proxy, aContext);
+    channel.asyncOpen2(proxy);
   },
 
   // nsIRequestObserver::onStopRequest
   onStopRequest: function(aRequest, aContext, aStatusCode) {
     if (!this.dataListener) {
       // Do nothing
       return;
     }
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf', ['exports
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdf = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.276';
-var pdfjsBuild = '41f978c';
+var pdfjsVersion = '1.5.281';
+var pdfjsBuild = '5a5bb99';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf.worker', ['
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdfWorker = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.276';
-var pdfjsBuild = '41f978c';
+var pdfjsVersion = '1.5.281';
+var pdfjsBuild = '5a5bb99';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -37205,17 +37205,17 @@ var PartialEvaluator = (function Partial
           var fn = operation.fn;
           args = operation.args;
           var advance, diff;
 
           switch (fn | 0) {
             case OPS.setFont:
               flushTextContentItem();
               textState.fontSize = args[1];
-              next(handleSetFont(args[0].name));
+              next(handleSetFont(args[0].name, null));
               return;
             case OPS.setTextRise:
               flushTextContentItem();
               textState.textRise = args[0];
               break;
             case OPS.setHScale:
               flushTextContentItem();
               textState.textHScale = args[0] / 100;
@@ -37411,31 +37411,27 @@ var PartialEvaluator = (function Partial
                   xobjsCache.texts = formTextContent;
                 }));
               return;
             case OPS.setGState:
               flushTextContentItem();
               var dictName = args[0];
               var extGState = resources.get('ExtGState');
 
-              if (!isDict(extGState) || !extGState.has(dictName.name)) {
-                break;
-              }
-
-              var gsStateMap = extGState.get(dictName.name);
-              var gsStateFont = null;
-              for (var key in gsStateMap) {
-                if (key === 'Font') {
-                  assert(!gsStateFont);
-                  gsStateFont = gsStateMap[key];
-                }
-              }
-              if (gsStateFont) {
-                textState.fontSize = gsStateFont[1];
-                next(handleSetFont(gsStateFont[0]));
+              if (!isDict(extGState) || !isName(dictName)) {
+                break;
+              }
+              var gState = extGState.get(dictName.name);
+              if (!isDict(gState)) {
+                break;
+              }
+              var gStateFont = gState.get('Font');
+              if (gStateFont) {
+                textState.fontSize = gStateFont[1];
+                next(handleSetFont(null, gStateFont[0]));
                 return;
               }
               break;
           } // switch
         } // while
         if (stop) {
           next(deferred);
           return;
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -2555,16 +2555,17 @@ exports.binarySearchFirstItem = binarySe
     });
     eventBus.on('find', function (e) {
       if (e.source === window) {
         return; // event comes from FirefoxCom, no need to replicate
       }
       var event = document.createEvent('CustomEvent');
       event.initCustomEvent('find' + e.type, true, true, {
         query: e.query,
+        phraseSearch: e.phraseSearch,
         caseSensitive: e.caseSensitive,
         highlightAll: e.highlightAll,
         findPrevious: e.findPrevious
       });
       window.dispatchEvent(event);
     });
     eventBus.on('attachmentsloaded', function (e) {
       var event = document.createEvent('CustomEvent');
@@ -3005,16 +3006,17 @@ var PDFFindController = (function PDFFin
   PDFFindController.prototype = {
     reset: function PDFFindController_reset() {
       this.startedTextExtraction = false;
       this.extractTextPromises = [];
       this.pendingFindMatches = Object.create(null);
       this.active = false; // If active, find results will be highlighted.
       this.pageContents = []; // Stores the text for each page.
       this.pageMatches = [];
+      this.pageMatchesLength = null;
       this.matchCount = 0;
       this.selected = { // Currently selected match.
         pageIdx: -1,
         matchIdx: -1
       };
       this.offset = { // Where the find algorithm currently is in the document.
         pageIdx: null,
         matchIdx: null
@@ -3031,51 +3033,151 @@ var PDFFindController = (function PDFFin
     },
 
     normalize: function PDFFindController_normalize(text) {
       return text.replace(this.normalizationRegex, function (ch) {
         return CHARACTERS_TO_NORMALIZE[ch];
       });
     },
 
+    // Helper for multiple search - fills matchesWithLength array
+    // and takes into account cases when one search term
+    // include another search term (for example, "tamed tame" or "this is").
+    // Looking for intersecting terms in the 'matches' and
+    // leave elements with a longer match-length.
+
+    _prepareMatches: function PDFFindController_prepareMatches(
+        matchesWithLength, matches, matchesLength) {
+
+      function isSubTerm(matchesWithLength, currentIndex) {
+        var currentElem, prevElem, nextElem;
+        currentElem = matchesWithLength[currentIndex];
+        nextElem = matchesWithLength[currentIndex + 1];
+        // checking for cases like "TAMEd TAME"
+        if (currentIndex < matchesWithLength.length - 1 &&
+            currentElem.match === nextElem.match) {
+          currentElem.skipped = true;
+          return true;
+        }
+        // checking for cases like "thIS IS"
+        for (var i = currentIndex - 1; i >= 0; i--) {
+          prevElem = matchesWithLength[i];
+          if (prevElem.skipped) {
+            continue;
+          }
+          if (prevElem.match + prevElem.matchLength < currentElem.match) {
+            break;
+          }
+          if (prevElem.match + prevElem.matchLength >=
+              currentElem.match + currentElem.matchLength) {
+            currentElem.skipped = true;
+            return true;
+          }
+        }
+        return false;
+      }
+
+      var i, len;
+      // Sorting array of objects { match: <match>, matchLength: <matchLength> }
+      // in increasing index first and then the lengths.
+      matchesWithLength.sort(function(a, b) {
+        return a.match === b.match ?
+        a.matchLength - b.matchLength : a.match - b.match;
+      });
+      for (i = 0, len = matchesWithLength.length; i < len; i++) {
+        if (isSubTerm(matchesWithLength, i)) {
+          continue;
+        }
+        matches.push(matchesWithLength[i].match);
+        matchesLength.push(matchesWithLength[i].matchLength);
+      }
+    },
+
+    calcFindPhraseMatch: function PDFFindController_calcFindPhraseMatch(
+      query, pageIndex, pageContent) {
+      var matches = [];
+      var queryLen = query.length;
+      var matchIdx = -queryLen;
+      while (true) {
+        matchIdx = pageContent.indexOf(query, matchIdx + queryLen);
+        if (matchIdx === -1) {
+          break;
+        }
+        matches.push(matchIdx);
+      }
+      this.pageMatches[pageIndex] = matches;
+    },
+
+    calcFindWordMatch: function PDFFindController_calcFindWordMatch(
+      query, pageIndex, pageContent) {
+      var matchesWithLength = [];
+      // Divide the query into pieces and search for text on each piece.
+      var queryArray = query.match(/\S+/g);
+      var subquery, subqueryLen, matchIdx;
+      for (var i = 0, len = queryArray.length; i < len; i++) {
+        subquery = queryArray[i];
+        subqueryLen = subquery.length;
+        matchIdx = -subqueryLen;
+        while (true) {
+          matchIdx = pageContent.indexOf(subquery, matchIdx + subqueryLen);
+          if (matchIdx === -1) {
+            break;
+          }
+          // Other searches do not, so we store the length.
+          matchesWithLength.push({
+            match: matchIdx,
+            matchLength: subqueryLen,
+            skipped: false
+          });
+        }
+      }
+      // Prepare arrays for store the matches.
+      if (!this.pageMatchesLength) {
+        this.pageMatchesLength = [];
+      }
+      this.pageMatchesLength[pageIndex] = [];
+      this.pageMatches[pageIndex] = [];
+      // Sort matchesWithLength, clean up intersecting terms
+      // and put the result into the two arrays.
+      this._prepareMatches(matchesWithLength, this.pageMatches[pageIndex],
+        this.pageMatchesLength[pageIndex]);
+    },
+
     calcFindMatch: function PDFFindController_calcFindMatch(pageIndex) {
       var pageContent = this.normalize(this.pageContents[pageIndex]);
       var query = this.normalize(this.state.query);
       var caseSensitive = this.state.caseSensitive;
+      var phraseSearch = this.state.phraseSearch;
       var queryLen = query.length;
 
       if (queryLen === 0) {
         // Do nothing: the matches should be wiped out already.
         return;
       }
 
       if (!caseSensitive) {
         pageContent = pageContent.toLowerCase();
         query = query.toLowerCase();
       }
 
-      var matches = [];
-      var matchIdx = -queryLen;
-      while (true) {
-        matchIdx = pageContent.indexOf(query, matchIdx + queryLen);
-        if (matchIdx === -1) {
-          break;
-        }
-        matches.push(matchIdx);
-      }
-      this.pageMatches[pageIndex] = matches;
+      if (phraseSearch) {
+        this.calcFindPhraseMatch(query, pageIndex, pageContent);
+      } else {
+        this.calcFindWordMatch(query, pageIndex, pageContent);
+      }
+
       this.updatePage(pageIndex);
       if (this.resumePageIdx === pageIndex) {
         this.resumePageIdx = null;
         this.nextPageMatch();
       }
 
       // Update the matches count
-      if (matches.length > 0) {
-        this.matchCount += matches.length;
+      if (this.pageMatches[pageIndex].length > 0) {
+        this.matchCount += this.pageMatches[pageIndex].length;
         this.updateUIResultsCount();
       }
     },
 
     extractText: function PDFFindController_extractText() {
       if (this.startedTextExtraction) {
         return;
       }
@@ -3160,16 +3262,17 @@ var PDFFindController = (function PDFFin
         this.dirtyMatch = false;
         this.selected.pageIdx = this.selected.matchIdx = -1;
         this.offset.pageIdx = currentPageIndex;
         this.offset.matchIdx = null;
         this.hadMatch = false;
         this.resumePageIdx = null;
         this.pageMatches = [];
         this.matchCount = 0;
+        this.pageMatchesLength = null;
         var self = this;
 
         for (var i = 0; i < numPages; i++) {
           // Wipe out any previous highlighted matches.
           this.updatePage(i);
 
           // As soon as the text is extracted start finding the matches.
           if (!(i in this.pendingFindMatches)) {
@@ -3975,16 +4078,17 @@ var PDFFindBar = (function PDFFindBarClo
     },
 
     dispatchEvent: function PDFFindBar_dispatchEvent(type, findPrev) {
       this.eventBus.dispatch('find', {
         source: this,
         type: type,
         query: this.findField.value,
         caseSensitive: this.caseSensitive.checked,
+        phraseSearch: true,
         highlightAll: this.highlightAll.checked,
         findPrevious: findPrev
       });
     },
 
     updateUIState:
         function PDFFindBar_updateUIState(state, previous, matchCount) {
       var notFound = false;
@@ -4646,16 +4750,23 @@ var PDFLinkService = (function () {
     },
 
     /**
      * @param {string} hash
      */
     setHash: function PDFLinkService_setHash(hash) {
       if (hash.indexOf('=') >= 0) {
         var params = parseQueryString(hash);
+        if ('search' in params) {
+          this.eventBus.dispatch('findfromurlhash', {
+            source: this,
+            query: params['search'].replace(/"/g, ''),
+            phraseSearch: (params['phrase'] === 'true')
+          });
+        }
         // borrowing syntax from "Parameters for Opening PDF Files"
         if ('nameddest' in params) {
           if (this.pdfHistory) {
             this.pdfHistory.updateNextHashParam(params.nameddest);
           }
           this.navigateTo(params.nameddest);
           return;
         }
@@ -5685,25 +5796,28 @@ var TextLayerBuilder = (function TextLay
       if (this.textLayerRenderTask) {
         this.textLayerRenderTask.cancel();
         this.textLayerRenderTask = null;
       }
       this.textContent = textContent;
       this.divContentDone = true;
     },
 
-    convertMatches: function TextLayerBuilder_convertMatches(matches) {
+    convertMatches: function TextLayerBuilder_convertMatches(matches,
+                                                             matchesLength) {
       var i = 0;
       var iIndex = 0;
       var bidiTexts = this.textContent.items;
       var end = bidiTexts.length - 1;
       var queryLen = (this.findController === null ?
                       0 : this.findController.state.query.length);
       var ret = [];
-
+      if (!matches) {
+        return ret;
+      }
       for (var m = 0, len = matches.length; m < len; m++) {
         // Calculate the start position.
         var matchIdx = matches[m];
 
         // Loop over the divIdxs.
         while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
           iIndex += bidiTexts[i].str.length;
           i++;
@@ -5716,17 +5830,21 @@ var TextLayerBuilder = (function TextLay
         var match = {
           begin: {
             divIdx: i,
             offset: matchIdx - iIndex
           }
         };
 
         // Calculate the end position.
-        matchIdx += queryLen;
+        if (matchesLength) { // multiterm search
+          matchIdx += matchesLength[m];
+        } else { // phrase search
+          matchIdx += queryLen;
+        }
 
         // Somewhat the same array as above, but use > instead of >= to get
         // the end position right.
         while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) {
           iIndex += bidiTexts[i].str.length;
           i++;
         }
 
@@ -5858,18 +5976,24 @@ var TextLayerBuilder = (function TextLay
       }
 
       if (this.findController === null || !this.findController.active) {
         return;
       }
 
       // Convert the matches on the page controller into the match format
       // used for the textLayer.
-      this.matches = this.convertMatches(this.findController === null ?
-        [] : (this.findController.pageMatches[this.pageIdx] || []));
+      var pageMatches, pageMatchesLength;
+      if (this.findController !== null) {
+        pageMatches = this.findController.pageMatches[this.pageIdx] || null;
+        pageMatchesLength = (this.findController.pageMatchesLength) ?
+          this.findController.pageMatchesLength[this.pageIdx] || null : null;
+      }
+
+      this.matches = this.convertMatches(pageMatches, pageMatchesLength);
       this.renderMatches(this.matches);
     },
 
     /**
      * Fixes text selection: adds additional div where mouse was clicked.
      * This reduces flickering of the content if mouse slowly dragged down/up.
      * @private
      */
@@ -7942,16 +8066,17 @@ var PDFViewerApplication = {
     eventBus.on('print', webViewerPrint);
     eventBus.on('download', webViewerDownload);
     eventBus.on('firstpage', webViewerFirstPage);
     eventBus.on('lastpage', webViewerLastPage);
     eventBus.on('rotatecw', webViewerRotateCw);
     eventBus.on('rotateccw', webViewerRotateCcw);
     eventBus.on('documentproperties', webViewerDocumentProperties);
     eventBus.on('find', webViewerFind);
+    eventBus.on('findfromurlhash', webViewerFindFromUrlHash);
   }
 };
 
 
 function loadAndEnablePDFBug(enabledTabs) {
   return new Promise(function (resolve, reject) {
     var appConfig = PDFViewerApplication.appConfig;
     var script = document.createElement('script');
@@ -8431,22 +8556,33 @@ function webViewerRotateCcw() {
 }
 function webViewerDocumentProperties() {
   PDFViewerApplication.pdfDocumentProperties.open();
 }
 
 function webViewerFind(e) {
   PDFViewerApplication.findController.executeCommand('find' + e.type, {
     query: e.query,
+    phraseSearch: e.phraseSearch,
     caseSensitive: e.caseSensitive,
     highlightAll: e.highlightAll,
     findPrevious: e.findPrevious
   });
 }
 
+function webViewerFindFromUrlHash(e) {
+  PDFViewerApplication.findController.executeCommand('find', {
+    query: e.query,
+    phraseSearch: e.phraseSearch,
+    caseSensitive: false,
+    highlightAll: true,
+    findPrevious: false
+  });
+}
+
 function webViewerScaleChanging(e) {
   var appConfig = PDFViewerApplication.appConfig;
   appConfig.toolbar.zoomOut.disabled = (e.scale === MIN_SCALE);
   appConfig.toolbar.zoomIn.disabled = (e.scale === MAX_SCALE);
 
   // Update the 'scaleSelect' DOM element.
   var predefinedValueFound = selectScaleOption(e.presetValue ||
                                                '' + e.scale);
@@ -8580,16 +8716,17 @@ window.addEventListener('keydown', funct
         }
         break;
       case 71: // g
         if (!PDFViewerApplication.supportsIntegratedFind) {
           var findState = PDFViewerApplication.findController.state;
           if (findState) {
             PDFViewerApplication.findController.executeCommand('findagain', {
               query: findState.query,
+              phraseSearch: findState.phraseSearch,
               caseSensitive: findState.caseSensitive,
               highlightAll: findState.highlightAll,
               findPrevious: cmd === 5 || cmd === 12
             });
           }
           handled = true;
         }
         break;
@@ -8953,16 +9090,17 @@ Preferences._readFromStorage = function 
   var handleEvent = function (evt) {
     if (!PDFViewerApplication.initialized) {
       return;
     }
     PDFViewerApplication.eventBus.dispatch('find', {
       source: window,
       type: evt.type.substring('find'.length),
       query: evt.detail.query,
+      phraseSearch: true,
       caseSensitive: !!evt.detail.caseSensitive,
       highlightAll: !!evt.detail.highlightAll,
       findPrevious: !!evt.detail.findPrevious
     });
   }.bind(this);
 
   for (var i = 0, len = events.length; i < len; i++) {
     window.addEventListener(events[i], handleEvent);
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -11,15 +11,20 @@ autoDisconnectSignIn.label = Sign in to 
 autoDisconnectSignIn.accessKey = S
 
 # LOCALIZATION NOTE (reconnectDescription) - %S = Email address of user's Firefox Account
 reconnectDescription = Reconnect %S
 
 # LOCALIZATION NOTE (verifyDescription) - %S = Email address of user's Firefox Account
 verifyDescription = Verify %S
 
-# These strings are shown in a flyout in the Sync preference panel after the
+# These strings are shown in a desktop notification after the
 # user requests we resend a verification email.
 verificationSentTitle = Verification Sent
-# LOCALIZATION NOTE (verificationSentFull) - %S = Email address of user's Firefox Account
-verificationSentFull = A verification link has been sent to %S. Please check your email and click the link to begin syncing.
+# LOCALIZATION NOTE (verificationSentBody) - %S = Email address of user's Firefox Account
+verificationSentBody = A verification link has been sent to %S.
 verificationNotSentTitle = Unable to Send Verification
-verificationNotSentFull = We are unable to send a verification mail at this time, please try again later.
+verificationNotSentBody = We are unable to send a verification mail at this time, please try again later.
+
+# LOCALIZATION NOTE (syncStartNotification.title, syncStartNotification.body)
+# These strings are used in a notification shown after Sync is connected.
+syncStartNotification.title = Sync enabled
+syncStartNotification.body = Firefox will begin syncing momentarily.
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -105,18 +105,16 @@ These should match what Safari and other
 <!ENTITY fullScreenCmd.macCommandKey "f">
 <!ENTITY showAllTabsCmd.label "Show All Tabs">
 <!ENTITY showAllTabsCmd.accesskey "A">
 
 <!ENTITY fxaSignIn.label "Sign in to &syncBrand.shortName.label;">
 <!ENTITY fxaSignedIn.tooltip "Open &syncBrand.shortName.label; preferences">
 <!ENTITY fxaSignInError.label "Reconnect to &syncBrand.shortName.label;">
 <!ENTITY fxaUnverified.label "Verify Your Account">
-<!ENTITY syncStartPanel2.heading "&syncBrand.shortName.label; enabled">
-<!ENTITY syncStartPanel2.subTitle "&brandShortName; will begin syncing momentarily.">
 
 
 <!ENTITY fullScreenMinimize.tooltip "Minimize">
 <!ENTITY fullScreenRestore.tooltip "Restore">
 <!ENTITY fullScreenClose.tooltip "Close">
 <!ENTITY fullScreenAutohide.label "Hide Toolbars">
 <!ENTITY fullScreenAutohide.accesskey "H">
 <!ENTITY fullScreenExit.label "Exit Full Screen Mode">
--- a/browser/locales/en-US/chrome/browser/syncSetup.properties
+++ b/browser/locales/en-US/chrome/browser/syncSetup.properties
@@ -47,18 +47,21 @@ wipeClient.change2.label = Firefox Sync 
 wipeRemote.change2.label = Firefox Sync will now replace all of the browser data in your Sync account with the data on this device.
 existingAccount.change.label = You can change this preference by selecting Sync Options below.
 
 # Several other strings are used (via Weave.Status.login), but they come from
 #  /services/sync
 
 # Firefox Accounts based setup.
 continue.label = Continue
+
+# LOCALIZATION NOTE (disconnect.label, disconnect.verify.title, disconnect.verify.bodyHeading, disconnect.verify.bodyText):
+# These strings are used in the confirmation dialog shown when the user hits the disconnect button
+# LOCALIZATION NOTE (disconnect.label): This is the label for the disconnect button
+disconnect.label = Disconnect
 disconnect.verify.title = Disconnect
-disconnect.verify.heading = Are you sure?
-# LOCALIZATION NOTE (disconnect.verify.description): %S will be replaced with
-# brandShortName
-disconnect.verify.description = %S will stop syncing with your account, but won’t delete any of your browsing data on this computer.
+disconnect.verify.bodyHeading = Disconnect from Sync?
+disconnect.verify.bodyText = Your browsing data will remain on this computer, but it will no longer sync with your account.
 
 relinkVerify.title = Merge Warning
 relinkVerify.heading = Are you sure you want to sign in to Sync?
 # LOCALIZATION NOTE (relinkVerify.description): Email address of a user previously signed into sync.
 relinkVerify.description = A different user was previously signed in to Sync on this computer. Signing in will merge this browser’s bookmarks, passwords and other settings with %S
--- a/browser/modules/PluginContent.jsm
+++ b/browser/modules/PluginContent.jsm
@@ -48,18 +48,16 @@ PluginContent.prototype = {
     global.addEventListener("unload",                this);
 
     global.addMessageListener("BrowserPlugins:ActivatePlugins", this);
     global.addMessageListener("BrowserPlugins:NotificationShown", this);
     global.addMessageListener("BrowserPlugins:ContextMenuCommand", this);
     global.addMessageListener("BrowserPlugins:NPAPIPluginProcessCrashed", this);
     global.addMessageListener("BrowserPlugins:CrashReportSubmitted", this);
     global.addMessageListener("BrowserPlugins:Test:ClearCrashData", this);
-
-    Services.obs.addObserver(this, "Plugin::HiddenPluginTouched", false);
   },
 
   uninit: function() {
     let global = this.global;
 
     global.removeEventListener("PluginBindingAttached", this, true);
     global.removeEventListener("PluginCrashed",         this, true);
     global.removeEventListener("PluginOutdated",        this, true);
@@ -72,18 +70,16 @@ PluginContent.prototype = {
     global.removeMessageListener("BrowserPlugins:ActivatePlugins", this);
     global.removeMessageListener("BrowserPlugins:NotificationShown", this);
     global.removeMessageListener("BrowserPlugins:ContextMenuCommand", this);
     global.removeMessageListener("BrowserPlugins:NPAPIPluginProcessCrashed", this);
     global.removeMessageListener("BrowserPlugins:CrashReportSubmitted", this);
     global.removeMessageListener("BrowserPlugins:Test:ClearCrashData", this);
     delete this.global;
     delete this.content;
-
-    Services.obs.removeObserver(this, "Plugin::HiddenPluginTouched");
   },
 
   receiveMessage: function (msg) {
     switch (msg.name) {
       case "BrowserPlugins:ActivatePlugins":
         this.activatePlugins(msg.data.pluginInfo, msg.data.newState);
         break;
       case "BrowserPlugins:NotificationShown":
@@ -115,25 +111,16 @@ PluginContent.prototype = {
       case "BrowserPlugins:Test:ClearCrashData":
         // This message should ONLY ever be sent by automated tests.
         if (Services.prefs.getBoolPref("plugins.testmode")) {
           this.pluginCrashData.clear();
         }
     }
   },
 
-  observe: function observe(aSubject, aTopic, aData) {
-    let pluginTag = aSubject;
-    if (aTopic == "Plugin::HiddenPluginTouched") {
-      this._showClickToPlayNotification(pluginTag, false);
-    } else {
-      Cu.reportError("unknown topic observed: " + aTopic);
-    }
-  },
-
   onPageShow: function (event) {
     // Ignore events that aren't from the main document.
     if (!this.content || event.target != this.content.document) {
       return;
     }
 
     // The PluginClickToPlay events are not fired when navigating using the
     // BF cache. |persisted| is true when the page is loaded from the
@@ -202,55 +189,16 @@ PluginContent.prototype = {
              pluginName: pluginName,
              pluginTag: pluginTag,
              permissionString: permissionString,
              fallbackType: fallbackType,
              blocklistState: blocklistState,
            };
   },
 
-  _getPluginInfoForTag: function (pluginTag, tagMimetype, fallbackType) {
-    let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
-
-    let pluginName = gNavigatorBundle.GetStringFromName("pluginInfo.unknownPlugin");
-    let permissionString = null;
-    let blocklistState = null;
-
-    if (pluginTag) {
-      pluginName = BrowserUtils.makeNicePluginName(pluginTag.name);
-
-      permissionString = pluginHost.getPermissionStringForTag(pluginTag);
-      blocklistState = pluginTag.blocklistState;
-
-      // Convert this from nsIPluginTag so it can be serialized.
-      let properties = ["name", "description", "filename", "version", "enabledState", "niceName"];
-      let pluginTagCopy = {};
-      for (let prop of properties) {
-        pluginTagCopy[prop] = pluginTag[prop];
-      }
-      pluginTag = pluginTagCopy;
-
-      // Make state-softblocked == state-notblocked for our purposes,
-      // they have the same UI. STATE_OUTDATED should not exist for plugin
-      // items, but let's alias it anyway, just in case.
-      if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED ||
-          blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) {
-        blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
-      }
-    }
-
-    return { mimetype: tagMimetype,
-             pluginName: pluginName,
-             pluginTag: pluginTag,
-             permissionString: permissionString,
-             fallbackType: fallbackType,
-             blocklistState: blocklistState,
-           };
-  },
-
   /**
    * Update the visibility of the plugin overlay.
    */
   setVisibility : function (plugin, overlay, shouldShow) {
     overlay.classList.toggle("visible", shouldShow);
     if (shouldShow) {
       overlay.removeAttribute("dismissed");
     }
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -11,16 +11,55 @@ var gStringBundle =
 
 this.SitePermissions = {
 
   UNKNOWN: Services.perms.UNKNOWN_ACTION,
   ALLOW: Services.perms.ALLOW_ACTION,
   BLOCK: Services.perms.DENY_ACTION,
   SESSION: Components.interfaces.nsICookiePermission.ACCESS_SESSION,
 
+  /* Returns a list of objects representing all permissions that are currently
+   * set for the given URI. Each object contains the following keys:
+   * - id: the permissionID of the permission
+   * - label: the localized label
+   * - state: a constant representing the current permission state
+   *   (e.g. SitePermissions.ALLOW)
+   * - availableStates: an array of all available states for that permission,
+   *   represented as objects with the keys:
+   *   - id: the state constant
+   *   - label: the translated label of that state
+   */
+  getPermissionsByURI: function (aURI) {
+    if (!this.isSupportedURI(aURI)) {
+      return [];
+    }
+
+    let permissions = [];
+    for (let permission of this.listPermissions()) {
+      let state = this.get(aURI, permission);
+      if (state === this.UNKNOWN) {
+        continue;
+      }
+
+      let availableStates = this.getAvailableStates(permission).map( state => {
+        return { id: state, label: this.getStateLabel(permission, state) };
+      });
+      let label = this.getPermissionLabel(permission);
+
+      permissions.push({
+        id: permission,
+        label: label,
+        state: state,
+        availableStates: availableStates,
+      });
+    }
+
+    return permissions;
+  },
+
   /* Returns a boolean indicating whether there are any granted
    * (meaning allowed or session-allowed) permissions for the given URI.
    * Will return false for invalid URIs (such as file:// URLs).
    */
   hasGrantedPermissions: function (aURI) {
     if (!this.isSupportedURI(aURI)) {
       return false;
     }
--- a/browser/modules/test/xpcshell/test_SitePermissions.js
+++ b/browser/modules/test/xpcshell/test_SitePermissions.js
@@ -9,16 +9,21 @@ Components.utils.import("resource://gre/
 add_task(function* testPermissionsListing() {
   Assert.deepEqual(SitePermissions.listPermissions().sort(),
     ["camera","cookie","desktop-notification","geo","image",
      "indexedDB","install","microphone","pointerLock","popup"],
     "Correct list of all permissions");
 });
 
 add_task(function* testHasGrantedPermissions() {
+  // check that it returns false on an invalid URI
+  // like a file URI, which doesn't support site permissions
+  let wrongURI = Services.io.newURI("file:///example.js", null, null)
+  Assert.equal(SitePermissions.hasGrantedPermissions(wrongURI), false);
+
   let uri = Services.io.newURI("https://example.com", null, null)
   Assert.equal(SitePermissions.hasGrantedPermissions(uri), false);
 
   // check that ALLOW states return true
   SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
   Assert.equal(SitePermissions.hasGrantedPermissions(uri), true);
 
   // removing the ALLOW state should revert to false
@@ -44,8 +49,70 @@ add_task(function* testHasGrantedPermiss
   Assert.equal(SitePermissions.hasGrantedPermissions(uri), true);
 
   // check that only BLOCK states will not return true
   SitePermissions.remove(uri, "geo");
   Assert.equal(SitePermissions.hasGrantedPermissions(uri), false);
 
   SitePermissions.remove(uri, "pointerLock");
 });
+
+add_task(function* testGetPermissionsByURI() {
+  // check that it returns an empty array on an invalid URI
+  // like a file URI, which doesn't support site permissions
+  let wrongURI = Services.io.newURI("file:///example.js", null, null)
+  Assert.deepEqual(SitePermissions.getPermissionsByURI(wrongURI), []);
+
+  let uri = Services.io.newURI("https://example.com", null, null)
+
+  SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
+  SitePermissions.set(uri, "cookie", SitePermissions.SESSION);
+  SitePermissions.set(uri, "popup", SitePermissions.BLOCK);
+
+  let permissions = SitePermissions.getPermissionsByURI(uri);
+
+  let camera = permissions.find(({id}) => id === "camera");
+  Assert.deepEqual(camera, {
+    id: "camera",
+    label: "Use the Camera",
+    state: SitePermissions.ALLOW,
+    availableStates: [
+      { id: SitePermissions.UNKNOWN, label: "Always Ask" },
+      { id: SitePermissions.ALLOW, label: "Allow" },
+      { id: SitePermissions.BLOCK, label: "Block" },
+    ]
+  });
+
+  // check that removed permissions (State.UNKNOWN) are skipped
+  SitePermissions.remove(uri, "camera");
+  permissions = SitePermissions.getPermissionsByURI(uri);
+
+  camera = permissions.find(({id}) => id === "camera");
+  Assert.equal(camera, undefined);
+
+  // check that different available state values are represented
+
+  let cookie = permissions.find(({id}) => id === "cookie");
+  Assert.deepEqual(cookie, {
+    id: "cookie",
+    label: "Set Cookies",
+    state: SitePermissions.SESSION,
+    availableStates: [
+      { id: SitePermissions.ALLOW, label: "Allow" },
+      { id: SitePermissions.SESSION, label: "Allow for Session" },
+      { id: SitePermissions.BLOCK, label: "Block" },
+    ]
+  });
+
+  let popup = permissions.find(({id}) => id === "popup");
+  Assert.deepEqual(popup, {
+    id: "popup",
+    label: "Open Pop-up Windows",
+    state: SitePermissions.BLOCK,
+    availableStates: [
+      { id: SitePermissions.ALLOW, label: "Allow" },
+      { id: SitePermissions.BLOCK, label: "Block" },
+    ]
+  });
+
+  SitePermissions.remove(uri, "cookie");
+  SitePermissions.remove(uri, "popup");
+});
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1742,43 +1742,16 @@ toolbarbutton.chevron > .toolbarbutton-i
 
 #ctrlTab-showAll {
   -moz-appearance: button;
   color: ButtonText;
   padding: 0 3px;
   margin-top: 10px;
 }
 
-/* Sync Panel */
-
-.sync-panel-icon {
-  height:32px;
-  width: 32px;
-  background: url("chrome://browser/content/abouthome/sync.png") top left no-repeat;
-}
-
-.sync-panel-inner {
-  width: 0;
-  padding-left: 10px;
-}
-
-.sync-panel-button-box {
-  margin-top: 1em;
-}
-
-#sync-start-panel-title {
-  font-size: 120%;
-  font-weight: bold;
-  margin-bottom: 5px;
-}
-
-#sync-start-panel-subtitle {
-  margin-bottom: 0;
-}
-
 /* Status panel */
 
 .statuspanel-label {
   margin: 0;
   padding: 2px 4px;
   background: linear-gradient(#fff, #ddd);
   border: 1px none #ccc;
   border-top-style: solid;
@@ -1982,8 +1955,12 @@ notification.pluginVulnerable > .notific
   padding-inline-end: 0 !important;
   margin-inline-end: 0 !important;
 }
 
 .browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
   overflow: hidden;
 }
+
+.menuitem-iconic[command="Browser:NewUserContextTab"] > .menu-iconic-left > .menu-iconic-icon {
+  visibility: visible;
+}
--- a/browser/themes/linux/searchbar.css
+++ b/browser/themes/linux/searchbar.css
@@ -1,14 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #PopupSearchAutoComplete {
-  margin-inline-start: -24px;
+  /* JS code forces the panel to have the width of the searchbar rather than
+   * the width of the textfield. Alignment of the panel with the searchbar is
+   * obtained with negative margins here: margin-inline-start when the text
+   * field is in the same direction as the rest of the UI, margin-inline-end
+   * when the textfield's direction has been reversed.
+   * (eg. using ctrl+shift+X) */
+  margin-inline-start: -23px;
+  margin-inline-end: -16px;
   padding: 1px;
 }
 
 .autocomplete-textbox-container {
   -moz-box-align: stretch;
 }
 
 .textbox-input-box {
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -60,17 +60,17 @@
   display: -moz-box;
   height: 2px;
   margin-top: -2px;
   position: relative;
   z-index: 2; /* navbar is at 1 */
 }
 
 @media (-moz-mac-yosemite-theme) {
-  #navigator-toolbox::after {
+  #navigator-toolbox:-moz-window-inactive::after {
     background-image: linear-gradient(to top, hsla(0,0%,0%,.1), hsla(0,0%,0%,.1) 1px, hsla(0,0%,100%,0) 1px, hsla(0,0%,100%,0) 2px, transparent 3px);
   }
 }
 
 #navigator-toolbox toolbarbutton:-moz-lwtheme {
   color: inherit;
   text-shadow: inherit;
 }
@@ -3255,65 +3255,16 @@ menulist.translate-infobar-element > .me
 .ctrlTab-preview:not(#ctrlTab-showAll):focus > * > .ctrlTab-preview-inner {
   margin: -10px -10px 0;
 }
 
 #ctrlTab-showAll {
   margin-top: .5em;
 }
 
-/* Sync Panels */
-
-.sync-panel-icon {
-  height:32px;
-  width: 32px;
-  background: url("chrome://browser/content/abouthome/sync.png") top left no-repeat;
-}
-
-@media (min-resolution: 2dppx) {
-  .sync-panel-icon {
-    background: url("chrome://browser/content/abouthome/sync@2x.png") top left no-repeat;
-    background-size: 32px 32px;
-  }
-}
-
-.sync-panel-inner {
-  width: 0;
-  padding-left: 10px;
-}
-
-.sync-panel-button-box {
-  margin-top: 1em;
-}
-
-.sync-panel-button {
-  @hudButton@
-  margin: 0;
-  min-width: 72px;
-  min-height: 22px;
-}
-
-.sync-panel-button:hover:active {
-  @hudButtonPressed@
-}
-
-.sync-panel-button:-moz-focusring {
-  @hudButtonFocused@
-}
-
-#sync-start-panel-title {
-  font-size: 120%;
-  font-weight: bold;
-  margin-bottom: 5px;
-}
-
-#sync-start-panel-subtitle {
-  margin-bottom: 0;
-}
-
 /* Status panel */
 
 .statuspanel-label {
   margin: 0;
   padding: 2px 4px;
   background: linear-gradient(#fff, #ddd);
   border: 1px none #ccc;
   border-top-style: solid;
--- a/browser/themes/osx/searchbar.css
+++ b/browser/themes/osx/searchbar.css
@@ -1,14 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #PopupSearchAutoComplete {
+  /* JS code forces the panel to have the width of the searchbar rather than
+   * the width of the textfield. Alignment of the panel with the searchbar is
+   * obtained with negative margins here: margin-inline-start when the text
+   * field is in the same direction as the rest of the UI, margin-inline-end
+   * when the textfield's direction has been reversed.
+   * (eg. using command+shift+X) */
   margin-inline-start: -23px;
+  margin-inline-end: -21px;
 }
 
 .searchbar-textbox {
   border-radius: 10000px;
 }
 
 .searchbar-popup {
   margin-top: 4px;
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -431,71 +431,16 @@ description > html|a {
   padding-right: 15px;
 }
 
 #noFxaGroup > vbox,
 #fxaGroup {
   -moz-box-align: start;
 }
 
-#syncStatusMessage {
-  visibility: collapse;
-  opacity: 0;
-  transition: opacity 1s linear;
-  padding: 14px 8px 14px 14px;
-  border-radius: 2px;
-}
-
-#syncStatusMessage[message-type] {
-  visibility: visible;
-  opacity: 1;
-}
-
-#syncStatusMessage[message-type="verify-success"] {
-  background-color: #74BF43;
-}
-
-#syncStatusMessage[message-type="verify-error"] {
-  background-color: #D74345;
-}
-
-#syncStatusMessage[message-type="migration"] {
-  background-color: #FF9500;
-}
-
-#syncStatusMessageWrapper {
-  -moz-box-flex: 1;
-  padding-right: 5px;
-}
-
-#syncStatusMessageTitle, #syncStatusMessageDescription {
-  color: #FBFBFB;
-}
-
-#syncStatusMessage[message-type="migration"] #syncStatusMessageTitle {
-  display: none;
-}
-
-#syncStatusMessageTitle {
-  font-weight: bold !important;
-  font-size: 16px;
-  line-height: 157%;
-  margin: 0 0 20px;
-}
-
-#syncStatusMessageDescription {
-  font-size: 14px;
-  line-height: 158%;
-  margin: 0 !important;
-}
-
-#syncStatusMessageClose {
-  margin: 0px;
-}
-
 #fxaSyncEngines > vbox:first-child {
   margin-right: 80px;
 }
 
 #fxaSyncComputerName {
   margin-inline-start: 0px;
   -moz-box-flex: 1;
 }
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2420,43 +2420,16 @@ notification[value="translation"] {
 .ctrlTab-preview:not(#ctrlTab-showAll):focus > * > .ctrlTab-preview-inner {
   margin: -10px -10px 0;
 }
 
 #ctrlTab-showAll {
   margin-top: .5em;
 }
 
-/* Sync Panel */
-
-.sync-panel-icon {
-  height:32px;
-  width: 32px;
-  background: url("chrome://browser/content/abouthome/sync.png") top left no-repeat;
-}
-
-.sync-panel-inner {
-  width: 0;
-  padding-left: 10px;
-}
-
-.sync-panel-button-box {
-  margin-top: 1em;
-}
-
-#sync-start-panel-title {
-  font-size: 120%;
-  font-weight: bold;
-  margin-bottom: 5px;
-}
-
-#sync-start-panel-subtitle {
-  margin-bottom: 0;
-}
-
 /* Status panel */
 
 .statuspanel-label {
   margin: 0;
   padding: 2px 4px;
   background: linear-gradient(#fff, #ddd);
   border: 1px none #ccc;
   border-top-style: solid;
--- a/browser/themes/windows/searchbar.css
+++ b/browser/themes/windows/searchbar.css
@@ -1,14 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #PopupSearchAutoComplete {
+  /* JS code forces the panel to have the width of the searchbar rather than
+   * the width of the textfield. Alignment of the panel with the searchbar is
+   * obtained with negative margins here: margin-inline-start when the text
+   * field is in the same direction as the rest of the UI, margin-inline-end
+   * when the textfield's direction has been reversed.
+   * (eg. using ctrl+shift+X) */
   margin-inline-start: -25px;
+  margin-inline-end: -18px;
 }
 
 .autocomplete-textbox-container {
   -moz-box-align: stretch;
 }
 
 .textbox-input-box {
   margin: 0;
new file mode 100644
--- /dev/null
+++ b/build/.gdbinit_python.in
@@ -0,0 +1,6 @@
+#filter substitution
+python
+import sys
+sys.path.append('@topsrcdir@/python/gdbpp')
+import gdbpp
+end
--- a/build/mobile/remoteautomation.py
+++ b/build/mobile/remoteautomation.py
@@ -81,27 +81,22 @@ class RemoteAutomation(Automation):
             env['MOZ_CRASHREPORTER_DISABLE'] = '1'
 
         # Crash on non-local network connections by default.
         # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
         # enable non-local connections for the purposes of local testing.
         # Don't override the user's choice here.  See bug 1049688.
         env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1')
 
-        # Disable Switchboard by default. This will prevent nonlocal
-        # network connections to the Switchboard server.
-        # Passing any value expect the empty string will disable it so to
-        # enable, don't pass a value.
-        env.setdefault('MOZ_DISABLE_SWITCHBOARD', '1')
-
-        # Disable Java telemetry by default to
-        # prevent network connections during testing.
-        # Passing any value expect the empty string will disable it so to
-        # enable, don't pass a value.
-        env.setdefault('MOZ_DISABLE_TELEMETRY', '1')
+        # Send an env var noting that we are in automation. Passing any
+        # value except the empty string will declare the value to exist.
+        #
+        # This may be used to disabled network connections during testing, e.g.
+        # Switchboard & telemetry uploads.
+        env.setdefault('MOZ_IN_AUTOMATION', '1')
 
         # Set WebRTC logging in case it is not set yet.
         # On Android, environment variables cannot contain ',' so the
         # standard WebRTC setting for NSPR_LOG_MODULES is not available.
         # env.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:5,datachannel:5,jsep:5,MediaPipelineFactory:5')
         env.setdefault('R_LOG_LEVEL', '6')
         env.setdefault('R_LOG_DESTINATION', 'stderr')
         env.setdefault('R_LOG_VERBOSE', '1')
--- a/build/moz.build
+++ b/build/moz.build
@@ -55,16 +55,18 @@ if CONFIG['ENABLE_TESTS'] or CONFIG['MOZ
         FINAL_TARGET_FILES += ['/tools/rb/fix_linux_stack.py']
 
 if CONFIG['MOZ_DMD']:
     FINAL_TARGET_FILES += ['/memory/replace/dmd/dmd.py']
 
 # Put a useful .gdbinit in the bin directory, to be picked up automatically
 # by GDB when we debug executables there.
 FINAL_TARGET_FILES += ['/.gdbinit']
+FINAL_TARGET_PP_FILES += ['.gdbinit_python.in']
+OBJDIR_FILES += ['!/dist/bin/.gdbinit_python']
 
 # Install the clang-cl runtime library for ASAN next to the binaries we produce.
 if CONFIG['MOZ_ASAN'] and CONFIG['CLANG_CL']:
     FINAL_TARGET_FILES += ['%' + CONFIG['MOZ_CLANG_RT_ASAN_LIB_PATH']]
 
 if CONFIG['MOZ_APP_BASENAME']:
     FINAL_TARGET_PP_FILES += ['application.ini']
     if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android' and CONFIG['MOZ_UPDATER']:
--- a/caps/BasePrincipal.cpp
+++ b/caps/BasePrincipal.cpp
@@ -40,70 +40,80 @@ PrincipalOriginAttributes::InheritFromDo
 
   // addonId is computed from the principal URI and never propagated
   mUserContextId = aAttrs.mUserContextId;
 
   // TODO:
   // Bug 1225349 - PrincipalOriginAttributes should inherit mSignedPkg
   // accordingly by URI
   mSignedPkg = aAttrs.mSignedPkg;
+
+  mPrivateBrowsingId = aAttrs.mPrivateBrowsingId;
 }
 
 void
 PrincipalOriginAttributes::InheritFromNecko(const NeckoOriginAttributes& aAttrs)
 {
   mAppId = aAttrs.mAppId;
   mInIsolatedMozBrowser = aAttrs.mInIsolatedMozBrowser;
 
   // addonId is computed from the principal URI and never propagated
   mUserContextId = aAttrs.mUserContextId;
   mSignedPkg = aAttrs.mSignedPkg;
+
+  mPrivateBrowsingId = aAttrs.mPrivateBrowsingId;
 }
 
 void
 DocShellOriginAttributes::InheritFromDocToChildDocShell(const PrincipalOriginAttributes& aAttrs)
 {
   mAppId = aAttrs.mAppId;
   mInIsolatedMozBrowser = aAttrs.mInIsolatedMozBrowser;
 
   // addonId is computed from the principal URI and never propagated
   mUserContextId = aAttrs.mUserContextId;
 
   // TODO:
   // Bug 1225353 - DocShell/NeckoOriginAttributes should inherit
   // mSignedPkg accordingly by mSignedPkgInBrowser
   mSignedPkg = aAttrs.mSignedPkg;
+
+  mPrivateBrowsingId = aAttrs.mPrivateBrowsingId;
 }
 
 void
 NeckoOriginAttributes::InheritFromDocToNecko(const PrincipalOriginAttributes& aAttrs)
 {
   mAppId = aAttrs.mAppId;
   mInIsolatedMozBrowser = aAttrs.mInIsolatedMozBrowser;
 
   // addonId is computed from the principal URI and never propagated
   mUserContextId = aAttrs.mUserContextId;
 
   // TODO:
   // Bug 1225353 - DocShell/NeckoOriginAttributes should inherit
   // mSignedPkg accordingly by mSignedPkgInBrowser
+
+  mPrivateBrowsingId = aAttrs.mPrivateBrowsingId;
 }
 
 void
 NeckoOriginAttributes::InheritFromDocShellToNecko(const DocShellOriginAttributes& aAttrs)
 {
   mAppId = aAttrs.mAppId;
   mInIsolatedMozBrowser = aAttrs.mInIsolatedMozBrowser;
 
   // addonId is computed from the principal URI and never propagated
   mUserContextId = aAttrs.mUserContextId;
 
   // TODO:
   // Bug 1225353 - DocShell/NeckoOriginAttributes should inherit
   // mSignedPkg accordingly by mSignedPkgInBrowser
+
+  mPrivateBrowsingId = aAttrs.mPrivateBrowsingId;
 }
 
 void
 OriginAttributes::CreateSuffix(nsACString& aStr) const
 {
   UniquePtr<URLParams> params(new URLParams());
   nsAutoString value;
 
@@ -140,16 +150,22 @@ OriginAttributes::CreateSuffix(nsACStrin
     params->Set(NS_LITERAL_STRING("userContextId"), value);
   }
 
   if (!mSignedPkg.IsEmpty()) {
     MOZ_RELEASE_ASSERT(mSignedPkg.FindCharInSet(dom::quota::QuotaManager::kReplaceChars) == kNotFound);
     params->Set(NS_LITERAL_STRING("signedPkg"), mSignedPkg);
   }
 
+  if (mPrivateBrowsingId) {
+    value.Truncate();
+    value.AppendInt(mPrivateBrowsingId);
+    params->Set(NS_LITERAL_STRING("privateBrowsingId"), value);
+  }
+
   aStr.Truncate();
 
   params->Serialize(value);
   if (!value.IsEmpty()) {
     aStr.AppendLiteral("^");
     aStr.Append(NS_ConvertUTF16toUTF8(value));
   }
 
@@ -166,16 +182,20 @@ namespace {
 class MOZ_STACK_CLASS PopulateFromSuffixIterator final
   : public URLParams::ForEachIterator
 {
 public:
   explicit PopulateFromSuffixIterator(OriginAttributes* aOriginAttributes)
     : mOriginAttributes(aOriginAttributes)
   {
     MOZ_ASSERT(aOriginAttributes);
+    // If mPrivateBrowsingId is passed in as >0 and is not present in the suffix,
+    // then it will remain >0 when it should be 0 according to the suffix. Set to 0 before
+    // iterating to fix this.
+    mOriginAttributes->mPrivateBrowsingId = 0;
   }
 
   bool URLParamsIterator(const nsString& aName,
                          const nsString& aValue) override
   {
     if (aName.EqualsLiteral("appId")) {
       nsresult rv;
       int64_t val  = aValue.ToInteger64(&rv);
@@ -212,16 +232,26 @@ public:
     }
 
     if (aName.EqualsLiteral("signedPkg")) {
       MOZ_RELEASE_ASSERT(mOriginAttributes->mSignedPkg.IsEmpty());
       mOriginAttributes->mSignedPkg.Assign(aValue);
       return true;
     }
 
+    if (aName.EqualsLiteral("privateBrowsingId")) {
+      nsresult rv;
+      int64_t val = aValue.ToInteger64(&rv);
+      NS_ENSURE_SUCCESS(rv, false);
+      NS_ENSURE_TRUE(val >= 0 && val <= UINT32_MAX, false);
+      mOriginAttributes->mPrivateBrowsingId = static_cast<uint32_t>(val);
+
+      return true;
+    }
+
     // No other attributes are supported.
     return false;
   }
 
 private:
   OriginAttributes* mOriginAttributes;
 };
 
@@ -257,16 +287,22 @@ OriginAttributes::PopulateFromOrigin(con
     aOriginNoSuffix = origin;
     return true;
   }
 
   aOriginNoSuffix = Substring(origin, 0, pos);
   return PopulateFromSuffix(Substring(origin, pos));
 }
 
+void
+OriginAttributes::SyncAttributesWithPrivateBrowsing(bool aInPrivateBrowsing)
+{
+  mPrivateBrowsingId = aInPrivateBrowsing ? 1 : 0;
+}
+
 BasePrincipal::BasePrincipal()
 {}
 
 BasePrincipal::~BasePrincipal()
 {}
 
 NS_IMETHODIMP
 BasePrincipal::GetOrigin(nsACString& aOrigin)
--- a/caps/BasePrincipal.h
+++ b/caps/BasePrincipal.h
@@ -28,17 +28,18 @@ class OriginAttributes : public dom::Ori
 {
 public:
   bool operator==(const OriginAttributes& aOther) const
   {
     return mAppId == aOther.mAppId &&
            mInIsolatedMozBrowser == aOther.mInIsolatedMozBrowser &&
            mAddonId == aOther.mAddonId &&
            mUserContextId == aOther.mUserContextId &&
-           mSignedPkg == aOther.mSignedPkg;
+           mSignedPkg == aOther.mSignedPkg &&
+           mPrivateBrowsingId == aOther.mPrivateBrowsingId;
   }
   bool operator!=(const OriginAttributes& aOther) const
   {
     return !(*this == aOther);
   }
 
   // Serializes/Deserializes non-default values into the suffix format, i.e.
   // |!key1=value1&key2=value2|. If there are no non-default attributes, this
@@ -46,16 +47,20 @@ public:
   void CreateSuffix(nsACString& aStr) const;
   bool PopulateFromSuffix(const nsACString& aStr);
 
   // Populates the attributes from a string like
   // |uri!key1=value1&key2=value2| and returns the uri without the suffix.
   bool PopulateFromOrigin(const nsACString& aOrigin,
                           nsACString& aOriginNoSuffix);
 
+  // Helper function to match mIsPrivateBrowsing to existing private browsing
+  // flags. Once all other flags are removed, this can be removed too.
+  void SyncAttributesWithPrivateBrowsing(bool aInPrivateBrowsing);
+
 protected:
   OriginAttributes() {}
   explicit OriginAttributes(const OriginAttributesDictionary& aOther)
     : OriginAttributesDictionary(aOther) {}
 };
 
 class PrincipalOriginAttributes;
 class DocShellOriginAttributes;
@@ -170,16 +175,56 @@ public:
     if (mUserContextId.WasPassed() && mUserContextId.Value() != aAttrs.mUserContextId) {
       return false;
     }
 
     if (mSignedPkg.WasPassed() && mSignedPkg.Value() != aAttrs.mSignedPkg) {
       return false;
     }
 
+    if (mPrivateBrowsingId.WasPassed() && mPrivateBrowsingId.Value() != aAttrs.mPrivateBrowsingId) {
+      return false;
+    }
+
+    return true;
+  }
+
+  bool Overlaps(const OriginAttributesPattern& aOther) const
+  {
+    if (mAppId.WasPassed() && aOther.mAppId.WasPassed() &&
+        mAppId.Value() != aOther.mAppId.Value()) {
+      return false;
+    }
+
+    if (mInIsolatedMozBrowser.WasPassed() &&
+        aOther.mInIsolatedMozBrowser.WasPassed() &&
+        mInIsolatedMozBrowser.Value() != aOther.mInIsolatedMozBrowser.Value()) {
+      return false;
+    }
+
+    if (mAddonId.WasPassed() && aOther.mAddonId.WasPassed() &&
+        mAddonId.Value() != aOther.mAddonId.Value()) {
+      return false;
+    }
+
+    if (mUserContextId.WasPassed() && aOther.mUserContextId.WasPassed() &&
+        mUserContextId.Value() != aOther.mUserContextId.Value()) {
+      return false;
+    }
+
+    if (mSignedPkg.WasPassed() && aOther.mSignedPkg.WasPassed() &&
+        mSignedPkg.Value() != aOther.mSignedPkg.Value()) {
+      return false;
+    }
+
+    if (mPrivateBrowsingId.WasPassed() && aOther.mPrivateBrowsingId.WasPassed() &&
+        mPrivateBrowsingId.Value() != aOther.mPrivateBrowsingId.Value()) {
+      return false;
+    }
+
     return true;
   }
 };
 
 /*
  * Base class from which all nsIPrincipal implementations inherit. Use this for
  * default implementations and other commonalities between principal
  * implementations.
--- a/caps/nsScriptSecurityManager.cpp
+++ b/caps/nsScriptSecurityManager.cpp
@@ -669,47 +669,42 @@ EqualOrSubdomain(nsIURI* aProbeArg, nsIU
         rv = probe->SetHost(newHost);
         NS_ENSURE_SUCCESS(rv, false);
     }
 }
 
 static bool
 AllSchemesMatch(nsIURI* aURI, nsIURI* aOtherURI)
 {
-    nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(aURI);
-    nsCOMPtr<nsINestedURI> nestedOtherURI = do_QueryInterface(aOtherURI);
     auto stringComparator = nsCaseInsensitiveCStringComparator();
-    if (!nestedURI && !nestedOtherURI) {
-        // Neither of the URIs is nested, compare their schemes directly:
-        nsAutoCString scheme, otherScheme;
-        aURI->GetScheme(scheme);
-        aOtherURI->GetScheme(otherScheme);
-        return scheme.Equals(otherScheme, stringComparator);
-    }
-    while (nestedURI && nestedOtherURI) {
-        nsCOMPtr<nsIURI> currentURI = do_QueryInterface(nestedURI);
-        nsCOMPtr<nsIURI> currentOtherURI = do_QueryInterface(nestedOtherURI);
+    nsCOMPtr<nsIURI> currentURI = aURI;
+    nsCOMPtr<nsIURI> currentOtherURI = aOtherURI;
+    while (currentURI && currentOtherURI) {
         nsAutoCString scheme, otherScheme;
         currentURI->GetScheme(scheme);
         currentOtherURI->GetScheme(otherScheme);
         if (!scheme.Equals(otherScheme, stringComparator)) {
             return false;
         }
-
+        nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(currentURI);
+        nsCOMPtr<nsINestedURI> nestedOtherURI = do_QueryInterface(currentOtherURI);
+        // If neither are nested and all schemes have matched so far
+        // (or we would have bailed already), we're the same:
+        if (!nestedURI && !nestedOtherURI) {
+            return true;
+        }
+        // If one is nested and the other not, they're not equal:
+        if (!nestedURI != !nestedOtherURI) {
+            return false;
+        }
+        // At this stage, both are still nested URIs, so let's play again:
         nestedURI->GetInnerURI(getter_AddRefs(currentURI));
         nestedOtherURI->GetInnerURI(getter_AddRefs(currentOtherURI));
-        nestedURI = do_QueryInterface(currentURI);
-        nestedOtherURI = do_QueryInterface(currentOtherURI);
     }
-    if (!!nestedURI != !!nestedOtherURI) {
-        // If only one of the scheme chains runs out at one point, clearly the chains
-        // aren't of the same length, so we bail:
-        return false;
-    }
-    return true;
+    return false;
 }
 
 NS_IMETHODIMP
 nsScriptSecurityManager::CheckLoadURIWithPrincipal(nsIPrincipal* aPrincipal,
                                                    nsIURI *aTargetURI,
                                                    uint32_t aFlags)
 {
     NS_PRECONDITION(aPrincipal, "CheckLoadURIWithPrincipal must have a principal");
--- a/caps/tests/mochitest/test_bug995943.xul
+++ b/caps/tests/mochitest/test_bug995943.xul
@@ -22,17 +22,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   const Ci = Components.interfaces;
   Cu.import("resource://gre/modules/Services.jsm");
   function debug(msg) { info(msg); }
 
   /** Test for CAPS file:// URI prefs. **/
   SimpleTest.waitForExplicitFinish();
   SimpleTest.requestCompleteLog();
   if (navigator.userAgent.indexOf("Mac OS X 10.10") != -1)
-    SimpleTest.expectAssertions(5); // See bug 1067022
+    SimpleTest.expectAssertions(6, 9); // See bug 1067022
   else if (Services.appinfo.OS == "WINNT")
     SimpleTest.expectAssertions(0, 1); // See bug 1067022
 
   var rootdir = Services.appinfo.OS == "WINNT" ? "file:///C:" : "file:///";
 
   function checkLoadFileURI(domain, shouldLoad) {
     debug("Invoking checkLoadFileURI with domain: " + domain + ", shouldLoad: " + shouldLoad);
     return new Promise(function(resolve, reject) {
--- a/caps/tests/mochitest/test_principal_jarprefix_origin_appid_appstatus.html
+++ b/caps/tests/mochitest/test_principal_jarprefix_origin_appid_appstatus.html
@@ -17,16 +17,19 @@ https://bugzilla.mozilla.org/show_bug.cg
 </div>
 <pre id="test">
 <script type="application/javascript;version=1.7">
 
 /** Test for Bug 758258 **/
 
 var Ci = Components.interfaces;
 SimpleTest.waitForExplicitFinish();
+if (navigator.userAgent.indexOf("Mac OS X 10.10") != -1) {
+  SimpleTest.expectAssertions(0, 4);
+}
 
 /*
  * gData is an array of objects. Each object represents a test case.
  * - app: gives the app manifest URL, will set mozapp to it on the iframe;
  * - origin: gives the origin of the iframe. This is the URL thas is going to
  *           to be passed as iframe.src but also the expected principal's
  *           origin.
  * - isapp: tells if the iframe is really a mozapp. If the manifest url isn't
--- a/config/milestone.txt
+++ b/config/milestone.txt
@@ -5,9 +5,9 @@
 #    x.x.x.x
 #    x.x.x+
 #
 # Referenced by milestone.py.
 # Hopefully I'll be able to automate replacement of *all*
 # hardcoded milestones in the tree from these two files.
 #--------------------------------------------------------
 
-49.0a1
+50.0a1
--- a/devtools/client/canvasdebugger/canvasdebugger.js
+++ b/devtools/client/canvasdebugger/canvasdebugger.js
@@ -7,18 +7,18 @@ var { classes: Cc, interfaces: Ci, utils
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 
 const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const promise = require("promise");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
-const { CallWatcherFront } = require("devtools/server/actors/call-watcher");
-const { CanvasFront } = require("devtools/server/actors/canvas");
+const { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
+const { CanvasFront } = require("devtools/shared/fronts/canvas");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const { Heritage, WidgetMethods, setNamedTimeout, clearNamedTimeout,
         setConditionalTimeout } = require("devtools/client/shared/widgets/view-helpers");
 
 const CANVAS_ACTOR_RECORDING_ATTEMPT = DevToolsUtils.testing ? 500 : 5000;
 
 const { Task } = require("devtools/shared/task");
--- a/devtools/client/canvasdebugger/panel.js
+++ b/devtools/client/canvasdebugger/panel.js
@@ -3,17 +3,17 @@
 /* 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 { Cc, Ci, Cu, Cr } = require("chrome");
 const promise = require("promise");
 const EventEmitter = require("devtools/shared/event-emitter");
-const { CanvasFront } = require("devtools/server/actors/canvas");
+const { CanvasFront } = require("devtools/shared/fronts/canvas");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 
 function CanvasDebuggerPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
   this._destroyer = null;
 
   EventEmitter.decorate(this);
--- a/devtools/client/canvasdebugger/test/head.js
+++ b/devtools/client/canvasdebugger/test/head.js
@@ -7,18 +7,18 @@ var { classes: Cc, interfaces: Ci, utils
 var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 
 var Services = require("Services");
 var promise = require("promise");
 var { gDevTools } = require("devtools/client/framework/devtools");
 var { DebuggerClient } = require("devtools/shared/client/main");
 var { DebuggerServer } = require("devtools/server/main");
-var { CallWatcherFront } = require("devtools/server/actors/call-watcher");
-var { CanvasFront } = require("devtools/server/actors/canvas");
+var { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
+var { CanvasFront } = require("devtools/shared/fronts/canvas");
 var { setTimeout } = require("sdk/timers");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var { TargetFactory } = require("devtools/client/framework/target");
 var { Toolbox } = require("devtools/client/framework/toolbox");
 var { isWebGLSupported } = require("devtools/client/shared/webgl-utils");
 var mm = null;
 
 const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
--- a/devtools/client/debugger/test/mochitest/browser_dbg_aaa_run_first_leaktest.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_aaa_run_first_leaktest.js
@@ -12,17 +12,17 @@
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   // Wait longer for this very simple test that comes first, to make sure that
   // GC from previous tests does not interfere with the debugger suite.
   requestLongerTimeout(2);
 
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     ok(aTab, "Should have a tab available.");
     ok(aPanel, "Should have a debugger pane available.");
 
     waitForSourceAndCaretAndScopes(aPanel, "-02.js", 1).then(() => {
       resumeDebuggerThenCloseAndFinish(aPanel);
--- a/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-01.js
@@ -5,24 +5,24 @@
 
 // Test auto pretty printing.
 
 const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gPrefs, gOptions, gView;
 
-var gFirstSourceLabel = "code_ugly-5.js";
-var gSecondSourceLabel = "code_ugly-6.js";
+var gFirstSource = EXAMPLE_URL + "code_ugly-5.js";
+var gSecondSource = EXAMPLE_URL + "code_ugly-6.js";
 
 var gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
 
 function test() {
   let options = {
-    source: gFirstSourceLabel,
+    source: gFirstSource,
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
@@ -32,19 +32,19 @@ function test() {
 
     Task.spawn(function* () {
       testSourceIsUgly();
 
       enableAutoPrettyPrint();
       testAutoPrettyPrintOn();
 
       reload(gPanel);
-      yield waitForSourceShown(gPanel, gFirstSourceLabel);
+      yield waitForSourceShown(gPanel, gFirstSource);
       testSourceIsUgly();
-      yield waitForSourceShown(gPanel, gFirstSourceLabel);
+      yield waitForSourceShown(gPanel, gFirstSource);
       testSourceIsPretty();
       disableAutoPrettyPrint();
       testAutoPrettyPrintOff();
 
       let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
       gSources.selectedIndex = 1;
       yield finished;
 
@@ -59,17 +59,17 @@ function test() {
 
 function testSourceIsUgly() {
   ok(!gEditor.getText().includes("\n  "),
     "The source shouldn't be pretty printed yet.");
 }
 
 function testSecondSourceLabel() {
   let source = gSources.selectedItem.attachment.source;
-  ok(source.url === EXAMPLE_URL + gSecondSourceLabel,
+  ok(source.url === gSecondSource,
     "Second source url is correct.");
 }
 
 function testProgressBarShown() {
   const deck = gDebugger.document.getElementById("editor-deck");
   is(deck.selectedIndex, 2, "The progress bar should be shown");
 }
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-02.js
@@ -9,25 +9,25 @@
  * that is already pretty printed.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-02.html";
 
 var gTab, gDebuggee, gPanel, gDebugger;
 var gEditor, gSources, gPrefs, gOptions, gView;
 
-var gFirstSourceLabel = "code_ugly-6.js";
-var gSecondSourceLabel = "code_ugly-7.js";
+var gFirstSource = EXAMPLE_URL + "code_ugly-6.js";
+var gSecondSource = EXAMPLE_URL + "code_ugly-7.js";
 
 var gOriginalPref = Services.prefs.getBoolPref("devtools.debugger.auto-pretty-print");
 Services.prefs.setBoolPref("devtools.debugger.auto-pretty-print", true);
 
 function test() {
   let options = {
-    source: gFirstSourceLabel,
+    source: gFirstSource,
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
     const gTab = aTab;
     const gDebuggee = aDebuggee;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
@@ -38,17 +38,17 @@ function test() {
 
     // Should be on by default.
     testAutoPrettyPrintOn();
 
     Task.spawn(function* () {
 
       testSourceIsUgly();
 
-      yield waitForSourceShown(gPanel, gFirstSourceLabel);
+      yield waitForSourceShown(gPanel, gFirstSource);
       testSourceIsPretty();
       testPrettyPrintButtonOn();
 
       // select second source
       yield selectSecondSource();
       testSecondSourceLabel();
 
       // select first source
@@ -79,23 +79,23 @@ function test() {
 
     function testSourceIsUgly() {
       ok(!gEditor.getText().includes("\n  "),
         "The source shouldn't be pretty printed yet.");
     }
 
     function testFirstSourceLabel() {
       let source = gSources.selectedItem.attachment.source;
-      ok(source.url === EXAMPLE_URL + gFirstSourceLabel,
+      ok(source.url === gFirstSource,
         "First source url is correct.");
     }
 
     function testSecondSourceLabel() {
       let source = gSources.selectedItem.attachment.source;
-      ok(source.url === EXAMPLE_URL + gSecondSourceLabel,
+      ok(source.url === gSecondSource,
         "Second source url is correct.");
     }
 
     function testAutoPrettyPrintOn() {
       is(gPrefs.autoPrettyPrint, true,
         "The auto-pretty-print pref should be on.");
       is(gOptions._autoPrettyPrint.getAttribute("checked"), "true",
         "The Auto pretty print menu item should be checked.");
--- a/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_auto-pretty-print-03.js
@@ -6,18 +6,18 @@
 /**
  * If auto pretty-printing it enabled, make sure that if
  * pretty-printing fails that it still properly shows the original
  * source.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_auto-pretty-print-02.html";
 
-var FIRST_SOURCE = "code_ugly-6.js";
-var SECOND_SOURCE = "code_ugly-7.js";
+var FIRST_SOURCE = EXAMPLE_URL + "code_ugly-6.js";
+var SECOND_SOURCE = EXAMPLE_URL + "code_ugly-7.js";
 
 function test() {
   let options = {
     source: FIRST_SOURCE,
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
     const gTab = aTab;
@@ -26,17 +26,17 @@ function test() {
 
     const gController = gDebugger.DebuggerController;
     const gEditor = gDebugger.DebuggerView.editor;
     const constants = gDebugger.require("./content/constants");
     const queries = gDebugger.require("./content/queries");
     const actions = bindActionCreators(gPanel);
 
     Task.spawn(function* () {
-      const secondSource = queries.getSourceByURL(gController.getState(), EXAMPLE_URL + SECOND_SOURCE);
+      const secondSource = queries.getSourceByURL(gController.getState(), SECOND_SOURCE);
       actions.selectSource(secondSource);
 
       // It should be showing the loading text
       is(gEditor.getText(), gDebugger.DebuggerView._loadingText,
         "The editor loading text is shown");
 
       gController.dispatch({
         type: constants.TOGGLE_PRETTY_PRINT,
--- a/devtools/client/debugger/test/mochitest/browser_dbg_bfcache.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_bfcache.js
@@ -13,17 +13,17 @@ const TAB_URL_2 = EXAMPLE_URL + "doc_rec
 
 var gTab, gDebuggee, gPanel, gDebugger;
 var gSources;
 
 const test = Task.async(function* () {
   info("Starting browser_dbg_bfcache.js's `test`.");
 
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   ([gTab, gDebuggee, gPanel]) = yield initDebugger(TAB_URL_1, options);
   gDebugger = gPanel.panelWin;
   gSources = gDebugger.DebuggerView.Sources;
 
   yield testFirstPage();
   yield testLocationChange();
--- a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-01.js
@@ -8,17 +8,17 @@
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_binary_search.html";
 
 var gTab, gPanel, gDebugger;
 
 function test() {
   let options = {
-    source: ".coffee",
+    source: EXAMPLE_URL + "code_binary_search.coffee",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
 
     testBlackBoxSource()
--- a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-05.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-05.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_binary_search.html";
 
 var gTab, gPanel, gDebugger;
 var gDeck;
 
 function test() {
   let options = {
-    source: ".coffee",
+    source: EXAMPLE_URL + "code_binary_search.coffee",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gDeck = gDebugger.document.getElementById("editor-deck");
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-06.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-06.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_blackboxing.html";
 
 var gTab, gPanel, gDebugger;
 var gSources;
 
 function test() {
   let options = {
-    source: "code_blackboxing_blackboxme.js",
+    source: EXAMPLE_URL + "code_blackboxing_blackboxme.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-07.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_blackboxing-07.js
@@ -9,17 +9,17 @@
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_blackboxing_unblackbox.html";
 
 var gTab, gPanel, gDebugger;
 
 function test() {
   let options = {
-    source: ".min.js"
+    source: EXAMPLE_URL + "code_blackboxing_unblackbox.min.js",
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
 
     testBlackBoxSource()
       .then(testBlackBoxReload)
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breadcrumbs-access.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breadcrumbs-access.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let gTab, gPanel, gDebugger;
   let gSources, gFrames;
 
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gFrames = gDebugger.DebuggerView.StackFrames;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_break-in-anon.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-in-anon.js
@@ -7,17 +7,17 @@
  * Make sure anonymous eval scripts can still break with a `debugger`
  * statement
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
 
 function test() {
   const options = {
-    source: "-eval.js",
+    source: EXAMPLE_URL + "code_script-eval.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gSources = gDebugger.DebuggerView.Sources;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next-console.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next-console.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
 
 function test() {
   let gTab, gPanel, gDebugger;
   let gSources, gBreakpoints, gTarget, gResumeButton, gResumeKey, gThreadClient;
 
   let options = {
-    source: "-eval.js",
+    source: EXAMPLE_URL + "code_script-eval.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gTarget = gDebugger.gTarget;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
 
 function test() {
   let gTab, gPanel, gDebugger;
   let gSources, gBreakpoints, gTarget, gResumeButton, gResumeKey, gThreadClient;
 
   const options = {
-    source: "-eval.js",
+    source: EXAMPLE_URL + "code_script-eval.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gTarget = gDebugger.gTarget;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-actual-location.js
@@ -7,17 +7,17 @@
  * Bug 737803: Setting a breakpoint in a line without code should move
  * the icon to the actual location.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-01.js
@@ -6,17 +6,17 @@
 /**
  * Test if the breakpoints toggle button works as advertised.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gSources = gDebugger.DebuggerView.Sources;
     const actions = bindActionCreators(gPanel);
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-button-02.js
@@ -7,17 +7,17 @@
  * Test if the breakpoints toggle button works as advertised when there are
  * some breakpoints already disabled.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gSources = gDebugger.DebuggerView.Sources;
     const actions = bindActionCreators(gPanel);
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu-add.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu-add.js
@@ -6,17 +6,17 @@
 /**
  * Test adding breakpoints from the source editor context menu
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-contextmenu.js
@@ -10,17 +10,17 @@
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   // Debug test slaves are a bit slow at this test.
   requestLongerTimeout(2);
 
   Task.spawn(function* () {
     const options = {
-      source: "-01.js",
+      source: EXAMPLE_URL + "code_script-switching-01.js",
       line: 1
     };
     const [gTab,, gPanel ] = yield initDebugger(TAB_URL, options);
     const gDebugger = gPanel.panelWin;
     const gSources = gDebugger.DebuggerView.Sources;
     const queries = gDebugger.require("./content/queries");
     const actions = bindActionCreators(gPanel);
     const getState = gDebugger.DebuggerController.getState;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-disabled-reload.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-disabled-reload.js
@@ -6,17 +6,17 @@
 /**
  * Test that disabled breakpoints survive target navigation.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gPanel = aPanel;
     const gTab = aTab;
     const gDebugger = gPanel.panelWin;
     const gEvents = gDebugger.EVENTS;
     const gEditor = gDebugger.DebuggerView.editor;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-editor.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-editor.js
@@ -7,17 +7,17 @@
  * Bug 723069: Test the debugger breakpoint API and connection to the
  * source editor.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-eval.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-eval.js
@@ -6,17 +6,17 @@
 /**
  * Test setting breakpoints on an eval script
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
 
 function test() {
   let options = {
-    source: "-eval.js",
+    source: EXAMPLE_URL + "code_script-eval.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gSources = gDebugger.DebuggerView.Sources;
     const actions = bindActionCreators(gPanel);
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-highlight.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-highlight.js
@@ -6,17 +6,17 @@
 /**
  * Test if breakpoints are highlighted when they should.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-other-tabs.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-other-tabs.js
@@ -7,17 +7,17 @@
  * Make sure that setting a breakpoint in one tab, doesn't cause another tab at
  * the same source to pause at that location.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_breakpoints-other-tabs.html";
 
 var test = Task.async(function* () {
   const options = {
-    source: "code_breakpoints-other-tabs.js",
+    source: EXAMPLE_URL + "code_breakpoints-other-tabs.js",
     line: 1
   };
   const [tab1,, panel1] = yield initDebugger(TAB_URL, options);
   const [tab2,, panel2] = yield initDebugger(TAB_URL, options);
   const queries = panel1.panelWin.require("./content/queries");
   const actions = bindActionCreators(panel1);
   const getState = panel1.panelWin.DebuggerController.getState;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-pane.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_breakpoints-pane.js
@@ -7,17 +7,17 @@
  * Bug 723071: Test adding a pane to display the list of breakpoints across
  * all sources in the debuggee.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_bug-896139.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_bug-896139.js
@@ -3,56 +3,37 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Bug 896139 - Breakpoints not triggering when reloading script.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_bug-896139.html";
-const SCRIPT_URL = "code_bug-896139.js";
+const SCRIPT_URL = EXAMPLE_URL + "code_bug-896139.js";
 
 function test() {
   Task.spawn(function* () {
     function testBreakpoint() {
       let promise = waitForDebuggerEvents(panel, win.EVENTS.FETCHED_SCOPES);
       callInTab(tab, "f");
       return promise.then(() => doResume(panel));
     }
 
-    let [tab,, panel] = yield initDebugger();
+    let options = {
+      source: SCRIPT_URL,
+      line: 1
+    };
+    let [tab,, panel] = yield initDebugger(TAB_URL, options);
     let win = panel.panelWin;
 
     let Sources = win.DebuggerView.Sources;
 
-    // Load the debugger against a blank document and load the test url only
-    // here and not via initDebugger. That, because this test load SCRIPT_URL
-    // dynamically, on load, and the debugger may be on TAB_URL or SCRIPT_URL
-    // depending on cpu speed. initDebugger expect to assert one precise
-    // source.
-    yield navigateActiveTabTo(panel,
-                              TAB_URL,
-                              win.EVENTS.SOURCE_SHOWN);
-
-    if (Sources.selectedItem.attachment.source.url.indexOf(SCRIPT_URL) === -1) {
-      // If there is only the html file, wait for the js file to be listed.
-      if (Sources.itemCount == 1) {
-        yield waitForDebuggerEvents(panel, win.EVENTS.NEW_SOURCE);
-        // Wait for it to be added to the UI
-        yield waitForTick();
-      }
-      // Select the js file.
-      let onSource = waitForSourceAndCaret(panel, SCRIPT_URL, 1);
-      Sources.selectedValue = getSourceActor(win.DebuggerView.Sources,
-                                             EXAMPLE_URL + SCRIPT_URL);
-      yield onSource;
-    }
-
     yield panel.addBreakpoint({
-      actor: getSourceActor(win.DebuggerView.Sources, EXAMPLE_URL + SCRIPT_URL),
+      actor: getSourceActor(win.DebuggerView.Sources, SCRIPT_URL),
       line: 6
     });
 
     // Race condition: the setBreakpoint request sometimes leaves the
     // debugger in paused state for a bit because we are called before
     // that request finishes (see bug 1156531 for plans to fix)
     if (panel.panelWin.gThreadClient.state !== "attached") {
       yield waitForThreadEvents(panel, "resumed");
--- a/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-01.js
@@ -7,17 +7,17 @@
  * Tests the public evaluation API from the debugger controller.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   Task.spawn(function* () {
     const options = {
-      source: "-01.js",
+      source: EXAMPLE_URL + "code_script-switching-01.js",
       line: 1
     };
     const [tab,, panel] = yield initDebugger(TAB_URL, options);
     const win = panel.panelWin;
     const frames = win.DebuggerController.StackFrames;
     const framesView = win.DebuggerView.StackFrames;
     const sourcesView = win.DebuggerView.Sources;
     const editorView = win.DebuggerView.editor;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_controller-evaluate-02.js
@@ -7,17 +7,17 @@
  * Tests the public evaluation API from the debugger controller.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   Task.spawn(function* () {
     const options = {
-      source: "-01.js",
+      source: EXAMPLE_URL + "code_script-switching-01.js",
       line: 1
     };
     const [tab,, panel] = yield initDebugger(TAB_URL, options);
     const win = panel.panelWin;
     const frames = win.DebuggerController.StackFrames;
     const framesView = win.DebuggerView.StackFrames;
     const sourcesView = win.DebuggerView.Sources;
     const editorView = win.DebuggerView.editor;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_editor-contextmenu.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_editor-contextmenu.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let gTab, gPanel, gDebugger;
   let gEditor, gSources, gContextMenu;
 
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_editor-mode.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_editor-mode.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources;
 
 function test() {
   let options = {
-    source: "code_script-switching-01.js?a=b",
+    source: EXAMPLE_URL + "code_script-switching-01.js?a=b",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_hide-toolbar-buttons.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_hide-toolbar-buttons.js
@@ -12,17 +12,17 @@ const TAB_URL = EXAMPLE_URL + "doc_auto-
 
 var { RootActor } = require("devtools/server/actors/root");
 
 function test() {
   RootActor.prototype.traits.noBlackBoxing = true;
   RootActor.prototype.traits.noPrettyPrinting = true;
 
   let options = {
-    source: "code_ugly-5.js",
+    source: EXAMPLE_URL + "code_ugly-5.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
     let document = aPanel.panelWin.document;
     let ppButton = document.querySelector("#pretty-print");
     let bbButton = document.querySelector("#black-box");
     let sep = document.querySelector("#sources-toolbar .devtools-separator");
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_iframes.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_iframes.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_iframes.html";
 
 function test() {
   let gTab, gDebuggee, gPanel, gDebugger;
   let gIframe, gEditor, gSources, gFrames;
 
   let options = {
-    source: "inline-debugger-statement.html",
+    source: EXAMPLE_URL + "doc_inline-debugger-statement.html",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gIframe = gDebuggee.frames[0];
--- a/devtools/client/debugger/test/mochitest/browser_dbg_interrupts.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_interrupts.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let gTab, gPanel, gDebugger;
   let gSources, gBreakpoints, gTarget, gResumeButton, gResumeKey, gThreadClient;
 
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gBreakpoints = gDebugger.DebuggerController.Breakpoints;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_jump-to-function-definition.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_jump-to-function-definition.js
@@ -10,17 +10,17 @@
 const TAB_URL = EXAMPLE_URL + "doc_function-jump.html";
 const SCRIPT_URI = EXAMPLE_URL + "code_function-jump-01.js";
 
 
 function test() {
   let gTab, gPanel, gDebugger, gSources;
 
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_function-jump-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-01.js
@@ -7,17 +7,17 @@
  * Make sure that clicking the pretty print button prettifies the source.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
 
 function test() {
   // Wait for debugger panel to be fully set and break on debugger statement
   let options = {
-    source: "code_ugly.js",
+    source: EXAMPLE_URL + "code_ugly.js",
     line: 2
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-02.js
@@ -8,17 +8,17 @@
  * item prettifies the source.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
 
 function test() {
   // Wait for debugger panel to be fully set and break on debugger statement
   let options = {
-    source: "code_ugly.js",
+    source: EXAMPLE_URL + "code_ugly.js",
     line: 2
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gContextMenu = gDebugger.document.getElementById("sourceEditorContextMenu");
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-03.js
@@ -7,17 +7,17 @@
  * Make sure that we have the correct line selected after pretty printing.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
 
 function test() {
   // Wait for debugger panel to be fully set and break on debugger statement
   let options = {
-    source: "code_ugly.js",
+    source: EXAMPLE_URL + "code_ugly.js",
     line: 2
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
 
     Task.spawn(function* () {
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-04.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-04.js
@@ -7,17 +7,17 @@
  * Tests that the function searching works with pretty printed sources.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
 
 function test() {
   // Wait for debugger panel to be fully set and break on debugger statement
   let options = {
-    source: "code_ugly.js",
+    source: EXAMPLE_URL + "code_ugly.js",
     line: 2
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-07.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-07.js
@@ -7,17 +7,17 @@
 // for bug 921252.
 
 var gTab, gPanel, gClient, gThreadClient, gSource;
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html";
 
 function test() {
   let options = {
-    source: "code_ugly-2.js",
+    source: EXAMPLE_URL + "code_ugly-2.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gClient = gPanel.panelWin.gClient;
     gThreadClient = gPanel.panelWin.DebuggerController.activeThread;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-08.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-08.js
@@ -6,17 +6,17 @@
 // Test stepping through pretty printed sources.
 
 var gTab, gPanel, gClient, gThreadClient, gSource;
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html";
 
 function test() {
   let options = {
-    source: "code_ugly-2.js",
+    source: EXAMPLE_URL + "code_ugly-2.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gClient = gPanel.panelWin.gClient;
     gThreadClient = gPanel.panelWin.DebuggerController.activeThread;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-09.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-09.js
@@ -10,17 +10,17 @@ var gThreadClient;
 var gSource;
 
 var gTab, gPanel, gClient, gThreadClient, gSource;
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html";
 
 function test() {
   let options = {
-    source: "code_ugly-2.js",
+    source: EXAMPLE_URL + "code_ugly-2.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gClient = gPanel.panelWin.gClient;
     gThreadClient = gPanel.panelWin.DebuggerController.activeThread;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-10.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-10.js
@@ -8,17 +8,17 @@
  * and that clicking it doesn't do anything.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
 
 function test() {
   // Wait for debugger panel to be fully set and break on debugger statement
   let options = {
-    source: "code_ugly.js",
+    source: EXAMPLE_URL + "code_ugly.js",
     line: 2
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-11.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-11.js
@@ -10,17 +10,17 @@
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources;
 
 function test() {
   // Wait for debugger panel to be fully set and break on debugger statement
   let options = {
-    source: "code_ugly.js",
+    source: EXAMPLE_URL + "code_ugly.js",
     line: 2
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-13.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-13.js
@@ -9,17 +9,17 @@
  * JavaScript.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print-3.html";
 
 function test() {
   // Wait for debugger panel to be fully set and break on debugger statement
   let options = {
-    source: "code_ugly-8",
+    source: EXAMPLE_URL + "code_ugly-8",
     line: 2
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-on-paused.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_pretty-print-on-paused.js
@@ -12,17 +12,17 @@ const TAB_URL = EXAMPLE_URL + "doc_prett
 
 var gTab, gPanel, gDebugger, gThreadClient, gSources;
 
 const SECOND_SOURCE_VALUE = EXAMPLE_URL + "code_ugly-2.js";
 
 function test() {
   // Wait for debugger panel to be fully set and break on debugger statement
   let options = {
-    source: "code_script-switching-02.js",
+    source: EXAMPLE_URL + "code_script-switching-02.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gThreadClient = gDebugger.gThreadClient;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_reload-preferred-script-02.js
@@ -11,17 +11,17 @@
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 const PREFERRED_URL = EXAMPLE_URL + "code_script-switching-02.js";
 
 var gTab, gPanel, gDebugger;
 var gSources;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-01.js
@@ -6,17 +6,17 @@
 /**
  * Make sure that switching the displayed source in the UI works as advertised.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-02.js
@@ -10,17 +10,17 @@
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-02.html";
 
 var gLabel1 = "code_script-switching-01.js";
 var gLabel2 = "code_script-switching-02.js";
 var gParams = "?foo=bar,baz|lol";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_scripts-switching-03.js
@@ -6,17 +6,17 @@
 /**
  * Make sure that the DebuggerView error loading source text is correct.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gView = gDebugger.DebuggerView;
     const gEditor = gDebugger.DebuggerView.editor;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-autofill-identifier.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-autofill-identifier.js
@@ -8,17 +8,17 @@
  * selected or manually passed and searching using certain operators.
  */
 "use strict";
 
 function test() {
   const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
 
   let options = {
-    source: "code_function-search-01.js",
+    source: EXAMPLE_URL + "code_function-search-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     let Debugger = aPanel.panelWin;
     let Editor = Debugger.DebuggerView.editor;
     let Filtering = Debugger.DebuggerView.Filtering;
 
     function doSearch(aOperator) {
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-02.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gSources, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1,
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-03.js
@@ -8,17 +8,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gSources, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1,
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-04.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-basic-04.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1,
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-01.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gSearchView, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-02.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gSearchView, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-03.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gSearchView, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-04.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-04.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gSearchView, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-05.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-05.js
@@ -11,17 +11,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gSearchView, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-global-06.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-global-06.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gSearchView, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-popup-jank.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-popup-jank.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html";
 
 var gTab, gPanel, gDebugger;
 var gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js?a=b",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-01.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   // Debug test slaves are a bit slow at this test.
   requestLongerTimeout(3);
 
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gSources = gDebugger.DebuggerView.Sources;
     const gSearchView = gDebugger.DebuggerView.Filtering.FilteredSources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-02.js
@@ -12,17 +12,17 @@ const TAB_URL = EXAMPLE_URL + "doc_edito
 var gTab, gPanel, gDebugger;
 var gSources, gSourceUtils, gSearchView, gSearchBox;
 
 function test() {
   // Debug test slaves are a bit slow at this test.
   requestLongerTimeout(3);
 
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js?a=b",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gSourceUtils = gDebugger.SourceUtils;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-03.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-sources-03.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_editor-mode.html";
 
 var gTab, gPanel, gDebugger;
 var gSources, gSearchBox;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js?a=b",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_search-symbols.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_search-symbols.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gSearchBox, gFilteredFunctions;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_function-search-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-01.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gSearchBox, gSearchBoxPanel;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
     gSearchBoxPanel = gDebugger.DebuggerView.Filtering._searchboxHelpPanel;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_searchbox-help-popup-02.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSearchBox, gSearchBoxPanel;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_source-maps-02.js
@@ -10,17 +10,17 @@
 const TAB_URL = EXAMPLE_URL + "doc_binary_search.html";
 const JS_URL = EXAMPLE_URL + "code_binary_search.js";
 
 var gTab, gPanel, gDebugger, gEditor;
 var gSources, gFrames, gPrefs, gOptions;
 
 function test() {
   let options = {
-    source: ".coffee",
+    source: EXAMPLE_URL + "code_binary_search.coffee",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_sources-cache.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-cache.js
@@ -7,17 +7,17 @@
  * Tests if the sources cache knows how to cache sources when prompted.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
 const TOTAL_SOURCES = 4;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_function-search-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
     const gTab = aTab;
     const gDebuggee = aDebuggee;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-01.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-01.js
@@ -9,17 +9,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
 
 function test() {
   let gTab, gPanel, gDebugger;
   let gSources, gBreakpoints;
 
   let options = {
-    source: "-eval.js",
+    source: EXAMPLE_URL + "code_script-eval.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gBreakpoints = gDebugger.DebuggerController.Breakpoints;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-02.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-eval-02.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-eval.html";
 
 function test() {
   let gTab, gPanel, gDebugger;
   let gSources, gBreakpoints, gEditor;
 
   let options = {
-    source: "-eval.js",
+    source: EXAMPLE_URL + "code_script-eval.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
     gBreakpoints = gDebugger.DebuggerController.Breakpoints;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_sources-large.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-large.js
@@ -8,17 +8,17 @@
  *   1) No parsing to determine current symbol is attempted when
  *      starting a search
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_function-search.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_function-search-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab, aDebuggee, aPanel]) => {
     const gTab = aTab;
     const gDebuggee = aDebuggee;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_sources-webext-contentscript.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_sources-webext-contentscript.js
@@ -4,16 +4,18 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Make sure eval scripts appear in the source list
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script_webext_contentscript.html";
 
+let {getExtensionUUID} = Cu.import("resource://gre/modules/Extension.jsm", {});
+
 function test() {
   let gPanel, gDebugger;
   let gSources, gAddon;
 
   let cleanup = function* (e) {
     if (gAddon) {
       // Remove the addon, if any.
       yield removeAddon(gAddon);
@@ -24,19 +26,20 @@ function test() {
     } else {
       // If no debugger panel was opened, call finish directly.
       finish();
     }
   };
 
   return Task.spawn(function* () {
     gAddon = yield addAddon(EXAMPLE_URL + "/addon-webext-contentscript.xpi");
+    let uuid = getExtensionUUID(gAddon.id);
 
     let options = {
-      source: "webext-content-script.js",
+      source: `moz-extension://${uuid}/webext-content-script.js`,
       line: 1
     };
     [,, gPanel] = yield initDebugger(TAB_URL, options);
     gDebugger = gPanel.panelWin;
     gSources = gDebugger.DebuggerView.Sources;
 
     is(gSources.values.length, 1, "Should have 1 source");
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg_stack-05.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-05.js
@@ -7,17 +7,17 @@
  * Test that switching between stack frames properly sets the current debugger
  * location in the source editor.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     const gTab = aTab;
     const gPanel = aPanel;
     const gDebugger = gPanel.panelWin;
     const gEditor = gDebugger.DebuggerView.editor;
     const gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_stack-06.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-06.js
@@ -10,17 +10,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gFrames, gClassicFrames;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/browser_dbg_stack-07.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_stack-07.js
@@ -11,17 +11,17 @@
 
 const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
 
 var gTab, gPanel, gDebugger;
 var gEditor, gSources, gFrames, gClassicFrames, gToolbar;
 
 function test() {
   let options = {
-    source: "-01.js",
+    source: EXAMPLE_URL + "code_script-switching-01.js",
     line: 1
   };
   initDebugger(TAB_URL, options).then(([aTab,, aPanel]) => {
     gTab = aTab;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -233,16 +233,34 @@ function waitForTick() {
 }
 
 function waitForTime(aDelay) {
   let deferred = promise.defer();
   setTimeout(deferred.resolve, aDelay);
   return deferred.promise;
 }
 
+function waitForSourceLoaded(aPanel, aUrl) {
+  let { Sources } = aPanel.panelWin.DebuggerView;
+  let isLoaded = Sources.items.some(item =>
+    item.attachment.source.url === aUrl);
+  if (isLoaded) {
+    info("The correct source has been loaded.");
+    return promise.resolve(null);
+  } else {
+    return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.NEW_SOURCE).then(() => {
+      // Wait for it to be loaded in the UI and appear into Sources.items.
+      return waitForTick();
+    }).then(() => {
+      return waitForSourceLoaded(aPanel, aUrl);
+    });
+  }
+
+}
+
 function waitForSourceShown(aPanel, aUrl) {
   return waitForDebuggerEvents(aPanel, aPanel.panelWin.EVENTS.SOURCE_SHOWN).then(aSource => {
     let sourceUrl = aSource.url || aSource.introductionUrl;
     info("Source shown: " + sourceUrl);
 
     if (!sourceUrl.includes(aUrl)) {
       return waitForSourceShown(aPanel, aUrl);
     } else {
@@ -554,16 +572,17 @@ let initDebugger = Task.async(function*(
   let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject;
   let target = TargetFactory.forTab(tab);
 
   let toolbox = yield gDevTools.showToolbox(target, "jsdebugger");
   info("Debugger panel shown successfully.");
 
   let debuggerPanel = toolbox.getCurrentPanel();
   let panelWin = debuggerPanel.panelWin;
+  let { Sources } = panelWin.DebuggerView;
 
   prepareDebugger(debuggerPanel);
 
   if (url && url != "about:blank") {
     let onCaretUpdated;
     if (line) {
       onCaretUpdated = waitForCaretUpdated(debuggerPanel, line);
     }
@@ -572,17 +591,25 @@ let initDebugger = Task.async(function*(
       // SOURCE_SHOWN event
       yield reload(debuggerPanel, url);
     } else {
       yield navigateActiveTabTo(debuggerPanel,
                                 url,
                                 panelWin.EVENTS.SOURCE_SHOWN);
     }
     if (source) {
-      ensureSourceIs(debuggerPanel, source);
+      let isSelected = Sources.selectedItem.attachment.source.url === source;
+      if (!isSelected) {
+        // Ensure that the source is loaded first before trying to select it
+        yield waitForSourceLoaded(debuggerPanel, source);
+        // Select the js file.
+        let onSource = waitForSourceAndCaret(debuggerPanel, source, line ? line : 1);
+        Sources.selectedValue = getSourceActor(Sources, source);
+        yield onSource;
+      }
     }
     yield onCaretUpdated;
   }
 
   return [tab, debuggee, debuggerPanel, window];
 });
 
 // Creates an add-on debugger for a given add-on. The returned AddonDebugger
--- a/devtools/client/eyedropper/test/browser.ini
+++ b/devtools/client/eyedropper/test/browser.ini
@@ -5,8 +5,9 @@ support-files =
   color-block.html
   head.js
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
 
 [browser_eyedropper_basic.js]
 skip-if = os == "win" && debug # bug 963492
 [browser_eyedropper_cmd.js]
+skip-if = true # bug 1278400
--- a/devtools/client/framework/selection.js
+++ b/devtools/client/framework/selection.js
@@ -1,17 +1,18 @@
 /* -*- 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";
 
-const { Cu, Ci } = require("chrome");
+const { Cu } = require("chrome");
+const nodeConstants = require("devtools/shared/dom-node-constants.js")
 const { getRootBindingParent } = require("devtools/shared/layout/utils");
 var EventEmitter = require("devtools/shared/event-emitter");
 
 /**
  * API
  *
  *   new Selection(walker=null, node=null, track={attributes,detached});
  *   destroy()
@@ -157,19 +158,19 @@ Selection.prototype = {
       return this.node.ownerDocument;
     }
     return null;
   },
 
   setNodeFront: function (value, reason = "unknown") {
     this.reason = reason;
 
-    // If a singleTextChild text node is being set, then set it's parent instead.
+    // If an inlineTextChild text node is being set, then set it's parent instead.
     let parentNode = value && value.parentNode();
-    if (value && parentNode && parentNode.singleTextChild === value) {
+    if (value && parentNode && parentNode.inlineTextChild === value) {
       value = parentNode;
     }
 
     // We used to return here if the node had not changed but we now need to
     // set the node even if it is already set otherwise it is not possible to
     // e.g. highlight the same node twice.
     let rawValue = null;
     if (value && value.isLocalToBeDeprecated()) {
@@ -257,58 +258,58 @@ Selection.prototype = {
 
   isHTMLNode: function () {
     let xhtml_ns = "http://www.w3.org/1999/xhtml";
     return this.isNode() && this.nodeFront.namespaceURI == xhtml_ns;
   },
 
   // Node type
 
-  isElementNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE;
+  isElementNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE;
   },
 
   isPseudoElementNode: function () {
     return this.isNode() && this.nodeFront.isPseudoElement;
   },
 
   isAnonymousNode: function () {
     return this.isNode() && this.nodeFront.isAnonymous;
   },
 
-  isAttributeNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ATTRIBUTE_NODE;
+  isAttributeNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE;
   },
 
-  isTextNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.TEXT_NODE;
+  isTextNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE;
   },
 
-  isCDATANode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.CDATA_SECTION_NODE;
+  isCDATANode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE;
   },
 
-  isEntityRefNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_REFERENCE_NODE;
+  isEntityRefNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE;
   },
 
-  isEntityNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_NODE;
+  isEntityNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE;
   },
 
-  isProcessingInstructionNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE;
+  isProcessingInstructionNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE;
   },
 
-  isCommentNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE;
+  isCommentNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE;
   },
 
-  isDocumentNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE;
+  isDocumentNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE;
   },
 
   /**
    * @returns true if the selection is the <body> HTML element.
    */
   isBodyNode: function () {
     return this.isHTMLNode() &&
            this.isConnected() &&
@@ -319,20 +320,20 @@ Selection.prototype = {
    * @returns true if the selection is the <head> HTML element.
    */
   isHeadNode: function () {
     return this.isHTMLNode() &&
            this.isConnected() &&
            this.nodeFront.nodeName === "HEAD";
   },
 
-  isDocumentTypeNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE;
+  isDocumentTypeNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE;
   },
 
-  isDocumentFragmentNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE;
+  isDocumentFragmentNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE;
   },
 
-  isNotationNode: function () {
-    return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.NOTATION_NODE;
+  isNotationNode: function() {
+    return this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE;
   },
 };
--- a/devtools/client/framework/test/browser_target_support.js
+++ b/devtools/client/framework/test/browser_target_support.js
@@ -2,17 +2,17 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test support methods on Target, such as `hasActor`, `getActorDescription`,
 // `actorHasMethod` and `getTrait`.
 
 var { WebAudioFront } =
-  require("devtools/server/actors/webaudio");
+  require("devtools/shared/fronts/webaudio");
 
 function* testTarget(client, target) {
   yield target.makeRemote();
 
   is(target.hasActor("timeline"), true, "target.hasActor() true when actor exists.");
   is(target.hasActor("webaudio"), true, "target.hasActor() true when actor exists.");
   is(target.hasActor("notreal"), false, "target.hasActor() false when actor does not exist.");
   // Create a front to ensure the actor is loaded
--- a/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
@@ -25,23 +25,27 @@ add_task(function* () {
     Toolbox.HostType.BOTTOM);
 
   yield toolbox.selectTool("inspector");
   yield toolbox.switchHost(Toolbox.HostType.WINDOW);
 
   is(getTitle(), `Developer Tools - Page title - ${URL}`,
     "Devtools title correct after switching to detached window host");
 
+  // Wait for tick to avoid unexpected 'popuphidden' event, which
+  // blocks the frame popup menu opened below. See also bug 1276873
+  yield waitForTick();
+
   // Open frame menu and wait till it's available on the screen.
   let btn = toolbox.doc.getElementById("command-button-frames");
   let menu = toolbox.showFramesMenu({target: btn});
   yield once(menu, "open");
 
   // Verify that the frame list menu is populated
-  let frames = menu.menuitems;
+  let frames = menu.items;
   is(frames.length, 2, "We have both frames in the list");
 
   let topFrameBtn = frames.filter(b => b.label == URL)[0];
   let iframeBtn = frames.filter(b => b.label == IFRAME_URL)[0];
   ok(topFrameBtn, "Got top level document in the list");
   ok(iframeBtn, "Got iframe document in the list");
 
   // Listen to will-navigate to check if the view is empty
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -62,17 +62,17 @@ loader.lazyRequireGetter(this, "DevTools
   "devtools/shared/DevToolsUtils");
 loader.lazyRequireGetter(this, "showDoorhanger",
   "devtools/client/shared/doorhanger", true);
 loader.lazyRequireGetter(this, "createPerformanceFront",
   "devtools/server/actors/performance", true);
 loader.lazyRequireGetter(this, "system",
   "devtools/shared/system");
 loader.lazyRequireGetter(this, "getPreferenceFront",
-  "devtools/server/actors/preference", true);
+  "devtools/shared/fronts/preference", true);
 loader.lazyRequireGetter(this, "KeyShortcuts",
   "devtools/client/shared/key-shortcuts", true);
 loader.lazyRequireGetter(this, "ZoomKeys",
   "devtools/client/shared/zoom-keys");
 
 loader.lazyGetter(this, "osString", () => {
   return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
 });
--- a/devtools/client/inspector/breadcrumbs.js
+++ b/devtools/client/inspector/breadcrumbs.js
@@ -17,16 +17,18 @@ const ELLIPSIS = Services.prefs.getCompl
     Ci.nsIPrefLocalizedString).data;
 const MAX_LABEL_LENGTH = 40;
 
 const NS_XHTML = "http://www.w3.org/1999/xhtml";
 const SCROLL_REPEAT_MS = 100;
 
 loader.lazyRequireGetter(this, "EventEmitter",
                          "devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "KeyShortcuts",
+                         "devtools/client/shared/key-shortcuts", true);
 
 /**
  * Component to replicate functionality of XUL arrowscrollbox
  * for breadcrumbs
  *
  * @param {Window} win The window containing the breadcrumbs
  * @parem {DOMNode} container The element in which to put the scroll box
  */
@@ -327,34 +329,42 @@ HTMLBreadcrumbs.prototype = {
 
   _init: function () {
     this.outer = this.chromeDoc.getElementById("inspector-breadcrumbs");
     this.arrowScrollBox = new ArrowScrollBox(
         this.chromeWin,
         this.outer);
 
     this.container = this.arrowScrollBox.inner;
+    this.scroll = this.scroll.bind(this);
     this.arrowScrollBox.on("overflow", this.scroll);
 
     // These separators are used for CSS purposes only, and are positioned
     // off screen, but displayed with -moz-element.
     this.separators = this.chromeDoc.createElementNS(NS_XHTML, "div");
     this.separators.className = "breadcrumb-separator-container";
     this.separators.innerHTML =
                       "<div id='breadcrumb-separator-before'></div>" +
                       "<div id='breadcrumb-separator-after'></div>" +
                       "<div id='breadcrumb-separator-normal'></div>";
     this.container.parentNode.appendChild(this.separators);
 
     this.outer.addEventListener("click", this, true);
-    this.outer.addEventListener("keypress", this, true);
     this.outer.addEventListener("mouseover", this, true);
     this.outer.addEventListener("mouseleave", this, true);
     this.outer.addEventListener("focus", this, true);
 
+    this.shortcuts = new KeyShortcuts({ window: this.chromeWin, target: this.outer });
+    this.handleShortcut = this.handleShortcut.bind(this);
+
+    this.shortcuts.on("Right", this.handleShortcut);
+    this.shortcuts.on("Left", this.handleShortcut);
+    this.shortcuts.on("Tab", this.handleShortcut);
+    this.shortcuts.on("Shift+Tab", this.handleShortcut);
+
     // We will save a list of already displayed nodes in this array.
     this.nodeHierarchy = [];
 
     // Last selected node in nodeHierarchy.
     this.currentIndex = -1;
 
     this.update = this.update.bind(this);
     this.updateSelectors = this.updateSelectors.bind(this);
@@ -463,18 +473,16 @@ HTMLBreadcrumbs.prototype = {
 
   /**
    * Generic event handler.
    * @param {DOMEvent} event.
    */
   handleEvent: function (event) {
     if (event.type == "click" && event.button == 0) {
       this.handleClick(event);
-    } else if (event.type == "keypress" && this.selection.isElementNode()) {
-      this.handleKeyPress(event);
     } else if (event.type == "mouseover") {
       this.handleMouseOver(event);
     } else if (event.type == "mouseleave") {
       this.handleMouseLeave(event);
     } else if (event.type == "focus") {
       this.handleFocus(event);
     }
   },
@@ -526,86 +534,71 @@ HTMLBreadcrumbs.prototype = {
    * On mouse leave, make sure to unhighlight.
    * @param {DOMEvent} event.
    */
   handleMouseLeave: function (event) {
     this.inspector.toolbox.highlighterUtils.unhighlight();
   },
 
   /**
-   * On keypress, navigate through the list of breadcrumbs with the left/right
-   * arrow keys.
-   * @param {DOMEvent} event.
+   * Handle a keyboard shortcut supported by the breadcrumbs widget.
+   *
+   * @param {String} name
+   *        Name of the keyboard shortcut received.
+   * @param {DOMEvent} event
+   *        Original event that triggered the shortcut.
    */
-  handleKeyPress: function (event) {
-    let win = this.chromeWin;
-    let {keyCode, shiftKey, metaKey, ctrlKey, altKey} = event;
-
-    // Only handle left, right, tab and shift tab, let anything else bubble up
-    // so native shortcuts work.
-    let hasModifier = metaKey || ctrlKey || altKey || shiftKey;
-    let isLeft = keyCode === win.KeyEvent.DOM_VK_LEFT && !hasModifier;
-    let isRight = keyCode === win.KeyEvent.DOM_VK_RIGHT && !hasModifier;
-    let isTab = keyCode === win.KeyEvent.DOM_VK_TAB && !hasModifier;
-    let isShiftTab = keyCode === win.KeyEvent.DOM_VK_TAB && shiftKey &&
-                     !metaKey && !ctrlKey && !altKey;
-
-    if (!isLeft && !isRight && !isTab && !isShiftTab) {
+  handleShortcut: function (name, event) {
+    if (!this.selection.isElementNode()) {
       return;
     }
 
     event.preventDefault();
     event.stopPropagation();
 
     this.keyPromise = (this.keyPromise || promise.resolve(null)).then(() => {
-      if (isLeft && this.currentIndex != 0) {
+      if (name === "Left" && this.currentIndex != 0) {
         let node = this.nodeHierarchy[this.currentIndex - 1].node;
         return this.selection.setNodeFront(node, "breadcrumbs");
-      } else if (isRight && this.currentIndex < this.nodeHierarchy.length - 1) {
+      } else if (name === "Right" && this.currentIndex < this.nodeHierarchy.length - 1) {
         let node = this.nodeHierarchy[this.currentIndex + 1].node;
         return this.selection.setNodeFront(node, "breadcrumbs");
-      } else if (isTab || isShiftTab) {
-        // Tabbing when breadcrumbs or its contents are focused should move
-        // focus to next/previous focusable element relative to breadcrumbs
-        // themselves.
-        let elm, type;
-        if (shiftKey) {
-          elm = this.container;
-          type = FocusManager.MOVEFOCUS_BACKWARD;
-        } else {
-          // To move focus to next element following the breadcrumbs, relative
-          // element needs to be the last element in breadcrumbs' subtree.
-          let last = this.container.lastChild;
-          while (last && last.lastChild) {
-            last = last.lastChild;
-          }
-          elm = last;
-          type = FocusManager.MOVEFOCUS_FORWARD;
+      } else if (name === "Tab") {
+        // To move focus to next element following the breadcrumbs, relative
+        // element needs to be the last element in breadcrumbs' subtree.
+        let last = this.container.lastChild;
+        while (last && last.lastChild) {
+          last = last.lastChild;
         }
-        FocusManager.moveFocus(win, elm, type, 0);
+        FocusManager.moveFocus(this.chromeWin, last, FocusManager.MOVEFOCUS_FORWARD, 0);
+      } else if (name === "Shift+Tab") {
+        // Tabbing when breadcrumbs or its contents are focused should move focus to
+        // previous focusable element relative to breadcrumbs themselves.
+        let elt = this.container;
+        FocusManager.moveFocus(this.chromeWin, elt, FocusManager.MOVEFOCUS_BACKWARD, 0);
       }
 
       return null;
     });
   },
 
   /**
    * Remove nodes and clean up.
    */
   destroy: function () {
     this.selection.off("new-node-front", this.update);
     this.selection.off("pseudoclass", this.updateSelectors);
     this.selection.off("attribute-changed", this.updateSelectors);
     this.inspector.off("markupmutation", this.update);
 
     this.container.removeEventListener("click", this, true);
-    this.container.removeEventListener("keypress", this, true);
     this.container.removeEventListener("mouseover", this, true);
     this.container.removeEventListener("mouseleave", this, true);
     this.container.removeEventListener("focus", this, true);
+    this.shortcuts.destroy();
 
     this.empty();
     this.separators.remove();
 
     this.arrowScrollBox.off("overflow", this.scroll);
     this.arrowScrollBox.destroy();
     this.arrowScrollBox = null;
     this.outer = null;
@@ -677,23 +670,16 @@ HTMLBreadcrumbs.prototype = {
    */
   buildButton: function (node) {
     let button = this.chromeDoc.createElementNS(NS_XHTML, "button");
     button.appendChild(this.prettyPrintNodeAsXHTML(node));
     button.className = "breadcrumbs-widget-item";
 
     button.setAttribute("title", this.prettyPrintNodeAsText(node));
 
-    button.onkeypress = function onBreadcrumbsKeypress(e) {
-      if (e.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
-          e.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
-        button.click();
-      }
-    };
-
     button.onclick = () => {
       button.focus();
     };
 
     button.onBreadcrumbsClick = () => {
       this.selection.setNodeFront(node, "breadcrumbs");
     };
 
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -19,16 +19,18 @@ const {OutputParser} = require("devtools
 const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
 const {createChild} = require("devtools/client/inspector/shared/utils");
 const {gDevTools} = require("devtools/client/framework/devtools");
 
 loader.lazyRequireGetter(this, "overlays",
   "devtools/client/inspector/shared/style-inspector-overlays");
 loader.lazyRequireGetter(this, "StyleInspectorMenu",
   "devtools/client/inspector/shared/style-inspector-menu");
+loader.lazyRequireGetter(this, "KeyShortcuts",
+  "devtools/client/shared/key-shortcuts", true);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 
 XPCOMUtils.defineLazyGetter(CssComputedView, "_strings", function () {
   return Services.strings.createBundle(
@@ -150,42 +152,42 @@ function CssComputedView(inspector, docu
   this._outputParser = new OutputParser(document);
 
   let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
     .getService(Ci.nsIXULChromeRegistry);
   this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
 
   // Create bound methods.
   this.focusWindow = this.focusWindow.bind(this);
-  this._onKeypress = this._onKeypress.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._onClick = this._onClick.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onFilterStyles = this._onFilterStyles.bind(this);
-  this._onFilterKeyPress = this._onFilterKeyPress.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
   this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this);
   this._onFilterTextboxContextMenu =
     this._onFilterTextboxContextMenu.bind(this);
 
   let doc = this.styleDocument;
   this.root = doc.getElementById("root");
   this.element = doc.getElementById("propertyContainer");
   this.searchField = doc.getElementById("computedview-searchbox");
   this.searchClearButton = doc.getElementById("computedview-searchinput-clear");
   this.includeBrowserStylesCheckbox =
     doc.getElementById("browser-style-checkbox");
 
-  this.styleDocument.addEventListener("keypress", this._onKeypress);
+  this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
+  this._onShortcut = this._onShortcut.bind(this);
+  this.shortcuts.on("CmdOrCtrl+F", this._onShortcut);
+  this.shortcuts.on("Escape", this._onShortcut);
   this.styleDocument.addEventListener("mousedown", this.focusWindow);
   this.element.addEventListener("click", this._onClick);
   this.element.addEventListener("copy", this._onCopy);
   this.element.addEventListener("contextmenu", this._onContextMenu);
   this.searchField.addEventListener("input", this._onFilterStyles);
-  this.searchField.addEventListener("keypress", this._onFilterKeyPress);
   this.searchField.addEventListener("contextmenu",
                                     this._onFilterTextboxContextMenu);
   this.searchClearButton.addEventListener("click", this._onClearSearch);
   this.includeBrowserStylesCheckbox.addEventListener("command",
     this._onIncludeBrowserStyles);
 
   this.searchClearButton.hidden = true;
 
@@ -497,27 +499,29 @@ CssComputedView.prototype = {
         }
       );
       this._refreshProcess.schedule();
       return deferred.promise;
     }).then(null, (err) => console.error(err));
   },
 
   /**
-   * Handle the keypress event in the computed view.
+   * Handle the shortcut events in the computed view.
    */
-  _onKeypress: function (event) {
+  _onShortcut: function (name, event) {
     if (!event.target.closest("#sidebar-panel-computedview")) {
       return;
     }
-    let isOSX = Services.appinfo.OS === "Darwin";
-
-    if (((isOSX && event.metaKey && !event.ctrlKey && !event.altKey) ||
-        (!isOSX && event.ctrlKey && !event.metaKey && !event.altKey)) &&
-        event.key === "f") {
+    // Handle the search box's keypress event. If the escape key is pressed,
+    // clear the search box field.
+    if (name === "Escape" && event.target === this.searchField &&
+        this._onClearSearch()) {
+      event.preventDefault();
+      event.stopPropagation();
+    } else if (name === "CmdOrCtrl+F") {
       this.searchField.focus();
       event.preventDefault();
     }
   },
 
   /**
    * Set the filter style search value.
    * @param {String} value
@@ -549,28 +553,16 @@ CssComputedView.prototype = {
       }
 
       this.refreshPanel();
       this._filterChangeTimeout = null;
     }, filterTimeout);
   },
 
   /**
-   * Handle the search box's keypress event. If the escape key is pressed,
-   * clear the search box field.
-   */
-  _onFilterKeyPress: function (event) {
-    if (event.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE &&
-        this._onClearSearch()) {
-      event.preventDefault();
-      event.stopPropagation();
-    }
-  },
-
-  /**
    * Context menu handler for filter style search box.
    */
   _onFilterTextboxContextMenu: function (event) {
     try {
       this.styleDocument.defaultView.focus();
       let contextmenu = this.inspector.toolbox.textboxContextMenuPopup;
       contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
     } catch (e) {
@@ -771,17 +763,16 @@ CssComputedView.prototype = {
     this.highlighters.destroy();
 
     // Remove bound listeners
     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
     this.element.removeEventListener("click", this._onClick);
     this.element.removeEventListener("copy", this._onCopy);
     this.element.removeEventListener("contextmenu", this._onContextMenu);
     this.searchField.removeEventListener("input", this._onFilterStyles);
-    this.searchField.removeEventListener("keypress", this._onFilterKeyPress);
     this.searchField.removeEventListener("contextmenu",
                                          this._onFilterTextboxContextMenu);
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
     this.includeBrowserStylesCheckbox.removeEventListener("command",
       this.includeBrowserStylesChanged);
 
     // Nodes used in templating
     this.root = null;
@@ -952,28 +943,25 @@ PropertyView.prototype = {
     // Build the container element
     this.onMatchedToggle = this.onMatchedToggle.bind(this);
     this.element = doc.createElementNS(HTML_NS, "div");
     this.element.setAttribute("class", this.propertyHeaderClassName);
     this.element.addEventListener("dblclick", this.onMatchedToggle, false);
 
     // Make it keyboard navigable
     this.element.setAttribute("tabindex", "0");
-    this.onKeyDown = (event) => {
-      let keyEvent = Ci.nsIDOMKeyEvent;
-      if (event.keyCode === keyEvent.DOM_VK_F1) {
-        this.mdnLinkClick();
-        event.preventDefault();
-      }
-      if (event.keyCode === keyEvent.DOM_VK_RETURN ||
-        event.keyCode === keyEvent.DOM_VK_SPACE) {
-        this.onMatchedToggle(event);
-      }
-    };
-    this.element.addEventListener("keydown", this.onKeyDown, false);
+    this.shortcuts = new KeyShortcuts({
+      window: this.tree.styleWindow,
+      target: this.element
+    });
+    this.shortcuts.on("F1", (name, event) => {
+      this.mdnLinkClick(event);
+    });
+    this.shortcuts.on("Return", (name, event) => this.onMatchedToggle(event));
+    this.shortcuts.on("Space", (name, event) => this.onMatchedToggle(event));
 
     let nameContainer = doc.createElementNS(HTML_NS, "div");
     nameContainer.className = "property-name-container";
     this.element.appendChild(nameContainer);
 
     // Build the twisty expand/collapse
     this.matchedExpander = doc.createElementNS(HTML_NS, "div");
     this.matchedExpander.className = "expander theme-twisty";
@@ -1111,17 +1099,21 @@ PropertyView.prototype = {
         target: "_blank",
         class: "link theme-link",
         title: selector.href,
         sourcelocation: selector.source,
         tabindex: "0",
         textContent: selector.source
       });
       link.addEventListener("click", selector.openStyleEditor, false);
-      link.addEventListener("keydown", selector.maybeOpenStyleEditor, false);
+      let shortcuts = new KeyShortcuts({
+        window: this.tree.styleWindow,
+        target: link
+      });
+      shortcuts.on("Return", () => selector.openStyleEditor());
 
       let status = createChild(p, "span", {
         dir: "ltr",
         class: "rule-text theme-fg-color3 " + selector.statusClass,
         title: selector.statusText,
         textContent: selector.sourceText
       });
       let valueSpan = createChild(status, "span", {
@@ -1186,24 +1178,25 @@ PropertyView.prototype = {
   mdnLinkClick: function (event) {
     let inspector = this.tree.inspector;
 
     if (inspector.target.tab) {
       let browserWin = inspector.target.tab.ownerDocument.defaultView;
       browserWin.openUILinkIn(this.link, "tab");
     }
     event.preventDefault();
+    event.stopPropagation();
   },
 
   /**
    * Destroy this property view, removing event listeners
    */
   destroy: function () {
     this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
-    this.element.removeEventListener("keydown", this.onKeyDown, false);
+    this.shortcuts.destroy();
     this.element = null;
 
     this.matchedExpander.removeEventListener("click", this.onMatchedToggle,
                                              false);
     this.matchedExpander = null;
 
     this.nameNode.removeEventListener("click", this.onFocus, false);
     this.nameNode = null;
@@ -1221,17 +1214,16 @@ PropertyView.prototype = {
  * @param selectorInfo
  */
 function SelectorView(tree, selectorInfo) {
   this.tree = tree;
   this.selectorInfo = selectorInfo;
   this._cacheStatusNames();
 
   this.openStyleEditor = this.openStyleEditor.bind(this);
-  this.maybeOpenStyleEditor = this.maybeOpenStyleEditor.bind(this);
 
   this.ready = this.updateSourceLink();
 }
 
 /**
  * Decode for cssInfo.rule.status
  * @see SelectorView.prototype._cacheStatusNames
  * @see CssLogic.STATUS
@@ -1364,26 +1356,16 @@ SelectorView.prototype = {
     }
 
     let oldSource = this.source;
     this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
     return promise.resolve(oldSource);
   },
 
   /**
-   * Open the style editor if the RETURN key was pressed.
-   */
-  maybeOpenStyleEditor: function (event) {
-    let keyEvent = Ci.nsIDOMKeyEvent;
-    if (event.keyCode === keyEvent.DOM_VK_RETURN) {
-      this.openStyleEditor();
-    }
-  },
-
-  /**
    * When a css link is clicked this method is called in order to either:
    *   1. Open the link in view source (for chrome stylesheets).
    *   2. Open the link in the style editor.
    *
    *   We can only view stylesheets contained in document.styleSheets inside the
    *   style editor.
    */
   openStyleEditor: function () {
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -13,16 +13,17 @@ const {Cc, Ci} = require("chrome");
 var Services = require("Services");
 var promise = require("promise");
 var EventEmitter = require("devtools/shared/event-emitter");
 var clipboard = require("sdk/clipboard");
 const {executeSoon} = require("devtools/shared/DevToolsUtils");
 var {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
 var {Task} = require("devtools/shared/task");
 const {initCssProperties} = require("devtools/shared/fronts/css-properties");
+const nodeConstants = require("devtools/shared/dom-node-constants");
 
 loader.lazyRequireGetter(this, "CSS", "CSS");
 
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "ComputedViewTool", "devtools/client/inspector/computed/computed", true);
 loader.lazyRequireGetter(this, "FontInspector", "devtools/client/inspector/fonts/fonts", true);
 loader.lazyRequireGetter(this, "HTMLBreadcrumbs", "devtools/client/inspector/breadcrumbs", true);
 loader.lazyRequireGetter(this, "InspectorSearch", "devtools/client/inspector/inspector-search", true);
@@ -1249,25 +1250,25 @@ InspectorPanel.prototype = {
    */
   copyOuterHTML: function () {
     if (!this.selection.isNode()) {
       return;
     }
     let node = this.selection.nodeFront;
 
     switch (node.nodeType) {
-      case Ci.nsIDOMNode.ELEMENT_NODE :
+      case nodeConstants.ELEMENT_NODE :
         this._copyLongString(this.walker.outerHTML(node));
         break;
-      case Ci.nsIDOMNode.COMMENT_NODE :
+      case nodeConstants.COMMENT_NODE :
         this._getLongString(node.getNodeValue()).then(comment => {
           clipboardHelper.copyString("<!--" + comment + "-->");
         });
         break;
-      case Ci.nsIDOMNode.DOCUMENT_TYPE_NODE :
+      case nodeConstants.DOCUMENT_TYPE_NODE :
         clipboardHelper.copyString(node.doctypeString);
         break;
     }
   },
 
   /**
    * Copy the data-uri for the currently selected image in the clipboard.
    */
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -270,57 +270,59 @@
                 <html:p class="font-css">&usedAs; "<html:span class="font-css-name"></html:span>"</html:p>
                 <html:pre class="font-css-code"></html:pre>
               </html:div>
             </html:section>
           </html:div>
         </tabpanel>
 
         <tabpanel id="sidebar-panel-layoutview" class="devtools-monospace theme-sidebar inspector-tabpanel">
-          <html:div id="layout-container">
-            <html:p id="layout-header">
-              <html:span id="layout-element-size"></html:span>
-              <html:section id="layout-position-group">
-                <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
-                <html:span id="layout-element-position"></html:span>
-              </html:section>
-            </html:p>
+          <html:div id="layout-wrapper">
+            <html:div id="layout-container">
+              <html:p id="layout-header">
+                <html:span id="layout-element-size"></html:span>
+                <html:section id="layout-position-group">
+                  <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
+                  <html:span id="layout-element-position"></html:span>
+                </html:section>
+              </html:p>
 
-            <html:div id="layout-main">
-              <html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
-              <html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
-                <html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
-                <html:div id="layout-borders" data-box="border" title="&border.tooltip;">
-                  <html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
-                  <html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
-                    <html:div id="layout-content" data-box="content" title="&content.tooltip;">
+              <html:div id="layout-main">
+                <html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
+                <html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
+                  <html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
+                  <html:div id="layout-borders" data-box="border" title="&border.tooltip;">
+                    <html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
+                    <html:div id="layout-padding" data-box="padding" title="&padding.tooltip;">
+                      <html:div id="layout-content" data-box="content" title="&content.tooltip;">
+                      </html:div>
                     </html:div>
                   </html:div>
                 </html:div>
+
+                <html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
+                <html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
+                <html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
+                <html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
+
+                <html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
+                <html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
+                <html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
+                <html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
+
+                <html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
+                <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
+                <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
+                <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
+
+                <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
               </html:div>
 
-              <html:p class="layout-border layout-top"><html:span data-box="border" class="layout-editable" title="border-top"></html:span></html:p>
-              <html:p class="layout-border layout-right"><html:span data-box="border" class="layout-editable" title="border-right"></html:span></html:p>
-              <html:p class="layout-border layout-bottom"><html:span data-box="border" class="layout-editable" title="border-bottom"></html:span></html:p>
-              <html:p class="layout-border layout-left"><html:span data-box="border" class="layout-editable" title="border-left"></html:span></html:p>
-
-              <html:p class="layout-margin layout-top"><html:span data-box="margin" class="layout-editable" title="margin-top"></html:span></html:p>
-              <html:p class="layout-margin layout-right"><html:span data-box="margin" class="layout-editable" title="margin-right"></html:span></html:p>
-              <html:p class="layout-margin layout-bottom"><html:span data-box="margin" class="layout-editable" title="margin-bottom"></html:span></html:p>
-              <html:p class="layout-margin layout-left"><html:span data-box="margin" class="layout-editable" title="margin-left"></html:span></html:p>
-
-              <html:p class="layout-padding layout-top"><html:span data-box="padding" class="layout-editable" title="padding-top"></html:span></html:p>
-              <html:p class="layout-padding layout-right"><html:span data-box="padding" class="layout-editable" title="padding-right"></html:span></html:p>
-              <html:p class="layout-padding layout-bottom"><html:span data-box="padding" class="layout-editable" title="padding-bottom"></html:span></html:p>
-              <html:p class="layout-padding layout-left"><html:span data-box="padding" class="layout-editable" title="padding-left"></html:span></html:p>
-
-              <html:p class="layout-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
-            </html:div>
-
-            <html:div style="display: none">
-              <html:p id="layout-dummy"></html:p>
+              <html:div style="display: none">
+                <html:p id="layout-dummy"></html:p>
+              </html:div>
             </html:div>
           </html:div>
         </tabpanel>
       </tabpanels>
     </tabbox>
   </box>
 </window>
--- a/devtools/client/inspector/layout/test/browser.ini
+++ b/devtools/client/inspector/layout/test/browser.ini
@@ -10,16 +10,17 @@ support-files =
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_layout.js]
 [browser_layout_editablemodel.js]
 # [browser_layout_editablemodel_allproperties.js]
 # Disabled for too many intermittent failures (bug 1009322)
+[browser_layout_editablemodel_bluronclick.js]
 [browser_layout_editablemodel_border.js]
 [browser_layout_editablemodel_stylerules.js]
 [browser_layout_guides.js]
 [browser_layout_rotate-labels-on-sides.js]
 [browser_layout_tooltips.js]
 [browser_layout_update-after-navigation.js]
 [browser_layout_update-after-reload.js]
 # [browser_layout_update-in-iframes.js]
--- a/devtools/client/inspector/layout/test/browser_layout_editablemodel.js
+++ b/devtools/client/inspector/layout/test/browser_layout_editablemodel.js
@@ -8,32 +8,29 @@
 // key bindings
 
 const TEST_URI = "<style>" +
   "div { margin: 10px; padding: 3px }" +
   "#div1 { margin-top: 5px }" +
   "#div2 { border-bottom: 1em solid black; }" +
   "#div3 { padding: 2em; }" +
   "#div4 { margin: 1px; }" +
-  "#div5 { margin: 1px; }" +
   "</style>" +
   "<div id='div1'></div><div id='div2'></div>" +
-  "<div id='div3'></div><div id='div4'></div>" +
-  "<div id='div5'></div>";
+  "<div id='div3'></div><div id='div4'></div>";
 
 add_task(function* () {
   yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
   let {inspector, view, testActor} = yield openLayoutView();
 
   yield testEditingMargins(inspector, view, testActor);
   yield testKeyBindings(inspector, view, testActor);
   yield testEscapeToUndo(inspector, view, testActor);
   yield testDeletingValue(inspector, view, testActor);
   yield testRefocusingOnClick(inspector, view, testActor);
-  yield testBluringOnClick(inspector, view);
 });
 
 function* testEditingMargins(inspector, view, testActor) {
   info("Test that editing margin dynamically updates the document, pressing " +
        "escape cancels the changes");
 
   is((yield getStyle(testActor, "#div1", "margin-top")), "",
      "Should be no margin-top on the element.");
@@ -190,29 +187,8 @@ function* testRefocusingOnClick(inspecto
   is((yield getStyle(testActor, "#div4", "margin-top")), "2px",
      "Should have updated the margin.");
   EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
 
   is((yield getStyle(testActor, "#div4", "margin-top")), "2px",
      "Should be the right margin-top on the element.");
   is(span.textContent, 2, "Should have the right value in the box model.");
 }
-
-function* testBluringOnClick(inspector, view) {
-  info("Test that clicking outside the editor blurs it");
-
-  yield selectNode("#div5", inspector);
-
-  let span = view.doc.querySelector(".layout-margin.layout-top > span");
-  is(span.textContent, 1, "Should have the right value in the box model.");
-
-  EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
-  let editor = view.doc.querySelector(".styleinspector-propertyeditor");
-  ok(editor, "Should have opened the editor.");
-
-  info("Click next to the opened editor input.");
-  let rect = editor.getBoundingClientRect();
-  EventUtils.synthesizeMouse(editor, rect.width + 10, rect.height / 2, {},
-    view.doc.defaultView);
-
-  is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
-    "Inplace editor has been removed.");
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/test/browser_layout_editablemodel_bluronclick.js
@@ -0,0 +1,73 @@
+/* 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 that inplace editors can be blurred by clicking outside of the editor.
+
+const TEST_URI =
+  `<style>
+    #div1 {
+      margin: 10px;
+      padding: 3px;
+    }
+  </style>
+  <div id="div1"></div>`;
+
+add_task(function* () {
+  // Make sure the toolbox is tall enough to have empty space below the layout-container.
+  yield pushPref("devtools.toolbox.footer.height", 500);
+
+  yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openLayoutView();
+
+  yield selectNode("#div1", inspector);
+  yield testClickingOutsideEditor(view);
+  yield testClickingBelowContainer(view);
+});
+
+function* testClickingOutsideEditor(view) {
+  info("Test that clicking outside the editor blurs it");
+  let span = view.doc.querySelector(".layout-margin.layout-top > span");
+  is(span.textContent, 10, "Should have the right value in the box model.");
+
+  EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+  let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+
+  info("Click next to the opened editor input.");
+  let onBlur = once(editor, "blur");
+  let rect = editor.getBoundingClientRect();
+  EventUtils.synthesizeMouse(editor, rect.width + 10, rect.height / 2, {},
+    view.doc.defaultView);
+  yield onBlur;
+
+  is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
+    "Inplace editor has been removed.");
+}
+
+function* testClickingBelowContainer(view) {
+  info("Test that clicking below the box-model container blurs it");
+  let span = view.doc.querySelector(".layout-margin.layout-top > span");
+  is(span.textContent, 10, "Should have the right value in the box model.");
+
+  info("Test that clicking below the layout-container blurs the opened editor");
+  EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+  let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+  ok(editor, "Should have opened the editor.");
+
+  let onBlur = once(editor, "blur");
+  let container = view.doc.querySelector("#layout-container");
+  // Using getBoxQuads here because getBoundingClientRect (and therefore synthesizeMouse)
+  // use an erroneous height of ~50px for the layout container.
+  let bounds = container.getBoxQuads({relativeTo: view.doc})[0].bounds;
+  EventUtils.synthesizeMouseAtPoint(
+    bounds.left + 10,
+    bounds.top + bounds.height + 10,
+    {}, view.doc.defaultView);
+  yield onBlur;
+
+  is(view.doc.querySelector(".styleinspector-propertyeditor"), null,
+    "Inplace editor has been removed.");
+}
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -39,23 +39,22 @@ const {Tooltip} = require("devtools/clie
 const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
 const {setImageTooltip, setBrokenImageTooltip} =
       require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 
 const EventEmitter = require("devtools/shared/event-emitter");
 const Heritage = require("sdk/core/heritage");
 const {parseAttribute} =
       require("devtools/client/shared/node-attribute-parser");
-const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis",
-      Ci.nsIPrefLocalizedString).data;
 const {Task} = require("devtools/shared/task");
 const {scrollIntoViewIfNeeded} = require("devtools/shared/layout/utils");
 const {PrefObserver} = require("devtools/client/styleeditor/utils");
 const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
 const {template} = require("devtools/shared/gcli/templater");
+const nodeConstants = require("devtools/shared/dom-node-constants");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyRequireGetter(this, "CSS", "CSS");
 loader.lazyGetter(this, "DOMParser", () => {
   return Cc["@mozilla.org/xmlextras/domparser;1"]
     .createInstance(Ci.nsIDOMParser);
 });
@@ -214,17 +213,17 @@ MarkupView.prototype = {
       if (target.tagName.toLowerCase() === "body") {
         return;
       }
       target = target.parentNode;
     }
 
     let container = target.container;
     if (this._hoveredNode !== container.node) {
-      if (container.node.nodeType !== Ci.nsIDOMNode.TEXT_NODE) {
+      if (container.node.nodeType !== nodeConstants.TEXT_NODE) {
         this._showBoxModel(container.node);
       } else {
         this._hideBoxModel();
       }
     }
     this._showContainerAsHovered(container.node);
 
     this.emit("node-hover");
@@ -838,17 +837,17 @@ MarkupView.prototype = {
    *
    * @param  {NodeFront} node
    *         The node to remove.
    * @param  {Boolean} moveBackward
    *         If set to true, focus the previous sibling, otherwise the next one.
    */
   deleteNode: function (node, moveBackward) {
     if (node.isDocumentElement ||
-        node.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE ||
+        node.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
         node.isAnonymous) {
       return;
     }
 
     let container = this.getContainer(node);
 
     // Retain the node so we can undo this...
     this.walker.retainNode(node).then(() => {
@@ -865,16 +864,18 @@ MarkupView.prototype = {
             focusNode = nextSibling || siblings.previousSibling || parent;
           }
 
           if (container.selected) {
             this.navigate(this.getContainer(focusNode));
           }
         });
       }, () => {
+        let isValidSibling = nextSibling && !nextSibling.isPseudoElement;
+        nextSibling = isValidSibling ? nextSibling : null;
         this.walker.insertBefore(node, parent, nextSibling);
       });
     }).then(null, console.error);
   },
 
   /**
    * If an editable item is focused, select its container.
    */
@@ -923,20 +924,20 @@ MarkupView.prototype = {
     }
 
     let container;
     let {nodeType, isPseudoElement} = node;
     if (node === this.walker.rootNode) {
       container = new RootContainer(this, node);
       this._elt.appendChild(container.elt);
       this._rootNode = node;
-    } else if (nodeType == Ci.nsIDOMNode.ELEMENT_NODE && !isPseudoElement) {
+    } else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) {
       container = new MarkupElementContainer(this, node, this._inspector);
-    } else if (nodeType == Ci.nsIDOMNode.COMMENT_NODE ||
-               nodeType == Ci.nsIDOMNode.TEXT_NODE) {
+    } else if (nodeType == nodeConstants.COMMENT_NODE ||
+               nodeType == nodeConstants.TEXT_NODE) {
       container = new MarkupTextContainer(this, node, this._inspector);
     } else {
       container = new MarkupReadOnlyContainer(this, node, this._inspector);
     }
 
     if (flashNode) {
       container.flashMutation();
     }
@@ -984,16 +985,20 @@ MarkupView.prototype = {
         container.update();
       } else if (type === "childList" || type === "nativeAnonymousChildList") {
         container.childrenDirty = true;
         // Update the children to take care of changes in the markup view DOM
         // and update container (and its subtree) DOM tree depth level for
         // accessibility where necessary.
         this._updateChildren(container, {flash: true}).then(() =>
           container.updateLevel());
+      } else if (type === "inlineTextChild") {
+        container.childrenDirty = true;
+        this._updateChildren(container, {flash: true});
+        container.update();
       }
     }
 
     this._waitForChildren().then(() => {
       if (this._destroyer) {
         console.warn("Could not fully update after markup mutations, " +
           "the markup-view was destroyed while waiting for children.");
         return;
@@ -1544,39 +1549,39 @@ MarkupView.prototype = {
     if (this._queuedChildUpdates.has(container)) {
       return this._queuedChildUpdates.get(container);
     }
 
     if (!container.childrenDirty) {
       return promise.resolve(container);
     }
 
-    if (container.singleTextChild
-        && container.singleTextChild != container.node.singleTextChild) {
+    if (container.inlineTextChild
+        && container.inlineTextChild != container.node.inlineTextChild) {
       // This container was doing double duty as a container for a single
       // text child, back that out.
-      this._containers.delete(container.singleTextChild);
-      container.clearSingleTextChild();
+      this._containers.delete(container.inlineTextChild);
+      container.clearInlineTextChild();
 
       if (container.hasChildren && container.selected) {
         container.setExpanded(true);
       }
     }
 
-    if (container.node.singleTextChild) {
+    if (container.node.inlineTextChild) {
       container.setExpanded(false);
       // this container will do double duty as the container for the single
       // text child.
       while (container.children.firstChild) {
         container.children.removeChild(container.children.firstChild);
       }
 
-      container.setSingleTextChild(container.node.singleTextChild);
-
-      this._containers.set(container.node.singleTextChild, container);
+      container.setInlineTextChild(container.node.inlineTextChild);
+
+      this._containers.set(container.node.inlineTextChild, container);
       container.childrenDirty = false;
       return promise.resolve(container);
     }
 
     if (!container.hasChildren) {
       while (container.children.firstChild) {
         container.children.removeChild(container.children.firstChild);
       }
@@ -1819,17 +1824,17 @@ MarkupView.prototype = {
     if (nextSibling && nextSibling.isBeforePseudoElement) {
       nextSibling = target.parentNode.parentNode.children[1].container.node;
     }
     if (nextSibling && nextSibling.isAfterPseudoElement) {
       parent = target.parentNode.container.node.parentNode();
       nextSibling = null;
     }
 
-    if (parent.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+    if (parent.nodeType !== nodeConstants.ELEMENT_NODE) {
       return null;
     }
 
     return {parent, nextSibling};
   }
 };
 
 /**
@@ -2003,17 +2008,17 @@ MarkupContainer.prototype = {
       doc.activeElement.blur();
     }
   },
 
   /**
    * True if the current node can be expanded.
    */
   get canExpand() {
-    return this._hasChildren && !this.node.singleTextChild;
+    return this._hasChildren && !this.node.inlineTextChild;
   },
 
   /**
    * True if this is the root <html> element and can't be collapsed.
    */
   get mustExpand() {
     return this.node._parent === this.markup.walker.rootNode;
   },
@@ -2563,19 +2568,19 @@ MarkupReadOnlyContainer.prototype =
  *         The node to display.
  * @param  {Inspector} inspector
  *         The inspector tool container the markup-view
  */
 function MarkupTextContainer(markupView, node) {
   MarkupContainer.prototype.initialize.call(this, markupView, node,
     "textcontainer");
 
-  if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
+  if (node.nodeType == nodeConstants.TEXT_NODE) {
     this.editor = new TextEditor(this, node, "text");
-  } else if (node.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
+  } else if (node.nodeType == nodeConstants.COMMENT_NODE) {
     this.editor = new TextEditor(this, node, "comment");
   } else {
     throw new Error("Invalid node for MarkupTextContainer");
   }
 
   this.tagLine.appendChild(this.editor.elt);
 }
 
@@ -2590,17 +2595,17 @@ MarkupTextContainer.prototype = Heritage
  *         The markup view that owns this container.
  * @param  {NodeFront} node
  *         The node to display.
  */
 function MarkupElementContainer(markupView, node) {
   MarkupContainer.prototype.initialize.call(this, markupView, node,
     "elementcontainer");
 
-  if (node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE) {
+  if (node.nodeType === nodeConstants.ELEMENT_NODE) {
     this.editor = new ElementEditor(this, node);
   } else {
     throw new Error("Invalid node for MarkupElementContainer");
   }
 
   this.tagLine.appendChild(this.editor.elt);
 }
 
@@ -2712,23 +2717,23 @@ MarkupElementContainer.prototype = Herit
     // for the tooltip, because we want the full-size image
     this.node.getImageData().then(data => {
       data.data.string().then(str => {
         clipboardHelper.copyString(str);
       });
     });
   },
 
-  setSingleTextChild: function (singleTextChild) {
-    this.singleTextChild = singleTextChild;
+  setInlineTextChild: function (inlineTextChild) {
+    this.inlineTextChild = inlineTextChild;
     this.editor.updateTextEditor();
   },
 
-  clearSingleTextChild: function () {
-    this.singleTextChild = undefined;
+  clearInlineTextChild: function () {
+    this.inlineTextChild = undefined;
     this.editor.updateTextEditor();
   },
 
   /**
    * Trigger new attribute field for input.
    */
   addAttribute: function () {
     this.editor.newAttr.editMode();
@@ -2813,17 +2818,17 @@ function GenericEditor(container, node) 
   this.markup = this.container.markup;
   this.template = this.markup.template.bind(this.markup);
   this.elt = null;
   this.template("generic", this);
 
   if (node.isPseudoElement) {
     this.tag.classList.add("theme-fg-color5");
     this.tag.textContent = node.isBeforePseudoElement ? "::before" : "::after";
-  } else if (node.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
+  } else if (node.nodeType == nodeConstants.DOCUMENT_TYPE_NODE) {
     this.elt.classList.add("comment");
     this.tag.textContent = node.doctypeString;
   } else {
     this.tag.textContent = node.nodeName;
   }
 }
 
 GenericEditor.prototype = {
@@ -2900,35 +2905,24 @@ TextEditor.prototype = {
     if (value === this._selected) {
       return;
     }
     this._selected = value;
     this.update();
   },
 
   update: function () {
-    if (!this.selected || !this.node.incompleteValue) {
-      let text = this.node.shortValue;
-      if (this.node.incompleteValue) {
-        text += ELLIPSIS;
-      }
-      this.value.textContent = text;
-    } else {
-      let longstr = null;
-      this.node.getNodeValue().then(ret => {
-        longstr = ret;
-        return longstr.string();
-      }).then(str => {
-        longstr.release().then(null, console.error);
-        if (this.selected) {
-          this.value.textContent = str;
-          this.markup.emit("text-expand");
-        }
-      }).then(null, console.error);
-    }
+    let longstr = null;
+    this.node.getNodeValue().then(ret => {
+      longstr = ret;
+      return longstr.string();
+    }).then(str => {
+      longstr.release().then(null, console.error);
+      this.value.textContent = str;
+    }).then(null, console.error);
   },
 
   destroy: function () {},
 
   /**
    * Stub method for consistency with ElementEditor.
    */
   getInfoAtNode: function () {
@@ -3109,17 +3103,17 @@ ElementEditor.prototype = {
 
     this.updateTextEditor();
   },
 
   /**
    * Update the inline text editor in case of a single text child node.
    */
   updateTextEditor: function () {
-    let node = this.node.singleTextChild;
+    let node = this.node.inlineTextChild;
 
     if (this.textEditor && this.textEditor.node != node) {
       this.elt.removeChild(this.textEditor.elt);
       this.textEditor = null;
     }
 
     if (node && !this.textEditor) {
       // Create a text editor added to this editor.
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -59,16 +59,17 @@ skip-if = os == "mac" # Full keyboard na
 [browser_markup_anonymous_02.js]
 skip-if = e10s # scratchpad.xul is not loading in e10s window
 [browser_markup_anonymous_03.js]
 [browser_markup_anonymous_04.js]
 [browser_markup_copy_image_data.js]
 subsuite = clipboard
 [browser_markup_css_completion_style_attribute_01.js]
 [browser_markup_css_completion_style_attribute_02.js]
+[browser_markup_css_completion_style_attribute_03.js]
 [browser_markup_dragdrop_autoscroll.js]
 [browser_markup_dragdrop_distance.js]
 [browser_markup_dragdrop_draggable.js]
 [browser_markup_dragdrop_dragRootNode.js]
 [browser_markup_dragdrop_escapeKeyPress.js]
 [browser_markup_dragdrop_invalidNodes.js]
 [browser_markup_dragdrop_reorder.js]
 [browser_markup_dragdrop_tooltip.js]
@@ -127,16 +128,17 @@ skip-if = e10s # Bug 1036409 - The last 
 [browser_markup_tag_edit_06.js]
 [browser_markup_tag_edit_07.js]
 [browser_markup_tag_edit_08.js]
 [browser_markup_tag_edit_09.js]
 [browser_markup_tag_edit_10.js]
 [browser_markup_tag_edit_11.js]
 [browser_markup_tag_edit_12.js]
 [browser_markup_tag_edit_13-other.js]
+[browser_markup_textcontent_display.js]
 [browser_markup_textcontent_edit_01.js]
 [browser_markup_textcontent_edit_02.js]
 [browser_markup_toggle_01.js]
 [browser_markup_toggle_02.js]
 [browser_markup_toggle_03.js]
 [browser_markup_update-on-navigtion.js]
 [browser_markup_void_elements_html.js]
 [browser_markup_void_elements_xhtml.js]
--- a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js
+++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_01.js
@@ -65,13 +65,12 @@ const TEST_DATA = [
    45, 55, false],
   ["VK_RIGHT", "style=\"display:  inherit; color :chartreuse !important; ",
    55, 55, false],
   ["VK_RETURN", "style=\"display:  inherit; color :chartreuse !important;\"",
    -1, -1, false]
 ];
 
 add_task(function* () {
-  info("Opening the inspector on the test URL");
   let {inspector} = yield openInspectorForURL(TEST_URL);
 
   yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA);
 });
--- a/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js
+++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_02.js
@@ -93,15 +93,14 @@ const TEST_DATA_INNER = [
   ["c", "style=\"background:url('1'); color", 29, 33, true],
   ["VK_RIGHT", "style=\"background:url('1'); color", 33, 33, false],
   [":", "style=\"background:url('1'); color:aliceblue", 34, 43, true],
   ["b", "style=\"background:url('1'); color:beige", 35, 39, true],
   ["VK_RETURN", "style=\"background:url('1'); color:beige\"", -1, -1, false]
 ];
 
 add_task(function* () {
-  info("Opening the inspector on the test URL");
   let {inspector} = yield openInspectorForURL(TEST_URL);
 
   yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_DOUBLE);
   yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_SINGLE);
   yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA_INNER);
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_css_completion_style_attribute_03.js
@@ -0,0 +1,54 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_style_attr_test_runner.js */
+
+"use strict";
+
+// Test CSS autocompletion of the style attribute can be triggered when the
+// caret is before a non-word character.
+
+loadHelperScript("helper_style_attr_test_runner.js");
+
+const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+
+// test data format :
+//  [
+//    what key to press,
+//    expected input box value after keypress,
+//    expected input.selectionStart,
+//    expected input.selectionEnd,
+//    is popup expected to be open ?
+//  ]
+const TEST_DATA = [
+  ["s", "s", 1, 1, false],
+  ["t", "st", 2, 2, false],
+  ["y", "sty", 3, 3, false],
+  ["l", "styl", 4, 4, false],
+  ["e", "style", 5, 5, false],
+  ["=", "style=", 6, 6, false],
+  ["\"", "style=\"", 7, 7, false],
+  ["\"", "style=\"\"", 8, 8, false],
+  ["VK_LEFT", "style=\"\"", 7, 7, false],
+  ["c", "style=\"color\"", 8, 12, true],
+  ["o", "style=\"color\"", 9, 12, true],
+  ["VK_RIGHT", "style=\"color\"", 12, 12, false],
+  [":", "style=\"color:aliceblue\"", 13, 22, true],
+  ["b", "style=\"color:beige\"", 14, 18, true],
+  ["VK_RIGHT", "style=\"color:beige\"", 18, 18, false],
+  [";", "style=\"color:beige;\"", 19, 19, false],
+  [";", "style=\"color:beige;;\"", 20, 20, false],
+  ["VK_LEFT", "style=\"color:beige;;\"", 19, 19, false],
+  ["p", "style=\"color:beige;padding;\"", 20, 26, true],
+  ["VK_RIGHT", "style=\"color:beige;padding;\"", 26, 26, false],
+  [":", "style=\"color:beige;padding:calc;\"", 27, 31, true],
+  ["0", "style=\"color:beige;padding:0;\"", 28, 28, false],
+  ["VK_RETURN", "style=\"color:beige;padding:0;\"",
+   -1, -1, false]
+];
+
+add_task(function* () {
+  let {inspector} = yield openInspectorForURL(TEST_URL);
+
+  yield runStyleAttributeAutocompleteTests(inspector, TEST_DATA);
+});
--- a/devtools/client/inspector/markup/test/browser_markup_mutation_01.js
+++ b/devtools/client/inspector/markup/test/browser_markup_mutation_01.js
@@ -84,87 +84,87 @@ const TEST_DATA = [
     test: function* (testActor) {
       yield testActor.eval(`
         let node1 = content.document.querySelector("#node1");
         node1.classList.remove("pseudo");
       `);
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(container.singleTextChild, "Has single text child.");
+      ok(container.inlineTextChild, "Has single text child.");
     }
   },
   {
     desc: "Updating the text-content",
     test: function* (testActor) {
       yield testActor.setProperty("#node1", "textContent", "newtext");
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(container.singleTextChild, "Has single text child.");
-      ok(!container.canExpand, "Can't expand container with singleTextChild.");
-      ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild.");
+      ok(container.inlineTextChild, "Has single text child.");
+      ok(!container.canExpand, "Can't expand container with inlineTextChild.");
+      ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
       is(container.editor.elt.querySelector(".text").textContent.trim(),
          "newtext", "Single text child editor updated.");
     }
   },
   {
     desc: "Adding a second text child",
     test: function* (testActor) {
       yield testActor.eval(`
         let node1 = content.document.querySelector("#node1");
         let newText = node1.ownerDocument.createTextNode("more");
         node1.appendChild(newText);
       `);
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(!container.singleTextChild, "Does not have single text child.");
+      ok(!container.inlineTextChild, "Does not have single text child.");
       ok(container.canExpand, "Can expand container with child nodes.");
       ok(container.editor.elt.querySelector(".text") == null,
         "Single text child editor removed.");
     },
   },
   {
     desc: "Go from 2 to 1 text child",
     test: function* (testActor) {
       yield testActor.setProperty("#node1", "textContent", "newtext");
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(container.singleTextChild, "Has single text child.");
-      ok(!container.canExpand, "Can't expand container with singleTextChild.");
-      ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild.");
+      ok(container.inlineTextChild, "Has single text child.");
+      ok(!container.canExpand, "Can't expand container with inlineTextChild.");
+      ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
       ok(container.editor.elt.querySelector(".text").textContent.trim(),
          "newtext", "Single text child editor updated.");
     },
   },
   {
     desc: "Removing an only text child",
     test: function* (testActor) {
       yield testActor.setProperty("#node1", "innerHTML", "");
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(!container.singleTextChild, "Does not have single text child.");
+      ok(!container.inlineTextChild, "Does not have single text child.");
       ok(!container.canExpand, "Can't expand empty container.");
       ok(container.editor.elt.querySelector(".text") == null,
         "Single text child editor removed.");
     },
   },
   {
     desc: "Go from 0 to 1 text child",
     test: function* (testActor) {
       yield testActor.setProperty("#node1", "textContent", "newtext");
     },
     check: function* (inspector) {
       let container = yield getContainerForSelector("#node1", inspector);
-      ok(container.singleTextChild, "Has single text child.");
-      ok(!container.canExpand, "Can't expand container with singleTextChild.");
-      ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild.");
+      ok(container.inlineTextChild, "Has single text child.");
+      ok(!container.canExpand, "Can't expand container with inlineTextChild.");
+      ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
       ok(container.editor.elt.querySelector(".text").textContent.trim(),
          "newtext", "Single text child editor updated.");
     },
   },
 
   {
     desc: "Updating the innerHTML",
     test: function* (testActor) {
--- a/devtools/client/inspector/markup/test/browser_markup_tag_edit_04.js
+++ b/devtools/client/inspector/markup/test/browser_markup_tag_edit_04.js
@@ -3,101 +3,115 @@
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests that a node can be deleted from the markup-view with the delete key.
 // Also checks that after deletion the correct element is highlighted.
 // The next sibling is preferred, but the parent is a fallback.
 
-const HTML = `<div id="parent">
+const HTML = `<style type="text/css">
+                #pseudo::before { content: 'before'; }
+                #pseudo::after { content: 'after'; }
+              </style>
+              <div id="parent">
                 <div id="first"></div>
                 <div id="second"></div>
                 <div id="third"></div>
+              </div>
+              <div id="only-child">
+                <div id="fourth"></div>
+              </div>
+              <div id="pseudo">
+                <div id="fifth"></div>
               </div>`;
 const TEST_URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML);
 
 // List of all the test cases. Each item is an object with the following props:
 // - selector: the css selector of the node that should be selected
 // - key: the key to press to delete the node (delete or back_space)
 // - focusedSelector: the css selector of the node we expect to be selected as
 //   a result of the deletion
-// - setup: an optional function that will be run before selecting and deleting
-//   the node
+// - pseudo: (optional) if the focused node is actually supposed to be a pseudo element
+//   of the specified selector.
 // Note that after each test case, undo is called.
 const TEST_DATA = [{
   selector: "#first",
   key: "delete",
   focusedSelector: "#second"
 }, {
   selector: "#second",
   key: "delete",
   focusedSelector: "#third"
 }, {
   selector: "#third",
   key: "delete",
   focusedSelector: "#second"
 }, {
+  selector: "#fourth",
+  key: "delete",
+  focusedSelector: "#only-child"
+}, {
+  selector: "#fifth",
+  key: "delete",
+  focusedSelector: "#pseudo",
+  pseudo: "after"
+}, {
   selector: "#first",
   key: "back_space",
   focusedSelector: "#second"
 }, {
   selector: "#second",
   key: "back_space",
   focusedSelector: "#first"
 }, {
   selector: "#third",
   key: "back_space",
   focusedSelector: "#second"
 }, {
-  setup: function* (inspector, testActor) {
-    // Removing the siblings of #first in order to test with an only child.
-    let mutated = inspector.once("markupmutation");
-    yield testActor.eval(`
-      for (let node of content.document.querySelectorAll("#second, #third")) {
-        node.remove();
-      }
-    `);
-    yield mutated;
-  },
-  selector: "#first",
-  key: "delete",
-  focusedSelector: "#parent"
+  selector: "#fourth",
+  key: "back_space",
+  focusedSelector: "#only-child"
 }, {
-  selector: "#first",
+  selector: "#fifth",
   key: "back_space",
-  focusedSelector: "#parent"
+  focusedSelector: "#pseudo",
+  pseudo: "before"
 }];
 
 add_task(function* () {
-  let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+  let {inspector} = yield openInspectorForURL(TEST_URL);
 
-  for (let {setup, selector, key, focusedSelector} of TEST_DATA) {
-    if (setup) {
-      yield setup(inspector, testActor);
-    }
-
-    yield checkDeleteAndSelection(inspector, key, selector, focusedSelector);
+  for (let data of TEST_DATA) {
+    yield checkDeleteAndSelection(inspector, data);
   }
 });
 
-function* checkDeleteAndSelection(inspector, key, selector, focusedSelector) {
+function* checkDeleteAndSelection(inspector, {key, selector, focusedSelector, pseudo}) {
   info("Test deleting node " + selector + " with " + key + ", " +
        "expecting " + focusedSelector + " to be focused");
 
   info("Select node " + selector + " and make sure it is focused");
   yield selectNode(selector, inspector);
   yield clickContainer(selector, inspector);
 
   info("Delete the node with: " + key);
   let mutated = inspector.once("markupmutation");
   EventUtils.sendKey(key, inspector.panelWin);
   yield Promise.all([mutated, inspector.once("inspector-updated")]);
 
   let nodeFront = yield getNodeFront(focusedSelector, inspector);
+  if (pseudo) {
+    // Update the selector for logging in case of failure.
+    focusedSelector = focusedSelector + "::" + pseudo;
+    // Retrieve the :before or :after pseudo element of the nodeFront.
+    let {nodes} = yield inspector.walker.children(nodeFront);
+    nodeFront = pseudo === "before" ? nodes[0] : nodes[nodes.length - 1];
+  }
+
   is(inspector.selection.nodeFront, nodeFront,
      focusedSelector + " is selected after deletion");
 
   info("Check that the node was really removed");
   let node = yield getNodeFront(selector, inspector);
   ok(!node, "The node can't be found in the page anymore");
 
   info("Undo the deletion to restore the original markup");
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_display.js
@@ -0,0 +1,89 @@
+/* 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 the rendering of text nodes in the markup view.
+
+const LONG_VALUE = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do " +
+  "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.";
+const SCHEMA = "data:text/html;charset=UTF-8,";
+const TEST_URL = `${SCHEMA}<!DOCTYPE html>
+  <html>
+  <body>
+    <div id="shorttext">Short text</div>
+    <div id="longtext">${LONG_VALUE}</div>
+    <div id="shortcomment"><!--Short comment--></div>
+    <div id="longcomment"><!--${LONG_VALUE}--></div>
+    <div id="shorttext-and-node">Short text<span>Other element</span></div>
+    <div id="longtext-and-node">${LONG_VALUE}<span>Other element</span></div>
+  </body>
+  </html>`;
+
+const TEST_DATA = [{
+  desc: "Test node containing a short text, short text nodes can be inlined.",
+  selector: "#shorttext",
+  inline: true,
+  value: "Short text",
+}, {
+  desc: "Test node containing a long text, long text nodes are not inlined.",
+  selector: "#longtext",
+  inline: false,
+  value: LONG_VALUE,
+}, {
+  desc: "Test node containing a short comment, comments are not inlined.",
+  selector: "#shortcomment",
+  inline: false,
+  value: "Short comment",
+}, {
+  desc: "Test node containing a long comment, comments are not inlined.",
+  selector: "#longcomment",
+  inline: false,
+  value: LONG_VALUE,
+}, {
+  desc: "Test node containing a short text and a span.",
+  selector: "#shorttext-and-node",
+  inline: false,
+  value: "Short text",
+}, {
+  desc: "Test node containing a long text and a span.",
+  selector: "#longtext-and-node",
+  inline: false,
+  value: LONG_VALUE,
+}];
+
+add_task(function* () {
+  let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
+
+  for (let data of TEST_DATA) {
+    yield checkNode(inspector, testActor, data);
+  }
+});
+
+function* checkNode(inspector, testActor, {desc, selector, inline, value}) {
+  info(desc);
+
+  let container = yield getContainerForSelector(selector, inspector);
+  let nodeValue = yield getFirstChildNodeValue(selector, testActor);
+  is(nodeValue, value, "The test node's text content is correct");
+
+  is(!!container.inlineTextChild, inline, "Container inlineTextChild is as expected");
+  is(!container.canExpand, inline, "Container canExpand property is as expected");
+
+  let textContainer;
+  if (inline) {
+    textContainer = container.elt.querySelector("pre");
+    ok(!!textContainer, "Text container is already rendered for inline text elements");
+  } else {
+    textContainer = container.elt.querySelector("pre");
+    ok(!textContainer, "Text container is not rendered for collapsed text nodes");
+    yield inspector.markup.expandNode(container.node);
+    yield waitForMultipleChildrenUpdates(inspector);
+
+    textContainer = container.elt.querySelector("pre");
+    ok(!!textContainer, "Text container is rendered after expanding the container");
+  }
+
+  is(textContainer.textContent, value, "The complete text node is rendered.");
+}
--- a/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js
@@ -2,16 +2,17 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test editing a node's text content
 
 const TEST_URL = URL_ROOT + "doc_markup_edit.html";
+const {DEFAULT_VALUE_SUMMARY_LENGTH} = require("devtools/server/actors/inspector");
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
 
   info("Expanding all nodes");
   yield inspector.markup.expandAll();
   yield waitForMultipleChildrenUpdates(inspector);
 
@@ -21,71 +22,63 @@ add_task(function* () {
     oldValue: "line6"
   });
 
   yield editContainer(inspector, testActor, {
     selector: "#node17",
     newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
               "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
     oldValue: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
-              "Donec posuere placerat magna et imperdiet.",
-    shortValue: true
+              "Donec posuere placerat magna et imperdiet."
   });
 
   yield editContainer(inspector, testActor, {
     selector: "#node17",
     newValue: "New value",
     oldValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
+              "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET."
+  });
+
+  yield editContainer(inspector, testActor, {
+    selector: "#node17",
+    newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
               "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
-    shortValue: true
+    oldValue: "New value"
   });
 });
 
-function* getNodeValue(selector, testActor) {
-  let nodeValue = yield testActor.eval(`
-    content.document.querySelector("${selector}").firstChild.nodeValue;
-  `);
-  return nodeValue;
-}
-
 function* editContainer(inspector, testActor,
-                        {selector, newValue, oldValue, shortValue}) {
-  let nodeValue = yield getNodeValue(selector, testActor);
+                        {selector, newValue, oldValue}) {
+  let nodeValue = yield getFirstChildNodeValue(selector, testActor);
   is(nodeValue, oldValue, "The test node's text content is correct");
 
   info("Changing the text content");
   let onMutated = inspector.once("markupmutation");
   let container = yield focusNode(selector, inspector);
-  let field = container.elt.querySelector("pre");
 
-  if (shortValue) {
-    is(oldValue.indexOf(
-       field.textContent.substring(0, field.textContent.length - 1)),
-       0,
-       "The shortened value starts with the full value " + field.textContent);
-    ok(oldValue.length > field.textContent.length,
-       "The shortened value is short");
-  } else {
-    is(field.textContent, oldValue,
-       "The text node has the correct original value");
-  }
+  let isOldValueInline = oldValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH;
+  is(!!container.inlineTextChild, isOldValueInline, "inlineTextChild is as expected");
+  is(!container.canExpand, isOldValueInline, "canExpand property is as expected");
 
-  inspector.markup.markNodeAsSelected(container.node);
-
-  if (shortValue) {
-    info("Waiting for the text to be updated");
-    yield inspector.markup.once("text-expand");
-  }
-
+  let field = container.elt.querySelector("pre");
   is(field.textContent, oldValue,
      "The text node has the correct original value after selecting");
   setEditableFieldValue(field, newValue, inspector);
 
   info("Listening to the markupmutation event");
   yield onMutated;
 
-  nodeValue = yield getNodeValue(selector, testActor);
+  nodeValue = yield getFirstChildNodeValue(selector, testActor);
   is(nodeValue, newValue, "The test node's text content has changed");
 
+  let isNewValueInline = newValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH;
+  is(!!container.inlineTextChild, isNewValueInline, "inlineTextChild is as expected");
+  is(!container.canExpand, isNewValueInline, "canExpand property is as expected");
+
+  if (isOldValueInline != isNewValueInline) {
+    is(container.expanded, !isNewValueInline,
+      "Container was automatically expanded/collapsed");
+  }
+
   info("Selecting the <body> to reset the selection");
   let bodyContainer = yield getContainerForSelector("body", inspector);
   inspector.markup.markNodeAsSelected(bodyContainer.node);
 }
--- a/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_02.js
@@ -12,17 +12,17 @@ const SELECTOR = ".node6";
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
 
   info("Expanding all nodes");
   yield inspector.markup.expandAll();
   yield waitForMultipleChildrenUpdates(inspector);
 
-  let nodeValue = yield getNodeValue(SELECTOR, testActor);
+  let nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor);
   let expectedValue = "line6";
   is(nodeValue, expectedValue, "The test node's text content is correct");
 
   info("Open editable field for .node6");
   let container = yield focusNode(SELECTOR, inspector);
   let field = container.elt.querySelector("pre");
   field.focus();
   EventUtils.sendKey("return", inspector.panelWin);
@@ -79,27 +79,20 @@ add_task(function* () {
   info("Caret should be back on the first line");
   checkSelectionPositions(editor, 1, 1);
 
   info("Commit the new value with RETURN, wait for the markupmutation event");
   let onMutated = inspector.once("markupmutation");
   yield sendKey("VK_RETURN", {}, editor, inspector.panelWin);
   yield onMutated;
 
-  nodeValue = yield getNodeValue(SELECTOR, testActor);
+  nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor);
   is(nodeValue, expectedValue, "The test node's text content is correct");
 });
 
-function* getNodeValue(selector, testActor) {
-  let nodeValue = yield testActor.eval(`
-    content.document.querySelector("${selector}").firstChild.nodeValue;
-  `);
-  return nodeValue;
-}
-
 /**
  * Check that the editor selection is at the expected positions.
  */
 function checkSelectionPositions(editor, expectedStart, expectedEnd) {
   is(editor.input.selectionStart, expectedStart,
     "Selection should start at " + expectedStart);
   is(editor.input.selectionEnd, expectedEnd,
     "Selection should end at " + expectedEnd);
--- a/devtools/client/inspector/markup/test/head.js
+++ b/devtools/client/inspector/markup/test/head.js
@@ -7,17 +7,17 @@
 
 // Import the inspector's head.js first (which itself imports shared-head.js).
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
 var {getInplaceEditorForSpan: inplaceEditor} = require("devtools/client/shared/inplace-editor");
 var clipboard = require("sdk/clipboard");
-var {ActorRegistryFront} = require("devtools/server/actors/actor-registry");
+var {ActorRegistryFront} = require("devtools/shared/fronts/actor-registry");
 
 // If a test times out we want to see the complete log and not just the last few
 // lines.
 SimpleTest.requestCompleteLog();
 
 // Set the testing flag on DevToolsUtils and reset it when the test ends
 DevToolsUtils.testing = true;
 registerCleanupFunction(() => {
@@ -85,16 +85,30 @@ var getContainerForSelector = Task.async
   info("Getting the markup-container for node " + selector);
   let nodeFront = yield getNodeFront(selector, inspector);
   let container = getContainerForNodeFront(nodeFront, inspector);
   info("Found markup-container " + container);
   return container;
 });
 
 /**
+ * Retrieve the nodeValue for the firstChild of a provided selector on the content page.
+ *
+ * @param {String} selector
+ * @param {TestActorFront} testActor The current TestActorFront instance.
+ * @return {String} the nodeValue of the first
+ */
+function* getFirstChildNodeValue(selector, testActor) {
+  let nodeValue = yield testActor.eval(`
+    content.document.querySelector("${selector}").firstChild.nodeValue;
+  `);
+  return nodeValue;
+}
+
+/**
  * Using the markupview's _waitForChildren function, wait for all queued
  * children updates to be handled.
  * @param {InspectorPanel} inspector The instance of InspectorPanel currently
  * loaded in the toolbox
  * @return a promise that resolves when all queued children updates have been
  * handled
  */
 function waitForChildrenUpdated({markup}) {
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -17,20 +17,16 @@ const {parseDeclarations} = require("dev
 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "osString", function () {
   return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
 });
 
-XPCOMUtils.defineLazyGetter(this, "domUtils", function () {
-  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
-});
-
 /**
  * Rule is responsible for the following:
  *   Manages a single style declaration or rule.
  *   Applies changes to the properties in a rule.
  *   Maintains a list of TextProperty objects.
  *
  * @param {ElementStyle} elementStyle
  *        The ElementStyle to which this rule belongs.
@@ -444,34 +440,34 @@ Rule.prototype = {
    * to parse the style's authoredText.
    */
   _getTextProperties: function () {
     let textProps = [];
     let store = this.elementStyle.store;
 
     // Starting with FF49, StyleRuleActors provide parsed declarations.
     let props = this.style.declarations;
-    if (!props) {
+    if (!props.length) {
       props = parseDeclarations(this.cssProperties.isKnown,
                                 this.style.authoredText, true);
     }
 
     for (let prop of props) {
       let name = prop.name;
       // If the authored text has an invalid property, it will show up
       // as nameless.  Skip these as we don't currently have a good
       // way to display them.
       if (!name) {
         continue;
       }
       // In an inherited rule, we only show inherited properties.
       // However, we must keep all properties in order for rule
       // rewriting to work properly.  So, compute the "invisible"
       // property here.
-      let invisible = this.inherited && !domUtils.isInheritedProperty(name);
+      let invisible = this.inherited && !this.cssProperties.isInherited(name);
       let value = store.userProperties.getProperty(this.style, name,
                                                    prop.value);
       let textProp = new TextProperty(this, name, value, prop.priority,
                                       !("commentOffsets" in prop),
                                       invisible);
       textProps.push(textProp);
     }
 
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -28,16 +28,18 @@ const {createChild, promiseWarn} =
 const {gDevTools} = require("devtools/client/framework/devtools");
 
 loader.lazyRequireGetter(this, "overlays",
   "devtools/client/inspector/shared/style-inspector-overlays");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/shared/event-emitter");
 loader.lazyRequireGetter(this, "StyleInspectorMenu",
   "devtools/client/inspector/shared/style-inspector-menu");
+loader.lazyRequireGetter(this, "KeyShortcuts",
+  "devtools/client/shared/key-shortcuts", true);
 
 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function () {
   return Cc["@mozilla.org/widget/clipboardhelper;1"]
     .getService(Ci.nsIClipboardHelper);
 });
 
 XPCOMUtils.defineLazyGetter(this, "_strings", function () {
   return Services.strings.createBundle(
@@ -156,23 +158,20 @@ function CssRuleView(inspector, document
   this.inspector = inspector;
   this.styleDocument = document;
   this.styleWindow = this.styleDocument.defaultView;
   this.store = store || {};
   this.pageStyle = pageStyle;
 
   this._outputParser = new OutputParser(document);
 
-  this._onKeydown = this._onKeydown.bind(this);
-  this._onKeypress = this._onKeypress.bind(this);
   this._onAddRule = this._onAddRule.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onFilterStyles = this._onFilterStyles.bind(this);
-  this._onFilterKeyPress = this._onFilterKeyPress.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
   this._onFilterTextboxContextMenu =
     this._onFilterTextboxContextMenu.bind(this);
   this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this);
   this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
 
   let doc = this.styleDocument;
   this.element = doc.getElementById("ruleview-container");
@@ -182,23 +181,26 @@ function CssRuleView(inspector, document
   this.pseudoClassPanel = doc.getElementById("pseudo-class-panel");
   this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle");
   this.hoverCheckbox = doc.getElementById("pseudo-hover-toggle");
   this.activeCheckbox = doc.getElementById("pseudo-active-toggle");
   this.focusCheckbox = doc.getElementById("pseudo-focus-toggle");
 
   this.searchClearButton.hidden = true;
 
-  this.styleDocument.addEventListener("keydown", this._onKeydown);
-  this.styleDocument.addEventListener("keypress", this._onKeypress);
+  this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
+  this._onShortcut = this._onShortcut.bind(this);
+  this.shortcuts.on("Escape", this._onShortcut);
+  this.shortcuts.on("Return", this._onShortcut);
+  this.shortcuts.on("Space", this._onShortcut);
+  this.shortcuts.on("CmdOrCtrl+F", this._onShortcut);
   this.element.addEventListener("copy", this._onCopy);
   this.element.addEventListener("contextmenu", this._onContextMenu);
   this.addRuleButton.addEventListener("click", this._onAddRule);
   this.searchField.addEventListener("input", this._onFilterStyles);
-  this.searchField.addEventListener("keypress", this._onFilterKeyPress);
   this.searchField.addEventListener("contextmenu",
                                     this._onFilterTextboxContextMenu);
   this.searchClearButton.addEventListener("click", this._onClearSearch);
   this.pseudoClassToggle.addEventListener("click",
                                           this._onTogglePseudoClassPanel);
   this.hoverCheckbox.addEventListener("click", this._onTogglePseudoClass);
   this.activeCheckbox.addEventListener("click", this._onTogglePseudoClass);
   this.focusCheckbox.addEventListener("click", this._onTogglePseudoClass);
@@ -699,28 +701,16 @@ CssRuleView.prototype = {
 
       this.inspector.emit("ruleview-filtered");
 
       this._filterChangeTimeout = null;
     }, filterTimeout);
   },
 
   /**
-   * Handle the search box's keypress event. If the escape key is pressed,
-   * clear the search box field.
-   */
-  _onFilterKeyPress: function (event) {
-    if (event.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE &&
-        this._onClearSearch()) {
-      event.preventDefault();
-      event.stopPropagation();
-    }
-  },
-
-  /**
    * Context menu handler for filter style search box.
    */
   _onFilterTextboxContextMenu: function (event) {
     try {
       this.styleWindow.focus();
       let contextmenu = this.inspector.toolbox.textboxContextMenuPopup;
       contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
     } catch (e) {
@@ -761,23 +751,21 @@ CssRuleView.prototype = {
       this._contextmenu.destroy();
       this._contextmenu = null;
     }
 
     this.tooltips.destroy();
     this.highlighters.destroy();
 
     // Remove bound listeners
-    this.styleDocument.removeEventListener("keydown", this._onKeydown);
-    this.styleDocument.removeEventListener("keypress", this._onKeypress);
+    this.shortcuts.destroy();
     this.element.removeEventListener("copy", this._onCopy);
     this.element.removeEventListener("contextmenu", this._onContextMenu);
     this.addRuleButton.removeEventListener("click", this._onAddRule);
     this.searchField.removeEventListener("input", this._onFilterStyles);
-    this.searchField.removeEventListener("keypress", this._onFilterKeyPress);
     this.searchField.removeEventListener("contextmenu",
       this._onFilterTextboxContextMenu);
     this.searchClearButton.removeEventListener("click", this._onClearSearch);
     this.pseudoClassToggle.removeEventListener("click",
       this._onTogglePseudoClassPanel);
     this.hoverCheckbox.removeEventListener("click", this._onTogglePseudoClass);
     this.activeCheckbox.removeEventListener("click", this._onTogglePseudoClass);
     this.focusCheckbox.removeEventListener("click", this._onTogglePseudoClass);
@@ -1485,42 +1473,39 @@ CssRuleView.prototype = {
    * the pseudo class for the current selected element.
    */
   _onTogglePseudoClass: function (event) {
     let target = event.currentTarget;
     this.inspector.togglePseudoClass(target.value);
   },
 
   /**
-   * Handle the keydown event in the rule view.
-   */
-  _onKeydown: function (event) {
-    if (this.element.classList.contains("non-interactive") &&
-        (event.code === "Enter" || event.code === " ")) {
-      event.preventDefault();
-    }
-  },
-
-  /**
    * Handle the keypress event in the rule view.
    */
-  _onKeypress: function (event) {
+  _onShortcut: function (name, event) {
     if (!event.target.closest("#sidebar-panel-ruleview")) {
       return;
     }
 
-    let isOSX = Services.appinfo.OS === "Darwin";
-
-    if (((isOSX && event.metaKey && !event.ctrlKey && !event.altKey) ||
-        (!isOSX && event.ctrlKey && !event.metaKey && !event.altKey)) &&
-        event.key === "f") {
+    if (name === "CmdOrCtrl+F") {
       this.searchField.focus();
       event.preventDefault();
+    } else if ((name === "Return" || name === "Space") &&
+               this.element.classList.contains("non-interactive")) {
+      event.preventDefault();
+    } else if (name === "Escape" &&
+               event.target === this.searchField &&
+               this._onClearSearch()) {
+      // Handle the search box's keypress event. If the escape key is pressed,
+      // clear the search box field.
+      event.preventDefault();
+      event.stopPropagation();
     }
   }
+
 };
 
 /**
  * Helper functions
  */
 
 /**
  * Walk up the DOM from a given node until a parent property holder is found.
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -122,16 +122,20 @@ skip-if = os == "mac" # Bug 1245996 : cl
 [browser_rules_edit-selector_02.js]
 [browser_rules_edit-selector_03.js]
 [browser_rules_edit-selector_04.js]
 [browser_rules_edit-selector_05.js]
 [browser_rules_edit-selector_06.js]
 [browser_rules_edit-selector_07.js]
 [browser_rules_edit-selector_08.js]
 [browser_rules_edit-selector_09.js]
+[browser_rules_edit-value-after-name_01.js]
+[browser_rules_edit-value-after-name_02.js]
+[browser_rules_edit-value-after-name_03.js]
+[browser_rules_edit-value-after-name_04.js]
 [browser_rules_editable-field-focus_01.js]
 [browser_rules_editable-field-focus_02.js]
 [browser_rules_eyedropper.js]
 [browser_rules_filtereditor-appears-on-swatch-click.js]
 [browser_rules_filtereditor-commit-on-ENTER.js]
 [browser_rules_filtereditor-revert-on-ESC.js]
 skip-if = (os == "win" && debug) # bug 963492: win.
 [browser_rules_guessIndentation.js]
--- a/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_colorpicker-and-image-tooltip_02.js
@@ -38,28 +38,28 @@ function* testColorChangeIsntRevertedWhe
 
   yield simulateColorPickerChange(ruleView, picker, [0, 0, 0, 1], {
     selector: "body",
     name: "background-color",
     value: "rgb(0, 0, 0)"
   });
 
   let spectrum = yield picker.spectrum;
-  let onModifications = ruleView.once("ruleview-changed");
+
+  let onModifications = waitForNEvents(ruleView, "ruleview-changed", 2);
   let onHidden = picker.tooltip.once("hidden");
   EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
   yield onHidden;
   yield onModifications;
 
   info("Open the image preview tooltip");
   let value = getRuleViewProperty(ruleView, "body", "background").valueSpan;
   let url = value.querySelector(".theme-link");
   onShown = ruleView.tooltips.previewTooltip.once("shown");
-  let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip,
-                                          url);
+  let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
   ruleView.tooltips.previewTooltip.show(anchor);
   yield onShown;
 
   info("Image tooltip is shown, verify that the swatch is still correct");
   swatch = value.querySelector(".ruleview-colorswatch");
   is(swatch.style.backgroundColor, "black",
     "The swatch's color is correct");
   is(swatch.nextSibling.textContent, "black", "The color name is correct");
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_01.js
@@ -0,0 +1,107 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that clicking on swatch-preceeded value while editing the property name
+// will result in editing the property value. Also tests that the value span is updated
+// only if the property name has changed. See also Bug 1248274.
+
+const TEST_URI = `
+  <style type="text/css">
+  #testid {
+    color: red;
+  }
+  </style>
+  <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+
+  yield selectNode("#testid", inspector);
+
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+  let propEditor = ruleEditor.rule.textProps[0].editor;
+
+  yield testColorValueSpanClickWithoutNameChange(propEditor, view);
+  yield testColorValueSpanClickAfterNameChange(propEditor, view);
+});
+
+function* testColorValueSpanClickWithoutNameChange(propEditor, view) {
+  info("Test click on color span while focusing property name editor");
+  let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color");
+
+  info("Focus the color name span");
+  yield focusEditableField(view, propEditor.nameSpan);
+  let editor = inplaceEditor(propEditor.doc.activeElement);
+
+  // We add a click event to make sure the color span won't be cleared
+  // on nameSpan blur (which would lead to the click event not being triggered)
+  let onColorSpanClick = once(colorSpan, "click");
+
+  // The property-value-updated is emitted when the valueSpan markup is being
+  // re-populated, which should not be the case when not modifying the property name
+  let onPropertyValueUpdated = function () {
+    ok(false, "The \"property-value-updated\" should not be emitted");
+  };
+  view.on("property-value-updated", onPropertyValueUpdated);
+
+  info("blur propEditor.nameSpan by clicking on the color span");
+  EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView);
+
+  info("wait for the click event on the color span");
+  yield onColorSpanClick;
+  ok(true, "Expected click event was emitted");
+
+  editor = inplaceEditor(propEditor.doc.activeElement);
+  is(inplaceEditor(propEditor.valueSpan), editor,
+    "The property value editor got focused");
+
+  // We remove this listener in order to not cause unwanted conflict in the next test
+  view.off("property-value-updated", onPropertyValueUpdated);
+
+  info("blur valueSpan editor to trigger ruleview-changed event and prevent " +
+    "having pending request");
+  let onRuleViewChanged = view.once("ruleview-changed");
+  editor.input.blur();
+  yield onRuleViewChanged;
+}
+
+function* testColorValueSpanClickAfterNameChange(propEditor, view) {
+  info("Test click on color span after property name change");
+  let colorSpan = propEditor.valueSpan.querySelector(".ruleview-color");
+
+  info("Focus the color name span");
+  yield focusEditableField(view, propEditor.nameSpan);
+  let editor = inplaceEditor(propEditor.doc.activeElement);
+
+  info("Modify the property to border-color to trigger the " +
+    "property-value-updated event");
+  editor.input.value = "border-color";
+
+  let onRuleViewChanged = view.once("ruleview-changed");
+  let onPropertyValueUpdate = view.once("property-value-updated");
+
+  info("blur propEditor.nameSpan by clicking on the color span");
+  EventUtils.synthesizeMouse(colorSpan, 1, 1, {}, propEditor.doc.defaultView);
+
+  info("wait for ruleview-changed event to be triggered to prevent pending requests");
+  yield onRuleViewChanged;
+
+  info("wait for the property value to be updated");
+  yield onPropertyValueUpdate;
+  ok(true, "Expected \"property-value-updated\" event was emitted");
+
+  editor = inplaceEditor(propEditor.doc.activeElement);
+  is(inplaceEditor(propEditor.valueSpan), editor,
+    "The property value editor got focused");
+
+  info("blur valueSpan editor to trigger ruleview-changed event and prevent " +
+    "having pending request");
+  onRuleViewChanged = view.once("ruleview-changed");
+  editor.input.blur();
+  yield onRuleViewChanged;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_02.js
@@ -0,0 +1,65 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that hitting shift + click on color swatch while editing the property
+// name will only change the color unit and not lead to edit the property value.
+// See also Bug 1248274.
+
+const TEST_URI = `
+  <style type="text/css">
+  #testid {
+    color: red;
+    background: linear-gradient(
+      90deg,
+      rgb(183,222,237),
+      rgb(33,180,226),
+      rgb(31,170,217),
+      rgba(200,170,140,0.5));
+  }
+  </style>
+  <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+
+  info("Test shift + click on color swatch while editing property name");
+
+  yield selectNode("#testid", inspector);
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+  let propEditor = ruleEditor.rule.textProps[1].editor;
+  let swatchSpan = propEditor.valueSpan.querySelectorAll(".ruleview-colorswatch")[2];
+
+  info("Focus the background name span");
+  yield focusEditableField(view, propEditor.nameSpan);
+  let editor = inplaceEditor(propEditor.doc.activeElement);
+
+  info("Modify the property to background-image to trigger the " +
+    "property-value-updated event");
+  editor.input.value = "background-image";
+
+  let onPropertyValueUpdate = view.once("property-value-updated");
+  let onSwatchUnitChange = swatchSpan.once("unit-change");
+  let onRuleViewChanged = view.once("ruleview-changed");
+
+  info("blur propEditor.nameSpan by clicking on the color swatch");
+  EventUtils.synthesizeMouseAtCenter(swatchSpan, {shiftKey: true},
+    propEditor.doc.defaultView);
+
+  info("wait for ruleview-changed event to be triggered to prevent pending requests");
+  yield onRuleViewChanged;
+
+  info("wait for the color unit to change");
+  yield onSwatchUnitChange;
+  ok(true, "the color unit was changed");
+
+  info("wait for the property value to be updated");
+  yield onPropertyValueUpdate;
+
+  ok(!inplaceEditor(propEditor.valueSpan), "The inplace editor wasn't shown " +
+    "as a result of the color swatch shift + click");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_03.js
@@ -0,0 +1,69 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that clicking on color swatch while editing the property name
+// will show the color tooltip with the correct value. See also Bug 1248274.
+
+const TEST_URI = `
+  <style type="text/css">
+  #testid {
+    color: red;
+    background: linear-gradient(
+      90deg,
+      rgb(183,222,237),
+      rgb(33,180,226),
+      rgb(31,170,217),
+      rgba(200,170,140,0.5));
+  }
+  </style>
+  <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+
+  info("Test click on color swatch while editing property name");
+
+  yield selectNode("#testid", inspector);
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+  let propEditor = ruleEditor.rule.textProps[1].editor;
+  let swatchSpan = propEditor.valueSpan.querySelectorAll(
+    ".ruleview-colorswatch")[3];
+  let colorPicker = view.tooltips.colorPicker;
+
+  info("Focus the background name span");
+  yield focusEditableField(view, propEditor.nameSpan);
+  let editor = inplaceEditor(propEditor.doc.activeElement);
+
+  info("Modify the background property to background-image to trigger the " +
+    "property-value-updated event");
+  editor.input.value = "background-image";
+
+  let onRuleViewChanged = view.once("ruleview-changed");
+  let onPropertyValueUpdate = view.once("property-value-updated");
+  let onShown = colorPicker.tooltip.once("shown");
+
+  info("blur propEditor.nameSpan by clicking on the color swatch");
+  EventUtils.synthesizeMouseAtCenter(swatchSpan, {},
+    propEditor.doc.defaultView);
+
+  info("wait for ruleview-changed event to be triggered to prevent pending requests");
+  yield onRuleViewChanged;
+
+  info("wait for the property value to be updated");
+  yield onPropertyValueUpdate;
+
+  info("wait for the color picker to be shown");
+  yield onShown;
+
+  ok(true, "The color picker was shown on click of the color swatch");
+  ok(!inplaceEditor(propEditor.valueSpan),
+    "The inplace editor wasn't shown as a result of the color swatch click");
+
+  let spectrum = yield colorPicker.spectrum;
+  is(spectrum.rgb, "200,170,140,0.5", "The correct color picker was shown");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-value-after-name_04.js
@@ -0,0 +1,62 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that clicking on a property's value URL while editing the property name
+// will open the link in a new tab. See also Bug 1248274.
+
+const TEST_URI = `
+  <style type="text/css">
+  #testid {
+      background: url("chrome://global/skin/icons/warning-64.png"), linear-gradient(white, #F06 400px);
+  }
+  </style>
+  <div id="testid">Styled Node</div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+
+  info("Test click on background-image url while editing property name");
+
+  yield selectNode("#testid", inspector);
+  let ruleEditor = getRuleViewRuleEditor(view, 1);
+  let propEditor = ruleEditor.rule.textProps[0].editor;
+  let anchor = propEditor.valueSpan.querySelector(".ruleview-propertyvalue .theme-link");
+
+  info("Focus the background name span");
+  yield focusEditableField(view, propEditor.nameSpan);
+  let editor = inplaceEditor(propEditor.doc.activeElement);
+
+  info("Modify the property to background to trigger the " +
+    "property-value-updated event");
+  editor.input.value = "background-image";
+
+  let onRuleViewChanged = view.once("ruleview-changed");
+  let onPropertyValueUpdate = view.once("property-value-updated");
+  let onTabOpened = waitForTab();
+
+  info("blur propEditor.nameSpan by clicking on the link");
+  // The url can be wrapped across multiple lines, and so we click the lower left corner
+  // of the anchor to make sure to target the link.
+  let rect = anchor.getBoundingClientRect();
+  EventUtils.synthesizeMouse(anchor, 2, rect.height - 2, {}, propEditor.doc.defaultView);
+
+  info("wait for ruleview-changed event to be triggered to prevent pending requests");
+  yield onRuleViewChanged;
+
+  info("wait for the property value to be updated");
+  yield onPropertyValueUpdate;
+
+  info("wait for the image to be open in a new tab");
+  let tab = yield onTabOpened;
+  ok(true, "A new tab opened");
+
+  is(tab.linkedBrowser.currentURI.spec, anchor.href,
+    "The URL for the new tab is correct");
+
+  gBrowser.removeTab(tab);
+});
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -18,16 +18,34 @@ const {
 } = require("devtools/client/inspector/shared/utils");
 const {
   parseDeclarations,
   parseSingleValue,
 } = require("devtools/shared/css-parsing-utils");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
+const SHARED_SWATCH_CLASS = "ruleview-swatch";
+const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
+const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
+const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
+const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
+
+/*
+ * An actionable element is an element which on click triggers a specific action
+ * (e.g. shows a color tooltip, opens a link, …).
+ */
+const ACTIONABLE_ELEMENTS_SELECTORS = [
+  `.${COLOR_SWATCH_CLASS}`,
+  `.${BEZIER_SWATCH_CLASS}`,
+  `.${FILTER_SWATCH_CLASS}`,
+  `.${ANGLE_SWATCH_CLASS}`,
+  "a"
+];
+
 /**
  * TextPropertyEditor is responsible for the following:
  *   Owns a TextProperty object.
  *   Manages changes to the TextProperty.
  *   Can be expanded to display computed properties.
  *   Can mark a property disabled or enabled.
  *
  * @param {RuleEditor} ruleEditor
@@ -39,30 +57,33 @@ function TextPropertyEditor(ruleEditor, 
   this.ruleEditor = ruleEditor;
   this.ruleView = this.ruleEditor.ruleView;
   this.doc = this.ruleEditor.doc;
   this.popup = this.ruleView.popup;
   this.prop = property;
   this.prop.editor = this;
   this.browserWindow = this.doc.defaultView.top;
   this._populatedComputed = false;
+  this._hasPendingClick = false;
+  this._clickedElementOptions = null;
 
   const toolbox = this.ruleView.inspector.toolbox;
   this.cssProperties = getCssProperties(toolbox);
 
   this._onEnableClicked = this._onEnableClicked.bind(this);
   this._onExpandClicked = this._onExpandClicked.bind(this);
   this._onStartEditing = this._onStartEditing.bind(this);
   this._onNameDone = this._onNameDone.bind(this);
   this._onValueDone = this._onValueDone.bind(this);
   this._onSwatchCommit = this._onSwatchCommit.bind(this);
   this._onSwatchPreview = this._onSwatchPreview.bind(this);
   this._onSwatchRevert = this._onSwatchRevert.bind(this);
   this._onValidate = throttle(this._previewValue, 10, this);
   this.update = this.update.bind(this);
+  this.updatePropertyState = this.updatePropertyState.bind(this);
 
   this._create();
   this.update();
 }
 
 TextPropertyEditor.prototype = {
   /**
    * Boolean indicating if the name or value is being currently edited.
@@ -190,17 +211,17 @@ TextPropertyEditor.prototype = {
           this.nameSpan.click();
         }
       }, false);
 
       editableField({
         start: this._onStartEditing,
         element: this.nameSpan,
         done: this._onNameDone,
-        destroy: this.update,
+        destroy: this.updatePropertyState,
         advanceChars: ":",
         contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
         popup: this.popup
       });
 
       // Auto blur name field on multiple CSS rules get pasted in.
       this.nameContainer.addEventListener("paste",
         blurOnMultipleProperties(this.cssProperties), false);
@@ -210,16 +231,47 @@ TextPropertyEditor.prototype = {
         event.stopPropagation();
 
         // Forward clicks on valueContainer to the editable valueSpan
         if (event.target === this.valueContainer) {
           this.valueSpan.click();
         }
       }, false);
 
+      // The mousedown event could trigger a blur event on nameContainer, which
+      // will trigger a call to the update function. The update function clears
+      // valueSpan's markup. Thus the regular click event does not bubble up, and
+      // listener's callbacks are not called.
+      // So we need to remember where the user clicks in order to re-trigger the click
+      // after the valueSpan's markup is re-populated. We only need to track this for
+      // valueSpan's child elements, because direct click on valueSpan will always
+      // trigger a click event.
+      this.valueSpan.addEventListener("mousedown", (event) => {
+        let clickedEl = event.target;
+        if (clickedEl === this.valueSpan) {
+          return;
+        }
+        this._hasPendingClick = true;
+
+        let matchedSelector = ACTIONABLE_ELEMENTS_SELECTORS.find(
+          (selector) => clickedEl.matches(selector));
+        if (matchedSelector) {
+          let similarElements = [...this.valueSpan.querySelectorAll(matchedSelector)];
+          this._clickedElementOptions = {
+            selector: matchedSelector,
+            index: similarElements.indexOf(clickedEl)
+          };
+        }
+      }, false);
+
+      this.valueSpan.addEventListener("mouseup", (event) => {
+        this._clickedElementOptions = null;
+        this._hasPendingClick = false;
+      }, false);
+
       this.valueSpan.addEventListener("click", (event) => {
         let target = event.target;
 
         if (target.nodeName === "a") {
           event.stopPropagation();
           event.preventDefault();
           this.browserWindow.openUILinkIn(target.href, "tab");
         }
@@ -258,37 +310,17 @@ TextPropertyEditor.prototype = {
   /**
    * Populate the span based on changes to the TextProperty.
    */
   update: function () {
     if (this.ruleView.isDestroyed) {
       return;
     }
 
-    if (this.prop.enabled) {
-      this.enable.style.removeProperty("visibility");
-      this.enable.setAttribute("checked", "");
-    } else {
-      this.enable.style.visibility = "visible";
-      this.enable.removeAttribute("checked");
-    }
-
-    this.warning.hidden = this.editing || this.isValid();
-    this.filterProperty.hidden = this.editing ||
-                                 !this.isValid() ||
-                                 !this.prop.overridden ||
-                                 this.ruleEditor.rule.isUnmatched;
-
-    if (!this.editing &&
-        (this.prop.overridden || !this.prop.enabled ||
-         !this.prop.isKnownProperty())) {
-      this.element.classList.add("ruleview-overridden");
-    } else {
-      this.element.classList.remove("ruleview-overridden");
-    }
+    this.updatePropertyState();
 
     let name = this.prop.name;
     this.nameSpan.textContent = name;
 
     // Combine the property's value and priority into one string for
     // the value.
     let store = this.rule.elementStyle.store;
     let val = store.userProperties.getProperty(this.rule.style, name,
@@ -300,43 +332,39 @@ TextPropertyEditor.prototype = {
     let propDirty = store.userProperties.contains(this.rule.style, name);
 
     if (propDirty) {
       this.element.setAttribute("dirty", "");
     } else {
       this.element.removeAttribute("dirty");
     }
 
-    const sharedSwatchClass = "ruleview-swatch ";
-    const colorSwatchClass = "ruleview-colorswatch";
-    const bezierSwatchClass = "ruleview-bezierswatch";
-    const filterSwatchClass = "ruleview-filterswatch";
-    const angleSwatchClass = "ruleview-angleswatch";
-
     let outputParser = this.ruleView._outputParser;
     let parserOptions = {
-      colorSwatchClass: sharedSwatchClass + colorSwatchClass,
+      colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
       colorClass: "ruleview-color",
-      bezierSwatchClass: sharedSwatchClass + bezierSwatchClass,
+      bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS,
       bezierClass: "ruleview-bezier",
-      filterSwatchClass: sharedSwatchClass + filterSwatchClass,
+      filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
       filterClass: "ruleview-filter",
-      angleSwatchClass: sharedSwatchClass + angleSwatchClass,
+      angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS,
       angleClass: "ruleview-angle",
       defaultColorType: !propDirty,
       urlClass: "theme-link",
       baseURI: this.sheetHref
     };
     let frag = outputParser.parseCssProperty(name, val, parserOptions);
     this.valueSpan.innerHTML = "";
     this.valueSpan.appendChild(frag);
 
+    this.ruleView.emit("property-value-updated", this.valueSpan);
+
     // Attach the color picker tooltip to the color swatches
     this._colorSwatchSpans =
-      this.valueSpan.querySelectorAll("." + colorSwatchClass);
+      this.valueSpan.querySelectorAll("." + COLOR_SWATCH_CLASS);
     if (this.ruleEditor.isEditable) {
       for (let span of this._colorSwatchSpans) {
         // Adding this swatch to the list of swatches our colorpicker
         // knows about
         this.ruleView.tooltips.colorPicker.addSwatch(span, {
           onShow: this._onStartEditing,
           onPreview: this._onSwatchPreview,
           onCommit: this._onSwatchCommit,
@@ -345,72 +373,120 @@ TextPropertyEditor.prototype = {
         span.on("unit-change", this._onSwatchCommit);
         let title = CssLogic.l10n("rule.colorSwatch.tooltip");
         span.setAttribute("title", title);
       }
     }
 
     // Attach the cubic-bezier tooltip to the bezier swatches
     this._bezierSwatchSpans =
-      this.valueSpan.querySelectorAll("." + bezierSwatchClass);
+      this.valueSpan.querySelectorAll("." + BEZIER_SWATCH_CLASS);
     if (this.ruleEditor.isEditable) {
       for (let span of this._bezierSwatchSpans) {
         // Adding this swatch to the list of swatches our colorpicker
         // knows about
         this.ruleView.tooltips.cubicBezier.addSwatch(span, {
           onShow: this._onStartEditing,
           onPreview: this._onSwatchPreview,
           onCommit: this._onSwatchCommit,
           onRevert: this._onSwatchRevert
         });
         let title = CssLogic.l10n("rule.bezierSwatch.tooltip");
         span.setAttribute("title", title);
       }
     }
 
     // Attach the filter editor tooltip to the filter swatch
-    let span = this.valueSpan.querySelector("." + filterSwatchClass);
+    let span = this.valueSpan.querySelector("." + FILTER_SWATCH_CLASS);
     if (this.ruleEditor.isEditable) {
       if (span) {
         parserOptions.filterSwatch = true;
 
         this.ruleView.tooltips.filterEditor.addSwatch(span, {
           onShow: this._onStartEditing,
           onPreview: this._onSwatchPreview,
           onCommit: this._onSwatchCommit,
           onRevert: this._onSwatchRevert
         }, outputParser, parserOptions);
         let title = CssLogic.l10n("rule.filterSwatch.tooltip");
         span.setAttribute("title", title);
       }
     }
 
     this.angleSwatchSpans =
-      this.valueSpan.querySelectorAll("." + angleSwatchClass);
+      this.valueSpan.querySelectorAll("." + ANGLE_SWATCH_CLASS);
     if (this.ruleEditor.isEditable) {
       for (let angleSpan of this.angleSwatchSpans) {
         angleSpan.on("unit-change", this._onSwatchCommit);
         let title = CssLogic.l10n("rule.angleSwatch.tooltip");
         angleSpan.setAttribute("title", title);
       }
     }
 
+    // Now that we have updated the property's value, we might have a pending
+    // click on the value container. If we do, we have to trigger a click event
+    // on the right element.
+    if (this._hasPendingClick) {
+      this._hasPendingClick = false;
+      let elToClick;
+
+      if (this._clickedElementOptions !== null) {
+        let {selector, index} = this._clickedElementOptions;
+        elToClick = this.valueSpan.querySelectorAll(selector)[index];
+
+        this._clickedElementOptions = null;
+      }
+
+      if (!elToClick) {
+        elToClick = this.valueSpan;
+      }
+      elToClick.click();
+    }
+
     // Populate the computed styles.
     this._updateComputed();
 
     // Update the rule property highlight.
     this.ruleView._updatePropertyHighlight(this);
   },
 
   _onStartEditing: function () {
     this.element.classList.remove("ruleview-overridden");
     this.enable.style.visibility = "hidden";
   },
 
   /**
+   * Update the visibility of the enable checkbox, the warning indicator and
+   * the filter property, as well as the overriden state of the property.
+   */
+  updatePropertyState: function () {
+    if (this.prop.enabled) {
+      this.enable.style.removeProperty("visibility");
+      this.enable.setAttribute("checked", "");
+    } else {
+      this.enable.style.visibility = "visible";
+      this.enable.removeAttribute("checked");
+    }
+
+    this.warning.hidden = this.editing || this.isValid();
+    this.filterProperty.hidden = this.editing ||
+                                 !this.isValid() ||
+                                 !this.prop.overridden ||
+                                 this.ruleEditor.rule.isUnmatched;
+
+    if (!this.editing &&
+        (this.prop.overridden || !this.prop.enabled ||
+         !this.prop.isKnownProperty())) {
+      this.element.classList.add("ruleview-overridden");
+    } else {
+      this.element.classList.remove("ruleview-overridden");
+    }
+  },
+
+  /**
    * Update the indicator for computed styles. The computed styles themselves
    * are populated on demand, when they become visible.
    */
   _updateComputed: function () {
     this.computed.innerHTML = "";
 
     let showExpander = this.prop.computed.some(c => c.name !== this.prop.name);
     this.expander.style.visibility = showExpander ? "visible" : "hidden";
--- a/devtools/client/inspector/shared/style-inspector-overlays.js
+++ b/devtools/client/inspector/shared/style-inspector-overlays.js
@@ -7,18 +7,24 @@
 "use strict";
 
 // The style-inspector overlays are:
 // - tooltips that appear when hovering over property values
 // - editor tooltips that appear when clicking color swatches, etc.
 // - in-content highlighters that appear when hovering over property values
 // - etc.
 
+const {getTheme} = require("devtools/client/shared/theme");
+const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
 const {
-  Tooltip,
+  getImageDimensions,
+  setImageTooltip,
+  setBrokenImageTooltip,
+} = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
+const {
   SwatchColorPickerTooltip,
   SwatchCubicBezierTooltip,
   CssDocsTooltip,
   SwatchFilterTooltip
 } = require("devtools/client/shared/widgets/Tooltip");
 const EventEmitter = require("devtools/shared/event-emitter");
 const promise = require("promise");
 const {Task} = require("devtools/shared/task");
@@ -268,17 +274,19 @@ TooltipsOverlay.prototype = {
   addToView: function () {
     if (this._isStarted || this._isDestroyed) {
       return;
     }
 
     let panelDoc = this.view.inspector.panelDoc;
 
     // Image, fonts, ... preview tooltip
-    this.previewTooltip = new Tooltip(panelDoc);
+    this.previewTooltip = new HTMLTooltip(this.view.inspector.toolbox, {
+      type: "arrow"
+    });
     this.previewTooltip.startTogglingOnHover(this.view.element,
       this._onPreviewTooltipTargetHover.bind(this));
 
     // MDN CSS help tooltip
     this.cssDocs = new CssDocsTooltip(panelDoc);
 
     if (this.isRuleView) {
       // Color picker tooltip
@@ -390,33 +398,95 @@ TooltipsOverlay.prototype = {
     if (this.isRuleView && this.filterEditor.tooltip.isShown()) {
       this.filterEditor.revert();
       this.filterEdtior.hide();
     }
 
     let inspector = this.view.inspector;
 
     if (type === TOOLTIP_IMAGE_TYPE) {
-      let dim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE);
-      // nodeInfo contains an absolute uri
-      let uri = nodeInfo.value.url;
-      yield this.previewTooltip.setRelativeImageContent(uri,
-        inspector.inspector, dim);
+      try {
+        yield this._setImagePreviewTooltip(nodeInfo.value.url);
+      } catch (e) {
+        yield setBrokenImageTooltip(this.previewTooltip, this.view.inspector.panelDoc);
+      }
       return true;
     }
 
     if (type === TOOLTIP_FONTFAMILY_TYPE) {
-      yield this.previewTooltip.setFontFamilyContent(nodeInfo.value.value,
-        inspector.selection.nodeFront);
+      let font = nodeInfo.value.value;
+      let nodeFront = inspector.selection.nodeFront;
+      yield this._setFontPreviewTooltip(font, nodeFront);
       return true;
     }
 
     return false;
   }),
 
+  /**
+   * Set the content of the preview tooltip to display an image preview. The image URL can
+   * be relative, a call will be made to the debuggee to retrieve the image content as an
+   * imageData URI.
+   *
+   * @param {String} imageUrl
+   *        The image url value (may be relative or absolute).
+   * @return {Promise} A promise that resolves when the preview tooltip content is ready
+   */
+  _setImagePreviewTooltip: Task.async(function* (imageUrl) {
+    let doc = this.view.inspector.panelDoc;
+    let maxDim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE);
+
+    let naturalWidth, naturalHeight;
+    if (imageUrl.startsWith("data:")) {
+      // If the imageUrl already is a data-url, save ourselves a round-trip
+      let size = yield getImageDimensions(doc, imageUrl);
+      naturalWidth = size.naturalWidth;
+      naturalHeight = size.naturalHeight;
+    } else {
+      let inspectorFront = this.view.inspector.inspector;
+      let {data, size} = yield inspectorFront.getImageDataFromURL(imageUrl, maxDim);
+      imageUrl = yield data.string();
+      naturalWidth = size.naturalWidth;
+      naturalHeight = size.naturalHeight;
+    }
+
+    yield setImageTooltip(this.previewTooltip, doc, imageUrl,
+      {maxDim, naturalWidth, naturalHeight});
+  }),
+
+  /**
+   * Set the content of the preview tooltip to display a font family preview.
+   *
+   * @param {String} font
+   *        The font family value.
+   * @param {object} nodeFront
+   *        The NodeActor that will used to retrieve the dataURL for the font
+   *        family tooltip contents.
+   * @return {Promise} A promise that resolves when the preview tooltip content is ready
+   */
+  _setFontPreviewTooltip: Task.async(function* (font, nodeFront) {
+    if (!font || !nodeFront || typeof nodeFront.getFontFamilyDataURL !== "function") {
+      throw new Error("Unable to create font preview tooltip content.");
+    }
+
+    font = font.replace(/"/g, "'");
+    font = font.replace("!important", "");
+    font = font.trim();
+
+    let fillStyle = getTheme() === "light" ? "black" : "white";
+    let {data, size: maxDim} = yield nodeFront.getFontFamilyDataURL(font, fillStyle);
+
+    let imageUrl = yield data.string();
+    let doc = this.view.inspector.panelDoc;
+    let {naturalWidth, naturalHeight} = yield getImageDimensions(doc, imageUrl);
+
+    yield setImageTooltip(this.previewTooltip, doc, imageUrl,
+      {hideDimensionLabel: true, maxDim, naturalWidth, naturalHeight});
+  }),
+
   _onNewSelection: function () {
     if (this.previewTooltip) {
       this.previewTooltip.hide();
     }
 
     if (this.colorPicker) {
       this.colorPicker.hide();
     }
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-background-image.js
@@ -60,33 +60,33 @@ function* testBodyRuleView(view) {
   ok(panel, "XUL panel exists");
 
   // Get the background-image property inside the rule view
   let {valueSpan} = getRuleViewProperty(view, "body", "background-image");
   let uriSpan = valueSpan.querySelector(".theme-link");
 
   yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
 
-  let images = panel.getElementsByTagName("image");
+  let images = panel.getElementsByTagName("img");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src")
     .indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1,
     "The image URL seems fine");
 }
 
 function* testDivRuleView(view) {
   let panel = view.tooltips.previewTooltip.panel;
 
   // Get the background property inside the rule view
   let {valueSpan} = getRuleViewProperty(view, ".test-element", "background");
   let uriSpan = valueSpan.querySelector(".theme-link");
 
   yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
 
-  let images = panel.getElementsByTagName("image");
+  let images = panel.getElementsByTagName("img");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"),
     "Tooltip contains a data-uri image as expected");
 }
 
 function* testTooltipAppearsEvenInEditMode(view) {
   info("Switching to edit mode in the rule view");
   let editor = yield turnToEditMode(view);
@@ -112,14 +112,14 @@ function* testComputedView(view) {
   let panel = tooltip.panel;
   ok(panel, "The computed-view tooltip has a XUL panel");
 
   let {valueSpan} = getComputedViewProperty(view, "background-image");
   let uriSpan = valueSpan.querySelector(".theme-link");
 
   yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
 
-  let images = panel.getElementsByTagName("image");
+  let images = panel.getElementsByTagName("img");
   is(images.length, 1, "Tooltip contains an image");
 
   ok(images[0].getAttribute("src").startsWith("data:"),
     "Tooltip contains a data-uri in the computed-view too");
 }
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-closes-on-new-selection.js
@@ -2,16 +2,17 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that if a tooltip is visible when a new selection is made, it closes
 
 const TEST_URI = "<div class='one'>el 1</div><div class='two'>el 2</div>";
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = yield openRuleView();
   yield selectNode(".one", inspector);
 
   info("Testing rule view tooltip closes on new selection");
   yield testRuleView(view, inspector);
@@ -20,17 +21,19 @@ add_task(function* () {
   view = selectComputedView(inspector);
   yield testComputedView(view, inspector);
 });
 
 function* testRuleView(ruleView, inspector) {
   info("Showing the tooltip");
 
   let tooltip = ruleView.tooltips.previewTooltip;
-  tooltip.setTextContent({messages: ["rule-view tooltip"]});
+  let tooltipContent = ruleView.styleDocument.createElementNS(XHTML_NS, "div");
+  yield tooltip.setContent(tooltipContent, 100, 30);
+
   // Stop listening for mouse movements because it's not needed for this test,
   // and causes intermittent failures on Linux. When this test runs in the suite
   // sometimes a mouseleave event is dispatched at the start, which causes the
   // tooltip to hide in the middle of being shown, which causes timeouts later.
   tooltip.stopTogglingOnHover();
 
   let onShown = tooltip.once("shown");
   tooltip.show(ruleView.styleDocument.firstElementChild);
@@ -43,17 +46,18 @@ function* testRuleView(ruleView, inspect
 
   ok(true, "Rule view tooltip closed after a new node got selected");
 }
 
 function* testComputedView(computedView, inspector) {
   info("Showing the tooltip");
 
   let tooltip = computedView.tooltips.previewTooltip;
-  tooltip.setTextContent({messages: ["computed-view tooltip"]});
+  let tooltipContent = computedView.styleDocument.createElementNS(XHTML_NS, "div");
+  yield tooltip.setContent(tooltipContent, 100, 30);
   // Stop listening for mouse movements because it's not needed for this test,
   // and causes intermittent failures on Linux. When this test runs in the suite
   // sometimes a mouseleave event is dispatched at the start, which causes the
   // tooltip to hide in the middle of being shown, which causes timeouts later.
   tooltip.stopTogglingOnHover();
 
   let onShown = tooltip.once("shown");
   tooltip.show(computedView.styleDocument.firstElementChild);
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-longhand-fontfamily.js
@@ -46,17 +46,17 @@ function* testRuleView(ruleView, nodeFro
 
   // Get the font family property inside the rule view
   let {valueSpan} = getRuleViewProperty(ruleView, "#testElement",
     "font-family");
 
   // And verify that the tooltip gets shown on this property
   yield assertHoverTooltipOn(tooltip, valueSpan);
 
-  let images = panel.getElementsByTagName("image");
+  let images = panel.getElementsByTagName("img");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"),
     "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL,
     "Tooltip contains the correct data-uri image");
 }
@@ -65,17 +65,17 @@ function* testComputedView(computedView,
   info("Testing font-family tooltips in the computed view");
 
   let tooltip = computedView.tooltips.previewTooltip;
   let panel = tooltip.panel;
   let {valueSpan} = getComputedViewProperty(computedView, "font-family");
 
   yield assertHoverTooltipOn(tooltip, valueSpan);
 
-  let images = panel.getElementsByTagName("image");
+  let images = panel.getElementsByTagName("img");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"),
     "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL,
     "Tooltip contains the correct data-uri image");
 }
@@ -92,17 +92,17 @@ function* testExpandedComputedViewProper
   let valueSpan = propertyView.matchedSelectorsContainer
     .querySelector(".bestmatch .other-property-value");
 
   let tooltip = computedView.tooltips.previewTooltip;
   let panel = tooltip.panel;
 
   yield assertHoverTooltipOn(tooltip, valueSpan);
 
-  let images = panel.getElementsByTagName("image");
+  let images = panel.getElementsByTagName("img");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"),
     "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL,
     "Tooltip contains the correct data-uri image");
 }
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-multiple-background-images.js
@@ -40,17 +40,17 @@ function* testComputedViewUrls(inspector
   yield performChecks(view, valueSpan);
 }
 
 /**
  * A helper that checks url() tooltips contain correct images
  */
 function* performChecks(view, propertyValue) {
   function checkTooltip(panel, imageSrc) {
-    let images = panel.getElementsByTagName("image");
+    let images = panel.getElementsByTagName("img");
     is(images.length, 1, "Tooltip contains an image");
     is(images[0].getAttribute("src"), imageSrc, "The image URL is correct");
   }
 
   let links = propertyValue.querySelectorAll(".theme-link");
   let panel = view.tooltips.previewTooltip.panel;
 
   info("Checking first link tooltip");
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
@@ -42,17 +42,17 @@ function* testRuleView(ruleView, nodeFro
 
   let rule = getRuleViewRule(ruleView, "#testElement");
   let valueSpan = rule
     .querySelector(".ruleview-computed .ruleview-propertyvalue");
 
   // And verify that the tooltip gets shown on this property
   yield assertHoverTooltipOn(tooltip, valueSpan);
 
-  let images = panel.getElementsByTagName("image");
+  let images = panel.getElementsByTagName("img");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src")
     .startsWith("data:"), "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL,
     "Tooltip contains the correct data-uri image");
 }
--- a/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_tooltip-size.js
@@ -34,22 +34,22 @@ function* testImageDimension(ruleView) {
   let uriSpan = valueSpan.querySelector(".theme-link");
 
   // Make sure there is a hover tooltip for this property, this also will fill
   // the tooltip with its content
   yield assertHoverTooltipOn(tooltip, uriSpan);
 
   info("Showing the tooltip");
   let onShown = tooltip.once("shown");
-  tooltip.show();
+  tooltip.show(uriSpan);
   yield onShown;
 
   // Let's not test for a specific size, but instead let's make sure it's at
   // least as big as the image
-  let imageRect = panel.querySelector("image").getBoundingClientRect();
+  let imageRect = panel.querySelector("img").getBoundingClientRect();
   let panelRect = panel.getBoundingClientRect();
 
   ok(panelRect.width >= imageRect.width,
     "The panel is wide enough to show the image");
   ok(panelRect.height >= imageRect.height,
     "The panel is high enough to show the image");
 
   let onHidden = tooltip.once("hidden");
--- a/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-iframes_02.js
@@ -45,15 +45,15 @@ function* switchToFrameContext(frameInde
   // Open frame menu and wait till it's available on the screen.
   let btn = toolbox.doc.getElementById("command-button-frames");
   let menu = toolbox.showFramesMenu({target: btn});
   yield once(menu, "open");
 
   info("Select the iframe in the frame list.");
   let newRoot = inspector.once("new-root");
 
-  menu.menuitems[frameIndex].click();
+  menu.items[frameIndex].click();
 
   yield newRoot;
   yield inspector.once("inspector-updated");
 
   info("Navigation to the iframe is done.");
 }
--- a/devtools/client/inspector/test/browser_inspector_select-docshell.js
+++ b/devtools/client/inspector/test/browser_inspector_select-docshell.js
@@ -28,17 +28,17 @@ add_task(function* () {
   let btn = toolbox.doc.getElementById("command-button-frames");
   ok(!btn.firstChild, "The frame list button doesn't have any children");
 
   // Open frame menu and wait till it's available on the screen.
   let menu = toolbox.showFramesMenu({target: btn});
   yield once(menu, "open");
 
   // Verify that the menu is popuplated.
-  let frames = menu.menuitems.slice();
+  let frames = menu.items.slice();
   is(frames.length, 2, "We have both frames in the menu");
 
   frames.sort(function (a, b) {
     return a.label.localeCompare(b.label);
   });
 
   is(frames[0].label, FrameURL, "Got top level document in the list");
   is(frames[1].label, URL, "Got iframe document in the list");
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -164,18 +164,17 @@ devtools.jar:
     skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg)
     skin/images/performance-icons.svg (themes/images/performance-icons.svg)
     skin/widgets.css (themes/widgets.css)
     skin/images/power.svg (themes/images/power.svg)
     skin/images/filetypes/dir-close.svg (themes/images/filetypes/dir-close.svg)
     skin/images/filetypes/dir-open.svg (themes/images/filetypes/dir-open.svg)
     skin/images/filetypes/globe.svg (themes/images/filetypes/globe.svg)
     skin/images/filetypes/store.svg (themes/images/filetypes/store.svg)
-    skin/images/commandline-icon.png (themes/images/commandline-icon.png)
-    skin/images/commandline-icon@2x.png (themes/images/commandline-icon@2x.png)
+    skin/images/commandline-icon.svg (themes/images/commandline-icon.svg)
     skin/images/alerticon-warning.png (themes/images/alerticon-warning.png)
     skin/images/alerticon-warning@2x.png (themes/images/alerticon-warning@2x.png)
     skin/rules.css (themes/rules.css)
     skin/commandline.css (themes/commandline.css)
     skin/images/command-paintflashing.svg (themes/images/command-paintflashing.svg)
     skin/images/command-screenshot.svg (themes/images/command-screenshot.svg)
     skin/images/command-responsivemode.svg (themes/images/command-responsivemode.svg)
     skin/images/command-scratchpad.svg (themes/images/command-scratchpad.svg)
--- a/devtools/client/locales/en-US/netmonitor.dtd
+++ b/devtools/client/locales/en-US/netmonitor.dtd
@@ -34,16 +34,20 @@
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.file): This is the label displayed
   -  in the network table toolbar, above the "file" column. -->
 <!ENTITY netmonitorUI.toolbar.file        "File">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.domain): This is the label displayed
   -  in the network table toolbar, above the "domain" column. -->
 <!ENTITY netmonitorUI.toolbar.domain      "Domain">
 
+<!-- LOCALIZATION NOTE (netmonitorUI.toolbar.cause): This is the label displayed
+  -  in the network table toolbar, above the "cause" column. -->
+<!ENTITY netmonitorUI.toolbar.cause        "Cause">
+
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.type): This is the label displayed
   -  in the network table toolbar, above the "type" column. -->
 <!ENTITY netmonitorUI.toolbar.type        "Type">
 
 <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.transferred): This is the label displayed
   -  in the network table toolbar, above the "transferred" column, which is the
   -  compressed / encoded size. -->
 <!ENTITY netmonitorUI.toolbar.transferred "Transferred">
--- a/devtools/client/netmonitor/netmonitor-controller.js
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -428,16 +428,23 @@ var NetMonitorController = {
 
   /**
    * Getter that tells if the server can do network performance statistics.
    * @type boolean
    */
   get supportsPerfStats() {
     return this.tabClient &&
            (this.tabClient.traits.reconfigure || !this._target.isApp);
+  },
+
+  /**
+   * Open a given source in Debugger
+   */
+  viewSourceInDebugger(sourceURL, sourceLine) {
+    return this._toolbox.viewSourceInDebugger(sourceURL, sourceLine);
   }
 };
 
 /**
  * Functions handling target-related lifetime events.
  */
 function TargetEventsHandler() {
   this._onTabNavigated = this._onTabNavigated.bind(this);
@@ -624,22 +631,24 @@ NetworkEventsHandler.prototype = {
    * @param object networkInfo
    *        The network request information.
    */
   _onNetworkEvent: function (type, networkInfo) {
     let { actor,
       startedDateTime,
       request: { method, url },
       isXHR,
+      cause,
       fromCache,
       fromServiceWorker
     } = networkInfo;
 
     NetMonitorView.RequestsMenu.addRequest(
-      actor, startedDateTime, method, url, isXHR, fromCache, fromServiceWorker
+      actor, startedDateTime, method, url, isXHR, cause, fromCache,
+        fromServiceWorker
     );
     window.emit(EVENTS.NETWORK_EVENT, actor);
   },
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param string type
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -25,17 +25,19 @@ const {LocalizationHelper} = require("de
 const {PrefsHelper} = require("devtools/client/shared/prefs");
 const {ViewHelpers, Heritage, WidgetMethods, setNamedTimeout} =
   require("devtools/client/shared/widgets/view-helpers");
 
 /**
  * Localization convenience methods.
  */
 const NET_STRINGS_URI = "chrome://devtools/locale/netmonitor.properties";
+const WEBCONSOLE_STRINGS_URI = "chrome://devtools/locale/webconsole.properties";
 var L10N = new LocalizationHelper(NET_STRINGS_URI);
+const WEBCONSOLE_L10N = new LocalizationHelper(WEBCONSOLE_STRINGS_URI);
 
 // ms
 const WDA_DEFAULT_VERIFY_INTERVAL = 50;
 
 // Use longer timeout during testing as the tests need this process to succeed
 // and two seconds is quite short on slow debug builds. The timeout here should
 // be at least equal to the general mochitest timeout of 45 seconds so that this
 // never gets hit during testing.
@@ -56,16 +58,18 @@ const HTML_NS = "http://www.w3.org/1999/
 const EPSILON = 0.001;
 // 100 KB in bytes
 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400;
 // ms
 const RESIZE_REFRESH_RATE = 50;
 // ms
 const REQUESTS_REFRESH_RATE = 50;
 const REQUESTS_TOOLTIP_POSITION = "topcenter bottomleft";
+// tooltip show/hide delay in ms
+const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
 // px
 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
 // px
 const REQUESTS_WATERFALL_SAFE_BOUNDS = 90;
 // ms
 const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5;
 // px
 const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60;
@@ -97,16 +101,41 @@ const CONTENT_MIME_TYPE_MAPPINGS = {
   "/xml": Editor.modes.html,
   "/atom": Editor.modes.html,
   "/soap": Editor.modes.html,
   "/vnd.mpeg.dash.mpd": Editor.modes.html,
   "/rdf": Editor.modes.css,
   "/rss": Editor.modes.css,
   "/css": Editor.modes.css
 };
+const LOAD_CAUSE_STRINGS = {
+  [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid",
+  [Ci.nsIContentPolicy.TYPE_OTHER]: "other",
+  [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script",
+  [Ci.nsIContentPolicy.TYPE_IMAGE]: "img",
+  [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
+  [Ci.nsIContentPolicy.TYPE_OBJECT]: "object",
+  [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document",
+  [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument",
+  [Ci.nsIContentPolicy.TYPE_REFRESH]: "refresh",
+  [Ci.nsIContentPolicy.TYPE_XBL]: "xbl",
+  [Ci.nsIContentPolicy.TYPE_PING]: "ping",
+  [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr",
+  [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "objectSubdoc",
+  [Ci.nsIContentPolicy.TYPE_DTD]: "dtd",
+  [Ci.nsIContentPolicy.TYPE_FONT]: "font",
+  [Ci.nsIContentPolicy.TYPE_MEDIA]: "media",
+  [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket",
+  [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp",
+  [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt",
+  [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon",
+  [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch",
+  [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset",
+  [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest"
+};
 const DEFAULT_EDITOR_CONFIG = {
   mode: Editor.modes.text,
   readOnly: true,
   lineNumbers: true
 };
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   lazyEmpty: true,
   // ms
@@ -426,16 +455,30 @@ RequestsMenuView.prototype = Heritage.ex
 
     this.widget = new SideMenuWidget($("#requests-menu-contents"));
     this._splitter = $("#network-inspector-view-splitter");
     this._summary = $("#requests-menu-network-summary-button");
     this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
     this.userInputTimer = Cc["@mozilla.org/timer;1"]
       .createInstance(Ci.nsITimer);
 
+    // Create a tooltip for the newly appended network request item.
+    this.tooltip = new Tooltip(document, {
+      closeOnEvents: [{
+        emitter: $("#requests-menu-contents"),
+        event: "scroll",
+        useCapture: true
+      }]
+    });
+    this.tooltip.startTogglingOnHover(this.widget, this._onHover, {
+      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
+      interactive: true
+    });
+    this.tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
+
     Prefs.filters.forEach(type => this.filterOn(type));
     this.sortContents(this._byTiming);
 
     this.allowFocusOnRightClick = true;
     this.maintainSelectionVisible = true;
 
     this.widget.addEventListener("select", this._onSelect, false);
     this.widget.addEventListener("swap", this._onSwap, false);
@@ -632,25 +675,30 @@ RequestsMenuView.prototype = Heritage.ex
    *        A string representation of when the request was started, which
    *        can be parsed by Date (for example "2012-09-17T19:50:03.699Z").
    * @param string method
    *        Specifies the request method (e.g. "GET", "POST", etc.)
    * @param string url
    *        Specifies the request's url.
    * @param boolean isXHR
    *        True if this request was initiated via XHR.
+   * @param object cause
+   *        Specifies the request's cause. Has the following properties:
+   *        - type: nsContentPolicyType constant
+   *        - loadingDocumentUri: URI of the request origin
+   *        - stacktrace: JS stacktrace of the request
    * @param boolean fromCache
    *        Indicates if the result came from the browser cache
    * @param boolean fromServiceWorker
    *        Indicates if the request has been intercepted by a Service Worker
    */
-  addRequest: function (id, startedDateTime, method, url, isXHR, fromCache,
-    fromServiceWorker) {
-    this._addQueue.push([id, startedDateTime, method, url, isXHR, fromCache,
-      fromServiceWorker]);
+  addRequest: function (id, startedDateTime, method, url, isXHR, cause,
+    fromCache, fromServiceWorker) {
+    this._addQueue.push([id, startedDateTime, method, url, isXHR, cause,
+      fromCache, fromServiceWorker]);
 
     // Lazy updating is disabled in some tests.
     if (!this.lazyUpdate) {
       return void this._flushRequests();
     }
 
     this._flushRequestsTask.arm();
     return undefined;
@@ -880,17 +928,18 @@ RequestsMenuView.prototype = Heritage.ex
   /**
    * Create a new custom request form populated with the data from
    * the currently selected request.
    */
   cloneSelectedRequest: function () {
     let selected = this.selectedItem.attachment;
 
     // Create the element node for the network request item.
-    let menuView = this._createMenuView(selected.method, selected.url);
+    let menuView = this._createMenuView(selected.method, selected.url,
+      selected.cause);
 
     // Append a network request item to this container.
     let newItem = this.push([menuView], {
       attachment: Object.create(selected, {
         isCustom: { value: true }
       })
     });
 
@@ -1447,29 +1496,16 @@ RequestsMenuView.prototype = Heritage.ex
       } else {
         requestTarget.setAttribute("odd", "");
         requestTarget.removeAttribute("even");
       }
     }
   },
 
   /**
-   * Refreshes the toggling anchor for the specified item's tooltip.
-   *
-   * @param object item
-   *        The network request item in this container.
-   */
-  refreshTooltip: function (item) {
-    let tooltip = item.attachment.tooltip;
-    tooltip.hide();
-    tooltip.startTogglingOnHover(item.target, this._onHover);
-    tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
-  },