Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 17 Jun 2015 12:10:37 -0400
changeset 280141 099d6cd6725e6973421edb9f8fb04d8ddd93bbc5
parent 280140 b55daa813afdbb540fc45fefc387f358085477e8 (current diff)
parent 279978 06339ff6a374db45d72d8eb4cbcf040a465a9d62 (diff)
child 280153 31d1aea7dc85d06787fc9063c5bc49f0c1e4f8f7
child 280206 3dcd15422d75d802725e6d0f6921d11543138686
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound. a=merge CLOSED TREE
browser/devtools/layoutview/view.css
--- a/addon-sdk/source/python-lib/cuddlefish/prefs.py
+++ b/addon-sdk/source/python-lib/cuddlefish/prefs.py
@@ -55,17 +55,16 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
     'media.gmp-manager.url.override': 'http://localhost/dummy-gmp-manager.xml',
     'browser.aboutHomeSnippets.updateUrl': 'https://localhost/snippet-dummy',
     'browser.newtab.url' : 'about:blank',
     'browser.search.update': False,
     'browser.search.suggest.enabled' : False,
     'browser.safebrowsing.enabled' : False,
     'browser.safebrowsing.updateURL': 'http://localhost/safebrowsing-dummy/update',
     'browser.safebrowsing.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
-    'browser.safebrowsing.reportURL': 'http://localhost/safebrowsing-dummy/report',
     'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport',
     'browser.selfsupport.url': 'https://localhost/selfsupport-dummy',
     'browser.trackingprotection.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
     'browser.trackingprotection.updateURL': 'http://localhost/safebrowsing-dummy/update',
 
     # Disable app update
     'app.update.enabled' : False,
     'app.update.staging.enabled': False,
--- a/addon-sdk/source/test/preferences/no-connections.json
+++ b/addon-sdk/source/test/preferences/no-connections.json
@@ -12,17 +12,16 @@
   "media.gmp-manager.url.override": "http://localhost/dummy-gmp-manager.xml",
   "browser.aboutHomeSnippets.updateUrl": "https://localhost/snippet-dummy",
   "browser.newtab.url": "about:blank",
   "browser.search.update": false,
   "browser.search.suggest.enabled": false,
   "browser.safebrowsing.enabled": false,
   "browser.safebrowsing.updateURL": "http://localhost/safebrowsing-dummy/update",
   "browser.safebrowsing.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
-  "browser.safebrowsing.reportURL": "http://localhost/safebrowsing-dummy/report",
   "browser.safebrowsing.malware.reportURL": "http://localhost/safebrowsing-dummy/malwarereport",
   "browser.selfsupport.url": "https://localhost/selfsupport-dummy",
   "browser.trackingprotection.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
   "browser.trackingprotection.updateURL": "http://localhost/safebrowsing-dummy/update",
   "browser.newtabpage.directory.source": "data:application/json,{'jetpack':1}",
   "browser.newtabpage.directory.ping": "",
   "extensions.update.url": "http://localhost/extensions-dummy/updateURL",
   "extensions.update.background.url": "http://localhost/extensions-dummy/updateBackgroundURL",
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -355,22 +355,19 @@ pref("dom.w3c_touch_events.safetyY", 120
 pref("browser.safebrowsing.enabled", false);
 
 // Prevent loading of pages identified as malware
 pref("browser.safebrowsing.malware.enabled", false);
 
 pref("browser.safebrowsing.debug", false);
 pref("browser.safebrowsing.updateURL", "https://safebrowsing.google.com/safebrowsing/downloads?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2&key=%GOOGLE_API_KEY%");
 pref("browser.safebrowsing.gethashURL", "https://safebrowsing.google.com/safebrowsing/gethash?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
-pref("browser.safebrowsing.reportURL", "https://safebrowsing.google.com/safebrowsing/report?");
-pref("browser.safebrowsing.reportGenericURL", "http://%LOCALE%.phish-generic.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportErrorURL", "http://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportPhishURL", "http://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportMalwareURL", "http://%LOCALE%.malware-report.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportMalwareErrorURL", "http://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%");
+pref("browser.safebrowsing.reportPhishMistakeURL", "https://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%&url=");
+pref("browser.safebrowsing.reportPhishURL", "https://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%&url=");
+pref("browser.safebrowsing.reportMalwareMistakeURL", "https://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%&url=");
 pref("browser.safebrowsing.appRepURL", "https://sb-ssl.google.com/safebrowsing/clientreport/download?key=%GOOGLE_API_KEY%");
 
 pref("browser.safebrowsing.id", "Firefox");
 
 // Tables for application reputation.
 pref("urlclassifier.downloadBlockTable", "goog-badbinurl-shavar");
 
 // Non-enhanced mode (local url lists) URL list to check for updates
--- a/b2g/config/aries/sources.xml
+++ b/b2g/config/aries/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="94516f787d477dc307e4566f415e0d2f0794f6b9"/>
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="94516f787d477dc307e4566f415e0d2f0794f6b9"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="87a2d8ab9248540910e56921654367b78a587095"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9d0e5057ee5404a31ec1bf76131cb11336a7c3b6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="94516f787d477dc307e4566f415e0d2f0794f6b9"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="94516f787d477dc307e4566f415e0d2f0794f6b9"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="94516f787d477dc307e4566f415e0d2f0794f6b9"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="87a2d8ab9248540910e56921654367b78a587095"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9d0e5057ee5404a31ec1bf76131cb11336a7c3b6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e862ab9177af664f00b4522e2350f4cb13866d73">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="94516f787d477dc307e4566f415e0d2f0794f6b9"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "6271f932e1e918a35ee89f54288bd13385143a71", 
+        "git_revision": "29e990805540fa4d1d03f1144bf9c95364ef6b51", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "4ed62eda01b5f428fa7f6d7a0ff5d662a97c9244", 
+    "revision": "40a9c300245a0e4a8f5c2299a351a970e5f32a3c", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="94516f787d477dc307e4566f415e0d2f0794f6b9"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="6271f932e1e918a35ee89f54288bd13385143a71"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="29e990805540fa4d1d03f1144bf9c95364ef6b51"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3477513bcd385571aa01c0d074849e35bd5e2376"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="46da1a05ac04157669685246d70ac59d48699c9e"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="94516f787d477dc307e4566f415e0d2f0794f6b9"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -976,23 +976,19 @@ pref("browser.safebrowsing.enabled", tru
 pref("browser.safebrowsing.malware.enabled", true);
 pref("browser.safebrowsing.downloads.enabled", true);
 pref("browser.safebrowsing.downloads.remote.enabled", true);
 pref("browser.safebrowsing.downloads.remote.timeout_ms", 10000);
 pref("browser.safebrowsing.debug", false);
 
 pref("browser.safebrowsing.updateURL", "https://safebrowsing.google.com/safebrowsing/downloads?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2&key=%GOOGLE_API_KEY%");
 pref("browser.safebrowsing.gethashURL", "https://safebrowsing.google.com/safebrowsing/gethash?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
-pref("browser.safebrowsing.reportURL", "https://safebrowsing.google.com/safebrowsing/report?");
-pref("browser.safebrowsing.reportGenericURL", "http://%LOCALE%.phish-generic.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportErrorURL", "http://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportPhishURL", "http://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportMalwareURL", "http://%LOCALE%.malware-report.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportMalwareErrorURL", "http://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%");
-
+pref("browser.safebrowsing.reportPhishMistakeURL", "https://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%&url=");
+pref("browser.safebrowsing.reportPhishURL", "https://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%&url=");
+pref("browser.safebrowsing.reportMalwareMistakeURL", "https://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%&url=");
 pref("browser.safebrowsing.malware.reportURL", "https://safebrowsing.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site=");
 
 pref("browser.safebrowsing.appRepURL", "https://sb-ssl.google.com/safebrowsing/clientreport/download?key=%GOOGLE_API_KEY%");
 
 #ifdef MOZILLA_OFFICIAL
 // Normally the "client ID" sent in updates is appinfo.name, but for
 // official Firefox releases from Mozilla we use a special identifier.
 pref("browser.safebrowsing.id", "navclient-auto-ffox");
--- a/browser/base/content/browser-safebrowsing.js
+++ b/browser/base/content/browser-safebrowsing.js
@@ -31,22 +31,12 @@ var gSafeBrowsing = {
   },
 
   /**
    * Used to report a phishing page or a false positive
    * @param name String One of "Phish", "Error", "Malware" or "MalwareError"
    * @return String the report phishing URL.
    */
   getReportURL: function(name) {
-    var reportUrl = SafeBrowsing.getReportURL(name);
-
-    var pageUri = gBrowser.currentURI.clone();
-
-    // Remove the query to avoid including potentially sensitive data
-    if (pageUri instanceof Ci.nsIURL)
-      pageUri.query = '';
-
-    reportUrl += "&url=" + encodeURIComponent(pageUri.asciiSpec);
-
-    return reportUrl;
+    return SafeBrowsing.getReportURL(name, gBrowser.currentURI);
   }
 }
 #endif
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3014,26 +3014,26 @@ let BrowserOnClick = {
 
     let title;
     if (reason === 'malware') {
       title = gNavigatorBundle.getString("safebrowsing.reportedAttackSite");
       buttons[1] = {
         label: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.label"),
         accessKey: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.accessKey"),
         callback: function() {
-          openUILinkIn(gSafeBrowsing.getReportURL('MalwareError'), 'tab');
+          openUILinkIn(gSafeBrowsing.getReportURL('MalwareMistake'), 'tab');
         }
       };
     } else if (reason === 'phishing') {
       title = gNavigatorBundle.getString("safebrowsing.reportedWebForgery");
       buttons[1] = {
         label: gNavigatorBundle.getString("safebrowsing.notAForgeryButton.label"),
         accessKey: gNavigatorBundle.getString("safebrowsing.notAForgeryButton.accessKey"),
         callback: function() {
-          openUILinkIn(gSafeBrowsing.getReportURL('Error'), 'tab');
+          openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');
         }
       };
     } else if (reason === 'unwanted') {
       title = gNavigatorBundle.getString("safebrowsing.reportedUnwantedSite");
       // There is no button for reporting errors since Google doesn't currently
       // provide a URL endpoint for these reports.
     }
 
--- a/browser/base/content/report-phishing-overlay.xul
+++ b/browser/base/content/report-phishing-overlay.xul
@@ -24,12 +24,12 @@
               observes="reportPhishingBroadcaster"
               oncommand="openUILink(gSafeBrowsing.getReportURL('Phish'), event);"
               onclick="checkForMiddleClick(this, event);"/>
     <menuitem id="menu_HelpPopup_reportPhishingErrortoolmenu"
               label="&safeb.palm.notforgery.label2;"
               accesskey="&reportPhishSiteMenu.accesskey;"
               insertbefore="aboutSeparator"
               observes="reportPhishingErrorBroadcaster"
-              oncommand="openUILinkIn(gSafeBrowsing.getReportURL('Error'), 'tab');"
+              oncommand="openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');"
               onclick="checkForMiddleClick(this, event);"/>
   </menupopup>
 </overlay>
--- a/browser/base/content/test/general/browser_trackingUI.js
+++ b/browser/base/content/test/general/browser_trackingUI.js
@@ -53,25 +53,33 @@ function doUpdate() {
 
 function testBenignPage(gTestBrowser)
 {
   // Make sure the doorhanger does NOT appear
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
   is(notification, null, "Tracking Content Doorhanger did NOT appear when protection was ON and tracking was NOT present");
 }
 
-function testTrackingPage(gTestBrowser)
+function* testTrackingPage(gTestBrowser)
 {
   // Make sure the doorhanger appears
   var notification = PopupNotifications.getNotification("bad-content", gTestBrowser);
   isnot(notification, null, "Tracking Content Doorhanger did appear when protection was ON and tracking was present");
   notification.reshow();
+
+  // Wait for the method to be attached after showing the popup
+  yield promiseWaitForCondition(() => {
+    return PopupNotifications.panel.firstChild.disableTrackingContentProtection;
+  });
+
+
   // Make sure the state of the doorhanger includes blocking tracking elements
-  isnot(PopupNotifications.panel.firstChild.isTrackingContentBlocked, 0,
-    "Tracking Content is being blocked");
+  is(PopupNotifications.panel.firstChild.isTrackingContentBlocked,
+     Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
+     "Tracking Content is being blocked");
 
   // Make sure the notification has no trackingblockdisabled attribute
   ok(!PopupNotifications.panel.firstChild.hasAttribute("trackingblockdisabled"),
     "Doorhanger must have no trackingblockdisabled attribute");
 
   // Disable Tracking Content Protection for the page (which reloads the page)
   PopupNotifications.panel.firstChild.disableTrackingContentProtection();
 }
@@ -122,17 +130,17 @@ add_task(function* () {
   Services.prefs.setBoolPref(PREF, true);
 
   // Point tab to a test page NOT containing tracking elements
   yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html");
   testBenignPage(gBrowser.getBrowserForTab(tab));
 
   // Point tab to a test page containing tracking elements
   yield promiseTabLoadEvent(tab, "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html");
-  testTrackingPage(gBrowser.getBrowserForTab(tab));
+  yield testTrackingPage(gBrowser.getBrowserForTab(tab));
 
   // Wait for tab to reload following tracking-protection page white-listing
   yield promiseTabLoadEvent(tab);
   // Tracking content must be white-listed (NOT blocked)
   testTrackingPageWhitelisted(gBrowser.getBrowserForTab(tab));
 
   // Disable Tracking Protection
   Services.prefs.setBoolPref(PREF, false);
--- a/browser/components/loop/content/css/panel.css
+++ b/browser/components/loop/content/css/panel.css
@@ -242,50 +242,16 @@ body {
   background-image: url("../shared/img/check.svg#check-blue");
 }
 
 .new-room-view > .context > .checkbox-wrapper > label {
   color: #3c3c3c;
   font-weight: 700;
 }
 
-.new-room-view > .context > .context-content {
-  border: 1px solid #0096dd;
-  border-radius: 3px;
-  background: #fff;
-  padding: .8em;
-  display: flex;
-  flex-flow: row nowrap;
-  line-height: 1.1em;
-}
-
-.new-room-view > .context > .context-content > .context-preview {
-  float: left;
-  width: 16px;
-  max-height: 16px;
-  -moz-margin-end: .8em;
-  flex: 0 1 auto;
-}
-
-html[dir="rtl"] .new-room-view > .context > .context-content > .context-preview {
-  float: left;
-}
-
-.new-room-view > .context > .context-content > .context-description {
-  flex: 0 1 auto;
-  display: block;
-}
-
-.new-room-view > .context > .context-content > .context-description > .context-url {
-  display: block;
-  color: #59A1D7;
-  font-weight: 700;
-  clear: both;
-}
-
 .new-room-view > .btn {
   display: block;
   font-size: 1rem;
   margin: 0 auto .5rem;
   width: 100%;
   padding: .5rem 1rem;
   border-radius: 0 0 3px 3px;
 }
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -760,30 +760,29 @@ loop.panel = (function(_, mozL10n) {
         // Empty catch - if there's an error, then we won't show the context.
       }
 
       var contextClasses = React.addons.classSet({
         context: true,
         hide: !hostname ||
           !this.props.mozLoop.getLoopPref("contextInConversations.enabled")
       });
-      var thumbnail = this.state.previewImage || "loop/shared/img/icons-16x16.svg#globe";
 
       return (
         React.createElement("div", {className: "new-room-view"}, 
           React.createElement("div", {className: contextClasses}, 
             React.createElement(Checkbox, {label: mozL10n.get("context_inroom_label"), 
                       onChange: this.onCheckboxChange}), 
-            React.createElement("div", {className: "context-content"}, 
-              React.createElement("img", {className: "context-preview", src: thumbnail}), 
-              React.createElement("span", {className: "context-description"}, 
-                this.state.description, 
-                React.createElement("span", {className: "context-url"}, hostname)
-              )
-            )
+            React.createElement(sharedViews.ContextUrlView, {
+              allowClick: false, 
+              description: this.state.description, 
+              showContextTitle: false, 
+              thumbnail: this.state.previewImage, 
+              url: this.state.url, 
+              useDesktopPaths: true})
           ), 
           React.createElement("button", {className: "btn btn-info new-room-button", 
                   onClick: this.handleCreateButtonClick, 
                   disabled: this.props.pendingOperation}, 
             mozL10n.get("rooms_new_room_button_label")
           )
         )
       );
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -760,30 +760,29 @@ loop.panel = (function(_, mozL10n) {
         // Empty catch - if there's an error, then we won't show the context.
       }
 
       var contextClasses = React.addons.classSet({
         context: true,
         hide: !hostname ||
           !this.props.mozLoop.getLoopPref("contextInConversations.enabled")
       });
-      var thumbnail = this.state.previewImage || "loop/shared/img/icons-16x16.svg#globe";
 
       return (
         <div className="new-room-view">
           <div className={contextClasses}>
             <Checkbox label={mozL10n.get("context_inroom_label")}
                       onChange={this.onCheckboxChange} />
-            <div className="context-content">
-              <img className="context-preview" src={thumbnail} />
-              <span className="context-description">
-                {this.state.description}
-                <span className="context-url">{hostname}</span>
-              </span>
-            </div>
+            <sharedViews.ContextUrlView
+              allowClick={false}
+              description={this.state.description}
+              showContextTitle={false}
+              thumbnail={this.state.previewImage}
+              url={this.state.url}
+              useDesktopPaths={true} />
           </div>
           <button className="btn btn-info new-room-button"
                   onClick={this.handleCreateButtonClick}
                   disabled={this.props.pendingOperation}>
             {mozL10n.get("rooms_new_room_button_label")}
           </button>
         </div>
       );
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -756,17 +756,20 @@ loop.roomViews = (function(mozL10n) {
               ), 
               React.createElement(DesktopRoomContextView, {
                 dispatcher: this.props.dispatcher, 
                 error: this.state.error, 
                 savingContext: this.state.savingContext, 
                 mozLoop: this.props.mozLoop, 
                 roomData: roomData, 
                 show: !shouldRenderInvitationOverlay && shouldRenderContextView}), 
-              React.createElement(sharedViews.TextChatView, {dispatcher: this.props.dispatcher})
+              React.createElement(sharedViews.TextChatView, {
+                dispatcher: this.props.dispatcher, 
+                showAlways: false, 
+                showRoomName: false})
             )
           );
         }
       }
     }
   });
 
   return {
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -756,17 +756,20 @@ loop.roomViews = (function(mozL10n) {
               </div>
               <DesktopRoomContextView
                 dispatcher={this.props.dispatcher}
                 error={this.state.error}
                 savingContext={this.state.savingContext}
                 mozLoop={this.props.mozLoop}
                 roomData={roomData}
                 show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
-              <sharedViews.TextChatView dispatcher={this.props.dispatcher} />
+              <sharedViews.TextChatView
+                dispatcher={this.props.dispatcher}
+                showAlways={false}
+                showRoomName={false} />
             </div>
           );
         }
       }
     }
   });
 
   return {
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -496,8 +496,64 @@ html[dir="rtl"] .checkbox {
 
 .checkbox.disabled {
   border: 1px solid #909090;
 }
 
 .checkbox.checked.disabled {
   background-image: url("../img/check.svg#check-disabled");
 }
+
+/* ContextUrlView classes */
+
+.context-content {
+  color: black;
+  text-align: left;
+}
+
+html[dir="rtl"] .context-content {
+  text-align: right;
+}
+
+.context-content > p {
+  font-weight: bold;
+  margin-bottom: .8em;
+  margin-top: 0;
+}
+
+.context-wrapper {
+  border: 1px solid #0096dd;
+  border-radius: 3px;
+  background: #fff;
+  padding: .8em;
+  /* Use the flex row mode to position the elements next to each other. */
+  display: flex;
+  flex-flow: row nowrap;
+  line-height: 1.1em;
+}
+
+.context-wrapper > .context-preview {
+  float: left;
+  /* 16px is standard height/width for a favicon */
+  width: 16px;
+  max-height: 16px;
+  margin-right: .8em;
+  flex: 0 1 auto;
+}
+
+html[dir="rtl"] .context-wrapper > .context-preview {
+  float: left;
+  margin-left: .8em;
+  margin-right: 0;
+}
+
+.context-wrapper > .context-description {
+  flex: 0 1 auto;
+  display: block;
+  color: black;
+}
+
+.context-wrapper > .context-description > .context-url {
+  display: block;
+  color: #59A1D7;
+  font-weight: 700;
+  clear: both;
+}
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -261,21 +261,16 @@
 }
 
 .standalone .local-stream,
 .standalone .remote-inset-stream {
   /* required to have it superimposed to the control toolbar */
   z-index: 1001;
 }
 
-.standalone .room-conversation .local-stream,
-.standalone .room-conversation .remote-inset-stream {
-  box-shadow: none;
-}
-
 /* Side by side video elements */
 
 .conversation .media.side-by-side .focus-stream {
   width: 50%;
   float: left;
 }
 
 .conversation .media.side-by-side .local-stream,
@@ -710,17 +705,16 @@ html, .fx-embedded, #main,
     display: none;
   }
 
   .standalone .local-stream {
     flex: 1;
     min-width: 120px;
     min-height: 150px;
     width: 100%;
-    box-shadow: none;
   }
 
   /* Nested video elements */
   .standalone .conversation .media.nested {
     display: flex;
     flex-direction: column;
     align-items: center;
     justify-content: center;
@@ -882,16 +876,23 @@ body[platform="win"] .share-service-drop
 .dropdown-menu-item:hover > .icon-add-share-service {
   background-image: url("../img/icons-16x16.svg#add-hover");
 }
 
 .dropdown-menu-item:hover:active > .icon-add-share-service {
   background-image: url("../img/icons-16x16.svg#add-active");
 }
 
+.context-url-view-wrapper {
+  padding-left: 1em;
+  padding-right: 1em;
+  padding-bottom: 0.5em;
+  background-color: #E8F6FE;
+}
+
 .room-context {
   background: rgba(0,0,0,.6);
   border-top: 2px solid #444;
   border-bottom: 2px solid #444;
   padding: .5rem;
   max-height: 400px;
   position: absolute;
   left: 0;
@@ -1253,32 +1254,49 @@ html[dir="rtl"] .room-context-btn-edit {
   overflow: scroll;
 }
 
 .text-chat-entry {
   text-align: end;
   margin-bottom: 1.5em;
 }
 
-.text-chat-entry > span {
+.text-chat-entry > p {
   border-width: 1px;
   border-style: solid;
   border-color: #0095dd;
   border-radius: 10000px;
   padding: .5em 1em;
+  /* Drop the default margins from the 'p' element. */
+  margin: 0;
+  /* inline-block stops the elements taking 100% of the text-chat-view width */
+  display: inline-block;
 }
 
 .text-chat-entry.received {
   text-align: start;
 }
 
-.text-chat-entry.received > span {
+.text-chat-entry.received > p {
   border-color: #d8d8d8;
 }
 
+.text-chat-entry.special > p {
+  border: none;
+}
+
+.text-chat-entry.special.room-name {
+  color: black;
+  font-weight: bold;
+  text-align: start;
+  background-color: #E8F6FE;
+  padding-bottom: 0;
+  margin-bottom: 0;
+}
+
 .text-chat-box {
   margin: auto;
 }
 
 .text-chat-box > form > input {
   width: 100%;
   height: 40px;
   padding: 0 .5em .5em;
--- a/browser/components/loop/content/shared/js/textChatStore.js
+++ b/browser/components/loop/content/shared/js/textChatStore.js
@@ -1,38 +1,42 @@
 /* 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 loop = loop || {};
 loop.store = loop.store || {};
 
-loop.store.TextChatStore = (function() {
+loop.store.TextChatStore = (function(mozL10n) {
   "use strict";
 
   var sharedActions = loop.shared.actions;
 
   var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES = {
     RECEIVED: "recv",
-    SENT: "sent"
+    SENT: "sent",
+    SPECIAL: "special"
   };
 
   var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES = {
-    TEXT: "chat-text"
+    CONTEXT: "chat-context",
+    TEXT: "chat-text",
+    ROOM_NAME: "room-name"
   };
 
   /**
    * A store to handle text chats. The store has a message list that may
    * contain different types of messages and data.
    */
   var TextChatStore = loop.store.createStore({
     actions: [
       "dataChannelsAvailable",
       "receivedTextChatMessage",
-      "sendTextChatMessage"
+      "sendTextChatMessage",
+      "updateRoomInfo"
     ],
 
     /**
      * Initializes the store.
      *
      * @param  {Object} options An object containing options for this store.
      *                          It should consist of:
      *                          - sdkDriver: The sdkDriver to use for sending
@@ -69,31 +73,39 @@ loop.store.TextChatStore = (function() {
     dataChannelsAvailable: function() {
       this.setStoreState({ textChatEnabled: true });
       window.dispatchEvent(new CustomEvent("LoopChatEnabled"));
     },
 
     /**
      * Appends a message to the store, which may be of type 'sent' or 'received'.
      *
-     * @param {String} type
-     * @param {sharedActions.ReceivedTextChatMessage|sharedActions.SendTextChatMessage} actionData
+     * @param {CHAT_MESSAGE_TYPES} type
+     * @param {Object} messageData Data for this message. Options are:
+     * - {CHAT_CONTENT_TYPES} contentType
+     * - {String}             message     The message detail.
+     * - {Object}             extraData   Extra data associated with the message.
      */
-    _appendTextChatMessage: function(type, actionData) {
+    _appendTextChatMessage: function(type, messageData) {
       // We create a new list to avoid updating the store's state directly,
       // which confuses the views.
       var message = {
         type: type,
-        contentType: actionData.contentType,
-        message: actionData.message
+        contentType: messageData.contentType,
+        message: messageData.message,
+        extraData: messageData.extraData
       };
       var newList = this._storeState.messageList.concat(message);
       this.setStoreState({ messageList: newList });
 
-      window.dispatchEvent(new CustomEvent("LoopChatMessageAppended"));
+      // Notify MozLoopService if appropriate that a message has been appended
+      // and it should therefore check if we need a different sized window or not.
+      if (type != CHAT_MESSAGE_TYPES.SPECIAL) {
+        window.dispatchEvent(new CustomEvent("LoopChatMessageAppended"));
+      }
     },
 
     /**
      * Handles received text chat messages.
      *
      * @param {sharedActions.ReceivedTextChatMessage} actionData
      */
     receivedTextChatMessage: function(actionData) {
@@ -109,13 +121,43 @@ loop.store.TextChatStore = (function() {
     /**
      * Handles sending of a chat message.
      *
      * @param {sharedActions.SendTextChatMessage} actionData
      */
     sendTextChatMessage: function(actionData) {
       this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SENT, actionData);
       this._sdkDriver.sendTextChatMessage(actionData);
+    },
+
+    /**
+     * Handles receiving information about the room - specifically the room name
+     * so it can be added to the list.
+     *
+     * @param  {sharedActions.UpdateRoomInfo} actionData
+     */
+    updateRoomInfo: function(actionData) {
+      // XXX When we add special messages to desktop, we'll need to not post
+      // multiple changes of room name, only the first. Bug 1171940 should fix this.
+      this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
+        contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
+        message: mozL10n.get("rooms_welcome_title", {conversationName: actionData.roomName})
+      });
+
+      // Append the context if we have any.
+      if ("urls" in actionData && actionData.urls.length) {
+        // We only support the first url at the moment.
+        var urlData = actionData.urls[0];
+
+        this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
+          contentType: CHAT_CONTENT_TYPES.CONTEXT,
+          message: urlData.description,
+          extraData: {
+            location: urlData.location,
+            thumbnail: urlData.thumbnail
+          }
+        });
+      }
     }
   });
 
   return TextChatStore;
-})();
+})(navigator.mozL10n || window.mozL10n);
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -1,54 +1,59 @@
 /* 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 loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = loop.shared.views || {};
-loop.shared.views.TextChatView = (function(mozl10n) {
+loop.shared.views.TextChatView = (function(mozL10n) {
   var sharedActions = loop.shared.actions;
+  var sharedViews = loop.shared.views;
   var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
   var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
 
   /**
    * Renders an individual entry for the text chat entries view.
    */
   var TextChatEntry = React.createClass({displayName: "TextChatEntry",
     mixins: [React.addons.PureRenderMixin],
 
     propTypes: {
+      contentType: React.PropTypes.string.isRequired,
       message: React.PropTypes.string.isRequired,
       type: React.PropTypes.string.isRequired
     },
 
     render: function() {
       var classes = React.addons.classSet({
         "text-chat-entry": true,
-        "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED
+        "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
+        "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
+        "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
       return (
         React.createElement("div", {className: classes}, 
-          React.createElement("span", null, this.props.message)
+          React.createElement("p", null, this.props.message)
         )
       );
     }
   });
 
   /**
    * Manages the text entries in the chat entries view. This is split out from
    * TextChatView so that scrolling can be managed more efficiently - this
    * component only updates when the message list is changed.
    */
   var TextChatEntriesView = React.createClass({displayName: "TextChatEntriesView",
     mixins: [React.addons.PureRenderMixin],
 
     propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       messageList: React.PropTypes.array.isRequired
     },
 
     componentWillUpdate: function() {
       var node = this.getDOMNode();
       if (!node) {
         return;
       }
@@ -71,47 +76,71 @@ loop.shared.views.TextChatView = (functi
         return null;
       }
 
       return (
         React.createElement("div", {className: "text-chat-entries"}, 
           React.createElement("div", {className: "text-chat-scroller"}, 
             
               this.props.messageList.map(function(entry, i) {
+                if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL &&
+                    entry.contentType === CHAT_CONTENT_TYPES.CONTEXT) {
+                  return (
+                    React.createElement("div", {className: "context-url-view-wrapper"}, 
+                      React.createElement(sharedViews.ContextUrlView, {
+                        allowClick: true, 
+                        description: entry.message, 
+                        dispatcher: this.props.dispatcher, 
+                        key: i, 
+                        showContextTitle: true, 
+                        thumbnail: entry.extraData.thumbnail, 
+                        url: entry.extraData.location, 
+                        useDesktopPaths: false})
+                    )
+                  );
+                }
+
                 return (
                   React.createElement(TextChatEntry, {key: i, 
+                                 contentType: entry.contentType, 
                                  message: entry.message, 
                                  type: entry.type})
                 );
               }, this)
             
           )
         )
       );
     }
   });
 
   /**
-   * Displays the text chat view. This includes the text chat messages as well
-   * as a field for entering new messages.
+   * Displays a text chat entry input box for sending messages.
+   *
+   * @property {loop.Dispatcher} dispatcher
+   * @property {Boolean} showPlaceholder    Set to true to show the placeholder message.
+   * @property {Boolean} textChatEnabled    Set to true to enable the box. If false, the
+   *                                        text chat box won't be displayed.
    */
-  var TextChatView = React.createClass({displayName: "TextChatView",
+  var TextChatInputView = React.createClass({displayName: "TextChatInputView",
     mixins: [
       React.addons.LinkedStateMixin,
-      loop.store.StoreMixin("textChatStore")
+      React.addons.PureRenderMixin
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      showPlaceholder: React.PropTypes.bool.isRequired,
+      textChatEnabled: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
-      return _.extend({
+      return {
         messageDetail: ""
-      }, this.getStoreState());
+      };
     },
 
     /**
      * Handles a key being pressed - looking for the return key for submitting
      * the form.
      *
      * @param {Object} event The DOM event.
      */
@@ -134,32 +163,89 @@ loop.shared.views.TextChatView = (functi
         message: this.state.messageDetail
       }));
 
       // Reset the form to empty, ready for the next message.
       this.setState({ messageDetail: "" });
     },
 
     render: function() {
-      if (!this.state.textChatEnabled) {
+      if (!this.props.textChatEnabled) {
         return null;
       }
 
-      var messageList = this.state.messageList;
+      return (
+        React.createElement("div", {className: "text-chat-box"}, 
+          React.createElement("form", {onSubmit: this.handleFormSubmit}, 
+            React.createElement("input", {type: "text", 
+                   placeholder: this.props.showPlaceholder ? mozL10n.get("chat_textbox_placeholder") : "", 
+                   onKeyDown: this.handleKeyDown, 
+                   valueLink: this.linkState("messageDetail")})
+          )
+        )
+      );
+    }
+  });
+
+  /**
+   * Displays the text chat view. This includes the text chat messages as well
+   * as a field for entering new messages.
+   *
+   * @property {loop.Dispatcher} dispatcher
+   * @property {Boolean} showAlways         If false, the view will not be rendered
+   *                                        if text chat is not enabled and the
+   *                                        message list is empty.
+   * @property {Boolean} showRoomName       Set to true to show the room name special
+   *                                        list item.
+   */
+  var TextChatView = React.createClass({displayName: "TextChatView",
+    mixins: [
+      React.addons.LinkedStateMixin,
+      loop.store.StoreMixin("textChatStore")
+    ],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      showAlways: React.PropTypes.bool.isRequired,
+      showRoomName: React.PropTypes.bool.isRequired
+    },
+
+    getInitialState: function() {
+      return this.getStoreState();
+    },
+
+    render: function() {
+      var messageList;
+      var hasNonSpecialMessages;
+
+      if (this.props.showRoomName) {
+        messageList = this.state.messageList;
+        hasNonSpecialMessages = messageList.some(function(item) {
+          return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
+        });
+      } else {
+        // XXX Desktop should be showing the initial context here (bug 1171940).
+        messageList = this.state.messageList.filter(function(item) {
+          return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
+        });
+        hasNonSpecialMessages = !!messageList.length;
+      }
+
+      if (!this.props.showAlways && !this.state.textChatEnabled && !messageList.length) {
+        return null;
+      }
 
       return (
         React.createElement("div", {className: "text-chat-view"}, 
-          React.createElement(TextChatEntriesView, {messageList: messageList}), 
-          React.createElement("div", {className: "text-chat-box"}, 
-            React.createElement("form", {onSubmit: this.handleFormSubmit}, 
-              React.createElement("input", {type: "text", 
-                     placeholder: messageList.length ? "" : mozl10n.get("chat_textbox_placeholder"), 
-                     onKeyDown: this.handleKeyDown, 
-                     valueLink: this.linkState("messageDetail")})
-            )
-          )
+          React.createElement(TextChatEntriesView, {
+            dispatcher: this.props.dispatcher, 
+            messageList: messageList}), 
+          React.createElement(TextChatInputView, {
+            dispatcher: this.props.dispatcher, 
+            showPlaceholder: !hasNonSpecialMessages, 
+            textChatEnabled: this.state.textChatEnabled})
         )
       );
     }
   });
 
   return TextChatView;
 })(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -1,54 +1,59 @@
 /* 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 loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = loop.shared.views || {};
-loop.shared.views.TextChatView = (function(mozl10n) {
+loop.shared.views.TextChatView = (function(mozL10n) {
   var sharedActions = loop.shared.actions;
+  var sharedViews = loop.shared.views;
   var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
   var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
 
   /**
    * Renders an individual entry for the text chat entries view.
    */
   var TextChatEntry = React.createClass({
     mixins: [React.addons.PureRenderMixin],
 
     propTypes: {
+      contentType: React.PropTypes.string.isRequired,
       message: React.PropTypes.string.isRequired,
       type: React.PropTypes.string.isRequired
     },
 
     render: function() {
       var classes = React.addons.classSet({
         "text-chat-entry": true,
-        "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED
+        "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
+        "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
+        "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
       return (
         <div className={classes}>
-          <span>{this.props.message}</span>
+          <p>{this.props.message}</p>
         </div>
       );
     }
   });
 
   /**
    * Manages the text entries in the chat entries view. This is split out from
    * TextChatView so that scrolling can be managed more efficiently - this
    * component only updates when the message list is changed.
    */
   var TextChatEntriesView = React.createClass({
     mixins: [React.addons.PureRenderMixin],
 
     propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       messageList: React.PropTypes.array.isRequired
     },
 
     componentWillUpdate: function() {
       var node = this.getDOMNode();
       if (!node) {
         return;
       }
@@ -71,47 +76,71 @@ loop.shared.views.TextChatView = (functi
         return null;
       }
 
       return (
         <div className="text-chat-entries">
           <div className="text-chat-scroller">
             {
               this.props.messageList.map(function(entry, i) {
+                if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL &&
+                    entry.contentType === CHAT_CONTENT_TYPES.CONTEXT) {
+                  return (
+                    <div className="context-url-view-wrapper">
+                      <sharedViews.ContextUrlView
+                        allowClick={true}
+                        description={entry.message}
+                        dispatcher={this.props.dispatcher}
+                        key={i}
+                        showContextTitle={true}
+                        thumbnail={entry.extraData.thumbnail}
+                        url={entry.extraData.location}
+                        useDesktopPaths={false} />
+                    </div>
+                  );
+                }
+
                 return (
                   <TextChatEntry key={i}
+                                 contentType={entry.contentType}
                                  message={entry.message}
                                  type={entry.type} />
                 );
               }, this)
             }
           </div>
         </div>
       );
     }
   });
 
   /**
-   * Displays the text chat view. This includes the text chat messages as well
-   * as a field for entering new messages.
+   * Displays a text chat entry input box for sending messages.
+   *
+   * @property {loop.Dispatcher} dispatcher
+   * @property {Boolean} showPlaceholder    Set to true to show the placeholder message.
+   * @property {Boolean} textChatEnabled    Set to true to enable the box. If false, the
+   *                                        text chat box won't be displayed.
    */
-  var TextChatView = React.createClass({
+  var TextChatInputView = React.createClass({
     mixins: [
       React.addons.LinkedStateMixin,
-      loop.store.StoreMixin("textChatStore")
+      React.addons.PureRenderMixin
     ],
 
     propTypes: {
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      showPlaceholder: React.PropTypes.bool.isRequired,
+      textChatEnabled: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
-      return _.extend({
+      return {
         messageDetail: ""
-      }, this.getStoreState());
+      };
     },
 
     /**
      * Handles a key being pressed - looking for the return key for submitting
      * the form.
      *
      * @param {Object} event The DOM event.
      */
@@ -134,32 +163,89 @@ loop.shared.views.TextChatView = (functi
         message: this.state.messageDetail
       }));
 
       // Reset the form to empty, ready for the next message.
       this.setState({ messageDetail: "" });
     },
 
     render: function() {
-      if (!this.state.textChatEnabled) {
+      if (!this.props.textChatEnabled) {
         return null;
       }
 
-      var messageList = this.state.messageList;
+      return (
+        <div className="text-chat-box">
+          <form onSubmit={this.handleFormSubmit}>
+            <input type="text"
+                   placeholder={this.props.showPlaceholder ? mozL10n.get("chat_textbox_placeholder") : ""}
+                   onKeyDown={this.handleKeyDown}
+                   valueLink={this.linkState("messageDetail")} />
+          </form>
+        </div>
+      );
+    }
+  });
+
+  /**
+   * Displays the text chat view. This includes the text chat messages as well
+   * as a field for entering new messages.
+   *
+   * @property {loop.Dispatcher} dispatcher
+   * @property {Boolean} showAlways         If false, the view will not be rendered
+   *                                        if text chat is not enabled and the
+   *                                        message list is empty.
+   * @property {Boolean} showRoomName       Set to true to show the room name special
+   *                                        list item.
+   */
+  var TextChatView = React.createClass({
+    mixins: [
+      React.addons.LinkedStateMixin,
+      loop.store.StoreMixin("textChatStore")
+    ],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      showAlways: React.PropTypes.bool.isRequired,
+      showRoomName: React.PropTypes.bool.isRequired
+    },
+
+    getInitialState: function() {
+      return this.getStoreState();
+    },
+
+    render: function() {
+      var messageList;
+      var hasNonSpecialMessages;
+
+      if (this.props.showRoomName) {
+        messageList = this.state.messageList;
+        hasNonSpecialMessages = messageList.some(function(item) {
+          return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
+        });
+      } else {
+        // XXX Desktop should be showing the initial context here (bug 1171940).
+        messageList = this.state.messageList.filter(function(item) {
+          return item.type !== CHAT_MESSAGE_TYPES.SPECIAL;
+        });
+        hasNonSpecialMessages = !!messageList.length;
+      }
+
+      if (!this.props.showAlways && !this.state.textChatEnabled && !messageList.length) {
+        return null;
+      }
 
       return (
         <div className="text-chat-view">
-          <TextChatEntriesView messageList={messageList} />
-          <div className="text-chat-box">
-            <form onSubmit={this.handleFormSubmit}>
-              <input type="text"
-                     placeholder={messageList.length ? "" : mozl10n.get("chat_textbox_placeholder")}
-                     onKeyDown={this.handleKeyDown}
-                     valueLink={this.linkState("messageDetail")} />
-            </form>
-          </div>
+          <TextChatEntriesView
+            dispatcher={this.props.dispatcher}
+            messageList={messageList} />
+          <TextChatInputView
+            dispatcher={this.props.dispatcher}
+            showPlaceholder={!hasNonSpecialMessages}
+            textChatEnabled={this.state.textChatEnabled} />
         </div>
       );
     }
   });
 
   return TextChatView;
 })(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -685,16 +685,104 @@ loop.shared.views = (function(_, l10n) {
     mixins: [React.addons.PureRenderMixin],
 
     render: function() {
         return React.createElement("div", {className: "avatar"});
     }
   });
 
   /**
+   * Renders a url that's part of context on the display.
+   *
+   * @property {Boolean} allowClick         Set to true to allow the url to be clicked. If this
+   *                                        is specified, then 'dispatcher' is also required.
+   * @property {String}  description        The description for the context url.
+   * @property {loop.Dispatcher} dispatcher
+   * @property {Boolean} showContextTitle   Whether or not to show the "Let's talk about" title.
+   * @property {String}  thumbnail          The thumbnail url (expected to be a data url) to
+   *                                        display. If not specified, a fallback url will be
+   *                                        shown.
+   * @property {String}  url                The url to be displayed. If not present or invalid,
+   *                                        then this view won't be displayed.
+   * @property {Boolean} useDesktopPaths    Whether or not to use the desktop paths for for the
+   *                                        fallback url.
+   */
+  var ContextUrlView = React.createClass({displayName: "ContextUrlView",
+    mixins: [React.addons.PureRenderMixin],
+
+    PropTypes: {
+      allowClick: React.PropTypes.bool.isRequired,
+      description: React.PropTypes.string.isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
+      showContextTitle: React.PropTypes.bool.isRequired,
+      thumbnail: React.PropTypes.string,
+      url: React.PropTypes.string,
+      useDesktopPaths: React.PropTypes.bool.isRequired
+    },
+
+    /**
+     * Dispatches an action to record when the link is clicked.
+     */
+    handleLinkClick: function() {
+      if (!this.props.allowClick) {
+        return;
+      }
+
+      this.props.dispatcher.dispatch(new sharedActions.RecordClick({
+        linkInfo: "Shared URL"
+      }));
+    },
+
+    /**
+     * Renders the context title ("Let's talk about") if necessary.
+     */
+    renderContextTitle: function() {
+      if (!this.props.showContextTitle) {
+        return null;
+      }
+
+      return React.createElement("p", null, l10n.get("context_inroom_label"));
+    },
+
+    render: function() {
+      var hostname;
+
+      try {
+        hostname = new URL(this.props.url).hostname;
+      } catch (ex) {
+        return null;
+      }
+
+      var thumbnail = this.props.thumbnail;
+
+      if (!thumbnail) {
+        thumbnail = this.props.useDesktopPaths ?
+          "loop/shared/img/icons-16x16.svg#globe" :
+          "shared/img/icons-16x16.svg#globe";
+      }
+
+      return (
+        React.createElement("div", {className: "context-content"}, 
+          this.renderContextTitle(), 
+          React.createElement("div", {className: "context-wrapper"}, 
+            React.createElement("img", {className: "context-preview", src: thumbnail}), 
+            React.createElement("span", {className: "context-description"}, 
+              this.props.description, 
+              React.createElement("a", {className: "context-url", 
+                 onClick: this.handleLinkClick, 
+                 href: this.props.allowClick ? this.props.url : null, 
+                 target: "_blank"}, hostname)
+            )
+          )
+        )
+      );
+    }
+  });
+
+  /**
    * Renders a media element for display. This also handles displaying an avatar
    * instead of the video, and attaching a video stream to the video element.
    */
   var MediaView = React.createClass({displayName: "MediaView",
     // srcVideoObject should be ok for a shallow comparison, so we are safe
     // to use the pure render mixin here.
     mixins: [React.addons.PureRenderMixin],
 
@@ -795,16 +883,17 @@ loop.shared.views = (function(_, l10n) {
     }
   });
 
   return {
     AvatarView: AvatarView,
     Button: Button,
     ButtonGroup: ButtonGroup,
     Checkbox: Checkbox,
+    ContextUrlView: ContextUrlView,
     ConversationView: ConversationView,
     ConversationToolbar: ConversationToolbar,
     MediaControlButton: MediaControlButton,
     MediaView: MediaView,
     ScreenShareControlButton: ScreenShareControlButton,
     NotificationListView: NotificationListView
   };
 })(_, navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -685,16 +685,104 @@ loop.shared.views = (function(_, l10n) {
     mixins: [React.addons.PureRenderMixin],
 
     render: function() {
         return <div className="avatar"/>;
     }
   });
 
   /**
+   * Renders a url that's part of context on the display.
+   *
+   * @property {Boolean} allowClick         Set to true to allow the url to be clicked. If this
+   *                                        is specified, then 'dispatcher' is also required.
+   * @property {String}  description        The description for the context url.
+   * @property {loop.Dispatcher} dispatcher
+   * @property {Boolean} showContextTitle   Whether or not to show the "Let's talk about" title.
+   * @property {String}  thumbnail          The thumbnail url (expected to be a data url) to
+   *                                        display. If not specified, a fallback url will be
+   *                                        shown.
+   * @property {String}  url                The url to be displayed. If not present or invalid,
+   *                                        then this view won't be displayed.
+   * @property {Boolean} useDesktopPaths    Whether or not to use the desktop paths for for the
+   *                                        fallback url.
+   */
+  var ContextUrlView = React.createClass({
+    mixins: [React.addons.PureRenderMixin],
+
+    PropTypes: {
+      allowClick: React.PropTypes.bool.isRequired,
+      description: React.PropTypes.string.isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
+      showContextTitle: React.PropTypes.bool.isRequired,
+      thumbnail: React.PropTypes.string,
+      url: React.PropTypes.string,
+      useDesktopPaths: React.PropTypes.bool.isRequired
+    },
+
+    /**
+     * Dispatches an action to record when the link is clicked.
+     */
+    handleLinkClick: function() {
+      if (!this.props.allowClick) {
+        return;
+      }
+
+      this.props.dispatcher.dispatch(new sharedActions.RecordClick({
+        linkInfo: "Shared URL"
+      }));
+    },
+
+    /**
+     * Renders the context title ("Let's talk about") if necessary.
+     */
+    renderContextTitle: function() {
+      if (!this.props.showContextTitle) {
+        return null;
+      }
+
+      return <p>{l10n.get("context_inroom_label")}</p>;
+    },
+
+    render: function() {
+      var hostname;
+
+      try {
+        hostname = new URL(this.props.url).hostname;
+      } catch (ex) {
+        return null;
+      }
+
+      var thumbnail = this.props.thumbnail;
+
+      if (!thumbnail) {
+        thumbnail = this.props.useDesktopPaths ?
+          "loop/shared/img/icons-16x16.svg#globe" :
+          "shared/img/icons-16x16.svg#globe";
+      }
+
+      return (
+        <div className="context-content">
+          {this.renderContextTitle()}
+          <div className="context-wrapper">
+            <img className="context-preview" src={thumbnail} />
+            <span className="context-description">
+              {this.props.description}
+              <a className="context-url"
+                 onClick={this.handleLinkClick}
+                 href={this.props.allowClick ? this.props.url : null}
+                 target="_blank">{hostname}</a>
+            </span>
+          </div>
+        </div>
+      );
+    }
+  });
+
+  /**
    * Renders a media element for display. This also handles displaying an avatar
    * instead of the video, and attaching a video stream to the video element.
    */
   var MediaView = React.createClass({
     // srcVideoObject should be ok for a shallow comparison, so we are safe
     // to use the pure render mixin here.
     mixins: [React.addons.PureRenderMixin],
 
@@ -795,16 +883,17 @@ loop.shared.views = (function(_, l10n) {
     }
   });
 
   return {
     AvatarView: AvatarView,
     Button: Button,
     ButtonGroup: ButtonGroup,
     Checkbox: Checkbox,
+    ContextUrlView: ContextUrlView,
     ConversationView: ConversationView,
     ConversationToolbar: ConversationToolbar,
     MediaControlButton: MediaControlButton,
     MediaView: MediaView,
     ScreenShareControlButton: ScreenShareControlButton,
     NotificationListView: NotificationListView
   };
 })(_, navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -360,23 +360,22 @@ p.standalone-btn-label {
 }
 
 /**
  * The .text-chat-* styles are very temporarily whilst we work on text chat
  * (bug 1108892 and dependencies).
  */
 .text-chat-view {
   height: 60px;
-  color: white;
+  color: black;
 }
 
 .text-chat-entries {
   /* XXX Should use flex, this is just for the initial implementation. */
   height: calc(100% - 2em);
-  width: 30%;
 }
 
 .text-chat-box {
   width: 30%;
   margin: auto;
 }
 
 .text-chat-box > form > input {
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -661,20 +661,25 @@ loop.standaloneRoomViews = (function(moz
       });
 
       var screenShareStreamClasses = React.addons.classSet({
         "screen": true,
         "focus-stream": this.state.receivingScreenShare,
         hide: !this.state.receivingScreenShare
       });
 
+      // XXX Temporarily showAlways = showRoomName = false for TextChatView
+      // until bug 1168829 is completed.
       return (
         React.createElement("div", {className: "room-conversation-wrapper"}, 
           React.createElement("div", {className: "beta-logo"}), 
-          React.createElement(sharedViews.TextChatView, {dispatcher: this.props.dispatcher}), 
+          React.createElement(sharedViews.TextChatView, {
+            dispatcher: this.props.dispatcher, 
+            showAlways: false, 
+            showRoomName: false}), 
           React.createElement(StandaloneRoomHeader, {dispatcher: this.props.dispatcher}), 
           React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState, 
                                   failureReason: this.state.failureReason, 
                                   joinRoom: this.joinRoom, 
                                   isFirefox: this.props.isFirefox, 
                                   activeRoomStore: this.props.activeRoomStore, 
                                   roomUsed: this.state.used}), 
           React.createElement("div", {className: "video-layout-wrapper"}, 
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -661,20 +661,25 @@ loop.standaloneRoomViews = (function(moz
       });
 
       var screenShareStreamClasses = React.addons.classSet({
         "screen": true,
         "focus-stream": this.state.receivingScreenShare,
         hide: !this.state.receivingScreenShare
       });
 
+      // XXX Temporarily showAlways = showRoomName = false for TextChatView
+      // until bug 1168829 is completed.
       return (
         <div className="room-conversation-wrapper">
           <div className="beta-logo" />
-          <sharedViews.TextChatView dispatcher={this.props.dispatcher} />
+          <sharedViews.TextChatView
+            dispatcher={this.props.dispatcher}
+            showAlways={false}
+            showRoomName={false} />
           <StandaloneRoomHeader dispatcher={this.props.dispatcher} />
           <StandaloneRoomInfoArea roomState={this.state.roomState}
                                   failureReason={this.state.failureReason}
                                   joinRoom={this.joinRoom}
                                   isFirefox={this.props.isFirefox}
                                   activeRoomStore={this.props.activeRoomStore}
                                   roomUsed={this.state.used} />
           <div className="video-layout-wrapper">
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -106,16 +106,19 @@ feedback_report_user_button=Report User
 ## replaced by the brand name
 first_time_experience_title={{clientShortname}} — Join the conversation
 first_time_experience_button_label=Get Started
 
 help_label=Help
 tour_label=Tour
 
 rooms_default_room_name_template=Conversation {{conversationLabel}}
+## LOCALIZATION_NOTE(rooms_welcome_title): {{conversationName}} will be replaced
+## by the user specified conversation name.
+rooms_welcome_title=Welcome to {{conversationName}}
 rooms_leave_button_label=Leave
 rooms_list_copy_url_tooltip=Copy Link
 rooms_list_delete_tooltip=Delete conversation
 rooms_list_deleteConfirmation_label=Are you sure?
 rooms_new_room_button_label=Start a conversation
 rooms_only_occupant_label=You're the first one here.
 rooms_panel_title=Choose a conversation or start a new one
 rooms_room_full_label=There are already two people in this conversation.
@@ -136,8 +139,14 @@ standalone_title_with_status={{clientShortname}} — {{currentStatus}}
 status_in_conversation=In conversation
 status_conversation_ended=Conversation ended
 status_error=Something went wrong
 support_link=Get Help
 
 # Text chat strings
 
 chat_textbox_placeholder=Type here…
+# LOCALIZATION NOTE (context_inroom_label): this string is followed by the
+# title/URL of the website you are having a conversation about, displayed on a
+# separate line. If this structure doesn't work for your locale, you might want
+# to consider this as a stand-alone title. See example screenshot:
+# https://bug1084991.bugzilla.mozilla.org/attachment.cgi?id=8614721
+context_inroom_label=Let's talk about:
--- a/browser/components/loop/test/shared/textChatStore_test.js
+++ b/browser/components/loop/test/shared/textChatStore_test.js
@@ -59,17 +59,18 @@ describe("loop.store.TextChatStore", fun
       store.receivedTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: message
       });
 
       expect(store.getStoreState("messageList")).eql([{
         type: CHAT_MESSAGE_TYPES.RECEIVED,
         contentType: CHAT_CONTENT_TYPES.TEXT,
-        message: message
+        message: message,
+        extraData: undefined
       }]);
     });
 
     it("should not add messages for unknown content types", function() {
       store.receivedTextChatMessage({
         contentType: "invalid type",
         message: "Hi"
       });
@@ -108,24 +109,88 @@ describe("loop.store.TextChatStore", fun
         message: "It's awesome!"
       };
 
       store.sendTextChatMessage(messageData);
 
       expect(store.getStoreState("messageList")).eql([{
         type: CHAT_MESSAGE_TYPES.SENT,
         contentType: messageData.contentType,
-        message: messageData.message
+        message: messageData.message,
+        extraData: undefined
       }]);
     });
 
     it("should dipatch a LoopChatMessageAppended event", function() {
       store.sendTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: "Hello!"
       });
 
       sinon.assert.calledOnce(window.dispatchEvent);
       sinon.assert.calledWithExactly(window.dispatchEvent,
         new CustomEvent("LoopChatMessageAppended"));
     });
   });
+
+  describe("#updateRoomInfo", function() {
+    it("should add the room name to the list", function() {
+      sandbox.stub(navigator.mozL10n, "get").returns("Let's really share!");
+
+      store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
+        roomName: "Let's share!",
+        roomOwner: "Mark",
+        roomUrl: "fake"
+      }));
+
+      expect(store.getStoreState("messageList")).eql([{
+        type: CHAT_MESSAGE_TYPES.SPECIAL,
+        contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
+        message: "Let's really share!",
+        extraData: undefined
+      }]);
+    });
+
+    it("should add the context to the list", function() {
+      sandbox.stub(navigator.mozL10n, "get").returns("Let's really share!");
+
+      store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
+        roomName: "Let's share!",
+        roomOwner: "Mark",
+        roomUrl: "fake",
+        urls: [{
+          description: "A wonderful event",
+          location: "http://wonderful.invalid",
+          thumbnail: "fake"
+        }]
+      }));
+
+      expect(store.getStoreState("messageList")).eql([
+        {
+          type: CHAT_MESSAGE_TYPES.SPECIAL,
+          contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
+          message: "Let's really share!",
+          extraData: undefined
+        }, {
+          type: CHAT_MESSAGE_TYPES.SPECIAL,
+          contentType: CHAT_CONTENT_TYPES.CONTEXT,
+          message: "A wonderful event",
+          extraData: {
+            location: "http://wonderful.invalid",
+            thumbnail: "fake"
+          }
+        }
+      ]);
+    });
+
+    it("should not dispatch a LoopChatMessageAppended event", function() {
+      sandbox.stub(navigator.mozL10n, "get").returns("Let's really share!");
+
+      store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
+        roomName: "Let's share!",
+        roomOwner: "Mark",
+        roomUrl: "fake"
+      }));
+
+      sinon.assert.notCalled(window.dispatchEvent);
+    });
+  });
 });
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 describe("loop.shared.views.TextChatView", function () {
   "use strict";
 
   var expect = chai.expect;
   var sharedActions = loop.shared.actions;
+  var sharedViews = loop.shared.views;
   var TestUtils = React.addons.TestUtils;
   var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
   var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
 
   var dispatcher, fakeSdkDriver, sandbox, store;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
@@ -34,33 +35,126 @@ describe("loop.shared.views.TextChatView
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("TextChatView", function() {
     var view;
 
-    function mountTestComponent() {
+    function mountTestComponent(extraProps) {
+      var props = _.extend({
+        dispatcher: dispatcher
+      }, extraProps);
       return TestUtils.renderIntoDocument(
-        React.createElement(loop.shared.views.TextChatView, {
-          dispatcher: dispatcher
-        }));
+        React.createElement(loop.shared.views.TextChatView, props));
     }
 
     beforeEach(function() {
       store.setStoreState({ textChatEnabled: true });
     });
 
-    it("should not display anything if text chat is disabled", function() {
+    it("should not display anything if no messages and text chat not enabled and showAlways is false", function() {
+      store.setStoreState({ textChatEnabled: false });
+
+      view = mountTestComponent({
+        showAlways: false
+      });
+
+      expect(view.getDOMNode()).eql(null);
+    });
+
+    it("should display the view if no messages and text chat not enabled and showAlways is true", function() {
       store.setStoreState({ textChatEnabled: false });
 
+      view = mountTestComponent({
+        showAlways: true
+      });
+
+      expect(view.getDOMNode()).not.eql(null);
+    });
+
+    it("should display the view if text chat is enabled", function() {
+      view = mountTestComponent({
+        showAlways: true
+      });
+
+      expect(view.getDOMNode()).not.eql(null);
+    });
+
+    it("should display only the text chat box if entry is enabled but there are no messages", function() {
+      view = mountTestComponent();
+
+      var node = view.getDOMNode();
+
+      expect(node.querySelector(".text-chat-box")).not.eql(null);
+      expect(node.querySelector(".text-chat-entries")).eql(null);
+    });
+
+    it("should render message entries when message were sent/ received", function() {
       view = mountTestComponent();
 
-      expect(view.getDOMNode()).eql(null);
+      store.receivedTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Hello!"
+      });
+      store.sendTextChatMessage({
+        contentType: CHAT_CONTENT_TYPES.TEXT,
+        message: "Is it me you're looking for?"
+      });
+
+      var node = view.getDOMNode();
+      expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
+
+      var entries = node.querySelectorAll(".text-chat-entry");
+      expect(entries.length).to.eql(2);
+      expect(entries[0].classList.contains("received")).to.eql(true);
+      expect(entries[1].classList.contains("received")).to.not.eql(true);
+    });
+
+    it("should render a room name special entry", function() {
+      view = mountTestComponent({
+        showRoomName: true
+      });
+
+      store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
+        roomName: "A wonderful surprise!",
+        roomOwner: "Chris",
+        roomUrl: "Fake"
+      }));
+
+      var node = view.getDOMNode();
+      expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
+
+      var entries = node.querySelectorAll(".text-chat-entry");
+      expect(entries.length).eql(1);
+      expect(entries[0].classList.contains("special")).eql(true);
+      expect(entries[0].classList.contains("room-name")).eql(true);
+    });
+
+    it("should render a special entry for the context url", function() {
+      view = mountTestComponent({
+        showRoomName: true
+      });
+
+      store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
+        roomName: "A Very Long Conversation Name",
+        roomOwner: "fake",
+        roomUrl: "http://showcase",
+        urls: [{
+          description: "A wonderful page!",
+          location: "http://wonderful.invalid"
+          // use the fallback thumbnail
+        }]
+      }));
+
+      var node = view.getDOMNode();
+      expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
+
+      expect(node.querySelector(".context-url-view-wrapper")).to.not.eql(null);
     });
 
     it("should dispatch SendTextChatMessage action when enter is pressed", function() {
       view = mountTestComponent();
 
       var entryNode = view.getDOMNode().querySelector(".text-chat-box > form > input");
 
       TestUtils.Simulate.change(entryNode, {
@@ -75,37 +169,10 @@ describe("loop.shared.views.TextChatView
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWithExactly(dispatcher.dispatch,
         new sharedActions.SendTextChatMessage({
           contentType: CHAT_CONTENT_TYPES.TEXT,
           message: "Hello!"
         }));
     });
-
-    it("should not render message entries when none are sent/ received yet", function() {
-      view = mountTestComponent();
-
-      expect(view.getDOMNode().querySelector(".text-chat-entries")).to.eql(null);
-    });
-
-    it("should render message entries when message were sent/ received", function() {
-      view = mountTestComponent();
-
-      store.receivedTextChatMessage({
-        contentType: CHAT_CONTENT_TYPES.TEXT,
-        message: "Hello!"
-      });
-      store.sendTextChatMessage({
-        contentType: CHAT_CONTENT_TYPES.TEXT,
-        message: "Is it me you're looking for?"
-      });
-
-      var node = view.getDOMNode();
-      expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
-
-      var entries = node.querySelectorAll(".text-chat-entry");
-      expect(entries.length).to.eql(2);
-      expect(entries[0].classList.contains("received")).to.eql(true);
-      expect(entries[1].classList.contains("received")).to.not.eql(true);
-    });
   });
 });
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -810,16 +810,99 @@ describe("loop.shared.views", function()
         sinon.assert.calledWithExactly(onChange, {
           checked: true,
           value: "some-value"
         });
       });
     });
   });
 
+  describe("ContextUrlView", function() {
+    var view;
+
+    function mountTestComponent(extraProps) {
+      var props = _.extend({
+        dispatcher: dispatcher
+      }, extraProps);
+      return TestUtils.renderIntoDocument(
+        React.createElement(sharedViews.ContextUrlView, props));
+    }
+
+    it("should display nothing if the url is invalid", function() {
+      view = mountTestComponent({
+        url: "fjrTykyw"
+      });
+
+      expect(view.getDOMNode()).eql(null);
+    });
+
+    it("should use a default thumbnail if one is not supplied", function() {
+      view = mountTestComponent({
+        url: "http://wonderful.invalid"
+      });
+
+      expect(view.getDOMNode().querySelector(".context-preview").getAttribute("src"))
+        .eql("shared/img/icons-16x16.svg#globe");
+    });
+
+    it("should use a default thumbnail for desktop if one is not supplied", function() {
+      view = mountTestComponent({
+        useDesktopPaths: true,
+        url: "http://wonderful.invalid"
+      });
+
+      expect(view.getDOMNode().querySelector(".context-preview").getAttribute("src"))
+        .eql("loop/shared/img/icons-16x16.svg#globe");
+    });
+
+    it("should not display a title if by default", function() {
+      view = mountTestComponent({
+        url: "http://wonderful.invalid"
+      });
+
+      expect(view.getDOMNode().querySelector(".context-content > p")).eql(null);
+    });
+
+    it("should display a title if required", function() {
+      view = mountTestComponent({
+        showContextTitle: true,
+        url: "http://wonderful.invalid"
+      });
+
+      expect(view.getDOMNode().querySelector(".context-content > p")).not.eql(null);
+    });
+
+    it("should set the href on the link if clicks are allowed", function() {
+      view = mountTestComponent({
+        allowClick: true,
+        url: "http://wonderful.invalid"
+      });
+
+      expect(view.getDOMNode().querySelector(".context-url").href)
+        .eql("http://wonderful.invalid/");
+    });
+
+    it("should dispatch an action to record link clicks", function() {
+      view = mountTestComponent({
+        allowClick: true,
+        url: "http://wonderful.invalid"
+      });
+
+      var linkNode = view.getDOMNode().querySelector(".context-url");
+
+      TestUtils.Simulate.click(linkNode);
+
+      sinon.assert.calledOnce(dispatcher.dispatch);
+      sinon.assert.calledWith(dispatcher.dispatch,
+        new sharedActions.RecordClick({
+          linkInfo: "Shared URL"
+        }));
+    });
+  });
+
   describe("MediaView", function() {
     var view;
 
     function mountTestComponent(props) {
       return TestUtils.renderIntoDocument(
         React.createElement(sharedViews.MediaView, props));
     }
 
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -146,8 +146,13 @@ body {
   margin-right: .5rem;
 }
 
 .svg-icon {
   display: inline-block;
   margin-left: .5rem;
   border: 0;
 }
+
+/* Temporary until bug 1168829 is completed */
+.standalone.text-chat-example .text-chat-view {
+  height: 400px;
+}
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -6,16 +6,18 @@
 
 (function() {
   "use strict";
 
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener("DOMContentLoaded", loop.panel.init);
   document.removeEventListener("DOMContentLoaded", loop.conversation.init);
 
+  var sharedActions = loop.shared.actions;
+
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
   var SignInRequestView = loop.panel.SignInRequestView;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var OngoingConversationView = loop.conversationViews.OngoingConversationView;
@@ -27,16 +29,17 @@
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var FeedbackView = loop.shared.views.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
+  var TextChatView = loop.shared.views.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
   var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Local helpers
   function returnTrue() {
@@ -249,16 +252,28 @@
     sdkDriver: mockSDK
   });
 
   textChatStore.setStoreState({
     // XXX Disabled until we start sorting out some of the layouts.
     textChatEnabled: false
   });
 
+  // Update the text chat store with the room info.
+  textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
+    roomName: "A Very Long Conversation Name",
+    roomOwner: "fake",
+    roomUrl: "http://showcase",
+    urls: [{
+      description: "A wonderful page!",
+      location: "http://wonderful.invalid"
+      // use the fallback thumbnail
+    }]
+  }));
+
   loop.store.StoreMixin.register({
     conversationStore: conversationStore,
     feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
 
   // Local mocks
 
@@ -932,16 +947,28 @@
                     roomState: ROOM_STATES.HAS_PARTICIPANTS, 
                     isFirefox: true, 
                     localPosterUrl: "sample-img/video-screen-local.png", 
                     remotePosterUrl: "sample-img/video-screen-remote.png"})
                 )
             )
           ), 
 
+          React.createElement(Section, {name: "TextChatView (standalone)"}, 
+            React.createElement(FramedExample, {width: 200, height: 400, 
+                          summary: "Standalone Text Chat conversation (200 x 400)"}, 
+              React.createElement("div", {className: "standalone text-chat-example"}, 
+                React.createElement(TextChatView, {
+                  dispatcher: dispatcher, 
+                  showAlways: true, 
+                  showRoomName: true})
+              )
+            )
+          ), 
+
           React.createElement(Section, {name: "SVG icons preview", className: "svg-icons"}, 
             React.createElement(Example, {summary: "10x10"}, 
               React.createElement(SVGIcons, {size: "10x10"})
             ), 
             React.createElement(Example, {summary: "14x14"}, 
               React.createElement(SVGIcons, {size: "14x14"})
             ), 
             React.createElement(Example, {summary: "16x16"}, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -6,16 +6,18 @@
 
 (function() {
   "use strict";
 
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener("DOMContentLoaded", loop.panel.init);
   document.removeEventListener("DOMContentLoaded", loop.conversation.init);
 
+  var sharedActions = loop.shared.actions;
+
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
   var SignInRequestView = loop.panel.SignInRequestView;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var OngoingConversationView = loop.conversationViews.OngoingConversationView;
@@ -27,16 +29,17 @@
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var FeedbackView = loop.shared.views.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
+  var TextChatView = loop.shared.views.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
   var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Local helpers
   function returnTrue() {
@@ -249,16 +252,28 @@
     sdkDriver: mockSDK
   });
 
   textChatStore.setStoreState({
     // XXX Disabled until we start sorting out some of the layouts.
     textChatEnabled: false
   });
 
+  // Update the text chat store with the room info.
+  textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
+    roomName: "A Very Long Conversation Name",
+    roomOwner: "fake",
+    roomUrl: "http://showcase",
+    urls: [{
+      description: "A wonderful page!",
+      location: "http://wonderful.invalid"
+      // use the fallback thumbnail
+    }]
+  }));
+
   loop.store.StoreMixin.register({
     conversationStore: conversationStore,
     feedbackStore: feedbackStore,
     textChatStore: textChatStore
   });
 
   // Local mocks
 
@@ -932,16 +947,28 @@
                     roomState={ROOM_STATES.HAS_PARTICIPANTS}
                     isFirefox={true}
                     localPosterUrl="sample-img/video-screen-local.png"
                     remotePosterUrl="sample-img/video-screen-remote.png" />
                 </div>
             </FramedExample>
           </Section>
 
+          <Section name="TextChatView (standalone)">
+            <FramedExample width={200} height={400}
+                          summary="Standalone Text Chat conversation (200 x 400)">
+              <div className="standalone text-chat-example">
+                <TextChatView
+                  dispatcher={dispatcher}
+                  showAlways={true}
+                  showRoomName={true} />
+              </div>
+            </FramedExample>
+          </Section>
+
           <Section name="SVG icons preview" className="svg-icons">
             <Example summary="10x10">
               <SVGIcons size="10x10"/>
             </Example>
             <Example summary="14x14">
               <SVGIcons size="14x14" />
             </Example>
             <Example summary="16x16">
--- a/browser/components/sessionstore/TabAttributes.jsm
+++ b/browser/components/sessionstore/TabAttributes.jsm
@@ -56,13 +56,15 @@ let TabAttributesInternal = {
   set: function (tab, data = {}) {
     // Clear attributes.
     for (let name of this._attrs) {
       tab.removeAttribute(name);
     }
 
     // Set attributes.
     for (let name in data) {
-      tab.setAttribute(name, data[name]);
+      if (!this._skipAttrs.has(name)) {
+        tab.setAttribute(name, data[name]);
+      }
     }
   }
 };
 
--- a/browser/components/sessionstore/TabState.jsm
+++ b/browser/components/sessionstore/TabState.jsm
@@ -228,14 +228,14 @@ let TabStateInternal = {
       }
 
       if (key === "history") {
         tabData.entries = value.entries;
 
         if (value.hasOwnProperty("index")) {
           tabData.index = value.index;
         }
-      } else if (value) {
+      } else {
         tabData[key] = value;
       }
     }
   }
 };
--- a/browser/components/sessionstore/test/browser_attributes.js
+++ b/browser/components/sessionstore/test/browser_attributes.js
@@ -31,26 +31,28 @@ add_task(function* test() {
   ss.persistTabAttribute("custom");
 
   ({attributes} = JSON.parse(ss.getTabState(tab)));
   is(attributes.custom, "foobar", "'custom' attribute is correct");
 
   // Make sure we're backwards compatible and restore old 'image' attributes.
   let state = {
     entries: [{url: "about:mozilla"}],
-    attributes: {custom: "foobaz", image: gBrowser.getIcon(tab)}
+    attributes: {custom: "foobaz"},
+    image: gBrowser.getIcon(tab)
   };
 
   // Prepare a pending tab waiting to be restored.
   let promise = promiseTabRestoring(tab);
   ss.setTabState(tab, JSON.stringify(state));
   yield promise;
 
   ok(tab.hasAttribute("pending"), "tab is pending");
-  is(gBrowser.getIcon(tab), state.attributes.image, "tab has correct icon");
+  is(gBrowser.getIcon(tab), state.image, "tab has correct icon");
+  ok(!state.attributes.image, "'image' attribute not saved");
 
   // Let the pending tab load.
   gBrowser.selectedTab = tab;
   yield promiseTabRestored(tab);
 
   // Ensure no 'image' or 'pending' attributes are stored.
   ({attributes} = JSON.parse(ss.getTabState(tab)));
   ok(!("image" in attributes), "'image' attribute not saved");
--- a/browser/components/sessionstore/test/browser_pending_tabs.js
+++ b/browser/components/sessionstore/test/browser_pending_tabs.js
@@ -22,11 +22,14 @@ add_task(function* () {
 
   // Flush to ensure the parent has all data.
   yield TabStateFlusher.flush(browser);
 
   // Check that the shistory index is the one we restored.
   let tabState = TabState.collect(tab);
   is(tabState.index, TAB_STATE.index, "correct shistory index");
 
+  // Check we don't collect userTypedValue when we shouldn't.
+  ok(!tabState.userTypedValue, "tab didn't have a userTypedValue");
+
   // Cleanup.
   gBrowser.removeTab(tab);
 });
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -40,16 +40,17 @@ support-files =
   code_ugly-4.js
   code_ugly-5.js
   code_ugly-6.js
   code_ugly-7.js
   code_ugly-8
   code_ugly-8^headers^
   code_WorkerActor.attach-worker1.js
   code_WorkerActor.attach-worker2.js
+  code_WorkerActor.attachThread-worker.js
   doc_auto-pretty-print-01.html
   doc_auto-pretty-print-02.html
   doc_binary_search.html
   doc_blackboxing.html
   doc_breakpoints-break-on-last-line-of-script-on-reload.html
   doc_breakpoints-other-tabs.html
   doc_breakpoints-reload.html
   doc_bug-896139.html
@@ -102,16 +103,17 @@ support-files =
   doc_step-out.html
   doc_terminate-on-tab-close.html
   doc_tracing-01.html
   doc_watch-expressions.html
   doc_watch-expression-button.html
   doc_with-frame.html
   doc_WorkerActor.attach-tab1.html
   doc_WorkerActor.attach-tab2.html
+  doc_WorkerActor.attachThread-tab.html
   head.js
   sjs_random-javascript.sjs
   testactors.js
 
 [browser_dbg_aaa_run_first_leaktest.js]
 skip-if = e10s && debug
 [browser_dbg_addonactor.js]
 [browser_dbg_addon-sources.js]
@@ -561,8 +563,10 @@ skip-if = e10s && debug
 [browser_dbg_variables-view-webidl.js]
 skip-if = e10s && debug
 [browser_dbg_watch-expressions-01.js]
 skip-if = e10s && debug
 [browser_dbg_watch-expressions-02.js]
 skip-if = e10s && debug
 [browser_dbg_WorkerActor.attach.js]
 skip-if = e10s && debug
+[browser_dbg_WorkerActor.attachThread.js]
+skip-if = e10s && debug
--- a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js
+++ b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js
@@ -22,17 +22,16 @@ function test() {
     yield listWorkers(tabClient);
 
     // If a page still has pending network requests, it will not be moved into
     // the bfcache. Consequently, we cannot use waitForWorkerListChanged here,
     // because the worker is not guaranteed to have finished loading when it is
     // registered. Instead, we have to wait for the promise returned by
     // createWorker in the tab to be resolved.
     yield createWorkerInTab(tab, WORKER1_URL);
-
     let { workers } = yield listWorkers(tabClient);
     let [, workerClient1] = yield attachWorker(tabClient,
                                                findWorker(workers, WORKER1_URL));
     is(workerClient1.isFrozen, false);
 
     executeSoon(() => {
       tab.linkedBrowser.loadURI(TAB2_URL);
     });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attachThread.js
@@ -0,0 +1,89 @@
+let TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
+let WORKER_URL = "code_WorkerActor.attachThread-worker.js";
+
+function test() {
+  Task.spawn(function* () {
+    DebuggerServer.init();
+    DebuggerServer.addBrowserActors();
+
+    let client1 = new DebuggerClient(DebuggerServer.connectPipe());
+    yield connect(client1);
+    let client2 = new DebuggerClient(DebuggerServer.connectPipe());
+    yield connect(client2);
+
+    let tab = yield addTab(TAB_URL);
+    let { tabs: tabs1 } = yield listTabs(client1);
+    let [, tabClient1] = yield attachTab(client1, findTab(tabs1, TAB_URL));
+    let { tabs: tabs2 } = yield listTabs(client2);
+    let [, tabClient2] = yield attachTab(client2, findTab(tabs2, TAB_URL));
+
+    yield listWorkers(tabClient1);
+    yield listWorkers(tabClient2);
+    yield createWorkerInTab(tab, WORKER_URL);
+    let { workers: workers1 } = yield listWorkers(tabClient1);
+    let [, workerClient1] = yield attachWorker(tabClient1,
+                                               findWorker(workers1, WORKER_URL));
+    let { workers: workers2 } = yield listWorkers(tabClient2);
+    let [, workerClient2] = yield attachWorker(tabClient2,
+                                               findWorker(workers2, WORKER_URL));
+
+    let location = { line: 5 };
+
+    let [, threadClient1] = yield attachThread(workerClient1);
+    let sources1 = yield getSources(threadClient1);
+    let sourceClient1 = threadClient1.source(findSource(sources1,
+                                                        EXAMPLE_URL + WORKER_URL));
+    let [, breakpointClient1] = yield setBreakpoint(sourceClient1, location);
+    yield resume(threadClient1);
+
+    let [, threadClient2] = yield attachThread(workerClient2);
+    let sources2 = yield getSources(threadClient2);
+    let sourceClient2 = threadClient2.source(findSource(sources2,
+                                                        EXAMPLE_URL + WORKER_URL));
+    let [, breakpointClient2] = yield setBreakpoint(sourceClient2, location);
+    yield resume(threadClient2);
+
+    postMessageToWorkerInTab(tab, WORKER_URL, "ping");
+    yield Promise.all([
+      waitForPause(threadClient1).then((packet) => {
+        is(packet.type, "paused");
+        let why = packet.why;
+        is(why.type, "breakpoint");
+        is(why.actors.length, 1);
+        is(why.actors[0], breakpointClient1.actor);
+        let frame = packet.frame;
+        let where = frame.where;
+        is(where.source.actor, sourceClient1.actor);
+        is(where.line, location.line);
+        let variables = frame.environment.bindings.variables;
+        is(variables.a.value, 1);
+        is(variables.b.value.type, "undefined");
+        is(variables.c.value.type, "undefined");
+        return resume(threadClient1);
+      }),
+      waitForPause(threadClient2).then((packet) => {
+        is(packet.type, "paused");
+        let why = packet.why;
+        is(why.type, "breakpoint");
+        is(why.actors.length, 1);
+        is(why.actors[0], breakpointClient2.actor);
+        let frame = packet.frame;
+        let where = frame.where;
+        is(where.source.actor, sourceClient2.actor);
+        is(where.line, location.line);
+        let variables = frame.environment.bindings.variables;
+        is(variables.a.value, 1);
+        is(variables.b.value.type, "undefined");
+        is(variables.c.value.type, "undefined");
+        return resume(threadClient2);
+      }),
+    ]);
+
+    terminateWorkerInTab(tab, WORKER_URL);
+    yield waitForWorkerClose(workerClient1);
+    yield waitForWorkerClose(workerClient2);
+    yield close(client1);
+    yield close(client2);
+    finish();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/code_WorkerActor.attachThread-worker.js
@@ -0,0 +1,16 @@
+"use strict";
+
+function f() {
+  var a = 1;
+  var b = 2;
+  var c = 3;
+}
+
+self.onmessage = function (event) {
+  if (event.data == "ping") {
+    f()
+    postMessage("pong");
+  }
+};
+
+postMessage("load");
--- a/browser/devtools/debugger/test/code_frame-script.js
+++ b/browser/devtools/debugger/test/code_frame-script.js
@@ -78,8 +78,20 @@ addMessageListener("jsonrpc", function (
   }, function (error) {
     sendAsyncMessage("jsonrpc", {
       result: null,
       error: error.message.toString(),
       id: id
     });
   });
 });
+
+addMessageListener("test:postMessageToWorker", function (message) {
+  dump("Posting message '" + message.data.message + "' to worker with url '" +
+       message.data.url + "'.\n");
+
+  let worker = workers[message.data.url];
+  worker.postMessage(message.data.message);
+  worker.addEventListener("message", function listener() {
+    worker.removeEventListener("message", listener);
+    sendAsyncMessage("test:postMessageToWorker");
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_WorkerActor.attachThread-tab.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8"/>
+  </head>
+  <body>
+  </body>
+</html>
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -507,19 +507,23 @@ function getTab(aTarget, aWindow) {
   if (aTarget instanceof XULElement) {
     return promise.resolve(aTarget);
   } else {
     return addTab(aTarget, aWindow);
   }
 }
 
 function getSources(aClient) {
+  info("Getting sources.");
+
   let deferred = promise.defer();
 
-  aClient.getSources(({sources}) => deferred.resolve(sources));
+  aClient.getSources((packet) => {
+    deferred.resolve(packet.sources);
+  });
 
   return deferred.promise;
 }
 
 function initDebugger(aTarget, aWindow) {
   info("Initializing a debugger panel.");
 
   return getTab(aTarget, aWindow).then(aTab => {
@@ -1124,16 +1128,25 @@ function waitForWorkerListChanged(tabCli
   return new Promise(function (resolve) {
     tabClient.addListener("workerListChanged", function listener() {
       tabClient.removeListener("workerListChanged", listener);
       resolve();
     });
   });
 }
 
+function attachThread(workerClient, options) {
+  info("Attaching to thread.");
+  return new Promise(function(resolve, reject) {
+    workerClient.attachThread(options, function (response, threadClient) {
+      resolve([response, threadClient]);
+    });
+  });
+}
+
 function waitForWorkerClose(workerClient) {
   info("Waiting for worker to close.");
   return new Promise(function (resolve) {
     workerClient.addOneTimeListener("close", function () {
       info("Worker did close.");
       resolve();
     });
   });
@@ -1151,8 +1164,57 @@ function waitForWorkerFreeze(workerClien
 function waitForWorkerThaw(workerClient) {
   info("Waiting for worker to thaw.");
   return new Promise(function (resolve) {
     workerClient.addOneTimeListener("thaw", function () {
       resolve();
     });
   });
 }
+
+function resume(threadClient) {
+  info("Resuming thread.");
+  return rdpInvoke(threadClient, threadClient.resume);
+}
+
+function findSource(sources, url) {
+  info("Finding source with url '" + url + "'.\n");
+  for (let source of sources) {
+    if (source.url === url) {
+      return source;
+    }
+  }
+  return null;
+}
+
+function setBreakpoint(sourceClient, location) {
+  info("Setting breakpoint.\n");
+  return new Promise(function (resolve) {
+    sourceClient.setBreakpoint(location, function (response, breakpointClient) {
+      resolve([response, breakpointClient]);
+    });
+  });
+}
+
+function waitForEvent(client, type, predicate) {
+  return new Promise(function (resolve) {
+    function listener(type, packet) {
+      if (!predicate(packet)) {
+        return;
+      }
+      client.removeListener(listener);
+      resolve(packet);
+    }
+
+    if (predicate) {
+      client.addListener(type, listener);
+    } else {
+      client.addOneTimeListener(type, function (type, packet) {
+        resolve(packet);
+      });
+    }
+  });
+}
+
+function waitForPause(threadClient) {
+  info("Waiting for pause.\n");
+  return waitForEvent(threadClient, "paused");
+}
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -26,17 +26,16 @@ browser.jar:
     content/browser/devtools/styleeditor.xul                           (styleeditor/styleeditor.xul)
     content/browser/devtools/styleeditor.css                           (styleeditor/styleeditor.css)
     content/browser/devtools/storage.xul                               (storage/storage.xul)
     content/browser/devtools/computedview.xhtml                        (styleinspector/computedview.xhtml)
     content/browser/devtools/cssruleview.xhtml                         (styleinspector/cssruleview.xhtml)
     content/browser/devtools/ruleview.css                              (styleinspector/ruleview.css)
     content/browser/devtools/layoutview/view.js                        (layoutview/view.js)
     content/browser/devtools/layoutview/view.xhtml                     (layoutview/view.xhtml)
-    content/browser/devtools/layoutview/view.css                       (layoutview/view.css)
     content/browser/devtools/fontinspector/font-inspector.js           (fontinspector/font-inspector.js)
     content/browser/devtools/fontinspector/font-inspector.xhtml        (fontinspector/font-inspector.xhtml)
     content/browser/devtools/fontinspector/font-inspector.css          (fontinspector/font-inspector.css)
     content/browser/devtools/animationinspector/animation-controller.js (animationinspector/animation-controller.js)
     content/browser/devtools/animationinspector/animation-panel.js     (animationinspector/animation-panel.js)
     content/browser/devtools/animationinspector/animation-inspector.xhtml (animationinspector/animation-inspector.xhtml)
     content/browser/devtools/codemirror/codemirror.js                  (sourceeditor/codemirror/codemirror.js)
     content/browser/devtools/codemirror/codemirror.css                 (sourceeditor/codemirror/codemirror.css)
deleted file mode 100644
--- a/browser/devtools/layoutview/view.css
+++ /dev/null
@@ -1,266 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-body {
-  max-width: 320px;
-  position: relative;
-  margin: 0px auto;
-  padding: 0;
-}
-
-#header {
-  box-sizing: border-box;
-  width: 100%;
-  padding: 4px 13px;
-  display: -moz-box;
-  vertical-align: top;
-}
-
-#header:-moz-dir(rtl) {
-  -moz-box-direction: reverse;
-}
-
-#header > span {
-  display: -moz-box;
-}
-
-#element-size {
-  -moz-box-flex: 1;
-}
-
-#element-size:-moz-dir(rtl) {
-  -moz-box-pack: end;
-}
-
-#main {
-  margin: 0 14px 10px 14px;
-  box-sizing: border-box;
-  width: calc(100% - 2 * 14px);
-  position: absolute;
-  border-width: 1px;
-}
-
-#content,
-#borders {
-  border-width: 1px;
-}
-
-#content {
-  height: 25px;
-}
-
-#margins,
-#padding {
-  border-style: solid;
-  border-width: 25px;
-}
-
-#borders {
-  padding: 25px;
-}
-
-.legend {
-  position: absolute;
-  margin: 5px 6px;
-  z-index: 1;
-}
-
-.legend[data-box="margin"] {
-  color: var(--theme-highlight-blue);
-}
-
-#main > p {
-  position: absolute;
-  pointer-events: none;
-}
-
-#main > p {
-  margin: 0;
-  text-align: center;
-}
-
-#main > p > span {
-  vertical-align: middle;
-  pointer-events: auto;
-}
-
-.size > span {
-  cursor: default;
-}
-
-.editable {
-  -moz-user-select: text;
-}
-
-.top,
-.bottom {
-  width: calc(100% - 2px);
-  text-align: center;
-}
-
-.padding.top {
-  top: 55px;
-}
-
-.padding.bottom {
-  bottom: 57px;
-}
-
-.border.top {
-  top: 30px;
-}
-
-.border.bottom {
-  bottom: 31px;
-}
-
-.margin.top {
-  top: 5px;
-}
-
-.margin.bottom {
-  bottom: 6px;
-}
-
-.size,
-.margin.left,
-.margin.right,
-.border.left,
-.border.right,
-.padding.left,
-.padding.right {
-  top: 22px;
-  line-height: 132px;
-}
-
-.size {
-  width: calc(100% - 2px);
-}
-
-.margin.right,
-.margin.left,
-.border.left,
-.border.right,
-.padding.right,
-.padding.left {
-  width: 25px;
-}
-
-.padding.left {
-  left: 52px;
-}
-
-.padding.right {
-  right: 51px;
-}
-
-.border.left {
-  left: 26px;
-}
-
-.border.right {
-  right: 26px;
-}
-
-.margin.right {
-  right: 0;
-}
-
-.margin.left {
-  left: 0;
-}
-
-.rotate.left:not(.editing) {
-  transform: rotate(-90deg);
-}
-
-.rotate.right:not(.editing) {
-  transform: rotate(90deg);
-}
-
-
-body.dim > #header > #element-position,
-body.dim > #main > p {
-  visibility: hidden;
-}
-
-@media (max-height: 228px) {
-  #header {
-    padding-top: 0;
-    padding-bottom: 0;
-    margin-top: 10px;
-    margin-bottom: 8px;
-  }
-
-  #margins,
-  #padding {
-    border-width: 21px;
-  }
-  #borders {
-    padding: 21px;
-  }
-
-  #content {
-    height: 21px;
-  }
-
-  .padding.top {
-    top: 46px;
-  }
-
-  .padding.bottom {
-    bottom: 46px;
-  }
-
-  .border.top {
-    top: 25px;
-  }
-
-  .border.bottom {
-    bottom: 25px;
-  }
-
-  .margin.top {
-    top: 4px;
-  }
-
-  .margin.bottom {
-    bottom: 4px;
-  }
-
-  .size,
-  .margin.left,
-  .margin.right,
-  .border.left,
-  .border.right,
-  .padding.left,
-  .padding.right {
-    line-height: 106px;
-  }
-
-  .margin.right,
-  .margin.left,
-  .border.left,
-  .border.right,
-  .padding.right,
-  .padding.left {
-    width: 21px;
-  }
-
-  .padding.left {
-    left: 43px;
-  }
-
-  .padding.right {
-    right: 43px;
-  }
-
-  .border.left {
-    left: 22px;
-  }
-
-  .border.right {
-    right: 22px;
-  }
-}
--- a/browser/devtools/layoutview/view.js
+++ b/browser/devtools/layoutview/view.js
@@ -1,32 +1,30 @@
 /* -*- 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/. */
+/* globals ViewHelpers, window, document */
 
 "use strict";
 
-const Cu = Components.utils;
-const Ci = Components.interfaces;
-const Cc = Components.classes;
+const {utils: Cu, interfaces: Ci, classes: Cc} = Components;
 
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource://gre/modules/devtools/Console.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
-const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
-const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor");
-const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
-const {ReflowFront} = devtools.require("devtools/server/actors/layout");
+const {require} = devtools;
+const {InplaceEditor, editableItem} = require("devtools/shared/inplace-editor");
+const {ReflowFront} = require("devtools/server/actors/layout");
 
-const SHARED_L10N = new ViewHelpers.L10N("chrome://browser/locale/devtools/shared.properties");
+const STRINGS_URI = "chrome://browser/locale/devtools/shared.properties";
+const SHARED_L10N = new ViewHelpers.L10N(STRINGS_URI);
 const NUMERIC = /^-?[\d\.]+$/;
 const LONG_TEXT_ROTATE_LIMIT = 3;
 
 /**
  * An instance of EditingSession tracks changes that have been made during the
  * modification of box model values. All of these changes can be reverted by
  * calling revert.
  *
@@ -200,23 +198,24 @@ LayoutView.prototype = {
       borderBottom: {selector: ".border.bottom > span",
                   property: "border-bottom-width",
                   value: undefined},
       borderLeft: {selector: ".border.left > span",
                   property: "border-left-width",
                   value: undefined},
       borderRight: {selector: ".border.right > span",
                   property: "border-right-width",
-                  value: undefined},
+                  value: undefined}
     };
 
     // Make each element the dimensions editable
     for (let i in this.map) {
-      if (i == "position")
+      if (i == "position") {
         continue;
+      }
 
       let dimension = this.map[i];
       editableItem({
         element: this.doc.querySelector(dimension.selector)
       }, (element, event) => {
         this.initEditor(element, event, dimension);
       });
     }
@@ -226,17 +225,18 @@ LayoutView.prototype = {
 
   /**
    * Start listening to reflows in the current tab.
    */
   trackReflows: function() {
     if (!this.reflowFront) {
       let toolbox = this.inspector.toolbox;
       if (toolbox.target.form.reflowActor) {
-        this.reflowFront = ReflowFront(toolbox.target.client, toolbox.target.form);
+        this.reflowFront = ReflowFront(toolbox.target.client,
+                                       toolbox.target.form);
       } else {
         return;
       }
     }
 
     this.reflowFront.on("reflows", this.update);
     this.reflowFront.start();
   },
@@ -260,21 +260,21 @@ LayoutView.prototype = {
     let { property } = dimension;
     let session = new EditingSession(document, this.elementRules);
     let initialValue = session.getProperty(property);
 
     let editor = new InplaceEditor({
       element: element,
       initial: initialValue,
 
-      start: (editor) => {
+      start: editor => {
         editor.elt.parentNode.classList.add("editing");
       },
 
-      change: (value) => {
+      change: value => {
         if (NUMERIC.test(value)) {
           value += "px";
         }
 
         let properties = [
           { name: property, value: value }
         ];
 
@@ -295,24 +295,36 @@ LayoutView.prototype = {
           session.revert();
           session.destroy();
         }
       }
     }, event);
   },
 
   /**
-   * Is the layoutview visible in the sidebar?
+   * Is the layoutview visible in the sidebar.
+   * @return {Boolean}
    */
-  isActive: function() {
+  isViewVisible: function() {
     return this.inspector &&
            this.inspector.sidebar.getCurrentTabID() == "layoutview";
   },
 
   /**
+   * Is the layoutview visible in the sidebar and is the current node valid to
+   * be displayed in the view.
+   * @return {Boolean}
+   */
+  isViewVisibleAndNodeValid: function() {
+    return this.isViewVisible() &&
+           this.inspector.selection.isConnected() &&
+           this.inspector.selection.isElementNode();
+  },
+
+  /**
    * Destroy the nodes. Remove listeners.
    */
   destroy: function() {
     this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
     this.inspector.selection.off("new-node-front", this.onNewSelection);
     this.inspector.sidebar.off("select", this.onSidebarSelect);
 
     this.sizeHeadingLabel = null;
@@ -323,80 +335,71 @@ LayoutView.prototype = {
     if (this.reflowFront) {
       this.untrackReflows();
       this.reflowFront.destroy();
       this.reflowFront = null;
     }
   },
 
   onSidebarSelect: function(e, sidebar) {
-    if (sidebar !== "layoutview") {
-      this.dim();
-    }
+    this.setActive(sidebar === "layoutview");
   },
 
   /**
    * Selection 'new-node-front' event handler.
    */
   onNewSelection: function() {
     let done = this.inspector.updating("layoutview");
-    this.onNewNode().then(done, (err) => { console.error(err); done() });
+    this.onNewNode().then(done, err => {
+      console.error(err);
+      done();
+    });
   },
 
   /**
    * @return a promise that resolves when the view has been updated
    */
   onNewNode: function() {
-    if (this.isActive() &&
-        this.inspector.selection.isConnected() &&
-        this.inspector.selection.isElementNode()) {
-      this.undim();
-    } else {
-      this.dim();
-    }
-
+    this.setActive(this.isViewVisibleAndNodeValid());
     return this.update();
   },
 
   /**
-   * Hide the layout boxes and stop refreshing on reflows. No node is selected
-   * or the layout-view sidebar is inactive.
+   * Stop tracking reflows and hide all values when no node is selected or the
+   * layout-view is hidden, otherwise track reflows and show values.
+   * @param {Boolean} isActive
    */
-  dim: function() {
-    this.untrackReflows();
-    this.doc.body.classList.add("dim");
-    this.dimmed = true;
-  },
+  setActive: function(isActive) {
+    if (isActive === this.isActive) {
+      return;
+    }
+    this.isActive = isActive;
 
-  /**
-   * Show the layout boxes and start refreshing on reflows. A node is selected
-   * and the layout-view side is active.
-   */
-  undim: function() {
-    this.trackReflows();
-    this.doc.body.classList.remove("dim");
-    this.dimmed = false;
+    this.doc.body.classList.toggle("inactive", !isActive);
+    if (isActive) {
+      this.trackReflows();
+    } else {
+      this.untrackReflows();
+    }
   },
 
   /**
    * Compute the dimensions of the node and update the values in
    * the layoutview/view.xhtml document.
    * @return a promise that will be resolved when complete.
    */
   update: function() {
     let lastRequest = Task.spawn((function*() {
-      if (!this.isActive() ||
-          !this.inspector.selection.isConnected() ||
-          !this.inspector.selection.isElementNode()) {
+      if (!this.isViewVisibleAndNodeValid()) {
         return;
       }
 
       let node = this.inspector.selection.nodeFront;
       let layout = yield this.inspector.pageStyle.getLayout(node, {
-        autoMargins: !this.dimmed
+        autoMargins: this.isActive
       });
       let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
 
       // If a subsequent request has been made, wait for that one instead.
       if (this._lastRequest != lastRequest) {
         return this._lastRequest;
       }
 
@@ -404,18 +407,18 @@ LayoutView.prototype = {
       let width = layout.width;
       let height = layout.height;
       let newLabel = SHARED_L10N.getFormatStr("dimensions", width, height);
 
       if (this.sizeHeadingLabel.textContent != newLabel) {
         this.sizeHeadingLabel.textContent = newLabel;
       }
 
-      // If the view is dimmed, no need to do anything more.
-      if (this.dimmed) {
+      // If the view isn't active, no need to do anything more.
+      if (!this.isActive) {
         this.inspector.emit("layoutview-updated");
         return null;
       }
 
       for (let i in this.map) {
         let property = this.map[i].property;
         if (!(property in layout)) {
           // Depending on the actor version, some properties
@@ -428,20 +431,28 @@ LayoutView.prototype = {
           // Useful for "position" for example.
           this.map[i].value = layout[property];
         } else {
           this.map[i].value = parsedValue;
         }
       }
 
       let margins = layout.autoMargins;
-      if ("top" in margins) this.map.marginTop.value = "auto";
-      if ("right" in margins) this.map.marginRight.value = "auto";
-      if ("bottom" in margins) this.map.marginBottom.value = "auto";
-      if ("left" in margins) this.map.marginLeft.value = "auto";
+      if ("top" in margins) {
+        this.map.marginTop.value = "auto";
+      }
+      if ("right" in margins) {
+        this.map.marginRight.value = "auto";
+      }
+      if ("bottom" in margins) {
+        this.map.marginBottom.value = "auto";
+      }
+      if ("left" in margins) {
+        this.map.marginLeft.value = "auto";
+      }
 
       for (let i in this.map) {
         let selector = this.map[i].selector;
         let span = this.doc.querySelector(selector);
         this.updateSourceRuleTooltip(span, this.map[i].property, styleEntries);
         if (span.textContent.length > 0 &&
             span.textContent == this.map[i].value) {
           continue;
@@ -533,41 +544,44 @@ LayoutView.prototype = {
     }
   }
 };
 
 let elts;
 
 let onmouseover = function(e) {
   let region = e.target.getAttribute("data-box");
+  if (!region) {
+    return false;
+  }
+
   this.layoutview.showBoxModel({region});
 
   return false;
 }.bind(window);
 
-let onmouseout = function(e) {
+let onmouseout = function() {
   this.layoutview.hideBoxModel();
-
   return false;
 }.bind(window);
 
 window.setPanel = function(panel) {
   this.layoutview = new LayoutView(panel, window);
 
   // Box model highlighter mechanism
   elts = document.querySelectorAll("*[title]");
   for (let i = 0; i < elts.length; i++) {
     let elt = elts[i];
     elt.addEventListener("mouseover", onmouseover, true);
     elt.addEventListener("mouseout", onmouseout, true);
   }
 
   // Mark document as RTL or LTR:
-  let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
-    getService(Ci.nsIXULChromeRegistry);
+  let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
+                  .getService(Ci.nsIXULChromeRegistry);
   let dir = chromeReg.isLocaleRTL("global");
   document.body.setAttribute("dir", dir ? "rtl" : "ltr");
 
   window.parent.postMessage("layoutview-ready", "*");
 };
 
 window.onunload = function() {
   if (this.layoutview) {
--- a/browser/devtools/layoutview/view.xhtml
+++ b/browser/devtools/layoutview/view.xhtml
@@ -14,17 +14,16 @@
 
     <script type="application/javascript;version=1.8"
             src="chrome://browser/content/devtools/theme-switching.js"/>
 
     <script type="application/javascript;version=1.8" src="view.js"></script>
 
     <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://browser/skin/devtools/layoutview.css" type="text/css"/>
-    <link rel="stylesheet" href="view.css" type="text/css"/>
 
   </head>
   <body class="theme-sidebar devtools-monospace">
 
     <p id="header">
       <span id="element-size"></span><span id="element-position"></span>
     </p>
 
--- a/browser/devtools/performance/modules/logic/jit.js
+++ b/browser/devtools/performance/modules/logic/jit.js
@@ -206,16 +206,29 @@ const JITOptimizations = function (rawSi
       column: data.column
     };
   }
 
   this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);;
 };
 
 /**
+ * Make JITOptimizations iterable.
+ */
+JITOptimizations.prototype = {
+  [Symbol.iterator]: function *() {
+    yield* this.optimizationSites;
+  },
+
+  get length() {
+    return this.optimizationSites.length;
+  }
+};
+
+/**
  * Takes an "outcome" string from an OptimizationAttempt and returns
  * a boolean indicating whether or not its a successful outcome.
  *
  * @return {boolean}
  */
 
 OptimizationSite.isSuccessfulOutcome = JITOptimizations.isSuccessfulOutcome = function (outcome) {
   return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome);
--- a/browser/devtools/performance/modules/logic/tree-model.js
+++ b/browser/devtools/performance/modules/logic/tree-model.js
@@ -230,19 +230,19 @@ ThreadNode.prototype = {
           calls = prevCalls;
         }
 
         let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame,
                                           mutableFrameKeyOptions.isMetaCategoryOut,
                                           leafTable);
         if (isLeaf) {
           frameNode.youngestFrameSamples++;
+          frameNode._addOptimizations(inflatedFrame.optimizations, stringTable);
         }
         frameNode.samples++;
-        frameNode._addOptimizations(inflatedFrame.optimizations, stringTable);
 
         prevFrameKey = frameKey;
         prevCalls = frameNode.calls;
         isLeaf = mutableFrameKeyOptions.isLeaf = false;
       }
 
       this.samples++;
     }
--- a/browser/devtools/performance/test/browser_perf-jit-view-01.js
+++ b/browser/devtools/performance/test/browser_perf-jit-view-01.js
@@ -26,22 +26,24 @@ function* spawnTest() {
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield DetailsView.selectView("js-calltree");
 
   yield injectAndRenderProfilerData();
 
-  // gRawSite1 and gRawSite2 are both optimizations on A, so they'll have
+  // A is never a leaf, so it's optimizations should not be shown.
+  yield checkFrame(1);
+
+  // gRawSite2 and gRawSite3 are both optimizations on B, so they'll have
   // indices in descending order of # of samples.
-  yield checkFrame(1, [{ i: 0, opt: gRawSite1 }, { i: 1, opt: gRawSite2 }]);
+  yield checkFrame(2, [{ i: 0, opt: gRawSite2 }, { i: 1, opt: gRawSite3 }]);
 
-  // gRawSite3 is the only optimization on B, so it'll have index 0.
-  yield checkFrame(2, [{ i: 0, opt: gRawSite3 }]);
+  // Leaf node (C) with no optimizations should not display any opts.
   yield checkFrame(3);
 
   let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
   let reset = once(JITOptimizationsView, EVENTS.OPTIMIZATIONS_RESET);
   RecordingsView.selectedIndex = 0;
   yield Promise.all([select, reset]);
   ok(true, "JITOptimizations view correctly reset when switching recordings.");
 
@@ -109,61 +111,61 @@ function* spawnTest() {
 
       for (let j = 0; j < ionTypes.length; j++) {
         ok($(`.tree-widget-container li[data-id='["${i}","${i}-types","${i}-types-${j}"]']`),
           "found an ion type row");
       }
 
       // The second and third optimization should display optimization failures.
       let warningIcon = $(`.tree-widget-container li[data-id='["${i}"]'] .opt-icon[severity=warning]`);
-      if (opt === gRawSite2 || opt === gRawSite3) {
+      if (opt === gRawSite3 || opt === gRawSite1) {
         ok(warningIcon, "did find a warning icon for all strategies failing.");
       } else {
         ok(!warningIcon, "did not find a warning icon for no successful strategies");
       }
     }
   }
 }
 
 let gUniqueStacks = new RecordingUtils.UniqueStacks();
 
 function uniqStr(s) {
   return gUniqueStacks.getOrAddStringIndex(s);
 }
 
 // Since deflateThread doesn't handle deflating optimization info, use
-// placeholder names A_O1, B_O3, and A_O2, which will be used to manually
+// placeholder names A_O1, B_O2, and B_O3, which will be used to manually
 // splice deduped opts into the profile.
 let gThread = RecordingUtils.deflateThread({
   samples: [{
     time: 0,
     frames: [
       { location: "(root)" }
     ]
   }, {
     time: 5,
     frames: [
       { location: "(root)" },
       { location: "A_O1" },
-      { location: "B_O3" },
+      { location: "B_O2" },
       { location: "C (http://foo/bar/baz:56)" }
     ]
   }, {
     time: 5 + 1,
     frames: [
       { location: "(root)" },
       { location: "A (http://foo/bar/baz:12)" },
-      { location: "B (http://foo/bar/boo:34)" },
+      { location: "B_O2" },
     ]
   }, {
     time: 5 + 1 + 2,
     frames: [
       { location: "(root)" },
-      { location: "A_O2" },
-      { location: "B (http://foo/bar/boo:34)" },
+      { location: "A_O1" },
+      { location: "B_O3" },
     ]
   }, {
     time: 5 + 1 + 2 + 7,
     frames: [
       { location: "(root)" },
       { location: "A_O1" },
       { location: "E (http://foo/bar/baz:90)" },
       { location: "F (http://foo/bar/baz:99)" }
@@ -192,43 +194,43 @@ let gRawSite1 = {
   attempts: {
     schema: {
       outcome: 0,
       strategy: 1
     },
     data: [
       [uniqStr("Failure1"), uniqStr("SomeGetter1")],
       [uniqStr("Failure2"), uniqStr("SomeGetter2")],
-      [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+      [uniqStr("Failure3"), uniqStr("SomeGetter3")]
     ]
   }
 };
 
 let gRawSite2 = {
-  _testFrameInfo: { name: "A", line: "12", file: "@baz" },
-  line: 12,
+  _testFrameInfo: { name: "B", line: "10", file: "@boo" },
+  line: 40,
   types: [{
     mirType: uniqStr("Int32"),
     site: uniqStr("Receiver")
   }],
   attempts: {
     schema: {
       outcome: 0,
       strategy: 1
     },
     data: [
       [uniqStr("Failure1"), uniqStr("SomeGetter1")],
       [uniqStr("Failure2"), uniqStr("SomeGetter2")],
-      [uniqStr("Failure3"), uniqStr("SomeGetter3")]
+      [uniqStr("Inlined"), uniqStr("SomeGetter3")]
     ]
   }
 };
 
 let gRawSite3 = {
-  _testFrameInfo: { name: "B", line: "34", file: "@boo" },
+  _testFrameInfo: { name: "B", line: "10", file: "@boo" },
   line: 34,
   types: [{
     mirType: uniqStr("Int32"),
     site: uniqStr("Receiver")
   }],
   attempts: {
     schema: {
       outcome: 0,
@@ -247,18 +249,18 @@ gThread.frameTable.data.forEach((frame) 
   const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
 
   let l = gThread.stringTable[frame[LOCATION_SLOT]];
   switch (l) {
   case "A_O1":
     frame[LOCATION_SLOT] = uniqStr("A (http://foo/bar/baz:12)");
     frame[OPTIMIZATIONS_SLOT] = gRawSite1;
     break;
-  case "A_O2":
-    frame[LOCATION_SLOT] = uniqStr("A (http://foo/bar/baz:12)");
+  case "B_O2":
+    frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:10)");
     frame[OPTIMIZATIONS_SLOT] = gRawSite2;
     break;
   case "B_O3":
-    frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:34)");
+    frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:10)");
     frame[OPTIMIZATIONS_SLOT] = gRawSite3;
     break;
   }
 });
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -7,16 +7,17 @@ const { classes: Cc, interfaces: Ci, uti
 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 let { Preferences } = Cu.import("resource://gre/modules/Preferences.jsm", {});
 let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
 let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+let { console } = devtools.require("resource://gre/modules/devtools/Console.jsm");
 let { merge } = devtools.require("sdk/util/object");
 let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 let { getPerformanceFront, PerformanceFront } = devtools.require("devtools/performance/front");
 let TargetFactory = devtools.TargetFactory;
 
 let mm = null;
 
 const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-utils.js"
--- a/browser/devtools/performance/test/unit/head.js
+++ b/browser/devtools/performance/test/unit/head.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+let { console } = devtools.require("resource://gre/modules/devtools/Console.jsm");
 const RecordingUtils = devtools.require("devtools/performance/recording-utils");
 
 const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";
 
 /**
  * Get a path in a FrameNode call tree.
  */
 function getFrameNodePath(root, path) {
--- a/browser/devtools/performance/test/unit/test_tree-model-06.js
+++ b/browser/devtools/performance/test/unit/test_tree-model-06.js
@@ -1,77 +1,113 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that when constructing FrameNodes, if optimization data is available,
- * the FrameNodes have the correct optimization data after iterating over samples.
+ * the FrameNodes have the correct optimization data after iterating over samples,
+ * and only youngest frames capture optimization data.
  */
 
+function run_test() {
+  run_next_test();
+}
+
+add_task(function test() {
+  let { ThreadNode } = devtools.require("devtools/performance/tree-model");
+  let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 0, endTime: 30 }), "(root)");
+
+  let A = getFrameNodePath(root, "A");
+  let B = getFrameNodePath(A, "B");
+  let C = getFrameNodePath(B, "C");
+  let Aopts = A.getOptimizations();
+  let Bopts = B.getOptimizations();
+  let Copts = C.getOptimizations();
+
+  ok(!Aopts, "A() was never youngest frame, so should not have optimization data");
+
+  equal(Bopts.length, 2, "B() only has optimization data when it was a youngest frame");
+
+  // Check a few properties on the OptimizationSites.
+  let optSitesObserved = new Set();
+  for (let opt of Bopts) {
+    if (opt.data.line === 12) {
+      equal(opt.samples, 2, "Correct amount of samples for B()'s first opt site");
+      equal(opt.data.attempts.length, 3, "First opt site has 3 attempts");
+      equal(opt.data.attempts[0].strategy, "SomeGetter1", "inflated strategy name");
+      equal(opt.data.attempts[0].outcome, "Failure1", "inflated outcome name");
+      equal(opt.data.types[0].typeset[0].keyedBy, "constructor", "inflates type info");
+      optSitesObserved.add("first");
+    } else {
+      equal(opt.samples, 1, "Correct amount of samples for B()'s second opt site");
+      optSitesObserved.add("second");
+    }
+  }
+
+  ok(optSitesObserved.has("first"), "first opt site for B() was checked");
+  ok(optSitesObserved.has("second"), "second opt site for B() was checked");
+
+  equal(Copts.length, 1, "C() always youngest frame, so has optimization data");
+});
+
 let gUniqueStacks = new RecordingUtils.UniqueStacks();
 
 function uniqStr(s) {
   return gUniqueStacks.getOrAddStringIndex(s);
 }
 
-let time = 1;
-
 let gThread = RecordingUtils.deflateThread({
   samples: [{
     time: 0,
     frames: [
       { location: "(root)" }
     ]
   }, {
-    time: time++,
-    frames: [
-      { location: "(root)" },
-      { location: "A_O1" },
-      { location: "B" },
-      { location: "C" }
-    ]
-  }, {
-    time: time++,
+    time: 10,
     frames: [
       { location: "(root)" },
-      { location: "A_O1" },
-      { location: "D" },
-      { location: "C" }
+      { location: "A" },
+      { location: "B_LEAF_1" }
     ]
   }, {
-    time: time++,
-    frames: [
-      { location: "(root)" },
-      { location: "A_O2" },
-      { location: "E_O3" },
-      { location: "C" }
-    ],
-  }, {
-    time: time++,
+    time: 15,
     frames: [
       { location: "(root)" },
       { location: "A" },
-      { location: "B" },
-      { location: "F" }
+      { location: "B_NOTLEAF" },
+      { location: "C" },
+    ]
+  }, {
+    time: 20,
+    frames: [
+      { location: "(root)" },
+      { location: "A" },
+      { location: "B_LEAF_2" }
+    ]
+  }, {
+    time: 25,
+    frames: [
+      { location: "(root)" },
+      { location: "A" },
+      { location: "B_LEAF_2" }
     ]
   }],
   markers: []
 }, gUniqueStacks);
 
-// 3 OptimizationSites
 let gRawSite1 = {
   line: 12,
   column: 2,
   types: [{
     mirType: uniqStr("Object"),
-    site: uniqStr("A (http://foo/bar/bar:12)"),
+    site: uniqStr("B (http://foo/bar:10)"),
     typeset: [{
         keyedBy: uniqStr("constructor"),
         name: uniqStr("Foo"),
-        location: uniqStr("A (http://foo/bar/baz:12)")
+        location: uniqStr("B (http://foo/bar:10)")
     }, {
         keyedBy: uniqStr("primitive"),
         location: uniqStr("self-hosted")
     }]
   }],
   attempts: {
     schema: {
       outcome: 0,
@@ -81,17 +117,17 @@ let gRawSite1 = {
       [uniqStr("Failure1"), uniqStr("SomeGetter1")],
       [uniqStr("Failure2"), uniqStr("SomeGetter2")],
       [uniqStr("Inlined"), uniqStr("SomeGetter3")]
     ]
   }
 };
 
 let gRawSite2 = {
-  line: 34,
+  line: 22,
   types: [{
     mirType: uniqStr("Int32"),
     site: uniqStr("Receiver")
   }],
   attempts: {
     schema: {
       outcome: 0,
       strategy: 1
@@ -99,83 +135,40 @@ let gRawSite2 = {
     data: [
       [uniqStr("Failure1"), uniqStr("SomeGetter1")],
       [uniqStr("Failure2"), uniqStr("SomeGetter2")],
       [uniqStr("Failure3"), uniqStr("SomeGetter3")]
     ]
   }
 };
 
-let gRawSite3 = {
-  line: 78,
-  types: [{
-    mirType: uniqStr("Object"),
-    site: uniqStr("A (http://foo/bar/bar:12)"),
-    typeset: [{
-      keyedBy: uniqStr("constructor"),
-      name: uniqStr("Foo"),
-      location: uniqStr("A (http://foo/bar/baz:12)")
-    }, {
-      keyedBy: uniqStr("primitive"),
-      location: uniqStr("self-hosted")
-    }]
-  }],
-  attempts: {
-    schema: {
-      outcome: 0,
-      strategy: 1
-    },
-    data: [
-      [uniqStr("Failure1"), uniqStr("SomeGetter1")],
-      [uniqStr("Failure2"), uniqStr("SomeGetter2")],
-      [uniqStr("GenericSuccess"), uniqStr("SomeGetter3")]
-    ]
-  }
-};
+function serialize (x) {
+  return JSON.parse(JSON.stringify(x));
+}
 
 gThread.frameTable.data.forEach((frame) => {
   const LOCATION_SLOT = gThread.frameTable.schema.location;
   const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
 
   let l = gThread.stringTable[frame[LOCATION_SLOT]];
   switch (l) {
-  case "A_O1":
-    frame[LOCATION_SLOT] = uniqStr("A");
-    frame[OPTIMIZATIONS_SLOT] = gRawSite1;
+  case "A":
+    frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+    break;
+  // Rename some of the location sites so we can register different
+  // frames with different opt sites
+  case "B_LEAF_1":
+    frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite2);
+    frame[LOCATION_SLOT] = uniqStr("B");
     break;
-  case "A_O2":
-    frame[LOCATION_SLOT] = uniqStr("A");
-    frame[OPTIMIZATIONS_SLOT] = gRawSite2;
+  case "B_LEAF_2":
+    frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+    frame[LOCATION_SLOT] = uniqStr("B");
     break;
-  case "E_O3":
-    frame[LOCATION_SLOT] = uniqStr("E");
-    frame[OPTIMIZATIONS_SLOT] = gRawSite3;
+  case "B_NOTLEAF":
+    frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+    frame[LOCATION_SLOT] = uniqStr("B");
+    break;
+  case "C":
+    frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
     break;
   }
 });
-
-function run_test() {
-  run_next_test();
-}
-
-add_task(function test() {
-  let { ThreadNode } = devtools.require("devtools/performance/tree-model");
-
-  let root = new ThreadNode(gThread, { startTime: 0, endTime: 4 });
-
-  let A = getFrameNodePath(root, "(root) > A");
-
-  let opts = A.getOptimizations();
-  let sites = opts.optimizationSites;
-  equal(sites.length, 2, "Frame A has two optimization sites.");
-  equal(sites[0].samples, 2, "first opt site has 2 samples.");
-  equal(sites[1].samples, 1, "second opt site has 1 sample.");
-
-  let E = getFrameNodePath(A, "E");
-  opts = E.getOptimizations();
-  sites = opts.optimizationSites;
-  equal(sites.length, 1, "Frame E has one optimization site.");
-  equal(sites[0].samples, 1, "first opt site has 1 samples.");
-
-  let D = getFrameNodePath(A, "D");
-  ok(!D.getOptimizations(),
-    "frames that do not have any opts data do not have JITOptimizations instances.");
-});
--- a/browser/devtools/sourceeditor/codemirror/README
+++ b/browser/devtools/sourceeditor/codemirror/README
@@ -106,25 +106,27 @@ diff --git a/browser/devtools/sourceedit
    }
 -  var queryDialog =
 -    'Search: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>';
 +  var queryDialog;
    function doSearch(cm, rev) {
 +    if (!queryDialog) {
 +      let doc = cm.getWrapperElement().ownerDocument;
 +      let inp = doc.createElement("input");
-+      let txt = doc.createTextNode(cm.l10n("findCmd.promptMessage"));
 +
-+      inp.type = "text";
-+      inp.style.width = "10em";
++      inp.type = "search";
++      inp.placeholder = cm.l10n("findCmd.promptMessage");
 +      inp.style.MozMarginStart = "1em";
++      inp.style.MozMarginEnd = "1em";
++      inp.style.flexGrow = "1";
++      inp.addEventListener("focus", () => inp.select());
 +
 +      queryDialog = doc.createElement("div");
-+      queryDialog.appendChild(txt);
 +      queryDialog.appendChild(inp);
++      queryDialog.style.display = "flex";
 +    }
      var state = getSearchState(cm);
      if (state.query) return findNext(cm, rev);
      dialog(cm, queryDialog, "Search for:", cm.getSelection(), function(query) {
        cm.operation(function() {
          if (!query || state.query) return;
          state.query = parseQuery(query);
          cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
--- a/browser/devtools/sourceeditor/codemirror/search/search.js
+++ b/browser/devtools/sourceeditor/codemirror/search/search.js
@@ -70,25 +70,27 @@
     }
     return query;
   }
   var queryDialog;
   function doSearch(cm, rev) {
     if (!queryDialog) {
       let doc = cm.getWrapperElement().ownerDocument;
       let inp = doc.createElement("input");
-      let txt = doc.createTextNode(cm.l10n("findCmd.promptMessage"));
 
-      inp.type = "text";
-      inp.style.width = "10em";
+      inp.type = "search";
+      inp.placeholder = cm.l10n("findCmd.promptMessage");
       inp.style.MozMarginStart = "1em";
+      inp.style.MozMarginEnd = "1em";
+      inp.style.flexGrow = "1";
+      inp.addEventListener("focus", () => inp.select());
 
       queryDialog = doc.createElement("div");
-      queryDialog.appendChild(txt);
       queryDialog.appendChild(inp);
+      queryDialog.style.display = "flex";
     }
     var state = getSearchState(cm);
     if (state.query) return findNext(cm, rev);
     dialog(cm, queryDialog, "Search for:", cm.getSelection(), function(query) {
       cm.operation(function() {
         if (!query || state.query) return;
         state.query = parseQuery(query);
         cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
--- a/browser/themes/shared/devtools/layoutview.css
+++ b/browser/themes/shared/devtools/layoutview.css
@@ -1,60 +1,339 @@
 /* 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/. */
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/ */
 
 .theme-sidebar {
   box-sizing: border-box;
 }
 
+body {
+  /* The view will grow bigger as the window gets resized, until 400px */
+  max-width: 400px;
+  margin: 0px auto;
+  padding: 0;
+  /* "Contain" the absolutely positioned #main element */
+  position: relative;
+}
+
+/* Header: contains the position and size of the element */
+
+#header {
+  box-sizing: border-box;
+  width: 100%;
+  padding: 4px 14px;
+  display: -moz-box;
+  vertical-align: top;
+}
+
+#header:-moz-dir(rtl) {
+  -moz-box-direction: reverse;
+}
+
+#header > span {
+  display: -moz-box;
+}
+
+#element-size {
+  -moz-box-flex: 1;
+}
+
+#element-size:-moz-dir(rtl) {
+  -moz-box-pack: end;
+}
+
+@media (max-height: 228px) {
+  #header {
+    padding-top: 0;
+    padding-bottom: 0;
+    margin-top: 10px;
+    margin-bottom: 8px;
+  }
+}
+
+/* Main: contains the box-model regions */
+
 #main {
+  position: absolute;
+  box-sizing: border-box;
+  /* The regions are semi-transparent, so the white background is partly
+     visible */
   background-color: white;
-  border-color: hsla(210,100%,85%,0.7);
-  border-style: dotted;
   color: var(--theme-selection-color);
+  /* Make sure there is some space between the window's edges and the regions */
+  margin: 0 14px 10px 14px;
+  width: calc(100% - 2 * 14px);
 }
 
 .margin,
 .size {
   color: var(--theme-highlight-blue);
 }
 
+/* Regions are 3 nested elements with wide borders and outlines */
+
 #content {
-  background-color: #87ceeb;
-  border-color: hsl(210,100%,85%);
-  border-style: dotted;
+  height: 25px;
 }
 
-#padding,
-#margins {
+#margins,
+#borders,
+#padding {
   border-color: hsla(210,100%,85%,0.2);
+  border-width: 25px;
+  border-style: solid;
   outline: dotted 1px hsl(210,100%,85%);
 }
 
-#padding {
-  background-color: #6a5acd;
+#margins {
+  /* This opacity applies to all of the regions, since they are nested */
+  opacity: .8;
+}
+
+/* Respond to window size change by changing the size of the regions */
+
+@media (max-height: 228px) {
+  #content {
+    height: 18px;
+  }
+
+  #margins,
+  #borders,
+  #padding {
+    border-width: 18px;
+  }
+}
+
+/* Regions colors */
+
+#margins {
+  border-color: #edff64;
 }
 
 #borders {
-  background-color: #444444;
-  border-style: dotted;
-  border-color: hsl(210,100%,85%);
+  border-color: #444444;
+}
+
+#padding {
+  border-color: #6a5acd;
+}
+
+#content {
+  background-color: #87ceeb;
+}
+
+/* Editable region sizes are contained in absolutely positioned <p> */
+
+#main > p {
+  position: absolute;
+  pointer-events: none;
+}
+
+#main > p {
+  margin: 0;
+  text-align: center;
+}
+
+#main > p > span {
+  vertical-align: middle;
+  pointer-events: auto;
+}
+
+/* Coordinates for the region sizes */
+
+.top,
+.bottom {
+  width: calc(100% - 2px);
+  text-align: center;
+}
+
+.padding.top {
+  top: 55px;
+}
+
+.padding.bottom {
+  bottom: 57px;
+}
+
+.border.top {
+  top: 30px;
+}
+
+.border.bottom {
+  bottom: 31px;
+}
+
+.margin.top {
+  top: 5px;
+}
+
+.margin.bottom {
+  bottom: 6px;
+}
+
+.size,
+.margin.left,
+.margin.right,
+.border.left,
+.border.right,
+.padding.left,
+.padding.right {
+  top: 22px;
+  line-height: 132px;
+}
+
+.size {
+  width: calc(100% - 2px);
+}
+
+.margin.right,
+.margin.left,
+.border.left,
+.border.right,
+.padding.right,
+.padding.left {
+  width: 25px;
+}
+
+.padding.left {
+  left: 52px;
+}
+
+.padding.right {
+  right: 51px;
+}
+
+.border.left {
+  left: 26px;
+}
+
+.border.right {
+  right: 26px;
 }
 
-#margins {
-  background-color: #edff64;
-  /* This opacity applies to all of the regions, since they are nested. */
-  opacity: .8;
+.margin.right {
+  right: 0;
+}
+
+.margin.left {
+  left: 0;
+}
+
+.rotate.left:not(.editing) {
+  transform: rotate(-90deg);
+}
+
+.rotate.right:not(.editing) {
+  transform: rotate(90deg);
 }
 
+/* Coordinates should be different when the window is small, because we make
+   the regions smaller then */
+
+@media (max-height: 228px) {
+  .padding.top {
+    top: 37px;
+  }
+
+  .padding.bottom {
+    bottom: 38px;
+  }
+
+  .border.top {
+    top: 19px;
+  }
+
+  .border.bottom {
+    bottom: 20px;
+  }
+
+  .margin.top {
+    top: 1px;
+  }
+
+  .margin.bottom {
+    bottom: 2px;
+  }
+
+  .size,
+  .margin.left,
+  .margin.right,
+  .border.left,
+  .border.right,
+  .padding.left,
+  .padding.right {
+    line-height: 80px;
+  }
+
+  .margin.right,
+  .margin.left,
+  .border.left,
+  .border.right,
+  .padding.right,
+  .padding.left {
+    width: 21px;
+  }
+
+  .padding.left {
+    left: 35px;
+  }
+
+  .padding.right {
+    right: 35px;
+  }
+
+  .border.left {
+    left: 16px;
+  }
+
+  .border.right {
+    right: 17px;
+  }
+}
+
+/* Legend, displayed inside regions */
+
+.legend {
+  position: absolute;
+  margin: 5px 6px;
+  z-index: 1;
+}
+
+.legend[data-box="margin"] {
+  color: var(--theme-highlight-blue);
+}
+
+@media (max-height: 228px) {
+  .legend {
+    margin: 2px 6px;
+  }
+}
+
+/* Editable fields */
+
 .editable {
   border: 1px dashed transparent;
+  -moz-user-select: text;
 }
 
 .editable:hover {
-  border-bottom-color: hsl(0,0%,50%);
+  border-bottom-color: hsl(0, 0%, 50%);
 }
 
 .styleinspector-propertyeditor {
-  border: 1px solid #CCC;
+  border: 1px solid #ccc;
   padding: 0;
 }
+
+/* Make sure the content size doesn't appear as editable like the other sizes */
+
+.size > span {
+  cursor: default;
+}
+
+/* Hide all values when the view is inactive */
+
+body.inactive > #header > #element-position,
+body.inactive > #header > #element-size,
+body.inactive > #main > p {
+   visibility: hidden;
+}
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -317,16 +317,17 @@ description > html|a {
 #dialogBox > .groupbox-title {
   padding: 3.5px 0;
   background-color: #F1F1F1;
   border-bottom: 1px solid #C1C1C1;
 }
 
 #dialogTitle {
   text-align: center;
+  -moz-user-select: none;
 }
 
 .close-icon {
   background-color: transparent !important;
   border: none;
   box-shadow: none;
   height: 18px;
   padding: 0;
--- a/dom/bluetooth/bluetooth2/tests/marionette/manifest.ini
+++ b/dom/bluetooth/bluetooth2/tests/marionette/manifest.ini
@@ -1,11 +1,17 @@
 [DEFAULT]
 b2g = true
 browser = false
 qemu = false
 
+disabled = Bug 1175389
 [test_dom_BluetoothManager_API2.js]
+disabled = Bug 1175389
 [test_dom_BluetoothAdapter_enable_API2.js]
+disabled = Bug 1175389
 [test_dom_BluetoothAdapter_setters_API2.js]
+disabled = Bug 1175389
 [test_dom_BluetoothAdapter_discovery_API2.js]
+disabled = Bug 1175389
 [test_dom_BluetoothDevice_API2.js]
+disabled = Bug 1175389
 [test_dom_BluetoothAdapter_pair_API2.js]
--- a/dom/camera/DOMCameraControl.cpp
+++ b/dom/camera/DOMCameraControl.cpp
@@ -196,16 +196,17 @@ nsDOMCameraControl::nsDOMCameraControl(u
                                        Promise* aPromise,
                                        nsPIDOMWindow* aWindow)
   : DOMMediaStream()
   , mCameraControl(nullptr)
   , mAudioChannelAgent(nullptr)
   , mGetCameraPromise(aPromise)
   , mWindow(aWindow)
   , mPreviewState(CameraControlListener::kPreviewStopped)
+  , mRecording(false)
   , mSetInitialConfig(false)
 {
   DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
   mInput = new CameraPreviewMediaStream(this);
 
   BindToOwner(aWindow);
 
   nsRefPtr<DOMCameraConfiguration> initialConfig =
@@ -737,66 +738,63 @@ nsDOMCameraControl::StartRecording(const
 {
   DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
 
   nsRefPtr<Promise> promise = CreatePromise(aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
 
-  if (mStartRecordingPromise) {
+  if (mStartRecordingPromise || mRecording) {
     promise->MaybeReject(NS_ERROR_IN_PROGRESS);
     return promise.forget();
   }
 
-  NotifyRecordingStatusChange(NS_LITERAL_STRING("starting"));
-
-#ifdef MOZ_B2G
-  if (!mAudioChannelAgent) {
-    mAudioChannelAgent = do_CreateInstance("@mozilla.org/audiochannelagent;1");
-    if (mAudioChannelAgent) {
-      // Camera app will stop recording when it falls to the background, so no callback is necessary.
-      mAudioChannelAgent->Init(mWindow, (int32_t)AudioChannel::Content, nullptr);
-      // Video recording doesn't output any sound, so it's not necessary to check canPlay.
-      int32_t canPlay;
-      mAudioChannelAgent->StartPlaying(&canPlay);
-    }
+  aRv = NotifyRecordingStatusChange(NS_LITERAL_STRING("starting"));
+  if (aRv.Failed()) {
+    return nullptr;
   }
-#endif
 
   mDSFileDescriptor = new DeviceStorageFileDescriptor();
   nsRefPtr<DOMRequest> request = aStorageArea.CreateFileDescriptor(aFilename,
                                                                    mDSFileDescriptor.get(),
                                                                    aRv);
   if (aRv.Failed()) {
+    NotifyRecordingStatusChange(NS_LITERAL_STRING("shutdown"));
     return nullptr;
   }
 
   mStartRecordingPromise = promise;
   mOptions = aOptions;
 
   EventListenerManager* elm = request->GetOrCreateListenerManager();
   if (!elm) {
+    NotifyRecordingStatusChange(NS_LITERAL_STRING("shutdown"));
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
 
+  mRecording = true;
   nsCOMPtr<nsIDOMEventListener> listener = new StartRecordingHelper(this);
   elm->AddEventListener(NS_LITERAL_STRING("success"), listener, false, false);
   elm->AddEventListener(NS_LITERAL_STRING("error"), listener, false, false);
   return promise.forget();
 }
 
 void
 nsDOMCameraControl::OnCreatedFileDescriptor(bool aSucceeded)
 {
   nsresult rv = NS_ERROR_FAILURE;
 
   if (!mCameraControl) {
     rv = NS_ERROR_NOT_AVAILABLE;
+  } else if (!mRecording) {
+    // Race condition where StopRecording comes in before we issue
+    // the start recording request to Gonk
+    rv = NS_ERROR_ABORT;
   } else if (aSucceeded && mDSFileDescriptor->mFileDescriptor.IsValid()) {
     ICameraControl::StartRecordingOptions o;
 
     o.rotation = mOptions.mRotation;
     o.maxFileSizeBytes = mOptions.mMaxFileSizeBytes;
     o.maxVideoLengthMs = mOptions.mMaxVideoLengthMs;
     o.autoEnableLowLightTorch = mOptions.mAutoEnableLowLightTorch;
     rv = mCameraControl->StartRecording(mDSFileDescriptor.get(), &o);
@@ -818,23 +816,18 @@ nsDOMCameraControl::OnCreatedFileDescrip
 }
 
 void
 nsDOMCameraControl::StopRecording(ErrorResult& aRv)
 {
   DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
   THROW_IF_NO_CAMERACONTROL();
 
-#ifdef MOZ_B2G
-  if (mAudioChannelAgent) {
-    mAudioChannelAgent->StopPlaying();
-    mAudioChannelAgent = nullptr;
-  }
-#endif
-
+  ReleaseAudioChannelAgent();
+  mRecording = false;
   aRv = mCameraControl->StopRecording();
 }
 
 void
 nsDOMCameraControl::ResumePreview(ErrorResult& aRv)
 {
   DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
   THROW_IF_NO_CAMERACONTROL();
@@ -1034,25 +1027,60 @@ nsDOMCameraControl::Shutdown()
   AbortPromise(mSetConfigurationPromise);
 
   if (mCameraControl) {
     mCameraControl->Stop();
     mCameraControl = nullptr;
   }
 }
 
+void
+nsDOMCameraControl::ReleaseAudioChannelAgent()
+{
+#ifdef MOZ_B2G
+  if (mAudioChannelAgent) {
+    mAudioChannelAgent->StopPlaying();
+    mAudioChannelAgent = nullptr;
+  }
+#endif
+}
+
 nsresult
 nsDOMCameraControl::NotifyRecordingStatusChange(const nsString& aMsg)
 {
   NS_ENSURE_TRUE(mWindow, NS_ERROR_FAILURE);
 
-  return MediaManager::NotifyRecordingStatusChange(mWindow,
-                                                   aMsg,
-                                                   true /* aIsAudio */,
-                                                   true /* aIsVideo */);
+  if (aMsg.EqualsLiteral("shutdown")) {
+    ReleaseAudioChannelAgent();
+  }
+
+  nsresult rv = MediaManager::NotifyRecordingStatusChange(mWindow,
+                                                          aMsg,
+                                                          true /* aIsAudio */,
+                                                          true /* aIsVideo */);
+
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+#ifdef MOZ_B2G
+  if (aMsg.EqualsLiteral("starting") && !mAudioChannelAgent) {
+    mAudioChannelAgent = do_CreateInstance("@mozilla.org/audiochannelagent;1");
+    if (!mAudioChannelAgent) {
+      return NS_ERROR_UNEXPECTED;
+    }
+
+    // Camera app will stop recording when it falls to the background, so no callback is necessary.
+    mAudioChannelAgent->Init(mWindow, (int32_t)AudioChannel::Content, nullptr);
+    // Video recording doesn't output any sound, so it's not necessary to check canPlay.
+    int32_t canPlay;
+    mAudioChannelAgent->StartPlaying(&canPlay);
+  }
+#endif
+  return rv;
 }
 
 already_AddRefed<Promise>
 nsDOMCameraControl::CreatePromise(ErrorResult& aRv)
 {
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
   if (!global) {
     aRv.Throw(NS_ERROR_FAILURE);
@@ -1226,17 +1254,17 @@ nsDOMCameraControl::OnPreviewStateChange
   DispatchPreviewStateEvent(aState);
 }
 
 void
 nsDOMCameraControl::OnRecorderStateChange(CameraControlListener::RecorderState aState,
                                           int32_t aArg, int32_t aTrackNum)
 {
   // For now, we do nothing with 'aStatus' and 'aTrackNum'.
-  DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
+  DOM_CAMERA_LOGT("%s:%d : this=%p, state=%u\n", __func__, __LINE__, this, aState);
   MOZ_ASSERT(NS_IsMainThread());
 
   ErrorResult ignored;
   nsString state;
 
   switch (aState) {
     case CameraControlListener::kRecorderStarted:
       {
@@ -1419,17 +1447,17 @@ nsDOMCameraControl::OnTakePictureComplet
                                                      eventInit);
 
   DispatchTrustedEvent(event);
 }
 
 void
 nsDOMCameraControl::OnUserError(CameraControlListener::UserContext aContext, nsresult aError)
 {
-  DOM_CAMERA_LOGI("DOM OnUserError : this=%paContext=%u, aError=0x%x\n",
+  DOM_CAMERA_LOGI("DOM OnUserError : this=%p, aContext=%u, aError=0x%x\n",
     this, aContext, aError);
   MOZ_ASSERT(NS_IsMainThread());
 
   nsRefPtr<Promise> promise;
 
   switch (aContext) {
     case CameraControlListener::kInStartCamera:
       promise = mGetCameraPromise.forget();
@@ -1472,16 +1500,18 @@ nsDOMCameraControl::OnUserError(CameraCo
       break;
 
     case CameraControlListener::kInTakePicture:
       promise = mTakePicturePromise.forget();
       break;
 
     case CameraControlListener::kInStartRecording:
       promise = mStartRecordingPromise.forget();
+      mRecording = false;
+      NotifyRecordingStatusChange(NS_LITERAL_STRING("shutdown"));
       break;
 
     case CameraControlListener::kInStartFaceDetection:
       // This method doesn't have any callbacks, so all we can do is log the
       // failure. This only happens after the hardware has been released.
       NS_WARNING("Failed to start face detection");
       return;
 
--- a/dom/camera/DOMCameraControl.h
+++ b/dom/camera/DOMCameraControl.h
@@ -177,16 +177,17 @@ protected:
   void OnRecorderStateChange(CameraControlListener::RecorderState aState, int32_t aStatus, int32_t aTrackNum);
   void OnConfigurationChange(DOMCameraConfiguration* aConfiguration);
   void OnShutter();
   void OnUserError(CameraControlListener::UserContext aContext, nsresult aError);
 
   bool IsWindowStillActive();
   nsresult SelectPreviewSize(const dom::CameraSize& aRequestedPreviewSize, ICameraControl::Size& aSelectedPreviewSize);
 
+  void ReleaseAudioChannelAgent();
   nsresult NotifyRecordingStatusChange(const nsString& aMsg);
 
   already_AddRefed<dom::Promise> CreatePromise(ErrorResult& aRv);
   void AbortPromise(nsRefPtr<dom::Promise>& aPromise);
   virtual void EventListenerAdded(nsIAtom* aType) override;
   void DispatchPreviewStateEvent(DOMCameraControlListener::PreviewState aState);
   void DispatchStateEvent(const nsString& aType, const nsString& aState);
 
@@ -218,17 +219,17 @@ protected:
   nsRefPtr<CameraPreviewMediaStream> mInput;
 
   // set once when this object is created
   nsCOMPtr<nsPIDOMWindow>   mWindow;
 
   dom::CameraStartRecordingOptions mOptions;
   nsRefPtr<DeviceStorageFileDescriptor> mDSFileDescriptor;
   DOMCameraControlListener::PreviewState mPreviewState;
-
+  bool mRecording;
   bool mSetInitialConfig;
 
 #ifdef MOZ_WIDGET_GONK
   // cached camera control, to improve start-up time
   static StaticRefPtr<ICameraControl> sCachedCameraControl;
   static nsresult sCachedCameraControlStartResult;
   static nsCOMPtr<nsITimer> sDiscardCachedCameraControlTimer;
 #endif
--- a/dom/camera/test/test_camera_record.html
+++ b/dom/camera/test/test_camera_record.html
@@ -14,32 +14,39 @@
 <script class="testbody" type="text/javascript;version=1.7">
 
 var suite = new CameraTestSuite();
 
 var baseConfig = {
   mode: 'video',
 };
 
+var testFilePath = 'test.3gp';
 var storage = navigator.getDeviceStorage("videos");
 
+function cleanup()
+{
+  return storage.delete(testFilePath).then(function(p) {
+  }, function(e) {
+    Promise.resolve();
+  });
+}
+
 suite.test('recording', function() {
-  storage.delete("test.3gp");
-
   function startRecording(p) {
     var eventPromise = new Promise(function(resolve, reject) {
       function onEvent(evt) {
         ok(evt.newState === 'Started', 'recorder state change event = ' + evt.newState);
         suite.camera.removeEventListener('recorderstatechange', onEvent);
         resolve();
       }
       suite.camera.addEventListener('recorderstatechange', onEvent);
     });
 
-    var domPromise = suite.camera.startRecording({}, storage, "test.3gp");
+    var domPromise = suite.camera.startRecording({}, storage, testFilePath);
     return Promise.all([domPromise, eventPromise]);
   }
 
   function stopRecording(p) {
     var eventPromise = new Promise(function(resolve, reject) {
       function onEvent(evt) {
         ok(evt.newState === 'Stopped', 'recorder state change event = ' + evt.newState);
         suite.camera.removeEventListener('recorderstatechange', onEvent);
@@ -56,20 +63,100 @@ suite.test('recording', function() {
         reject(e);
       }
     });
 
     return Promise.all([domPromise, eventPromise]);
   }
 
   return suite.getCamera(undefined, baseConfig)
-    .then(startRecording, suite.rejectGetCamera)
+    .then(cleanup, suite.rejectGetCamera)
+    .then(startRecording)
     .then(stopRecording, suite.rejectStartRecording)
     .catch(suite.rejectStopRecording);
 });
 
+// bug 1152500
+suite.test('interrupt-record', function() {
+  function startRecording(p) {
+    var startPromise = suite.camera.startRecording({}, storage, testFilePath);
+    suite.camera.stopRecording();
+    return startPromise;
+  }
+
+  function rejectStartRecording(e) {
+    ok(e.name === 'NS_ERROR_ABORT', 'onError called correctly on startRecording interrupted: ' + e);
+  }
+
+  return suite.getCamera(undefined, baseConfig)
+    .then(cleanup, suite.rejectGetCamera)
+    .then(startRecording)
+    .then(suite.expectedRejectStartRecording, rejectStartRecording);
+});
+
+// bug 1152500
+suite.test('already-initiated-recording', function() {
+  function startRecording(p) {
+    return new Promise(function(resolve, reject) {
+      var firstCall = false;
+      var secondCall = false;
+
+      function end() {
+        if (firstCall && secondCall) {
+          resolve();
+        }
+      }
+
+      suite.camera.startRecording({}, storage, testFilePath).then(function(p) {
+        ok(true, "First call to startRecording() succeeded");
+        firstCall = true;
+        end();
+      }, function(e) {
+        ok(false, "First call to startRecording() failed unexpectedly with: " + e);
+        firstCall = true;
+        end();
+      });
+
+      suite.camera.startRecording({}, storage, testFilePath).then(function(p) {
+        ok(false, "Second call to startRecording() succeeded unexpectedly");
+        secondCall = true;
+        end();
+      }, function(e) {
+        ok(e.name === 'NS_ERROR_IN_PROGRESS', "Second call to startRecording() failed expectedly with: " + e);
+        secondCall = true;
+        end();
+      });
+    });
+  }
+
+  return suite.getCamera(undefined, baseConfig)
+    .then(cleanup, suite.rejectGetCamera)
+    .then(startRecording);
+});
+
+// bug 1152500
+suite.test('already-started-recording', function() {
+  function startRecording(p) {
+    return suite.camera.startRecording({}, storage, testFilePath);
+  }
+
+  function startRecordingAgain(p) {
+    return suite.camera.startRecording({}, storage, testFilePath);
+  }
+
+  function rejectStartRecordingAgain(e) {
+    ok(e.name === 'NS_ERROR_IN_PROGRESS', "Second call to startRecording() failed expectedly with: " + e);
+  }
+
+  return suite.getCamera(undefined, baseConfig)
+    .then(cleanup, suite.rejectGetCamera)
+    .then(startRecording)
+    .then(startRecordingAgain, suite.rejectStartRecording)
+    .then(suite.expectedRejectStartRecording, rejectStartRecordingAgain)
+});
+
 suite.setup()
   .then(suite.run);
 
 </script>
 </body>
 
 </html>
--- a/dom/devicestorage/DeviceStorage.h
+++ b/dom/devicestorage/DeviceStorage.h
@@ -99,17 +99,17 @@ public:
   void CollectFiles(nsTArray<nsRefPtr<DeviceStorageFile> >& aFiles,
                     PRTime aSince = 0);
   void collectFilesInternal(nsTArray<nsRefPtr<DeviceStorageFile> >& aFiles,
                             PRTime aSince, nsAString& aRootPath);
 
   void AccumDiskUsage(uint64_t* aPicturesSoFar, uint64_t* aVideosSoFar,
                       uint64_t* aMusicSoFar, uint64_t* aTotalSoFar);
 
-  void GetDiskFreeSpace(int64_t* aSoFar);
+  void GetStorageFreeSpace(int64_t* aSoFar);
   void GetStatus(nsAString& aStatus);
   void GetStorageStatus(nsAString& aStatus);
   void DoFormat(nsAString& aStatus);
   void DoMount(nsAString& aStatus);
   void DoUnmount(nsAString& aStatus);
   static void GetRootDirectoryForType(const nsAString& aStorageType,
                                       const nsAString& aStorageName,
                                       nsIFile** aFile);
@@ -178,16 +178,18 @@ public:
   void EventListenerWasAdded(const nsAString& aType,
                              ErrorResult& aRv,
                              JSCompartment* aCompartment) override;
 
   explicit nsDOMDeviceStorage(nsPIDOMWindow* aWindow);
 
   static int InstanceCount() { return sInstanceCount; }
 
+  static void InvalidateVolumeCaches();
+
   nsresult Init(nsPIDOMWindow* aWindow, const nsAString& aType,
                 const nsAString& aVolName);
 
   bool IsAvailable();
   bool IsFullPath(const nsAString& aPath)
   {
     return aPath.Length() > 0 && aPath.CharAt(0) == '/';
   }
--- a/dom/devicestorage/DeviceStorageAreaListener.cpp
+++ b/dom/devicestorage/DeviceStorageAreaListener.cpp
@@ -3,16 +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/. */
 
 #include "mozilla/dom/DeviceStorageAreaListener.h"
 #include "mozilla/dom/DeviceStorageAreaListenerBinding.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Services.h"
+#include "DeviceStorage.h"
 #include "nsIObserverService.h"
 #ifdef MOZ_WIDGET_GONK
 #include "nsIVolume.h"
 #include "nsIVolumeService.h"
 #endif
 
 namespace mozilla {
 namespace dom {
@@ -81,16 +82,18 @@ NS_IMPL_RELEASE_INHERITED(DeviceStorageA
 NS_INTERFACE_MAP_BEGIN(DeviceStorageAreaListener)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
 DeviceStorageAreaListener::DeviceStorageAreaListener(nsPIDOMWindow* aWindow)
   : DOMEventTargetHelper(aWindow)
 {
   MOZ_ASSERT(aWindow);
 
+  MOZ_ASSERT(NS_IsMainThread());
+
   mVolumeStateObserver = new VolumeStateObserver(this);
 #ifdef MOZ_WIDGET_GONK
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   if (obs) {
     obs->AddObserver(mVolumeStateObserver, NS_VOLUME_STATE_CHANGED, false);
   }
 #endif
 }
@@ -134,16 +137,18 @@ DeviceStorageAreaListener::DispatchStora
   init.mStorageName = aStorageName;
 
   nsRefPtr<DeviceStorageAreaChangedEvent> event =
     DeviceStorageAreaChangedEvent::Constructor(this,
                                                NS_LITERAL_STRING("storageareachanged"),
                                                init);
   event->SetTrusted(true);
 
+  mStorageAreaStateMap[aStorageName] = aOperation;
+
+  nsDOMDeviceStorage::InvalidateVolumeCaches();
+
   bool ignore;
   DOMEventTargetHelper::DispatchEvent(event, &ignore);
-
-  mStorageAreaStateMap[aStorageName] = aOperation;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/devicestorage/DeviceStorageRequestParent.cpp
+++ b/dom/devicestorage/DeviceStorageRequestParent.cpp
@@ -723,17 +723,17 @@ DeviceStorageRequestParent::FreeSpaceFil
 
 nsresult
 DeviceStorageRequestParent::FreeSpaceFileEvent::CancelableRun()
 {
   MOZ_ASSERT(!NS_IsMainThread());
 
   int64_t freeSpace = 0;
   if (mFile) {
-    mFile->GetDiskFreeSpace(&freeSpace);
+    mFile->GetStorageFreeSpace(&freeSpace);
   }
 
   nsCOMPtr<nsIRunnable> r;
   r = new PostFreeSpaceResultEvent(mParent, static_cast<uint64_t>(freeSpace));
   return NS_DispatchToMainThread(r);
 }
 
 DeviceStorageRequestParent::UsedSpaceFileEvent::
--- a/dom/devicestorage/nsDeviceStorage.cpp
+++ b/dom/devicestorage/nsDeviceStorage.cpp
@@ -143,17 +143,17 @@ static int64_t
 GetFreeBytes(const nsAString& aStorageName)
 {
   // This function makes the assumption that the various types
   // are all stored on the same filesystem. So we use pictures.
 
   nsRefPtr<DeviceStorageFile> dsf(new DeviceStorageFile(NS_LITERAL_STRING(DEVICESTORAGE_PICTURES),
                                                         aStorageName));
   int64_t freeBytes = 0;
-  dsf->GetDiskFreeSpace(&freeBytes);
+  dsf->GetStorageFreeSpace(&freeBytes);
   return freeBytes;
 }
 
 nsresult
 DeviceStorageUsedSpaceCache::AccumUsedSizes(const nsAString& aStorageName,
                                             uint64_t* aPicturesSoFar,
                                             uint64_t* aVideosSoFar,
                                             uint64_t* aMusicSoFar,
@@ -1623,17 +1623,17 @@ DeviceStorageFile::AccumDirectoryUsage(n
         *aMusicSoFar += size;
       }
       *aTotalSoFar += size;
     }
   }
 }
 
 void
-DeviceStorageFile::GetDiskFreeSpace(int64_t* aSoFar)
+DeviceStorageFile::GetStorageFreeSpace(int64_t* aSoFar)
 {
   DeviceStorageTypeChecker* typeChecker
     = DeviceStorageTypeChecker::CreateOrGet();
   if (!typeChecker) {
     return;
   }
   if (!mFile || !IsAvailable()) {
     return;
@@ -2833,17 +2833,17 @@ public:
   ~FreeSpaceFileEvent() {}
 
   NS_IMETHOD Run()
   {
     MOZ_ASSERT(!NS_IsMainThread());
 
     int64_t freeSpace = 0;
     if (mFile) {
-      mFile->GetDiskFreeSpace(&freeSpace);
+      mFile->GetStorageFreeSpace(&freeSpace);
     }
 
     nsCOMPtr<nsIRunnable> r;
     r = new PostResultEvent(mRequest.forget(),
                             static_cast<uint64_t>(freeSpace));
     return NS_DispatchToMainThread(r);
   }
 
@@ -3486,20 +3486,33 @@ nsDOMDeviceStorage::Shutdown()
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
   obs->RemoveObserver(this, kFileWatcherUpdate);
   obs->RemoveObserver(this, "disk-space-watcher");
 }
 
 StaticAutoPtr<nsTArray<nsString>> nsDOMDeviceStorage::sVolumeNameCache;
 
 // static
+void nsDOMDeviceStorage::InvalidateVolumeCaches()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Currently there is only the one volume cache. DeviceStorageAreaListener
+  // calls this function any time it detects a volume was added or removed.
+
+  sVolumeNameCache = nullptr;
+}
+
+// static
 void
 nsDOMDeviceStorage::GetOrderedVolumeNames(
   nsDOMDeviceStorage::VolumeNameArray &aVolumeNames)
 {
+  MOZ_ASSERT(NS_IsMainThread());
+
   if (sVolumeNameCache && sVolumeNameCache->Length() > 0) {
     aVolumeNames.AppendElements(*sVolumeNameCache);
     return;
   }
 #ifdef MOZ_WIDGET_GONK
   nsCOMPtr<nsIVolumeService> vs = do_GetService(NS_VOLUMESERVICE_CONTRACTID);
   if (vs) {
     nsCOMPtr<nsIArray> volNames;
@@ -3602,16 +3615,20 @@ nsDOMDeviceStorage::CreateDeviceStorageB
     }
     NS_ADDREF(*aStore = storage.get());
     return;
   }
 
   nsRefPtr<nsDOMDeviceStorage> storage = GetStorageByNameAndType(aWin,
                                                                  aName,
                                                                  aType);
+  if (!storage) {
+    *aStore = nullptr;
+    return;
+  }
   NS_ADDREF(*aStore = storage.get());
 }
 
 // static
 bool
 nsDOMDeviceStorage::ParseFullPath(const nsAString& aFullPath,
                                   nsAString& aOutStorageName,
                                   nsAString& aOutStoragePath)
--- a/dom/devicestorage/test/mochitest.ini
+++ b/dom/devicestorage/test/mochitest.ini
@@ -20,17 +20,17 @@ support-files = devicestorage_common.js
 [test_lastModificationFilter.html]
 [test_overrideDir.html]
 [test_overwrite.html]
 [test_sanity.html]
 [test_usedSpace.html]
 [test_watch.html]
 [test_watchOther.html]
 [test_storageAreaListener.html]
-skip-if = (toolkit != 'gonk') || (toolkit == 'gonk' && debug) # Bug 1173484
+skip-if = (toolkit != 'gonk')
 
 # FileSystem API tests
 [test_fs_basic.html]
 [test_fs_createDirectory.html]
 [test_fs_get.html]
 [test_fs_remove.html]
 [test_fs_createFile.html]
 [test_fs_appendFile.html]
--- a/dom/devicestorage/test/test_storageAreaListener.html
+++ b/dom/devicestorage/test/test_storageAreaListener.html
@@ -48,14 +48,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 			volumeService.removeFakeVolume(volName);
 		}
 		else if (e.operation == "removed") {
 			ok (true, "got removal event");
 			devicestorage_cleanup();
 		}
 	});
 
+	storage = navigator.getDeviceStorageByNameAndType(volName, "sdcard");
+	ok(!storage, "storage area doesn't exist");
+
 	volumeService.createFakeVolume(volName, mountPoint);
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/plugins/ipc/PluginModuleChild.cpp
+++ b/dom/plugins/ipc/PluginModuleChild.cpp
@@ -1324,27 +1324,22 @@ void
     ENSURE_PLUGIN_THREAD_VOID();
     NS_WARNING("Not yet implemented!");
 }
 
 void
 _memfree(void* aPtr)
 {
     PLUGIN_LOG_DEBUG_FUNCTION;
-    // Only assert plugin thread here for consistency with in-process plugins.
-    AssertPluginThread();
     free(aPtr);
 }
 
 uint32_t
 _memflush(uint32_t aSize)
 {
-    PLUGIN_LOG_DEBUG_FUNCTION;
-    // Only assert plugin thread here for consistency with in-process plugins.
-    AssertPluginThread();
     return 0;
 }
 
 void
 _reloadplugins(NPBool aReloadPages)
 {
     PLUGIN_LOG_DEBUG_FUNCTION;
     ENSURE_PLUGIN_THREAD_VOID();
@@ -1393,18 +1388,16 @@ const char*
     ENSURE_PLUGIN_THREAD(nullptr);
     return PluginModuleChild::GetChrome()->GetUserAgent();
 }
 
 void*
 _memalloc(uint32_t aSize)
 {
     PLUGIN_LOG_DEBUG_FUNCTION;
-    // Only assert plugin thread here for consistency with in-process plugins.
-    AssertPluginThread();
     return moz_xmalloc(aSize);
 }
 
 // Deprecated entry points for the old Java plugin.
 void* /* OJI type: JRIEnv* */
 _getjavaenv(void)
 {
     PLUGIN_LOG_DEBUG_FUNCTION;
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -594,23 +594,19 @@ pref("dom.w3c_touch_events.enabled", 1);
 
 #ifdef MOZ_SAFE_BROWSING
 pref("browser.safebrowsing.enabled", true);
 pref("browser.safebrowsing.malware.enabled", true);
 pref("browser.safebrowsing.debug", false);
 
 pref("browser.safebrowsing.updateURL", "https://safebrowsing.google.com/safebrowsing/downloads?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2&key=%GOOGLE_API_KEY%");
 pref("browser.safebrowsing.gethashURL", "https://safebrowsing.google.com/safebrowsing/gethash?client=SAFEBROWSING_ID&appver=%VERSION%&pver=2.2");
-pref("browser.safebrowsing.reportURL", "https://safebrowsing.google.com/safebrowsing/report?");
-pref("browser.safebrowsing.reportGenericURL", "http://%LOCALE%.phish-generic.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportErrorURL", "http://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportPhishURL", "http://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportMalwareURL", "http://%LOCALE%.malware-report.mozilla.com/?hl=%LOCALE%");
-pref("browser.safebrowsing.reportMalwareErrorURL", "http://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%");
-
+pref("browser.safebrowsing.reportPhishMistakeURL", "https://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%&url=");
+pref("browser.safebrowsing.reportPhishURL", "https://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%&url=");
+pref("browser.safebrowsing.reportMalwareMistakeURL", "https://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%&url=");
 pref("browser.safebrowsing.malware.reportURL", "https://safebrowsing.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site=");
 
 pref("browser.safebrowsing.id", @MOZ_APP_UA_NAME@);
 
 // Name of the about: page contributed by safebrowsing to handle display of error
 // pages on phishing/malware hits.  (bug 399233)
 pref("urlclassifier.alternate_error_page", "blocked");
 
--- a/mobile/android/base/AndroidGamepadManager.java
+++ b/mobile/android/base/AndroidGamepadManager.java
@@ -1,27 +1,27 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Timer;
 import java.util.TimerTask;
 
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.hardware.input.InputManager;
+import android.util.SparseArray;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
 
 public class AndroidGamepadManager {
     // This is completely arbitrary.
     private static final float TRIGGER_PRESSED_THRESHOLD = 0.25f;
@@ -124,56 +124,55 @@ public class AndroidGamepadManager {
                 } else {
                     triggerAxes = null;
                 }
             }
         }
     }
 
     private static boolean sStarted;
-    private static HashMap<Integer, Gamepad> sGamepads;
-    private static HashMap<Integer, List<KeyEvent>> sPendingGamepads;
+    private static final SparseArray<Gamepad> sGamepads = new SparseArray<>();
+    private static final SparseArray<List<KeyEvent>> sPendingGamepads = new SparseArray<>();
     private static InputManager.InputDeviceListener sListener;
     private static Timer sPollTimer;
 
     private AndroidGamepadManager() {
     }
 
     public static void startup() {
         ThreadUtils.assertOnUiThread();
         if (!sStarted) {
-            sGamepads = new HashMap<Integer, Gamepad>();
-            sPendingGamepads = new HashMap<Integer, List<KeyEvent>>();
             scanForGamepads();
             addDeviceListener();
             sStarted = true;
         }
     }
 
     public static void shutdown() {
         ThreadUtils.assertOnUiThread();
         if (sStarted) {
             removeDeviceListener();
-            sPendingGamepads = null;
-            sGamepads = null;
+            sPendingGamepads.clear();
+            sGamepads.clear();
             sStarted = false;
         }
     }
 
     public static void gamepadAdded(int deviceId, int serviceId) {
         ThreadUtils.assertOnUiThread();
         if (!sStarted) {
             return;
         }
-        if (!sPendingGamepads.containsKey(deviceId)) {
+
+        final List<KeyEvent> pending = sPendingGamepads.get(deviceId);
+        if (pending == null) {
             removeGamepad(deviceId);
             return;
         }
 
-        List<KeyEvent> pending = sPendingGamepads.get(deviceId);
         sPendingGamepads.remove(deviceId);
         sGamepads.put(deviceId, new Gamepad(serviceId, deviceId));
         // Handle queued KeyEvents
         for (KeyEvent ev : pending) {
             handleKeyEvent(ev);
         }
     }
 
@@ -195,22 +194,22 @@ public class AndroidGamepadManager {
     }
 
     public static boolean handleMotionEvent(MotionEvent ev) {
         ThreadUtils.assertOnUiThread();
         if (!sStarted) {
             return false;
         }
 
-        if (!sGamepads.containsKey(ev.getDeviceId())) {
+        final Gamepad gamepad = sGamepads.get(ev.getDeviceId());
+        if (gamepad == null) {
             // Not a device we care about.
             return false;
         }
 
-        Gamepad gamepad = sGamepads.get(ev.getDeviceId());
         // First check the analog stick axes
         boolean[] valid = new boolean[Axis.values().length];
         float[] axes = new float[Axis.values().length];
         boolean anyValidAxes = false;
         for (Axis axis : Axis.values()) {
             float value = deadZone(ev, axis.axis);
             int i = axis.ordinal();
             if (value != gamepad.axes[i]) {
@@ -249,23 +248,24 @@ public class AndroidGamepadManager {
 
     public static boolean handleKeyEvent(KeyEvent ev) {
         ThreadUtils.assertOnUiThread();
         if (!sStarted) {
             return false;
         }
 
         int deviceId = ev.getDeviceId();
-        if (sPendingGamepads.containsKey(deviceId)) {
+        final List<KeyEvent> pendingGamepad = sPendingGamepads.get(deviceId);
+        if (pendingGamepad != null) {
             // Queue up key events for pending devices.
-            sPendingGamepads.get(deviceId).add(ev);
+            pendingGamepad.add(ev);
             return true;
         }
 
-        if (!sGamepads.containsKey(deviceId)) {
+        if (sGamepads.get(deviceId) == null) {
             InputDevice device = ev.getDevice();
             if (device != null &&
                 (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
                 // This is a gamepad we haven't seen yet.
                 addGamepad(device);
                 sPendingGamepads.get(deviceId).add(ev);
                 return true;
             }
@@ -331,17 +331,18 @@ public class AndroidGamepadManager {
 
     private static void addDeviceListener() {
         if (Versions.preJB) {
             // Poll known gamepads to see if they've disappeared.
             sPollTimer = new Timer();
             sPollTimer.scheduleAtFixedRate(new TimerTask() {
                     @Override
                     public void run() {
-                        for (Integer deviceId : sGamepads.keySet()) {
+                        for (int i = 0; i < sGamepads.size(); ++i) {
+                            final int deviceId = sGamepads.keyAt(i);
                             if (InputDevice.getDevice(deviceId) == null) {
                                 removeGamepad(deviceId);
                             }
                         }
                     }
                 }, POLL_TIMER_PERIOD, POLL_TIMER_PERIOD);
             return;
         }
@@ -354,23 +355,23 @@ public class AndroidGamepadManager {
                     }
                     if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
                         addGamepad(device);
                     }
                 }
 
                 @Override
                 public void onInputDeviceRemoved(int deviceId) {
-                    if (sPendingGamepads.containsKey(deviceId)) {
+                    if (sPendingGamepads.get(deviceId) != null) {
                         // Got removed before Gecko's ack reached us.
                         // gamepadAdded will deal with it.
                         sPendingGamepads.remove(deviceId);
                         return;
                     }
-                    if (sGamepads.containsKey(deviceId)) {
+                    if (sGamepads.get(deviceId) != null) {
                         removeGamepad(deviceId);
                     }
                 }
 
                 @Override
                 public void onInputDeviceChanged(int deviceId) {
                 }
             };
--- a/mobile/android/base/IntentHelper.java
+++ b/mobile/android/base/IntentHelper.java
@@ -11,29 +11,39 @@ import org.mozilla.gecko.util.JSONUtils;
 import org.mozilla.gecko.util.WebActivityMapper;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.app.Activity;
 import android.content.Intent;
+import android.net.Uri;
+import android.text.TextUtils;
 import android.util.Log;
+import android.widget.Toast;
 
+import java.net.URISyntaxException;
 import java.util.Arrays;
 import java.util.List;
 
 public final class IntentHelper implements GeckoEventListener {
     private static final String LOGTAG = "GeckoIntentHelper";
     private static final String[] EVENTS = {
         "Intent:GetHandlers",
         "Intent:Open",
         "Intent:OpenForResult",
+        "Intent:OpenNoHandler",
         "WebActivity:Open"
     };
+
+    // via http://developer.android.com/distribute/tools/promote/linking.html
+    private static String MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id=";
+    private static String EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url";
+
     private static IntentHelper instance;
 
     private final Activity activity;
 
     private IntentHelper(Activity activity) {
         this.activity = activity;
         EventDispatcher.getInstance().registerGeckoThreadListener(this, EVENTS);
     }
@@ -59,16 +69,18 @@ public final class IntentHelper implemen
     public void handleMessage(String event, JSONObject message) {
         try {
             if (event.equals("Intent:GetHandlers")) {
                 getHandlers(message);
             } else if (event.equals("Intent:Open")) {
                 open(message);
             } else if (event.equals("Intent:OpenForResult")) {
                 openForResult(message);
+            } else if (event.equals("Intent:OpenNoHandler")) {
+                openNoHandler(message);
             } else if (event.equals("WebActivity:Open")) {
                 openWebActivity(message);
             }
         } catch (JSONException e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
@@ -106,16 +118,76 @@ public final class IntentHelper implemen
         final ResultHandler handler = new ResultHandler(message);
         try {
             ActivityHandlerHelper.startIntentForActivity(activity, intent, handler);
         } catch (SecurityException e) {
             Log.w(LOGTAG, "Forbidden to launch activity.", e);
         }
     }
 
+    /**
+     * Opens a URI without any valid handlers on device. In the best case, a package is specified
+     * and we can bring the user directly to the application page in an app market. If a package is
+     * not specified and there is a fallback url in the intent extras, we open that url. If neither
+     * is present, we alert the user that we were unable to open the link.
+     */
+    private void openNoHandler(final JSONObject msg) {
+        final String uri = msg.optString("uri");
+
+        if (TextUtils.isEmpty(uri)) {
+            displayToastCannotOpenLink();
+            Log.w(LOGTAG, "Received empty URL. Ignoring...");
+            return;
+        }
+
+        final Intent intent;
+        try {
+            // TODO (bug 1173626): This will not handle android-app uris on non 5.1 devices.
+            intent = Intent.parseUri(uri, 0);
+        } catch (final URISyntaxException e) {
+            displayToastCannotOpenLink();
+            // Don't log the exception to prevent leaking URIs.
+            Log.w(LOGTAG, "Unable to parse Intent URI");
+            return;
+        }
+
+        // For this flow, we follow Chrome's lead:
+        //   https://developer.chrome.com/multidevice/android/intents
+        //
+        // Note on alternative flows: we could get the intent package from a component, however, for
+        // security reasons, components are ignored when opening URIs (bug 1168998) so we should
+        // ignore it here too.
+        //
+        // Our old flow used to prompt the user to search for their app in the market by scheme and
+        // while this could help the user find a new app, there is not always a correlation in
+        // scheme to application name and we could end up steering the user wrong (potentially to
+        // malicious software). Better to leave that one alone.
+        if (intent.getPackage() != null) {
+            final String marketUri = MARKET_INTENT_URI_PACKAGE_PREFIX + intent.getPackage();
+            final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(marketUri));
+            marketIntent.addCategory(Intent.CATEGORY_BROWSABLE);
+            marketIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            activity.startActivity(marketIntent);
+
+        } else if (intent.hasExtra(EXTRA_BROWSER_FALLBACK_URL)) {
+            final String fallbackUrl = intent.getStringExtra(EXTRA_BROWSER_FALLBACK_URL);
+            Tabs.getInstance().loadUrl(fallbackUrl);
+
+        }  else {
+            displayToastCannotOpenLink();
+            // Don't log the URI to prevent leaking it.
+            Log.w(LOGTAG, "Unable to handle URI");
+        }
+    }
+
+    private void displayToastCannotOpenLink() {
+        final String errText = activity.getResources().getString(R.string.intent_uri_cannot_open);
+        Toast.makeText(activity, errText, Toast.LENGTH_LONG).show();
+    }
+
     private void openWebActivity(JSONObject message) throws JSONException {
         final Intent intent = WebActivityMapper.getIntentForWebActivity(message.getJSONObject("activity"));
         ActivityHandlerHelper.startIntentForActivity(activity, intent, new ResultHandler(message));
     }
 
     private static class ResultHandler implements ActivityResultHandler {
         private final JSONObject message;
 
--- a/mobile/android/base/SiteIdentity.java
+++ b/mobile/android/base/SiteIdentity.java
@@ -172,17 +172,16 @@ public class SiteIdentity {
             try {
                 mOrigin = identityData.getString("origin");
                 mHost = identityData.getString("host");
                 mOwner = identityData.optString("owner", null);
                 mSupplemental = identityData.optString("supplemental", null);
                 mVerifier = identityData.getString("verifier");
                 mEncrypted = identityData.optBoolean("encrypted", false);
             } catch (Exception e) {
-                Log.e(LOGTAG, "Error fetching Site identity host info", e);
                 resetIdentity();
             }
         } catch (Exception e) {
             reset();
         }
     }
 
     public SecurityMode getSecurityMode() {
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -654,8 +654,10 @@ just addresses the organization to follo
 <!ENTITY remote_tabs_last_synced "Last synced: &formatS;">
 <!-- Localization note: Used when the sync has not happend yet, showed in place of a date -->
 <!ENTITY remote_tabs_never_synced "Last synced: never">
 
 <!-- Find-In-Page strings -->
 <!-- LOCALIZATION NOTE (find_matchcase): This is meant to appear as an icon that changes color
      if match-case is activated. i.e. No more than two letters, one uppercase, one lowercase. -->
 <!ENTITY find_matchcase "Aa">
+
+<!ENTITY intent_uri_cannot_open "Cannot open link">
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -533,9 +533,11 @@
   <!-- Miscellaneous -->
   <string name="ellipsis">&ellipsis;</string>
 
   <string name="colon">&colon;</string>
 
   <string name="percent">&percent;</string>
 
   <string name="remote_tabs_last_synced">&remote_tabs_last_synced;</string>
+
+  <string name="intent_uri_cannot_open">&intent_uri_cannot_open;</string>
 </resources>
--- a/mobile/android/components/ContentDispatchChooser.js
+++ b/mobile/android/components/ContentDispatchChooser.js
@@ -44,33 +44,27 @@ ContentDispatchChooser.prototype =
     // specific results.
     aHandler = this.protoSvc.getProtocolHandlerInfoFromOS(aURI.spec, {});
 
     // The first handler in the set is the Android Application Chooser (which will fall back to a default if one is set)
     // If we have more than one option, let the OS handle showing a list (if needed).
     if (aHandler.possibleApplicationHandlers.length > 1) {
       aHandler.launchWithURI(aURI, aWindowContext);
     } else {
+      // xpcshell tests do not have an Android Bridge but we require Android
+      // Bridge when using Messaging so we guard against this case. xpcshell
+      // tests also do not have a window, so we use this state to guard.
       let win = this._getChromeWin();
-      if (win && win.NativeWindow) {
-        let bundle = Services.strings.createBundle("chrome://browser/locale/handling.properties");
-        let failedText = bundle.GetStringFromName("protocol.failed");
-        let searchText = bundle.GetStringFromName("protocol.toast.search");
+      if (!win) {
+        return;
+      }
 
-        win.NativeWindow.toast.show(failedText, "long", {
-          button: {
-            label: searchText,
-            callback: function() {
-              let message = {
-                type: "Intent:Open",
-                url: "market://search?q=" + aURI.scheme,
-              };
+      let msg = {
+        type: "Intent:OpenNoHandler",
+        uri: aURI.spec,
+      };
 
-              Messaging.sendRequest(message);
-            }
-          }
-        });
-      }
+      Messaging.sendRequest(msg);
     }
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentDispatchChooser]);
--- a/mobile/android/locales/en-US/chrome/handling.properties
+++ b/mobile/android/locales/en-US/chrome/handling.properties
@@ -1,8 +1,5 @@
 # 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/.
 
 download.blocked=Unable to download file
-protocol.failed=Couldn't find an app to open this link
-# A very short string shown in the button toast when no application can open the url
-protocol.toast.search=Search
--- a/testing/marionette/client/marionette/tests/webapi-tests.ini
+++ b/testing/marionette/client/marionette/tests/webapi-tests.ini
@@ -7,17 +7,17 @@ browser = true
 
 ; true if the test is compatible with b2g, otherwise false
 b2g = true
 
 ; true if the test should be skipped
 skip = false
 
 ; webapi tests
-[include:../../../../../dom/bluetooth/bluetooth1/tests/marionette/manifest.ini]
+[include:../../../../../dom/bluetooth/bluetooth2/tests/marionette/manifest.ini]
 [include:../../../../../dom/telephony/test/marionette/manifest.ini]
 [include:../../../../../dom/voicemail/test/marionette/manifest.ini]
 [include:../../../../../dom/battery/test/marionette/manifest.ini]
 [include:../../../../../dom/mobilemessage/tests/marionette/manifest.ini]
 [include:../../../../../dom/mobileconnection/tests/marionette/manifest.ini]
 [include:../../../../../dom/system/gonk/tests/marionette/manifest.ini]
 [include:../../../../../dom/icc/tests/marionette/manifest.ini]
 [include:../../../../../dom/system/tests/marionette/manifest.ini]
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -858,18 +858,29 @@ LoginManagerPrompter.prototype = {
         chromeDoc.getElementById("password-notification-password").value;
     };
 
     let onInput = () => {
       readDataFromUI();
       updateButtonLabel();
     };
 
-    let onPasswordFocus = () => {
-      chromeDoc.getElementById("password-notification-password").type = "";
+    let onPasswordFocus = (focusEvent) => {
+      let passwordField = chromeDoc.getElementById("password-notification-password");
+      // Gets the caret position before changing the type of the textbox
+      let selectionStart = passwordField.selectionStart;
+      let selectionEnd = passwordField.selectionEnd;
+      if (focusEvent.rangeParent != null) {
+        // Check for a click over the SHOW placeholder
+        selectionStart = passwordField.value.length;
+        selectionEnd = passwordField.value.length;
+      }
+      passwordField.type = "";
+      passwordField.selectionStart = selectionStart;
+      passwordField.selectionEnd = selectionEnd;
     };
 
     let onPasswordBlur = () => {
       chromeDoc.getElementById("password-notification-password").type = "password";
     };
 
     let persistData = () => {
       let foundLogins = Services.logins.findLogins({}, login.hostname,
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/data/engine-suggestions.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-suggestions.xml</ShortName>
+<Url type="application/x-suggestions+json"
+     method="GET"
+     template="http://localhost:9000/suggest?{searchTerms}"/>
+<Url type="text/html"
+     method="GET"
+     template="http://localhost:9000/search"
+     rel="searchform"/>
+</SearchPlugin>
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -3,16 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
 
 // Import common head.
 {
   let commonFile = do_get_file("../head_common.js", false);
   let uri = Services.io.newFileURI(commonFile);
   Services.scriptloader.loadSubScript(uri.spec, this);
 }
 
@@ -24,17 +25,24 @@ function run_test() {
   run_next_test();
 }
 
 function* cleanup() {
   Services.prefs.clearUserPref("browser.urlbar.autocomplete.enabled");
   Services.prefs.clearUserPref("browser.urlbar.autoFill");
   Services.prefs.clearUserPref("browser.urlbar.autoFill.typed");
   Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
-  for (let type of ["history", "bookmark", "history.onlyTyped", "openpage"]) {
+  let suggestPrefs = [
+    "history",
+    "bookmark",
+    "history.onlyTyped",
+    "openpage",
+    "searches",
+  ];
+  for (let type of suggestPrefs) {
     Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
   }
   yield PlacesUtils.bookmarks.eraseEverything();
   yield PlacesTestUtils.clearHistory();
 }
 do_register_cleanup(cleanup);
 
 /**
@@ -217,17 +225,17 @@ function* check_autocomplete(test) {
           // Make it undefined so we don't process it again
           matches[j] = undefined;
           break;
         }
       }
 
       // We didn't hit the break, so we must have not found it
       if (j == matches.length)
-        do_throw(`Didn't find the current result ("${result.value}", "${result.comment}") in matches`);
+        do_throw(`Didn't find the current result ("${result.value}", "${result.comment}") in matches`); //' (Emacs syntax highlighting fix)
     }
 
     Assert.equal(controller.matchCount, matches.length,
                  "Got as many results as expected");
 
     // If we expect results, make sure we got matches.
     do_check_eq(controller.searchStatus, matches.length ?
                 Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH :
@@ -388,22 +396,66 @@ function setFaviconForHref(href, iconHre
       NetUtil.newURI(iconHref),
       true,
       PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
       resolve
     );
   });
 }
 
+function makeTestServer(port=-1) {
+  let httpServer = new HttpServer();
+  httpServer.start(port);
+  do_register_cleanup(() => httpServer.stop(() => {}));
+  return httpServer;
+}
+
+function* addTestEngine(basename, httpServer=undefined) {
+  httpServer = httpServer || makeTestServer();
+  httpServer.registerDirectory("/", do_get_cwd());
+  let dataUrl =
+    "http://localhost:" + httpServer.identity.primaryPort + "/data/";
+
+  do_print("Adding engine: " + basename);
+  return yield new Promise(resolve => {
+    Services.obs.addObserver(function obs(subject, topic, data) {
+      let engine = subject.QueryInterface(Ci.nsISearchEngine);
+      do_print("Observed " + data + " for " + engine.name);
+      if (data != "engine-added" || engine.name != basename) {
+        return;
+      }
+
+      Services.obs.removeObserver(obs, "browser-search-engine-modified");
+      do_register_cleanup(() => Services.search.removeEngine(engine));
+      resolve(engine);
+    }, "browser-search-engine-modified", false);
+
+    do_print("Adding engine from URL: " + dataUrl + basename);
+    Services.search.addEngine(dataUrl + basename,
+                              Ci.nsISearchEngine.DATA_XML, null, false);
+  });
+}
+
 // Ensure we have a default search engine and the keyword.enabled preference
 // set.
 add_task(function ensure_search_engine() {
   // keyword.enabled is necessary for the tests to see keyword searches.
   Services.prefs.setBoolPref("keyword.enabled", true);
 
+  // Initialize the search service, but first set this geo IP pref to a dummy
+  // string.  When the search service is initialized, it contacts the URI named
+  // in this pref, which breaks the test since outside connections aren't
+  // allowed.
+  let geoPref = "browser.search.geoip.url";
+  Services.prefs.setCharPref(geoPref, "");
+  do_register_cleanup(() => Services.prefs.clearUserPref(geoPref));
+  yield new Promise(resolve => {
+    Services.search.init(resolve);
+  });
+
   // Remove any existing engines before adding ours.
   for (let engine of Services.search.getEngines()) {
     Services.search.removeEngine(engine);
   }
   Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
                                        "http://s.example.com/search");
   let engine = Services.search.getEngineByName("MozSearch");
   Services.search.currentEngine = engine;
--- a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_host.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_host.js
@@ -1,47 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://testing-common/httpd.js");
-
-function* addTestEngines(items) {
-  let httpServer = new HttpServer();
-  httpServer.start(-1);
-  httpServer.registerDirectory("/", do_get_cwd());
-  let gDataUrl = "http://localhost:" + httpServer.identity.primaryPort + "/data/";
-  do_register_cleanup(() => httpServer.stop(() => {}));
-
-  let engines = [];
-
-  for (let item of items) {
-    do_print("Adding engine: " + item);
-    yield new Promise(resolve => {
-      Services.obs.addObserver(function obs(subject, topic, data) {
-        let engine = subject.QueryInterface(Ci.nsISearchEngine);
-        do_print("Observed " + data + " for " + engine.name);
-        if (data != "engine-added" || engine.name != item) {
-          return;
-        }
-
-        Services.obs.removeObserver(obs, "browser-search-engine-modified");
-        engines.push(engine);
-        resolve();
-      }, "browser-search-engine-modified", false);
-
-      do_print("`Adding engine from URL: " + gDataUrl + item);
-      Services.search.addEngine(gDataUrl + item,
-                                Ci.nsISearchEngine.DATA_XML, null, false);
-    });
-  }
-
-  return engines;
-}
-
-
 add_task(function* test_searchEngine_autoFill() {
   Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
   Services.search.addEngineWithDetails("MySearchEngine", "", "", "",
                                        "GET", "http://my.search.com/");
   let engine = Services.search.getEngineByName("MySearchEngine");
   do_register_cleanup(() => Services.search.removeEngine(engine));
 
   // Add an uri that matches the search string with high frecency.
@@ -62,18 +26,17 @@ add_task(function* test_searchEngine_aut
     completed: "http://my.search.com"
   });
 
   yield cleanup();
 });
 
 add_task(function* test_searchEngine_noautoFill() {
   let engineName = "engine-rel-searchform.xml";
-  let [engine] = yield addTestEngines([engineName]);
-  do_register_cleanup(() => Services.search.removeEngine(engine));
+  let engine = yield addTestEngine(engineName);
   equal(engine.searchForm, "http://example.com/?search");
 
   Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
   yield PlacesTestUtils.addVisits(NetUtil.newURI("http://example.com/my/"));
 
   do_print("Check search domain is not autoFilled if it matches a visited domain");
   yield check_autocomplete({
     search: "example",
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_searchSuggestions.js
@@ -0,0 +1,147 @@
+Cu.import("resource://gre/modules/FormHistory.jsm");
+
+const ENGINE_NAME = "engine-suggestions.xml";
+const SERVER_PORT = 9000;
+const SUGGEST_PREF = "browser.urlbar.suggest.searches";
+
+// Set this to some other function to change how the server converts search
+// strings into suggestions.
+let suggestionsFromSearchString = searchStr => {
+  let suffixes = ["foo", "bar"];
+  return suffixes.map(s => searchStr + " " + s);
+};
+
+add_task(function* setUp() {
+  // Set up a server that provides some suggestions by appending strings onto
+  // the search query.
+  let server = makeTestServer(SERVER_PORT);
+  server.registerPathHandler("/suggest", (req, resp) => {
+    // URL query params are x-www-form-urlencoded, which converts spaces into
+    // plus signs, so un-convert any plus signs back to spaces.
+    let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
+    let suggestions = suggestionsFromSearchString(searchStr);
+    let data = [searchStr, suggestions];
+    resp.setHeader("Content-Type", "application/json", false);
+    resp.write(JSON.stringify(data));
+  });
+
+  // Install the test engine.
+  let oldCurrentEngine = Services.search.currentEngine;
+  do_register_cleanup(() => Services.search.currentEngine = oldCurrentEngine);
+  let engine = yield addTestEngine(ENGINE_NAME, server);
+  Services.search.currentEngine = engine;
+
+  yield cleanup();
+});
+
+add_task(function* disabled() {
+  Services.prefs.setBoolPref(SUGGEST_PREF, false);
+  yield check_autocomplete({
+    search: "hello",
+    matches: [],
+  });
+  yield cleanup();
+});
+
+add_task(function* singleWordQuery() {
+  Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+  let searchStr = "hello";
+  yield check_autocomplete({
+    search: searchStr,
+    matches: [{
+      uri: makeActionURI(("searchengine"), {
+        engineName: ENGINE_NAME,
+        input: searchStr,
+        searchQuery: searchStr,
+        searchSuggestion: "hello foo",
+      }),
+      title: ENGINE_NAME,
+      style: ["action", "searchengine"],
+      icon: "",
+    }, {
+      uri: makeActionURI(("searchengine"), {
+        engineName: ENGINE_NAME,
+        input: searchStr,
+        searchQuery: searchStr,
+        searchSuggestion: "hello bar",
+      }),
+      title: ENGINE_NAME,
+      style: ["action", "searchengine"],
+      icon: "",
+    }],
+  });
+
+  yield cleanup();
+});
+
+add_task(function* multiWordQuery() {
+  Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+  let searchStr = "hello world";
+  yield check_autocomplete({
+    search: searchStr,
+    matches: [{
+      uri: makeActionURI(("searchengine"), {
+        engineName: ENGINE_NAME,
+        input: searchStr,
+        searchQuery: searchStr,
+        searchSuggestion: "hello world foo",
+      }),
+      title: ENGINE_NAME,
+      style: ["action", "searchengine"],
+      icon: "",
+    }, {
+      uri: makeActionURI(("searchengine"), {
+        engineName: ENGINE_NAME,
+        input: searchStr,
+        searchQuery: searchStr,
+        searchSuggestion: "hello world bar",
+      }),
+      title: ENGINE_NAME,
+      style: ["action", "searchengine"],
+      icon: "",
+    }],
+  });
+
+  yield cleanup();
+});
+
+add_task(function* suffixMatch() {
+  Services.prefs.setBoolPref(SUGGEST_PREF, true);
+
+  let oldFn = suggestionsFromSearchString;
+  suggestionsFromSearchString = searchStr => {
+    let prefixes = ["baz", "quux"];
+    return prefixes.map(p => p + " " + searchStr);
+  };
+
+  let searchStr = "hello";
+  yield check_autocomplete({
+    search: searchStr,
+    matches: [{
+      uri: makeActionURI(("searchengine"), {
+        engineName: ENGINE_NAME,
+        input: searchStr,
+        searchQuery: searchStr,
+        searchSuggestion: "baz hello",
+      }),
+      title: ENGINE_NAME,
+      style: ["action", "searchengine"],
+      icon: "",
+    }, {
+      uri: makeActionURI(("searchengine"), {
+        engineName: ENGINE_NAME,
+        input: searchStr,
+        searchQuery: searchStr,
+        searchSuggestion: "quux hello",
+      }),
+      title: ENGINE_NAME,
+      style: ["action", "searchengine"],
+      icon: "",
+    }],
+  });
+
+  suggestionsFromSearchString = oldFn;
+  yield cleanup();
+});
--- a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
+++ b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
@@ -1,15 +1,15 @@
 [DEFAULT]
 head = head_autocomplete.js
 tail = 
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
   data/engine-rel-searchform.xml
-
+  data/engine-suggestions.xml
 
 [test_416211.js]
 [test_416214.js]
 [test_417798.js]
 [test_418257.js]
 [test_422277.js]
 [test_autocomplete_functional.js]
 [test_autocomplete_on_value_removed_479089.js]
@@ -29,16 +29,17 @@ support-files =
 [test_keywords.js]
 [test_match_beginning.js]
 [test_multi_word_search.js]
 [test_queryurl.js]
 [test_searchEngine_alias.js]
 [test_searchEngine_current.js]
 [test_searchEngine_host.js]
 [test_searchEngine_restyle.js]
+[test_searchSuggestions.js]
 [test_special_search.js]
 [test_swap_protocol.js]
 [test_tabmatches.js]
 [test_trimming.js]
 [test_typed.js]
 [test_visiturl.js]
 [test_word_boundary_search.js]
 [test_zero_frecency.js]
--- a/toolkit/components/url-classifier/SafeBrowsing.jsm
+++ b/toolkit/components/url-classifier/SafeBrowsing.jsm
@@ -85,25 +85,46 @@ this.SafeBrowsing = {
   initialized:     false,
   phishingEnabled: false,
   malwareEnabled:  false,
 
   updateURL:             null,
   gethashURL:            null,
 
   reportURL:             null,
-  reportGenericURL:      null,
-  reportErrorURL:        null,
-  reportPhishURL:        null,
-  reportMalwareURL:      null,
-  reportMalwareErrorURL: null,
+
+  getReportURL: function(kind, URI) {
+    let pref;
+    switch (kind) {
+      case "Phish":
+        pref = "browser.safebrowsing.reportPhishURL";
+        break;
+      case "PhishMistake":
+        pref = "browser.safebrowsing.reportPhishMistakeURL";
+        break;
+      case "MalwareMistake":
+        pref = "browser.safebrowsing.reportMalwareMistakeURL";
+        break;
 
+      default:
+        let err = "SafeBrowsing getReportURL() called with unknown kind: " + kind;
+        Components.utils.reportError(err);
+        throw err;
+    }
+    let reportUrl = Services.urlFormatter.formatURLPref(pref);
 
-  getReportURL: function(kind) {
-    return this["report"  + kind + "URL"];
+    let pageUri = URI.clone();
+
+    // Remove the query to avoid including potentially sensitive data
+    if (pageUri instanceof Ci.nsIURL)
+      pageUri.query = '';
+
+    reportUrl += encodeURIComponent(pageUri.asciiSpec);
+
+    return reportUrl;
   },
 
 
   readPrefs: function() {
     log("reading prefs");
 
     debug = Services.prefs.getBoolPref("browser.safebrowsing.debug");
     this.phishingEnabled = Services.prefs.getBoolPref("browser.safebrowsing.enabled");
@@ -123,29 +144,20 @@ this.SafeBrowsing = {
   updateProviderURLs: function() {
     try {
       var clientID = Services.prefs.getCharPref("browser.safebrowsing.id");
     } catch(e) {
       var clientID = Services.appinfo.name;
     }
 
     log("initializing safe browsing URLs, client id ", clientID);
-    let basePref = "browser.safebrowsing.";
-
-    // Urls to HTML report pages
-    this.reportURL             = Services.urlFormatter.formatURLPref(basePref + "reportURL");
-    this.reportGenericURL      = Services.urlFormatter.formatURLPref(basePref + "reportGenericURL");
-    this.reportErrorURL        = Services.urlFormatter.formatURLPref(basePref + "reportErrorURL");
-    this.reportPhishURL        = Services.urlFormatter.formatURLPref(basePref + "reportPhishURL");
-    this.reportMalwareURL      = Services.urlFormatter.formatURLPref(basePref + "reportMalwareURL");
-    this.reportMalwareErrorURL = Services.urlFormatter.formatURLPref(basePref + "reportMalwareErrorURL");
 
     // Urls used to update DB
-    this.updateURL  = Services.urlFormatter.formatURLPref(basePref + "updateURL");
-    this.gethashURL = Services.urlFormatter.formatURLPref(basePref + "gethashURL");
+    this.updateURL  = Services.urlFormatter.formatURLPref("browser.safebrowsing.updateURL");
+    this.gethashURL = Services.urlFormatter.formatURLPref("browser.safebrowsing.gethashURL");
 
     this.updateURL  = this.updateURL.replace("SAFEBROWSING_ID", clientID);
     this.gethashURL = this.gethashURL.replace("SAFEBROWSING_ID", clientID);
     this.trackingUpdateURL = Services.urlFormatter.formatURLPref(
       "browser.trackingprotection.updateURL");
     this.trackingGethashURL = Services.urlFormatter.formatURLPref(
       "browser.trackingprotection.gethashURL");
   },
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -1355,66 +1355,88 @@ TabClient.prototype = {
   attachWorker: function (aWorkerActor, aOnResponse) {
     this.client.attachWorker(aWorkerActor, aOnResponse);
   }
 };
 
 eventSource(TabClient.prototype);
 
 function WorkerClient(aClient, aForm) {
-  this._client = aClient;
+  this.client = aClient;
   this._actor = aForm.from;
   this._isClosed = false;
   this._isFrozen = aForm.isFrozen;
 
   this._onClose = this._onClose.bind(this);
   this._onFreeze = this._onFreeze.bind(this);
   this._onThaw = this._onThaw.bind(this);
 
   this.addListener("close", this._onClose);
   this.addListener("freeze", this._onFreeze);
   this.addListener("thaw", this._onThaw);
 }
 
 WorkerClient.prototype = {
   get _transport() {
-    return this._client._transport;
+    return this.client._transport;
   },
 
   get request() {
-    return this._client.request;
+    return this.client.request;
   },
 
   get actor() {
     return this._actor;
   },
 
   get isClosed() {
     return this._isClosed;
   },
 
   get isFrozen() {
     return this._isFrozen;
   },
 
   detach: DebuggerClient.requester({ type: "detach" }, {
     after: function (aResponse) {
-      this._client.unregisterClient(this);
+      this.client.unregisterClient(this);
       return aResponse;
     },
 
     telemetry: "WORKERDETACH"
   }),
 
+  attachThread: function(aOptions = {}, aOnResponse = noop) {
+    if (this.thread) {
+      DevToolsUtils.executeSoon(() => aOnResponse({
+        type: "connected",
+        threadActor: this.thread._actor,
+      }, this.thread));
+      return;
+    }
+
+    this.request({
+      to: this._actor,
+      type: "connect",
+      options: aOptions,
+    }, (aResponse) => {
+      if (!aResponse.error) {
+        this.thread = new ThreadClient(this, aResponse.threadActor);
+        this.client.registerClient(this.thread);
+      }
+      aOnResponse(aResponse, this.thread);
+    });
+  },
+
   _onClose: function () {
     this.removeListener("close", this._onClose);
     this.removeListener("freeze", this._onFreeze);
     this.removeListener("thaw", this._onThaw);
 
-    this._client.unregisterClient(this);
+    this.client.unregisterClient(this);
     this._closed = true;
   },
 
   _onFreeze: function () {
     this._isFrozen = true;
   },
 
   _onThaw: function () {
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -10,16 +10,17 @@ const Services = require("Services");
 const { Cc, Ci, Cu, components, ChromeWorker } = require("chrome");
 const { ActorPool, OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
 const { ObjectActor, createValueGrip, longStringGrip } = require("devtools/server/actors/object");
 const { DebuggerServer } = require("devtools/server/main");
 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 const { dbg_assert, dumpn, update, fetch } = DevToolsUtils;
 const { dirname, joinURI } = require("devtools/toolkit/path");
 const promise = require("promise");
+const PromiseDebugging = require("PromiseDebugging");
 const xpcInspector = require("xpcInspector");
 const ScriptStore = require("./utils/ScriptStore");
 const {DevToolsWorker} = require("devtools/toolkit/shared/worker.js");
 
 const { defer, resolve, reject, all } = require("devtools/toolkit/deprecated-sync-thenables");
 
 loader.lazyGetter(this, "Debugger", () => {
   let Debugger = require("Debugger");
@@ -1489,17 +1490,17 @@ ThreadActor.prototype = {
     if (aFrame) {
       aFrame.onStep = undefined;
       aFrame.onPop = undefined;
     }
 
     // Clear DOM event breakpoints.
     // XPCShell tests don't use actual DOM windows for globals and cause
     // removeListenerForAllEvents to throw.
-    if (this.global && !this.global.toString().includes("Sandbox")) {
+    if (!isWorker && this.global && !this.global.toString().includes("Sandbox")) {
       let els = Cc["@mozilla.org/eventlistenerservice;1"]
                 .getService(Ci.nsIEventListenerService);
       els.removeListenerForAllEvents(this.global, this._allEventsListener, true);
       for (let [,bp] of this._hiddenBreakpoints) {
         bp.onDelete();
       }
       this._hiddenBreakpoints.clear();
     }
@@ -1928,17 +1929,17 @@ ThreadActor.prototype = {
               generatedLocations
             );
           }
         }));
       }
     }
 
     if (promises.length > 0) {
-      this.synchronize(Promise.all(promises));
+      this.synchronize(promise.all(promises));
     }
 
     return true;
   },
 
 
   /**
    * Get prototypes and properties of multiple objects.
@@ -2865,20 +2866,20 @@ SourceActor.prototype = {
   },
 
   _setBreakpointAtOriginalLocation: function (actor, originalLocation) {
     if (!this.isSourceMapped) {
       if (!this._setBreakpointAtGeneratedLocation(
         actor,
         GeneratedLocation.fromOriginalLocation(originalLocation)
       )) {
-        return Promise.resolve(null);
+        return promise.resolve(null);
       }
 
-      return Promise.resolve(originalLocation);
+      return promise.resolve(originalLocation);
     } else {
       return this.sources.getAllGeneratedLocations(originalLocation)
                          .then((generatedLocations) => {
         if (!this._setBreakpointAtAllGeneratedLocations(
           actor,
           generatedLocations
         )) {
           return null;
--- a/toolkit/devtools/server/actors/utils/TabSources.js
+++ b/toolkit/devtools/server/actors/utils/TabSources.js
@@ -5,17 +5,17 @@
 "use strict";
 
 const { Ci, Cu } = require("chrome");
 const Services = require("Services");
 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 const { dbg_assert, fetch } = DevToolsUtils;
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const { OriginalLocation, GeneratedLocation, getOffsetColumn } = require("devtools/server/actors/common");
-const { resolve } = Promise;
+const { resolve } = require("promise");
 
 loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true);
 loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true);
 
 /**
  * Manages the sources for a thread. Handles source maps, locations in the
--- a/toolkit/devtools/server/actors/worker.js
+++ b/toolkit/devtools/server/actors/worker.js
@@ -1,11 +1,12 @@
 "use strict";
 
 let { Ci, Cu } = require("chrome");
+let { DebuggerServer } = require("devtools/server/main");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(
   this, "wdm",
   "@mozilla.org/dom/workers/workerdebuggermanager;1",
   "nsIWorkerDebuggerManager"
 );
@@ -23,16 +24,18 @@ function matchWorkerDebugger(dbg, option
   }
 
   return true;
 }
 
 function WorkerActor(dbg) {
   this._dbg = dbg;
   this._isAttached = false;
+  this._threadActor = null;
+  this._transport = null;
 }
 
 WorkerActor.prototype = {
   actorPrefix: "worker",
 
   form: function () {
     return {
       actor: this.actorID,
@@ -61,41 +64,79 @@ WorkerActor.prototype = {
       return { error: "wrongState" };
     }
 
     this._detach();
 
     return { type: "detached" };
   },
 
+  onConnect: function (request) {
+    if (!this._isAttached) {
+      return { error: "wrongState" };
+    }
+
+    if (this._threadActor !== null) {
+      return {
+        type: "connected",
+        threadActor: this._threadActor
+      };
+    }
+
+    return DebuggerServer.connectToWorker(
+      this.conn, this._dbg, this.actorID, request.options
+    ).then(({ threadActor, transport }) => {
+      this._threadActor = threadActor;
+      this._transport = transport;
+
+      return {
+        type: "connected",
+        threadActor: this._threadActor
+      };
+    }, (error) => {
+      return { error: error.toString() };
+    });
+  },
+
   onClose: function () {
     if (this._isAttached) {
       this._detach();
     }
 
     this.conn.sendActorEvent(this.actorID, "close");
   },
 
+  onError: function (filename, lineno, message) {
+    reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n");
+  },
+
   onFreeze: function () {
     this.conn.sendActorEvent(this.actorID, "freeze");
   },
 
   onThaw: function () {
     this.conn.sendActorEvent(this.actorID, "thaw");
   },
 
   _detach: function () {
+    if (this._threadActor !== null) {
+      this._transport.close();
+      this._transport = null;
+      this._threadActor = null;
+    }
+
     this._dbg.removeListener(this);
     this._isAttached = false;
   }
 };
 
 WorkerActor.prototype.requestTypes = {
   "attach": WorkerActor.prototype.onAttach,
-  "detach": WorkerActor.prototype.onDetach
+  "detach": WorkerActor.prototype.onDetach,
+  "connect": WorkerActor.prototype.onConnect
 };
 
 exports.WorkerActor = WorkerActor;
 
 function WorkerActorList(options) {
   this._options = options;
   this._actors = new Map();
   this._onListChanged = null;
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -9,17 +9,17 @@
 /**
  * Toolkit glue for the remote debugging protocol, loaded into the
  * debugging global.
  */
 let { Ci, Cc, CC, Cu, Cr } = require("chrome");
 let Services = require("Services");
 let { ActorPool, OriginalLocation, RegisteredActorFactory,
       ObservedActorFactory } = require("devtools/server/actors/common");
-let { LocalDebuggerTransport, ChildDebuggerTransport } =
+let { LocalDebuggerTransport, ChildDebuggerTransport, WorkerDebuggerTransport } =
   require("devtools/toolkit/transport/transport");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 let { dumpn, dumpv, dbg_assert } = DevToolsUtils;
 let EventEmitter = require("devtools/toolkit/event-emitter");
 let Debugger = require("Debugger");
 
 DevToolsUtils.defineLazyGetter(this, "DebuggerSocket", () => {
   let { DebuggerSocket } = require("devtools/toolkit/security/socket");
@@ -680,20 +680,23 @@ var DebuggerServer = {
    * nsIMessageSender messages with our parent process.
    *
    * @param aPrefix
    *    The prefix we should use in our nsIMessageSender message names and
    *    actor names. This connection will use messages named
    *    "debug:<prefix>:packet", and all its actors will have names
    *    beginning with "<prefix>/".
    */
-  connectToParent: function(aPrefix, aMessageManager) {
+  connectToParent: function(aPrefix, aScopeOrManager) {
     this._checkInit();
 
-    let transport = new ChildDebuggerTransport(aMessageManager, aPrefix);
+    let transport = isWorker ?
+                    new WorkerDebuggerTransport(aScopeOrManager, aPrefix) :
+                    new ChildDebuggerTransport(aScopeOrManager, aPrefix);
+
     return this._onConnection(transport, aPrefix, true);
   },
 
   connectToContent: function (aConnection, aMm) {
     let deferred = defer();
 
     let prefix = aConnection.allocID("content-process");
     let actor, childTransport;
@@ -750,16 +753,93 @@ var DebuggerServer = {
     Services.obs.addObserver(onMessageManagerClose,
                              "message-manager-close", false);
 
     events.on(aConnection, "closed", onClose);
 
     return deferred.promise;
   },
 
+  connectToWorker: function (aConnection, aDbg, aId, aOptions) {
+    return new Promise((resolve, reject) => {
+      // Step 1: Initialize the worker debugger.
+      aDbg.initialize("resource://gre/modules/devtools/server/worker.js");
+
+      // Step 2: Send a connect request to the worker debugger.
+      aDbg.postMessage(JSON.stringify({
+        type: "connect",
+        id: aId,
+        options: aOptions
+      }));
+
+      // Steps 3-5 are performed on the worker thread (see worker.js).
+
+      // Step 6: Wait for a response from the worker debugger.
+      let listener = {
+        onClose: () => {
+          aDbg.removeListener(listener);
+
+          reject("closed");
+        },
+
+        onMessage: (message) => {
+          let packet = JSON.parse(message);
+          if (packet.type !== "message" || packet.id !== aId) {
+            return;
+          }
+
+          message = packet.message;
+          if (message.error) {
+            reject(error);
+          }
+
+          if (message.type !== "paused") {
+            return;
+          }
+
+          aDbg.removeListener(listener);
+
+          // Step 7: Create a transport for the connection to the worker.
+          let transport = new WorkerDebuggerTransport(aDbg, aId);
+          transport.ready();
+          transport.hooks = {
+            onClosed: () => {
+              if (!aDbg.isClosed) {
+                aDbg.postMessage(JSON.stringify({
+                  type: "disconnect",
+                  id: aId
+                }));
+              }
+
+              aConnection.cancelForwarding(aId);
+            },
+
+            onPacket: (packet) => {
+              // Ensure that any packets received from the server on the worker
+              // thread are forwarded to the client on the main thread, as if
+              // they had been sent by the server on the main thread.
+              aConnection.send(packet);
+            }
+          };
+
+          // Ensure that any packets received from the client on the main thread
+          // to actors on the worker thread are forwarded to the server on the
+          // worker thread.
+          aConnection.setForwarding(aId, transport);
+
+          resolve({
+            threadActor: message.from,
+            transport: transport
+          });
+        }
+      };
+      aDbg.addListener(listener);
+    });
+  },
+
   /**
    * Check if the caller is running in a content child process.
    *
    * @return boolean
    *         true if the caller is running in a content
    */
   get isInChildProcess() {
     return !!this.parentMessageManager;
@@ -1437,23 +1517,26 @@ DebuggerServerConnection.prototype = {
   onPacket: function DSC_onPacket(aPacket) {
     // If the actor's name begins with a prefix we've been asked to
     // forward, do so.
     //
     // Note that the presence of a prefix alone doesn't indicate that
     // forwarding is needed: in DebuggerServerConnection instances in child
     // processes, every actor has a prefixed name.
     if (this._forwardingPrefixes.size > 0) {
-      let separator = aPacket.to.indexOf('/');
-      if (separator >= 0) {
+      let to = aPacket.to;
+      let separator = to.lastIndexOf('/');
+      while (separator >= 0) {
+        to = to.substring(0, separator);
         let forwardTo = this._forwardingPrefixes.get(aPacket.to.substring(0, separator));
         if (forwardTo) {
           forwardTo.send(aPacket);
           return;
         }
+        separator = to.lastIndexOf('/');
       }
     }
 
     let actor = this._getOrCreateActor(aPacket.to);
     if (!actor) {
       return;
     }
 
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -46,16 +46,17 @@ EXTRA_JS_MODULES.devtools += [
     'dbg-server.jsm',
 ]
 
 EXTRA_JS_MODULES.devtools.server += [
     'child.js',
     'content-globals.js',
     'main.js',
     'protocol.js',
+    'worker.js'
 ]
 
 EXTRA_JS_MODULES.devtools.server.actors += [
     'actors/actor-registry.js',
     'actors/addon.js',
     'actors/animation.js',
     'actors/call-watcher.js',
     'actors/canvas.js',
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/worker.js
@@ -0,0 +1,66 @@
+"use strict"
+
+loadSubScript("resource://gre/modules/devtools/worker-loader.js");
+
+let { ActorPool } = worker.require("devtools/server/actors/common");
+let { ThreadActor } = worker.require("devtools/server/actors/script");
+let { TabSources } = worker.require("devtools/server/actors/utils/TabSources");
+let makeDebugger = worker.require("devtools/server/actors/utils/make-debugger");
+let { DebuggerServer } = worker.require("devtools/server/main");
+
+DebuggerServer.init();
+DebuggerServer.createRootActor = function () {
+  throw new Error("Should never get here!");
+};
+
+let connections = Object.create(null);
+
+this.addEventListener("message",  function (event) {
+  let packet = JSON.parse(event.data);
+  switch (packet.type) {
+  case "connect":
+    // Step 3: Create a connection to the parent.
+    let connection = DebuggerServer.connectToParent(packet.id, this);
+    connections[packet.id] = connection;
+
+    // Step 4: Create a thread actor for the connection to the parent.
+    let pool = new ActorPool(connection);
+    connection.addActorPool(pool);
+
+    let sources = null;
+
+    let actor = new ThreadActor({
+      makeDebugger: makeDebugger.bind(null, {
+        findDebuggees: () => {
+          return [this.global];
+        },
+
+        shouldAddNewGlobalAsDebuggee: () => {
+          return true;
+        },
+      }),
+
+      get sources() {
+        if (sources === null) {
+          sources = new TabSources(actor);
+        }
+        return sources;
+      }
+    }, global);
+
+    pool.addActor(actor);
+
+    // Step 5: Attach to the thread actor.
+    //
+    // This will cause a packet to be sent over the connection to the parent.
+    // Because this connection uses WorkerDebuggerTransport internally, this
+    // packet will be sent using WorkerDebuggerGlobalScope.postMessage, causing
+    // an onMessage event to be fired on the WorkerDebugger in the main thread.
+    actor.onAttach({});
+    break;
+
+  case "disconnect":
+    connections[packet.id].close();
+    break;
+  };
+});
--- a/toolkit/devtools/worker-loader.js
+++ b/toolkit/devtools/worker-loader.js
@@ -430,30 +430,32 @@ let {
       loadSubScript,
       reportError,
       setImmediate,
       xpcInspector
     };
   } else { // Worker thread
     let requestors = [];
 
+    let scope = this;
+
     let xpcInspector = {
       get lastNestRequestor() {
         return requestors.length === 0 ? null : requestors[0];
       },
 
       enterNestedEventLoop: function (requestor) {
         requestors.push(requestor);
-        this.enterEventLoop();
+        scope.enterEventLoop();
         return requestors.length;
       },
 
       exitNestedEventLoop: function () {
         requestors.pop();
-        this.leaveEventLoop();
+        scope.leaveEventLoop();
         return requestors.length;
       }
     };
 
     return {
       Debugger: this.Debugger,
       createSandbox: this.createSandbox,
       dump: this.dump,
--- a/toolkit/mozapps/extensions/content/extensions.css
+++ b/toolkit/mozapps/extensions/content/extensions.css
@@ -238,17 +238,16 @@ richlistitem:not([selected]) * {
   display: none;
 }
 
 .view-pane[type="experiment"] .error,
 .view-pane[type="experiment"] .warning,
 .view-pane[type="experiment"] .addon:not([pending="uninstall"]) .pending,
 .view-pane[type="experiment"] .disabled-postfix,
 .view-pane[type="experiment"] .update-postfix,
-.view-pane[type="experiment"] .version,
 #detail-view[type="experiment"] .alert-container,
 #detail-view[type="experiment"] #detail-version,
 #detail-view[type="experiment"] #detail-creator {
   display: none;
 }
 
 .view-pane:not([type="experiment"]) .experiment-container,
 .view-pane:not([type="experiment"]) #detail-experiment-container {
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -455,16 +455,41 @@ var gEventManager = {
             countMenuItemsBeforeSep++;
         }
       }
 
       // Hide the separator if there are no visible menu items before it
       menuSep.hidden = (countMenuItemsBeforeSep == 0);
 
     }, false);
+
+    let addonTooltip = document.getElementById("addonitem-tooltip");
+    addonTooltip.addEventListener("popupshowing", function() {
+      let addonItem = document.tooltipNode;
+      // The way the test triggers the tooltip the richlistitem is the
+      // tooltipNode but in normal use it is the anonymous node. This allows
+      // any case
+      if (addonItem.localName != "richlistitem")
+        addonItem = document.getBindingParent(addonItem);
+
+      let tiptext = addonItem.getAttribute("name");
+
+      if (addonItem.mAddon) {
+        if (shouldShowVersionNumber(addonItem.mAddon)) {
+          tiptext += " " + (addonItem.hasAttribute("upgrade") ? addonItem.mManualUpdate.version
+                                                              : addonItem.mAddon.version);
+        }
+      }
+      else {
+        if (shouldShowVersionNumber(addonItem.mInstall))
+          tiptext += " " + addonItem.mInstall.version;
+      }
+
+      addonTooltip.label = tiptext;
+    }, false);
   },
 
   shutdown: function gEM_shutdown() {
     AddonManager.removeManagerListener(this);
     AddonManager.removeInstallListener(this);
     AddonManager.removeAddonListener(this);
   },
 
@@ -1443,16 +1468,20 @@ function isInState(aInstall, aState) {
   var state = AddonManager["STATE_" + aState.toUpperCase()];
   return aInstall.state == state;
 }
 
 function shouldShowVersionNumber(aAddon) {
   if (!aAddon.version)
     return false;
 
+  // The version number is hidden for experiments.
+  if (aAddon.type == "experiment")
+    return false;
+
   // The version number is hidden for lightweight themes.
   if (aAddon.type == "theme")
     return !/@personas\.mozilla\.org$/.test(aAddon.id);
 
   return true;
 }
 
 function createItem(aObj, aIsInstall, aIsRemote) {
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -790,18 +790,17 @@
       <xul:hbox class="content-container" align="center">
         <xul:vbox class="icon-container">
           <xul:image anonid="icon" class="icon"/>
         </xul:vbox>
         <xul:vbox class="content-inner-container" flex="1">
           <xul:hbox class="basicinfo-container">
               <xul:hbox class="name-container">
                 <xul:label anonid="name" class="name" crop="end" flex="1"
-                           xbl:inherits="value=name,tooltiptext=name"/>
-                <xul:label anonid="version" class="version"/>
+                           tooltip="addonitem-tooltip" xbl:inherits="value=name"/>
                 <xul:label class="disabled-postfix" value="&addon.disabled.postfix;"/>
                 <xul:label class="update-postfix" value="&addon.update.postfix;"/>
                 <xul:spacer flex="5000"/> <!-- Necessary to make the name crop -->
               </xul:hbox>
             <xul:label anonid="date-updated" class="date-updated"
                        unknown="&addon.unknownDate;"/>
           </xul:hbox>
           <xul:hbox class="experiment-container">
@@ -974,19 +973,16 @@
       <field name="_infoContainer">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "info-container");
       </field>
       <field name="_info">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "info");
       </field>
-      <field name="_version">
-        document.getAnonymousElementByAttribute(this, "anonid", "version");
-      </field>
       <field name="_experimentState">
         document.getAnonymousElementByAttribute(this, "anonid", "experiment-state");
       </field>
       <field name="_experimentTime">
         document.getAnonymousElementByAttribute(this, "anonid", "experiment-time");
       </field>
       <field name="_icon">
         document.getAnonymousElementByAttribute(this, "anonid", "icon");
@@ -1113,21 +1109,16 @@
           this.setAttribute("name", aAddon.name);
 
           var iconURL = this.mAddon.iconURL;
           if (iconURL)
             this._icon.src = iconURL;
           else
             this._icon.src = "";
 
-          if (shouldShowVersionNumber(this.mAddon))
-            this._version.value = this.mAddon.version;
-          else
-            this._version.hidden = true;
-
           if (this.mAddon.description)
             this._description.value = this.mAddon.description;
           else
             this._description.hidden = true;
 
           if (!("applyBackgroundUpdates" in this.mAddon) ||
               (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE ||
                (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DEFAULT &&
@@ -1409,24 +1400,16 @@
               }
 
               this._experimentTime.value = timeMessage;
             }
           }
         ]]></body>
       </method>
 
-      <method name="_updateUpgradeInfo">
-        <body><![CDATA[
-          // Only update the version string if we're displaying the upgrade info
-          if (this.hasAttribute("upgrade") && shouldShowVersionNumber(this.mAddon))
-            this._version.value = this.mManualUpdate.version;
-        ]]></body>
-      </method>
-
       <method name="_fetchReleaseNotes">
         <parameter name="aURI"/>
         <body><![CDATA[
           var self = this;
           if (!aURI || this._relNotesLoaded) {
             sendToggleEvent();
             return;
           }
@@ -1706,17 +1689,16 @@
           if (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_ENABLE)
             return;
           if (this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DEFAULT &&
               AddonManager.autoUpdateDefault)
             return;
 
           this.mManualUpdate = aInstall;
           this._showStatus("update-available");
-          this._updateUpgradeInfo();
         ]]></body>
       </method>
 
       <method name="onDownloadStarted">
         <parameter name="aInstall"/>
         <body><![CDATA[
           this._updateState();
           this._showStatus("progress");
@@ -1908,18 +1890,17 @@
       <xul:hbox class="content-container">
         <xul:vbox class="icon-outer-container">
           <xul:vbox class="icon-container">
             <xul:image anonid="icon" class="icon"/>
           </xul:vbox>
         </xul:vbox>
         <xul:vbox class="fade name-outer-container" flex="1">
           <xul:hbox class="name-container">
-            <xul:label anonid="name" class="name" crop="end"/>
-            <xul:label anonid="version" class="version" hidden="true"/>
+            <xul:label anonid="name" class="name" crop="end" tooltip="addonitem-tooltip"/>
           </xul:hbox>
         </xul:vbox>
         <xul:vbox class="install-status-container">
           <xul:hbox anonid="install-status" class="install-status"/>
         </xul:vbox>
       </xul:hbox>
     </content>
 
@@ -1931,19 +1912,16 @@
       ]]></constructor>
 
       <field name="_icon">
         document.getAnonymousElementByAttribute(this, "anonid", "icon");
       </field>
       <field name="_name">
         document.getAnonymousElementByAttribute(this, "anonid", "name");
       </field>
-      <field name="_version">
-        document.getAnonymousElementByAttribute(this, "anonid", "version");
-      </field>
       <field name="_warning">
         document.getAnonymousElementByAttribute(this, "anonid", "warning");
       </field>
       <field name="_warningLink">
         document.getAnonymousElementByAttribute(this, "anonid", "warning-link");
       </field>
       <field name="_installStatus">
         document.getAnonymousElementByAttribute(this, "anonid",
@@ -1961,44 +1939,29 @@
 
       <method name="refreshInfo">
         <body><![CDATA[
           this.mAddon = this.mAddon || this.mInstall.addon;
           if (this.mAddon) {
             this._icon.src = this.mAddon.iconURL ||
                              (this.mInstall ? this.mInstall.iconURL : "");
             this._name.value = this.mAddon.name;
-
-            if (this.mAddon.version) {
-              this._version.value = this.mAddon.version;
-              this._version.hidden = false;
-            } else {
-              this._version.hidden = true;
-            }
-
           } else {
             this._icon.src = this.mInstall.iconURL;
             // AddonInstall.name isn't always available - fallback to filename
             if (this.mInstall.name) {
               this._name.value = this.mInstall.name;
             } else if (this.mInstall.sourceURI) {
               var url = Components.classes["@mozilla.org/network/standard-url;1"]
                                   .createInstance(Components.interfaces.nsIStandardURL);
               url.init(url.URLTYPE_STANDARD, 80, this.mInstall.sourceURI.spec,
                        null, null);
               url.QueryInterface(Components.interfaces.nsIURL);
               this._name.value = url.fileName;
             }
-
-            if (this.mInstall.version) {
-              this._version.value = this.mInstall.version;
-              this._version.hidden = false;
-            } else {
-              this._version.hidden = true;
-            }
           }
 
           if (this.mInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) {
             this.setAttribute("notification", "warning");
             this._warning.textContent = gStrings.ext.formatStringFromName(
               "notification.downloadError",
               [this._name.value], 1
             );
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -66,16 +66,18 @@
 #endif
       <menuitem id="menuitem_findUpdates" command="cmd_findItemUpdates"
                 label="&cmd.findUpdates.label;"
                 accesskey="&cmd.findUpdates.accesskey;"/>
       <menuitem id="menuitem_about" command="cmd_showItemAbout"
                 label="&cmd.about.label;"
                 accesskey="&cmd.about.accesskey;"/>
     </menupopup>
+
+    <tooltip id="addonitem-tooltip"/>
   </popupset>
 
   <!-- global commands - these act on all addons, or affect the addons manager
        in some other way -->
   <commandset id="globalCommandSet">
     <command id="cmd_focusSearch"/>
     <command id="cmd_findAllUpdates"/>
     <command id="cmd_restartApp"/>
--- a/toolkit/mozapps/extensions/test/browser/browser_bug580298.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug580298.js
@@ -4,19 +4,17 @@
 
 // Tests that certain types of addons do not have their version number
 // displayed. This currently only includes lightweight themes.
 
 var gManagerWindow;
 var gCategoryUtilities;
 var gProvider;
 
-function test() {
-  waitForExplicitFinish();
-
+add_task(function test() {
   gProvider = new MockProvider();
 
   gProvider.createAddons([{
     id: "extension@tests.mozilla.org",
     name: "Extension 1",
     type: "extension",
     version: "123"
   }, {
@@ -26,86 +24,75 @@ function test() {
     version: "456"
   }, {
     id: "lwtheme@personas.mozilla.org",
     name: "Persona 3",
     type: "theme",
     version: "789"
   }]);
 
-  open_manager(null, function(aWindow) {
-    gManagerWindow = aWindow;
-    gCategoryUtilities = new CategoryUtilities(gManagerWindow);
-    run_next_test();
-  });
-}
-
-function end_test() {
-  close_manager(gManagerWindow, finish);
-}
+  gManagerWindow = yield open_manager();
+  gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+});
 
 function get(aId) {
   return gManagerWindow.document.getElementById(aId);
 }
 
 function get_node(parent, anonid) {
   return parent.ownerDocument.getAnonymousElementByAttribute(parent, "anonid", anonid);
 }
 
 function open_details(aList, aItem, aCallback) {
   aList.ensureElementIsVisible(aItem);
   EventUtils.synthesizeMouseAtCenter(aItem, { clickCount: 1 }, gManagerWindow);
   EventUtils.synthesizeMouseAtCenter(aItem, { clickCount: 2 }, gManagerWindow);
-  wait_for_view_load(gManagerWindow, aCallback);
+  return new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
 }
 
-function check_addon_has_version(aList, aName, aVersion) {
+let check_addon_has_version = Task.async(function*(aList, aName, aVersion) {
   for (let i = 0; i < aList.itemCount; i++) {
     let item = aList.getItemAtIndex(i);
     if (get_node(item, "name").value === aName) {
       ok(true, "Item with correct name found");
-      is(get_node(item, "version").value, aVersion, "Item has correct version");
+      let { version } = yield get_tooltip_info(item);
+      is(version, aVersion, "Item has correct version");
       return item;
     }
   }
   ok(false, "Item with correct name was not found");
   return null;
-}
+});
 
-add_test(function() {
-  gCategoryUtilities.openType("extension", function() {
-    info("Extension");
-    let list = gManagerWindow.document.getElementById("addon-list");
-    let item = check_addon_has_version(list, "Extension 1", "123");
-    open_details(list, item, function() {
-      is_element_visible(get("detail-version"), "Details view has version visible");
-      is(get("detail-version").value, "123", "Details view has correct version");
-      run_next_test();
-    });
-  });
+add_task(function*() {
+  yield gCategoryUtilities.openType("extension");
+  info("Extension");
+  let list = gManagerWindow.document.getElementById("addon-list");
+  let item = yield check_addon_has_version(list, "Extension 1", "123");
+  yield open_details(list, item);
+  is_element_visible(get("detail-version"), "Details view has version visible");
+  is(get("detail-version").value, "123", "Details view has correct version");
 });
 
-add_test(function() {
-  gCategoryUtilities.openType("theme", function() {
-    info("Normal theme");
-    let list = gManagerWindow.document.getElementById("addon-list");
-    let item = check_addon_has_version(list, "Theme 2", "456");
-    open_details(list, item, function() {
-      is_element_visible(get("detail-version"), "Details view has version visible");
-      is(get("detail-version").value, "456", "Details view has correct version");
-      run_next_test();
-    });
-  });
+add_task(function*() {
+  yield gCategoryUtilities.openType("theme");
+  info("Normal theme");
+  let list = gManagerWindow.document.getElementById("addon-list");
+  let item = yield check_addon_has_version(list, "Theme 2", "456");
+  yield open_details(list, item);
+  is_element_visible(get("detail-version"), "Details view has version visible");
+  is(get("detail-version").value, "456", "Details view has correct version");
 });
 
-add_test(function() {
-  gCategoryUtilities.openType("theme", function() {
-    info("Lightweight theme");
-    let list = gManagerWindow.document.getElementById("addon-list");
-    // See that the version isn't displayed
-    let item = check_addon_has_version(list, "Persona 3", "");
-    open_details(list, item, function() {
-      is_element_hidden(get("detail-version"), "Details view has version hidden");
-      // If the version element is hidden then we don't care about its value
-      run_next_test();
-    });
-  });
+add_task(function*() {
+  yield gCategoryUtilities.openType("theme");
+  info("Lightweight theme");
+  let list = gManagerWindow.document.getElementById("addon-list");
+  // See that the version isn't displayed
+  let item = yield check_addon_has_version(list, "Persona 3", undefined);
+  yield open_details(list, item);
+  is_element_hidden(get("detail-version"), "Details view has version hidden");
+  // If the version element is hidden then we don't care about its value
 });
+
+add_task(function end_test() {
+  close_manager(gManagerWindow, finish);
+});
--- a/toolkit/mozapps/extensions/test/browser/browser_bug596336.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug596336.js
@@ -3,180 +3,156 @@
  */
 
 // Tests that upgrading bootstrapped add-ons behaves correctly while the
 // manager is open
 
 var gManagerWindow;
 var gCategoryUtilities;
 
-function test() {
+add_task(function* test() {
   waitForExplicitFinish();
 
-  open_manager("addons://list/extension", function(aWindow) {
-    gManagerWindow = aWindow;
-    gCategoryUtilities = new CategoryUtilities(gManagerWindow);
-    run_next_test();
-  });
-}
-
-function end_test() {
-  close_manager(gManagerWindow, finish);
-}
+  gManagerWindow = yield open_manager("addons://list/extension");
+  gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+});
 
 function get_list_item_count() {
   return get_test_items_in_list(gManagerWindow).length;
 }
 
 function get_node(parent, anonid) {
   return parent.ownerDocument.getAnonymousElementByAttribute(parent, "anonid", anonid);
 }
 
 function get_class_node(parent, cls) {
   return parent.ownerDocument.getAnonymousElementByAttribute(parent, "class", cls);
 }
 
-function install_addon(aXpi, aCallback) {
-  AddonManager.getInstallForURL(TESTROOT + "addons/" + aXpi + ".xpi",
-                                function(aInstall) {
-    aInstall.addListener({
-      onInstallEnded: function(aInstall) {
-        executeSoon(aCallback);
-      }
-    });
-    aInstall.install();
-  }, "application/x-xpinstall");
+function install_addon(aXpi) {
+  return new Promise(resolve => {
+    AddonManager.getInstallForURL(TESTROOT + "addons/" + aXpi + ".xpi",
+                                  function(aInstall) {
+      aInstall.addListener({
+        onInstallEnded: function(aInstall) {
+          resolve();
+        }
+      });
+      aInstall.install();
+    }, "application/x-xpinstall");
+  });
 }
 
-function check_addon(aAddon, version) {
+let check_addon = Task.async(function*(aAddon, aVersion) {
   is(get_list_item_count(), 1, "Should be one item in the list");
-  is(aAddon.version, version, "Add-on should have the right version");
+  is(aAddon.version, aVersion, "Add-on should have the right version");
 
   let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
   ok(!!item, "Should see the add-on in the list");
 
   // Force XBL to apply
   item.clientTop;
 
-  is(get_node(item, "version").value, version, "Version should be correct");
+  let { version } = yield get_tooltip_info(item);
+  is(version, aVersion, "Version should be correct");
 
   if (aAddon.userDisabled)
     is_element_visible(get_class_node(item, "disabled-postfix"), "Disabled postfix should be hidden");
   else
     is_element_hidden(get_class_node(item, "disabled-postfix"), "Disabled postfix should be hidden");
-}
+});
 
 // Install version 1 then upgrade to version 2 with the manager open
-add_test(function() {
-  install_addon("browser_bug596336_1", function() {
-    AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
-      check_addon(aAddon, "1.0");
-      ok(!aAddon.userDisabled, "Add-on should not be disabled");
+add_task(function() {
+  yield install_addon("browser_bug596336_1");
+  let [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
+  yield check_addon(aAddon, "1.0");
+  ok(!aAddon.userDisabled, "Add-on should not be disabled");
 
-      install_addon("browser_bug596336_2", function() {
-        AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
-          check_addon(aAddon, "2.0");
-          ok(!aAddon.userDisabled, "Add-on should not be disabled");
-
-          aAddon.uninstall();
+  yield install_addon("browser_bug596336_2");
+  [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
+  yield check_addon(aAddon, "2.0");
+  ok(!aAddon.userDisabled, "Add-on should not be disabled");
 
-          is(get_list_item_count(), 0, "Should be no items in the list");
+  aAddon.uninstall();
 
-          run_next_test();
-        });
-      });
-    });
-  });
+  is(get_list_item_count(), 0, "Should be no items in the list");
 });
 
 // Install version 1 mark it as disabled then upgrade to version 2 with the
 // manager open
-add_test(function() {
-  install_addon("browser_bug596336_1", function() {
-    AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
-      aAddon.userDisabled = true;
-      check_addon(aAddon, "1.0");
-      ok(aAddon.userDisabled, "Add-on should be disabled");
+add_task(function() {
+  yield install_addon("browser_bug596336_1");
+  let [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
+  aAddon.userDisabled = true;
+  yield check_addon(aAddon, "1.0");
+  ok(aAddon.userDisabled, "Add-on should be disabled");
 
-      install_addon("browser_bug596336_2", function() {
-        AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
-          check_addon(aAddon, "2.0");
-          ok(aAddon.userDisabled, "Add-on should be disabled");
-
-          aAddon.uninstall();
+  yield install_addon("browser_bug596336_2");
+  [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
+  yield check_addon(aAddon, "2.0");
+  ok(aAddon.userDisabled, "Add-on should be disabled");
 
-          is(get_list_item_count(), 0, "Should be no items in the list");
+  aAddon.uninstall();
 
-          run_next_test();
-        });
-      });
-    });
-  });
+  is(get_list_item_count(), 0, "Should be no items in the list");
 });
 
 // Install version 1 click the remove button and then upgrade to version 2 with
 // the manager open
-add_test(function() {
-  install_addon("browser_bug596336_1", function() {
-    AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
-      check_addon(aAddon, "1.0");
-      ok(!aAddon.userDisabled, "Add-on should not be disabled");
+add_task(function() {
+  yield install_addon("browser_bug596336_1");
+  let [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
+  yield check_addon(aAddon, "1.0");
+  ok(!aAddon.userDisabled, "Add-on should not be disabled");
 
-      let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
-      EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
+  let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+  EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
 
-      // Force XBL to apply
-      item.clientTop;
+  // Force XBL to apply
+  item.clientTop;
 
-      ok(aAddon.userDisabled, "Add-on should be disabled");
-      ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
-      is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
-
-      install_addon("browser_bug596336_2", function() {
-        AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
-          check_addon(aAddon, "2.0");
-          ok(!aAddon.userDisabled, "Add-on should not be disabled");
+  ok(aAddon.userDisabled, "Add-on should be disabled");
+  ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
+  is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
 
-          aAddon.uninstall();
-
-          is(get_list_item_count(), 0, "Should be no items in the list");
+  yield install_addon("browser_bug596336_2");
+  [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
+  yield check_addon(aAddon, "2.0");
+  ok(!aAddon.userDisabled, "Add-on should not be disabled");
 
-          run_next_test();
-        });
-      });
-    });
-  });
+  aAddon.uninstall();
+
+  is(get_list_item_count(), 0, "Should be no items in the list");
 });
 
 // Install version 1, disable it, click the remove button and then upgrade to
 // version 2 with the manager open
-add_test(function() {
-  install_addon("browser_bug596336_1", function() {
-    AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
-      aAddon.userDisabled = true;
-      check_addon(aAddon, "1.0");
-      ok(aAddon.userDisabled, "Add-on should be disabled");
+add_task(function() {
+  yield install_addon("browser_bug596336_1");
+  let [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
+  aAddon.userDisabled = true;
+  yield check_addon(aAddon, "1.0");
+  ok(aAddon.userDisabled, "Add-on should be disabled");
 
-      let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
-      EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
+  let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+  EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
 
-      // Force XBL to apply
-      item.clientTop;
+  // Force XBL to apply
+  item.clientTop;
 
-      ok(aAddon.userDisabled, "Add-on should be disabled");
-      ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
-      is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
+  ok(aAddon.userDisabled, "Add-on should be disabled");
+  ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
+  is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
 
-      install_addon("browser_bug596336_2", function() {
-        AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
-          check_addon(aAddon, "2.0");
-          ok(aAddon.userDisabled, "Add-on should be disabled");
+  yield install_addon("browser_bug596336_2");
+  [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
+  yield check_addon(aAddon, "2.0");
+  ok(aAddon.userDisabled, "Add-on should be disabled");
 
-          aAddon.uninstall();
-
-          is(get_list_item_count(), 0, "Should be no items in the list");
+  aAddon.uninstall();
 
-          run_next_test();
-        });
-      });
-    });
-  });
+  is(get_list_item_count(), 0, "Should be no items in the list");
 });
+
+add_task(function end_test() {
+  close_manager(gManagerWindow, finish);
+});
--- a/toolkit/mozapps/extensions/test/browser/browser_experiments.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_experiments.js
@@ -421,18 +421,18 @@ add_task(function testActivateRealExperi
   }
 
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "error-container");
   is_element_hidden(el, "error-container should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "warning-container");
   is_element_hidden(el, "warning-container should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "pending-container");
   is_element_hidden(el, "pending-container should be hidden.");
-  el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "version");
-  is_element_hidden(el, "version should be hidden.");
+  let { version } = yield get_tooltip_info(item);
+  Assert.equal(version, undefined, "version should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix");
   is_element_hidden(el, "disabled-postfix should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "update-postfix");
   is_element_hidden(el, "update-postfix should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "experiment-bullet");
   is_element_visible(el, "experiment-bullet should be visible.");
 
   // Check the previous experiment.
@@ -454,18 +454,18 @@ add_task(function testActivateRealExperi
   }
 
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "error-container");
   is_element_hidden(el, "error-container should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "warning-container");
   is_element_hidden(el, "warning-container should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "pending-container");
   is_element_hidden(el, "pending-container should be hidden.");
-  el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "version");
-  is_element_hidden(el, "version should be hidden.");
+  ({ version }) = yield get_tooltip_info(item);
+  Assert.equal(version, undefined, "version should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix");
   is_element_hidden(el, "disabled-postfix should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "update-postfix");
   is_element_hidden(el, "update-postfix should be hidden.");
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "experiment-bullet");
   is_element_visible(el, "experiment-bullet should be visible.");
 
   // Install an "older" experiment.
--- a/toolkit/mozapps/extensions/test/browser/browser_list.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_list.js
@@ -27,20 +27,17 @@ var gLWTheme = {
                 author: "Pixel Pusher",
                 homepageURL: "http://mochi.test:8888/data/index.html",
                 headerURL: "http://mochi.test:8888/data/header.png",
                 footerURL: "http://mochi.test:8888/data/footer.png",
                 previewURL: "http://mochi.test:8888/data/preview.png",
                 iconURL: "http://mochi.test:8888/data/icon.png"
               };
 
-
-function test() {
-  waitForExplicitFinish();
-
+add_task(function*() {
   gProvider = new MockProvider();
 
   gProvider.createAddons([{
     id: "addon1@tests.mozilla.org",
     name: "Test add-on",
     version: "1.0",
     description: "A test add-on",
     longDescription: " A longer description",
@@ -103,28 +100,19 @@ function test() {
   }, {
     id: "addon11@tests.mozilla.org",
     name: "Test add-on 11",
     signedState: AddonManager.SIGNEDSTATE_MISSING,
     isActive: false,
     appDisabled: true,
   }]);
 
-  open_manager(null, function(aWindow) {
-    gManagerWindow = aWindow;
-    gCategoryUtilities = new CategoryUtilities(gManagerWindow);
-    run_next_test();
-  });
-}
-
-function end_test() {
-  close_manager(gManagerWindow, function() {
-    finish();
-  });
-}
+  gManagerWindow = yield open_manager(null);
+  gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+});
 
 function get_test_items() {
   var tests = "@tests.mozilla.org";
 
   var items = {};
   var item = gManagerWindow.document.getElementById("addon-list").firstChild;
 
   while (item) {
@@ -142,591 +130,607 @@ function get_node(parent, anonid) {
 }
 
 function get_class_node(parent, cls) {
   return parent.ownerDocument.getAnonymousElementByAttribute(parent, "class", cls);
 }
 
 // Check that the list appears to have displayed correctly and trigger some
 // changes
-add_test(function() {
-  gCategoryUtilities.openType("extension", function() {
-    let items = get_test_items();
-    is(Object.keys(items).length, 11, "Should be the right number of add-ons installed");
+add_task(function*() {
+  yield gCategoryUtilities.openType("extension");
+  let items = get_test_items();
+  is(Object.keys(items).length, 11, "Should be the right number of add-ons installed");
 
-    info("Addon 1");
-    let addon = items["Test add-on"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on", "Name should be correct");
-    is_element_visible(get_node(addon, "version"), "Version should be visible");
-    is(get_node(addon, "version").value, "1.0", "Version should be correct");
-    is_element_visible(get_node(addon, "description"), "Description should be visible");
-    is(get_node(addon, "description").value, "A test add-on", "Description should be correct");
-    is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
-    is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
-    is(get_node(addon, "date-updated").value, formatDate(gDate), "Update date should be correct");
+  info("Addon 1");
+  let addon = items["Test add-on"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  let { name, version } = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on", "Name should be correct");
+  is(name, "Test add-on", "Tooltip name should be correct");
+  is(version, "1.0", "Tooltip version should be correct");
+  is(get_node(addon, "description").value, "A test add-on", "Description should be correct");
+  is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+  is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
+  is(get_node(addon, "date-updated").value, formatDate(gDate), "Update date should be correct");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    info("Disabling");
-    EventUtils.synthesizeMouseAtCenter(get_node(addon, "disable-btn"), {}, gManagerWindow);
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-    is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Disabling");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "disable-btn"), {}, gManagerWindow);
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be visible");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
-    is(get_node(addon, "pending").textContent, "Test add-on will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be visible");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+  is(get_node(addon, "pending").textContent, "Test add-on will be disabled after you restart " + gApp + ".", "Pending message should be correct");
 
-    info("Addon 2");
-    addon = items["Test add-on 2"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 2", "Name should be correct");
-    is_element_visible(get_node(addon, "version"), "Version should be visible");
-    is(get_node(addon, "version").value, "2.0", "Version should be correct");
-    is_element_hidden(get_node(addon, "description"), "Description should be hidden");
-    is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
-    is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
-    is(get_node(addon, "date-updated").value, "Unknown", "Date should be correct");
+  info("Addon 2");
+  addon = items["Test add-on 2"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 2", "Name should be correct");
+  is(name, "Test add-on 2", "Tooltip name should be correct");
+  is(version, "2.0", "Tooltip version should be correct");
+  is_element_hidden(get_node(addon, "description"), "Description should be hidden");
+  is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
+  is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
+  is(get_node(addon, "date-updated").value, "Unknown", "Date should be correct");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-    is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    info("Enabling");
-    EventUtils.synthesizeMouseAtCenter(get_node(addon, "enable-btn"), {}, gManagerWindow);
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Enabling");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "enable-btn"), {}, gManagerWindow);
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
-    is(get_node(addon, "pending").textContent, "Test add-on 2 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+  is(get_node(addon, "pending").textContent, "Test add-on 2 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
 
-    info("Addon 3");
-    addon = items["Test add-on 3"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 3", "Name should be correct");
-    is_element_hidden(get_node(addon, "version"), "Version should be hidden");
+  info("Addon 3");
+  addon = items["Test add-on 3"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 3", "Name should be correct");
+  is(name, "Test add-on 3", "Tooltip name should be correct");
+  is(version, undefined, "Tooltip version should be hidden");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-    is_element_hidden(get_node(addon, "remove-btn"), "Remove button should be hidden");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_hidden(get_node(addon, "remove-btn"), "Remove button should be hidden");
 
-    is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
-    is(get_node(addon, "warning").textContent, "Test add-on 3 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
+  is(get_node(addon, "warning").textContent, "Test add-on 3 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    info("Addon 4");
-    addon = items["Test add-on 4"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 4", "Name should be correct");
+  info("Addon 4");
+  addon = items["Test add-on 4"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 4", "Name should be correct");
+  is(name, "Test add-on 4", "Tooltip name should be correct");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-    is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
-    is(get_node(addon, "warning").textContent, "Test add-on 4 is known to cause security or stability issues.", "Warning message should be correct");
-    is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
-    is(get_node(addon, "warning-link").value, "More Information", "Warning link text should be correct");
-    is(get_node(addon, "warning-link").href, "http://example.com/addon4@tests.mozilla.org", "Warning link should be correct");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
+  is(get_node(addon, "warning").textContent, "Test add-on 4 is known to cause security or stability issues.", "Warning message should be correct");
+  is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+  is(get_node(addon, "warning-link").value, "More Information", "Warning link text should be correct");
+  is(get_node(addon, "warning-link").href, "http://example.com/addon4@tests.mozilla.org", "Warning link should be correct");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    info("Enabling");
-    EventUtils.synthesizeMouseAtCenter(get_node(addon, "enable-btn"), {}, gManagerWindow);
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Enabling");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "enable-btn"), {}, gManagerWindow);
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
-    is(get_node(addon, "pending").textContent, "Test add-on 4 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+  is(get_node(addon, "pending").textContent, "Test add-on 4 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
 
-    info("Addon 5");
-    addon = items["Test add-on 5"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 5", "Name should be correct");
+  info("Addon 5");
+  addon = items["Test add-on 5"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 5", "Name should be correct");
+  is(name, "Test add-on 5", "Tooltip name should be correct");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_visible(get_node(addon, "error"), "Error message should be visible");
-    is(get_node(addon, "error").textContent, "Test add-on 5 has been disabled due to security or stability issues.", "Error message should be correct");
-    is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
-    is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
-    is(get_node(addon, "error-link").href, "http://example.com/addon5@tests.mozilla.org", "Error link should be correct");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_visible(get_node(addon, "error"), "Error message should be visible");
+  is(get_node(addon, "error").textContent, "Test add-on 5 has been disabled due to security or stability issues.", "Error message should be correct");
+  is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
+  is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
+  is(get_node(addon, "error-link").href, "http://example.com/addon5@tests.mozilla.org", "Error link should be correct");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    info("Addon 6");
-    addon = items["Test add-on 6"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 6", "Name should be correct");
-    is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+  info("Addon 6");
+  addon = items["Test add-on 6"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 6", "Name should be correct");
+  is(name, "Test add-on 6", "Tooltip name should be correct");
+  is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_hidden(get_node(addon, "error"), "Error message should be visible");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be visible");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+  info("Disabling");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "disable-btn"), {}, gManagerWindow);
+  is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
 
-    info("Disabling");
-    EventUtils.synthesizeMouseAtCenter(get_node(addon, "disable-btn"), {}, gManagerWindow);
-    is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-    is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be visible");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_hidden(get_node(addon, "error"), "Error message should be visible");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  info("Addon 7");
+  addon = items["Test add-on 7"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 7", "Name should be correct");
+  is(name, "Test add-on 7", "Tooltip name should be correct");
 
-    info("Addon 7");
-    addon = items["Test add-on 7"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 7", "Name should be correct");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_visible(get_node(addon, "warning"), "Warning message should be hidden");
+  is(get_node(addon, "warning").textContent, "An important update is available for Test add-on 7.", "Warning message should be correct");
+  is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+  is(get_node(addon, "warning-link").value, "Update Now", "Warning link text should be correct");
+  is(get_node(addon, "warning-link").href, gPluginURL, "Warning link should be correct");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    is_element_visible(get_node(addon, "warning"), "Warning message should be hidden");
-    is(get_node(addon, "warning").textContent, "An important update is available for Test add-on 7.", "Warning message should be correct");
-    is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
-    is(get_node(addon, "warning-link").value, "Update Now", "Warning link text should be correct");
-    is(get_node(addon, "warning-link").href, gPluginURL, "Warning link should be correct");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  info("Disabling");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "disable-btn"), {}, gManagerWindow);
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    info("Disabling");
-    EventUtils.synthesizeMouseAtCenter(get_node(addon, "disable-btn"), {}, gManagerWindow);
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-    is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be visible");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+  is(get_node(addon, "pending").textContent, "Test add-on 7 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be visible");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
-    is(get_node(addon, "pending").textContent, "Test add-on 7 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+  info("Addon 8");
+  addon = items["Test add-on 8"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 8", "Name should be correct");
+  is(name, "Test add-on 8", "Tooltip name should be correct");
 
-    info("Addon 8");
-    addon = items["Test add-on 8"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 8", "Name should be correct");
-
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_visible(get_node(addon, "error"), "Error message should be visible");
-    is(get_node(addon, "error").textContent, "Test add-on 8 is known to be vulnerable and should be updated.", "Error message should be correct");
-    is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
-    is(get_node(addon, "error-link").value, "Update Now", "Error link text should be correct");
-    is(get_node(addon, "error-link").href, "http://example.com/addon8@tests.mozilla.org", "Error link should be correct");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_visible(get_node(addon, "error"), "Error message should be visible");
+  is(get_node(addon, "error").textContent, "Test add-on 8 is known to be vulnerable and should be updated.", "Error message should be correct");
+  is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
+  is(get_node(addon, "error-link").value, "Update Now", "Error link text should be correct");
+  is(get_node(addon, "error-link").href, "http://example.com/addon8@tests.mozilla.org", "Error link should be correct");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    info("Addon 9");
-    addon = items["Test add-on 9"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 9", "Name should be correct");
+  info("Addon 9");
+  addon = items["Test add-on 9"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 9", "Name should be correct");
+  is(name, "Test add-on 9", "Tooltip name should be correct");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_visible(get_node(addon, "error"), "Error message should be visible");
-    is(get_node(addon, "error").textContent, "Test add-on 9 is known to be vulnerable. Use with caution.", "Error message should be correct");
-    is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
-    is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
-    is(get_node(addon, "error-link").href, "http://example.com/addon9@tests.mozilla.org", "Error link should be correct");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_visible(get_node(addon, "error"), "Error message should be visible");
+  is(get_node(addon, "error").textContent, "Test add-on 9 is known to be vulnerable. Use with caution.", "Error message should be correct");
+  is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
+  is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
+  is(get_node(addon, "error-link").href, "http://example.com/addon9@tests.mozilla.org", "Error link should be correct");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    info("Addon 10");
-    addon = items["Test add-on 10"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 10", "Name should be correct");
+  info("Addon 10");
+  addon = items["Test add-on 10"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 10", "Name should be correct");
+  is(name, "Test add-on 10", "Tooltip name should be correct");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
-    is(get_node(addon, "warning").textContent, "Test add-on 10 could not be verified for use in " + gApp + ". Proceed with caution.", "Warning message should be correct");
-    is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
-    is(get_node(addon, "warning-link").value, "More Information", "Warning link text should be correct");
-    is(get_node(addon, "warning-link").href, infoURL, "Warning link should be correct");
-    is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-    is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
+  is(get_node(addon, "warning").textContent, "Test add-on 10 could not be verified for use in " + gApp + ". Proceed with caution.", "Warning message should be correct");
+  is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+  is(get_node(addon, "warning-link").value, "More Information", "Warning link text should be correct");
+  is(get_node(addon, "warning-link").href, infoURL, "Warning link should be correct");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    info("Addon 11");
-    addon = items["Test add-on 11"];
-    addon.parentNode.ensureElementIsVisible(addon);
-    is(get_node(addon, "name").value, "Test add-on 11", "Name should be correct");
+  info("Addon 11");
+  addon = items["Test add-on 11"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 11", "Name should be correct");
+  is(name, "Test add-on 11", "Tooltip name should be correct");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-    is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-    is_element_visible(get_node(addon, "error"), "Error message should be visible");
-    is(get_node(addon, "error").textContent, "Test add-on 11 could not be verified for use in " + gApp + " and has been disabled.", "Error message should be correct");
-    is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
-    is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
-    is(get_node(addon, "error-link").href, infoURL, "Error link should be correct");
-    is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_visible(get_node(addon, "error"), "Error message should be visible");
+  is(get_node(addon, "error").textContent, "Test add-on 11 could not be verified for use in " + gApp + " and has been disabled.", "Error message should be correct");
+  is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
+  is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
+  is(get_node(addon, "error-link").href, infoURL, "Error link should be correct");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-    info("Filter for disabled unsigned extensions");
-    let filterButton = gManagerWindow.document.getElementById("show-disabled-unsigned-extensions");
-    let showAllButton = gManagerWindow.document.getElementById("show-all-extensions");
-    let signingInfoUI = gManagerWindow.document.getElementById("disabled-unsigned-addons-info");
-    is_element_visible(filterButton, "Button for showing disabled unsigned extensions should be visible");
-    is_element_hidden(showAllButton, "Button for showing all extensions should be hidden");
-    is_element_hidden(signingInfoUI, "Signing info UI should be hidden");
+  info("Filter for disabled unsigned extensions");
+  let filterButton = gManagerWindow.document.getElementById("show-disabled-unsigned-extensions");
+  let showAllButton = gManagerWindow.document.getElementById("show-all-extensions");
+  let signingInfoUI = gManagerWindow.document.getElementById("disabled-unsigned-addons-info");
+  is_element_visible(filterButton, "Button for showing disabled unsigned extensions should be visible");
+  is_element_hidden(showAllButton, "Button for showing all extensions should be hidden");
+  is_element_hidden(signingInfoUI, "Signing info UI should be hidden");
 
-    filterButton.click();
-    wait_for_view_load(gManagerWindow, () => {
-      is_element_hidden(filterButton, "Button for showing disabled unsigned extensions should be hidden");
-      is_element_visible(showAllButton, "Button for showing all extensions should be visible");
-      is_element_visible(signingInfoUI, "Signing info UI should be visible");
+  filterButton.click();
+
+  yield new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
 
-      items = get_test_items();
-      is(Object.keys(items).length, 1, "Only one add-on should be shown");
-      is(Object.keys(items)[0], "Test add-on 11", "The disabled unsigned extension should be shown");
+  is_element_hidden(filterButton, "Button for showing disabled unsigned extensions should be hidden");
+  is_element_visible(showAllButton, "Button for showing all extensions should be visible");
+  is_element_visible(signingInfoUI, "Signing info UI should be visible");
 
-      showAllButton.click();
-      wait_for_view_load(gManagerWindow, () => {
-        items = get_test_items();
-        is(Object.keys(items).length, 11, "All add-ons should be shown again");
-        is_element_visible(filterButton, "Button for showing disabled unsigned extensions should be visible again");
-        is_element_hidden(showAllButton, "Button for showing all extensions should be hidden again");
-        is_element_hidden(signingInfoUI, "Signing info UI should be hidden again");
+  items = get_test_items();
+  is(Object.keys(items).length, 1, "Only one add-on should be shown");
+  is(Object.keys(items)[0], "Test add-on 11", "The disabled unsigned extension should be shown");
+
+  showAllButton.click();
 
-        run_next_test();
-      });
-    });
-  });
+  yield new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
+
+  items = get_test_items();
+  is(Object.keys(items).length, 11, "All add-ons should be shown again");
+  is_element_visible(filterButton, "Button for showing disabled unsigned extensions should be visible again");
+  is_element_hidden(showAllButton, "Button for showing all extensions should be hidden again");
+  is_element_hidden(signingInfoUI, "Signing info UI should be hidden again");
 });
 
 // Check the add-ons are now in the right state
-add_test(function() {
-  AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                               "addon2@tests.mozilla.org",
-                               "addon4@tests.mozilla.org",
-                               "addon6@tests.mozilla.org"],
-                               function([a1, a2, a4, a6]) {
-    is(a1.pendingOperations, AddonManager.PENDING_DISABLE, "Add-on 1 should be pending disable");
-    is(a2.pendingOperations, AddonManager.PENDING_ENABLE, "Add-on 2 should be pending enable");
-    is(a4.pendingOperations, AddonManager.PENDING_ENABLE, "Add-on 4 should be pending enable");
+add_task(function*() {
+  let [a1, a2, a4, a6] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                                                   "addon2@tests.mozilla.org",
+                                                   "addon4@tests.mozilla.org",
+                                                   "addon6@tests.mozilla.org"]);
 
-    run_next_test();
-  });
+  is(a1.pendingOperations, AddonManager.PENDING_DISABLE, "Add-on 1 should be pending disable");
+  is(a2.pendingOperations, AddonManager.PENDING_ENABLE, "Add-on 2 should be pending enable");
+  is(a4.pendingOperations, AddonManager.PENDING_ENABLE, "Add-on 4 should be pending enable");
 });
 
 // Reload the list to make sure the changes are still pending and that undoing
 // works
-add_test(function() {
-  gCategoryUtilities.openType("plugin", function() {
-    gCategoryUtilities.openType("extension", function() {
-      let items = get_test_items();
-      is(Object.keys(items).length, 11, "Should be the right number of add-ons installed");
+add_task(function*() {
+  yield gCategoryUtilities.openType("plugin");
+  yield gCategoryUtilities.openType("extension");
 
-      info("Addon 1");
-      let addon = items["Test add-on"];
-      addon.parentNode.ensureElementIsVisible(addon);
-      is(get_node(addon, "name").value, "Test add-on", "Name should be correct");
-      is_element_visible(get_node(addon, "version"), "Version should be visible");
-      is(get_node(addon, "version").value, "1.0", "Version should be correct");
-      is_element_visible(get_node(addon, "description"), "Description should be visible");
-      is(get_node(addon, "description").value, "A test add-on", "Description should be correct");
-      is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
-      is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
-      is(get_node(addon, "date-updated").value, formatDate(gDate), "Update date should be correct");
+  let items = get_test_items();
+  is(Object.keys(items).length, 11, "Should be the right number of add-ons installed");
 
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-      is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
-
-      is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-      is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-      is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
-      is(get_node(addon, "pending").textContent, "Test add-on will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+  info("Addon 1");
+  let addon = items["Test add-on"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  let { name, version } = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on", "Name should be correct");
+  is(name, "Test add-on", "Tooltip name should be correct");
+  is(version, "1.0", "Tooltip version should be correct");
+  is_element_visible(get_node(addon, "description"), "Description should be visible");
+  is(get_node(addon, "description").value, "A test add-on", "Description should be correct");
+  is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+  is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
+  is(get_node(addon, "date-updated").value, formatDate(gDate), "Update date should be correct");
 
-      info("Undoing");
-      EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-      is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-      is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-      is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-      is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+  is(get_node(addon, "pending").textContent, "Test add-on will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+
+  info("Undoing");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-      info("Addon 2");
-      addon = items["Test add-on 2"];
-      addon.parentNode.ensureElementIsVisible(addon);
-      is(get_node(addon, "name").value, "Test add-on 2", "Name should be correct");
-      is_element_visible(get_node(addon, "version"), "Version should be visible");
-      is(get_node(addon, "version").value, "2.0", "Version should be correct");
-      is_element_hidden(get_node(addon, "description"), "Description should be hidden");
-      is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
-      is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
-      is(get_node(addon, "date-updated").value, "Unknown", "Date should be correct");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-      is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Addon 2");
+  addon = items["Test add-on 2"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 2", "Name should be correct");
+  is(name, "Test add-on 2", "Tooltip name should be correct");
+  is(version, "2.0", "Tooltip version should be correct");
+  is_element_hidden(get_node(addon, "description"), "Description should be hidden");
+  is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
+  is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
+  is(get_node(addon, "date-updated").value, "Unknown", "Date should be correct");
 
-      is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-      is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-      is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
-      is(get_node(addon, "pending").textContent, "Test add-on 2 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-      info("Undoing");
-      EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-      is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+  is(get_node(addon, "pending").textContent, "Test add-on 2 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
 
-      is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-      is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-      is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  info("Undoing");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-      info("Addon 4");
-      addon = items["Test add-on 4"];
-      addon.parentNode.ensureElementIsVisible(addon);
-      is(get_node(addon, "name").value, "Test add-on 4", "Name should be correct");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-      is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Addon 4");
+  addon = items["Test add-on 4"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 4", "Name should be correct");
+  is(name, "Test add-on 4", "Tooltip name should be correct");
 
-      is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-      is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-      is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
-      is(get_node(addon, "pending").textContent, "Test add-on 4 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-      info("Undoing");
-      EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-      is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+  is(get_node(addon, "pending").textContent, "Test add-on 4 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+
+  info("Undoing");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-      is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
-      is(get_node(addon, "warning").textContent, "Test add-on 4 is known to cause security or stability issues.", "Warning message should be correct");
-      is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
-      is(get_node(addon, "warning-link").value, "More Information", "Warning link text should be correct");
-      is(get_node(addon, "warning-link").href, "http://example.com/addon4@tests.mozilla.org", "Warning link should be correct");
-      is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
-
-      info("Addon 6");
-      addon = items["Test add-on 6"];
-      addon.parentNode.ensureElementIsVisible(addon);
-      is(get_node(addon, "name").value, "Test add-on 6", "Name should be correct");
-      is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
+  is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
+  is(get_node(addon, "warning").textContent, "Test add-on 4 is known to cause security or stability issues.", "Warning message should be correct");
+  is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+  is(get_node(addon, "warning-link").value, "More Information", "Warning link text should be correct");
+  is(get_node(addon, "warning-link").href, "http://example.com/addon4@tests.mozilla.org", "Warning link should be correct");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-      is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Addon 6");
+  addon = items["Test add-on 6"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 6", "Name should be correct");
+  is(name, "Test add-on 6", "Tooltip name should be correct");
+  is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
 
-      is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-      is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-      is_element_hidden(get_node(addon, "error"), "Error message should be visible");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-      info("Enabling");
-      EventUtils.synthesizeMouseAtCenter(get_node(addon, "enable-btn"), {}, gManagerWindow);
-      is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be visible");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-      is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Enabling");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "enable-btn"), {}, gManagerWindow);
+  is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-      is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
-      is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-      is_element_hidden(get_node(addon, "error"), "Error message should be visible");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be visible");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 
-      info("Addon 7");
-      addon = items["Test add-on 7"];
-      addon.parentNode.ensureElementIsVisible(addon);
-      is(get_node(addon, "name").value, "Test add-on 7", "Name should be correct");
-
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
-      is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Addon 7");
+  addon = items["Test add-on 7"];
+  addon.parentNode.ensureElementIsVisible(addon);
+  ({ name, version }) = yield get_tooltip_info(addon);
+  is(get_node(addon, "name").value, "Test add-on 7", "Name should be correct");
+  is(name, "Test add-on 7", "Tooltip name should be correct");
 
-      is_element_hidden(get_node(addon, "warning"), "Warning message should be visible");
-      is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
-      is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
-      is(get_node(addon, "pending").textContent, "Test add-on 7 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+  is_element_hidden(get_node(addon, "warning"), "Warning message should be visible");
+  is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+  is(get_node(addon, "pending").textContent, "Test add-on 7 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
 
-      info("Undoing");
-      EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
-      is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-      is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-      is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-      is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Undoing");
+  EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-      is_element_visible(get_node(addon, "warning"), "Warning message should be hidden");
-      is(get_node(addon, "warning").textContent, "An important update is available for Test add-on 7.", "Warning message should be correct");
-      is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
-      is(get_node(addon, "warning-link").value, "Update Now", "Warning link text should be correct");
-      is(get_node(addon, "warning-link").href, gPluginURL, "Warning link should be correct");
-      is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
-      is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
-      is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
-
-      run_next_test();
-    });
-  });
+  is_element_visible(get_node(addon, "warning"), "Warning message should be hidden");
+  is(get_node(addon, "warning").textContent, "An important update is available for Test add-on 7.", "Warning message should be correct");
+  is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+  is(get_node(addon, "warning-link").value, "Update Now", "Warning link text should be correct");
+  is(get_node(addon, "warning-link").href, gPluginURL, "Warning link should be correct");
+  is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+  is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+  is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
 });
 
 // Check the add-ons are now in the right state
-add_test(function() {
-  AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
-                               "addon2@tests.mozilla.org",
-                               "addon4@tests.mozilla.org"],
-                               function([a1, a2, a4]) {
-    is(a1.pendingOperations, 0, "Add-on 1 should not have any pending operations");
-    is(a2.pendingOperations, 0, "Add-on 1 should not have any pending operations");
-    is(a4.pendingOperations, 0, "Add-on 1 should not have any pending operations");
+add_task(function*() {
+  let [a1, a2, a4] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                                               "addon2@tests.mozilla.org",
+                                               "addon4@tests.mozilla.org"]);
 
-    run_next_test();
-  });
+  is(a1.pendingOperations, 0, "Add-on 1 should not have any pending operations");
+  is(a2.pendingOperations, 0, "Add-on 1 should not have any pending operations");
+  is(a4.pendingOperations, 0, "Add-on 1 should not have any pending operations");
 });
 
 // Check that upgrades with onExternalInstall take effect immediately
-add_test(function() {
+add_task(function*() {
   gProvider.createAddons([{
     id: "addon1@tests.mozilla.org",
     name: "Test add-on replacement",
     version: "2.0",
     description: "A test add-on with a new description",
     updateDate: gDate,
     operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
   }]);
 
   let items = get_test_items();
   is(Object.keys(items).length, 11, "Should be the right number of add-ons installed");
 
   let addon = items["Test add-on replacement"];
   addon.parentNode.ensureElementIsVisible(addon);
+  let { name, version } = yield get_tooltip_info(addon);
   is(get_node(addon, "name").value, "Test add-on replacement", "Name should be correct");
-  is_element_visible(get_node(addon, "version"), "Version should be visible");
-  is(get_node(addon, "version").value, "2.0", "Version should be correct");
+  is(name, "Test add-on replacement", "Tooltip name should be correct");
+  is(version, "2.0", "Tooltip version should be correct");
   is_element_visible(get_node(addon, "description"), "Description should be visible");
   is(get_node(addon, "description").value, "A test add-on with a new description", "Description should be correct");
   is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
   is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
   is(get_node(addon, "date-updated").value, formatDate(gDate), "Update date should be correct");
 
   is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
   is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
   is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
   is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
   is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
   is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
   is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
   is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
   is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
-
-  run_next_test();
 });
 
 // Check that focus changes correctly move around the selected list item
-add_test(function() {
+add_task(function*() {
   function is_node_in_list(aNode) {
     var list = gManagerWindow.document.getElementById("addon-list");
 
     while (aNode && aNode != list)
       aNode = aNode.parentNode;
 
     if (aNode)
       return true;
@@ -769,69 +773,65 @@ add_test(function() {
 
   EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, gManagerWindow);
   ok(!is_node_in_list(fm.focusedElement), "Focus should be outside the list");
 
   try {
     Services.prefs.clearUserPref("accessibility.tabfocus_applies_to_xul");
   }
   catch (e) { }
-
-  run_next_test();
 });
 
 
-add_test(function() {
+add_task(function*() {
   info("Enabling lightweight theme");
   LightweightThemeManager.currentTheme = gLWTheme;
   
   gManagerWindow.loadView("addons://list/theme");
-  wait_for_view_load(gManagerWindow, function() {
-    var addon = get_addon_element(gManagerWindow, "4@personas.mozilla.org");
+  yield new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
+
+  var addon = get_addon_element(gManagerWindow, "4@personas.mozilla.org");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
-
-    info("Disabling lightweight theme");
-    LightweightThemeManager.currentTheme = null;
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
 
-    is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
-    is_element_visible(get_node(addon, "enable-btn"), "Enable button should be hidden");
-    is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be visible");
-    is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+  info("Disabling lightweight theme");
+  LightweightThemeManager.currentTheme = null;
 
-    AddonManager.getAddonByID("4@personas.mozilla.org", function(aAddon) {
-      aAddon.uninstall();
-      run_next_test();
-    });
-  });
+  is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+  is_element_visible(get_node(addon, "enable-btn"), "Enable button should be hidden");
+  is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be visible");
+  is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+  let [aAddon] = yield promiseAddonsByIDs(["4@personas.mozilla.org"]);
+  aAddon.uninstall();
 });
 
 // Check that onPropertyChanges for appDisabled updates the UI
-add_test(function() {
+add_task(function*() {
   info("Checking that onPropertyChanges for appDisabled updates the UI");
 
-  AddonManager.getAddonByID("addon2@tests.mozilla.org", function(aAddon) {
-    aAddon.userDisabled = true;
-    aAddon.isCompatible = true;
-    aAddon.appDisabled = false;
+  let [aAddon] = yield promiseAddonsByIDs(["addon2@tests.mozilla.org"]);
+  aAddon.userDisabled = true;
+  aAddon.isCompatible = true;
+  aAddon.appDisabled = false;
 
-    gManagerWindow.loadView("addons://list/extension");
-    wait_for_view_load(gManagerWindow, function() {
-      var el = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
+  gManagerWindow.loadView("addons://list/extension");
+  yield new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
+  var el = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
 
-      is(el.getAttribute("active"), "false", "Addon should not be marked as active");
-      is_element_hidden(get_node(el, "warning"), "Warning message should not be visible");
+  is(el.getAttribute("active"), "false", "Addon should not be marked as active");
+  is_element_hidden(get_node(el, "warning"), "Warning message should not be visible");
 
-      info("Making addon incompatible and appDisabled");
-      aAddon.isCompatible = false;
-      aAddon.appDisabled = true;
+  info("Making addon incompatible and appDisabled");
+  aAddon.isCompatible = false;
+  aAddon.appDisabled = true;
 
-      is(el.getAttribute("active"), "false", "Addon should not be marked as active");
-      is_element_visible(get_node(el, "warning"), "Warning message should be visible");
-      is(get_node(el, "warning").textContent, "Test add-on 2 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+  is(el.getAttribute("active"), "false", "Addon should not be marked as active");
+  is_element_visible(get_node(el, "warning"), "Warning message should be visible");
+  is(get_node(el, "warning").textContent, "Test add-on 2 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+});
 
-      run_next_test();
-    });
-  });
+add_task(function*() {
+  return close_manager(gManagerWindow);
 });
--- a/toolkit/mozapps/extensions/test/browser/browser_manualupdates.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_manualupdates.js
@@ -50,33 +50,42 @@ add_test(function() {
   
   is(gCategoryUtilities.isVisible(gAvailableCategory), false, "Available Updates category should still be hidden");
 
   run_next_test();
 });
 
 
 add_test(function() {
+  let finished = 0;
+  function maybeRunNext() {
+    if (++finished == 2)
+      run_next_test();
+  }
+
   gAvailableCategory.addEventListener("CategoryBadgeUpdated", function() {
     gAvailableCategory.removeEventListener("CategoryBadgeUpdated", arguments.callee, false);
     is(gCategoryUtilities.isVisible(gAvailableCategory), true, "Available Updates category should now be visible");
     is(gAvailableCategory.badgeCount, 1, "Badge for Available Updates should now be 1");
-    run_next_test();
+    maybeRunNext();
   }, false);
 
   gCategoryUtilities.openType("extension", function() {
     gProvider.createInstalls([{
       name: "manually updating addon (new and improved!)",
       existingAddon: gProvider.addons[1],
       version: "1.1",
       releaseNotesURI: Services.io.newURI(TESTROOT + "thereIsNoFileHere.xhtml", null, null)
     }]);
 
     var item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
-    is(item._version.value, "1.0", "Should still show the old version in the normal list");
+    get_tooltip_info(item).then(({ version }) => {
+      is(version, "1.0", "Should still show the old version in the tooltip");
+      maybeRunNext();
+    });
   });
 });
 
 
 add_test(function() {
   wait_for_view_load(gManagerWindow, function() {
     is(gManagerWindow.document.getElementById("categories").selectedItem.value, "addons://updates/available", "Available Updates category should now be selected");
     is(gManagerWindow.gViewController.currentViewId, "addons://updates/available", "Available Updates view should be the current view");
@@ -87,76 +96,71 @@ add_test(function() {
 
 
 add_test(function() {
   var list = gManagerWindow.document.getElementById("updates-list");
   is(list.itemCount, 1, "Should be 1 available update listed");
   var item = list.firstChild;
   is(item.mAddon.id, "addon2@tests.mozilla.org", "Update item should be for the manually updating addon");
   
-  // for manual update items, update-related properties are updated asynchronously,
-  // so we poll for one of the expected changes to know when its done
-  function waitForAsyncInit() {
-    if (item._version.value == "1.1") {
-      run_next_test();
-      return;
-    }
-    info("Update item not initialized yet, checking again in 100ms");
-    setTimeout(waitForAsyncInit, 100);
-  }
-  waitForAsyncInit();
+  // The item in the list will be checking for update information asynchronously
+  // so we have to wait for it to complete. Doing the same async request should
+  // make our callback be called later.
+  AddonManager.getAllInstalls(run_next_test);
 });
 
 add_test(function() {
   var list = gManagerWindow.document.getElementById("updates-list");
   var item = list.firstChild;
-  is(item._version.value, "1.1", "Update item should have version number of the update");
-  var postfix = gManagerWindow.document.getAnonymousElementByAttribute(item, "class", "update-postfix");
-  is_element_visible(postfix, "'Update' postfix should be visible");
-  is_element_visible(item._updateAvailable, "");
-  is_element_visible(item._relNotesToggle, "Release notes toggle should be visible");
-  is_element_hidden(item._warning, "Incompatible warning should be hidden");
-  is_element_hidden(item._error, "Blocklist error should be hidden");
+  get_tooltip_info(item).then(({ version }) => {
+    is(version, "1.1", "Update item should have version number of the update");
+    var postfix = gManagerWindow.document.getAnonymousElementByAttribute(item, "class", "update-postfix");
+    is_element_visible(postfix, "'Update' postfix should be visible");
+    is_element_visible(item._updateAvailable, "");
+    is_element_visible(item._relNotesToggle, "Release notes toggle should be visible");
+    is_element_hidden(item._warning, "Incompatible warning should be hidden");
+    is_element_hidden(item._error, "Blocklist error should be hidden");
 
-  info("Opening release notes");
-  item.addEventListener("RelNotesToggle", function() {
-    item.removeEventListener("RelNotesToggle", arguments.callee, false);
-    info("Release notes now open");
-
-    is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
-    is_element_visible(item._relNotesError, "Release notes error message should be visible");
-    is(item._relNotes.childElementCount, 0, "Release notes should be empty");
-
-    info("Closing release notes");
+    info("Opening release notes");
     item.addEventListener("RelNotesToggle", function() {
       item.removeEventListener("RelNotesToggle", arguments.callee, false);
-      info("Release notes now closed");
-      info("Setting Release notes URI to something that should load");
-      gProvider.installs[0].releaseNotesURI = Services.io.newURI(TESTROOT + "releaseNotes.xhtml", null, null)
+      info("Release notes now open");
 
-      info("Re-opening release notes");
+      is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
+      is_element_visible(item._relNotesError, "Release notes error message should be visible");
+      is(item._relNotes.childElementCount, 0, "Release notes should be empty");
+
+      info("Closing release notes");
       item.addEventListener("RelNotesToggle", function() {
         item.removeEventListener("RelNotesToggle", arguments.callee, false);
-        info("Release notes now open");
+        info("Release notes now closed");
+        info("Setting Release notes URI to something that should load");
+        gProvider.installs[0].releaseNotesURI = Services.io.newURI(TESTROOT + "releaseNotes.xhtml", null, null)
+
+        info("Re-opening release notes");
+        item.addEventListener("RelNotesToggle", function() {
+          item.removeEventListener("RelNotesToggle", arguments.callee, false);
+          info("Release notes now open");
 
-        is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
-        is_element_hidden(item._relNotesError, "Release notes error message should be hidden");
-        isnot(item._relNotes.childElementCount, 0, "Release notes should have been inserted into container");
-        run_next_test();
+          is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
+          is_element_hidden(item._relNotesError, "Release notes error message should be hidden");
+          isnot(item._relNotes.childElementCount, 0, "Release notes should have been inserted into container");
+          run_next_test();
+
+        }, false);
+        EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
+        is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
 
       }, false);
       EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
-      is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
 
     }, false);
     EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
-
-  }, false);
-  EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
-  is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
+    is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
+  });
 });
 
 
 add_test(function() {
   var badgeUpdated = false;
   var installCompleted = false;
 
   gAvailableCategory.addEventListener("CategoryBadgeUpdated", function() {
--- a/toolkit/mozapps/extensions/test/browser/browser_updateid.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_updateid.js
@@ -44,26 +44,30 @@ add_test(function() {
     }]);
     var newAddon = new MockAddon("addon2@tests.mozilla.org");
     newAddon.name = "updated add-on";
     newAddon.version = "2.0";
     newAddon.pendingOperations = AddonManager.PENDING_INSTALL;
     gProvider.installs[0]._addonToInstall = newAddon;
 
     var item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
-    is(item._version.value, "1.0", "Should still show the old version in the normal list");
     var name = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "name");
     is(name.value, "manually updating addon", "Should show the old name in the list");
-    var update = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "update-btn");
-    is_element_visible(update, "Update button should be visible");
+    get_tooltip_info(item).then(({ name, version }) => {
+      is(name, "manually updating addon", "Should show the old name in the tooltip");
+      is(version, "1.0", "Should still show the old version in the tooltip");
 
-    item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
-    is(item, null, "Should not show the new version in the list");
+      var update = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "update-btn");
+      is_element_visible(update, "Update button should be visible");
 
-    run_next_test();
+      item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
+      is(item, null, "Should not show the new version in the list");
+
+      run_next_test();
+    });
   });
 });
 
 add_test(function() {
   var item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
   var update = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "update-btn");
   EventUtils.synthesizeMouseAtCenter(update, { }, gManagerWindow);
 
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -222,16 +222,55 @@ function run_next_test() {
     info("Running test " + gTestsRun + " (" + test.name + ")");
   else
     info("Running test " + gTestsRun);
 
   gTestStart = Date.now();
   executeSoon(() => log_exceptions(test));
 }
 
+let get_tooltip_info = Task.async(function*(addon) {
+  let managerWindow = addon.ownerDocument.defaultView;
+
+  // The popup code uses a triggering event's target to set the
+  // document.tooltipNode property.
+  let nameNode = addon.ownerDocument.getAnonymousElementByAttribute(addon, "anonid", "name");
+  let event = new managerWindow.CustomEvent("TriggerEvent");
+  nameNode.dispatchEvent(event);
+
+  let tooltip = managerWindow.document.getElementById("addonitem-tooltip");
+
+  let promise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+  tooltip.openPopup(nameNode, "after_start", 0, 0, false, false, event);
+  yield promise;
+
+  let tiptext = tooltip.label;
+
+  promise = BrowserTestUtils.waitForEvent(tooltip, "popuphidden");
+  tooltip.hidePopup();
+  yield promise;
+
+  let expectedName = addon.getAttribute("name");
+  ok(tiptext.substring(0, expectedName.length), expectedName,
+     "Tooltip should always start with the expected name");
+
+  if (expectedName.length == tiptext.length) {
+    return {
+      name: tiptext,
+      version: undefined
+    };
+  }
+  else {
+    return {
+      name: tiptext.substring(0, expectedName.length),
+      version: tiptext.substring(expectedName.length + 1)
+    };
+  }
+});
+
 function get_addon_file_url(aFilename) {
   try {
     var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].
              getService(Ci.nsIChromeRegistry);
     var fileurl = cr.convertChromeURL(makeURI(CHROMEROOT + "addons/" + aFilename));
     return fileurl.QueryInterface(Ci.nsIFileURL);
   } catch(ex) {
     var jar = getJar(CHROMEROOT + "addons/" + aFilename);
@@ -484,16 +523,21 @@ function is_element_visible(aElement, aM
   ok(!is_hidden(aElement), aMsg || (aElement + " should be visible"));
 }
 
 function is_element_hidden(aElement, aMsg) {
   isnot(aElement, null, "Element should not be null, when checking visibility");
   ok(is_hidden(aElement), aMsg || (aElement + " should be hidden"));
 }
 
+function promiseAddonsByIDs(aIDs) {
+  return new Promise(resolve => {
+    AddonManager.getAddonsByIDs(aIDs, resolve);
+  });
+}
 /**
  * Install an add-on and call a callback when complete.
  *
  * The callback will receive the Addon for the installed add-on.
  */
 function install_addon(path, cb, pathPrefix=TESTROOT) {
   let p = new Promise((resolve, reject) => {
     AddonManager.getInstallForURL(pathPrefix + path, (install) => {