Backed out changeset e9e43e8256e1 (bug 1287827) for breaking tests
authorMark Banner <standard8@mozilla.com>
Fri, 29 Jul 2016 18:14:08 +0100
changeset 349336 5d0e37d0ee2dcee5eb41c16ef682164cffd47d33
parent 349335 da25267afa2979db4ef3466d4a9ac485b4359ce4
child 349337 9346c1a86c6c5f09817f60eb600543bcac65ce94
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1287827
milestone50.0a1
backs oute9e43e8256e1c141d4c395067f35bfce1757b293
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
Backed out changeset e9e43e8256e1 (bug 1287827) for breaking tests
browser/app/profile/firefox.js
browser/base/content/browser.xul
browser/base/content/test/general/browser_parsable_css.js
browser/base/content/test/webrtc/browser.ini
browser/base/content/test/webrtc/browser_devices_get_user_media_about_urls.js
browser/components/about/AboutRedirector.cpp
browser/components/build/nsModule.cpp
browser/components/customizableui/CustomizableUI.jsm
browser/components/customizableui/content/panelUI.inc.xul
browser/components/uitour/UITour.jsm
browser/components/uitour/test/browser.ini
browser/components/uitour/test/browser_UITour_availableTargets.js
browser/components/uitour/test/browser_UITour_loop.js
browser/components/uitour/test/browser_UITour_loop_panel.js
browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
browser/locales/en-US/chrome/browser/loop/loop.properties
browser/locales/jar.mn
browser/modules/PanelFrame.jsm
browser/modules/webrtcUI.jsm
mobile/android/app/mobile.js
testing/profiles/prefs_general.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1472,9 +1472,9 @@ pref("extensions.pocket.enabled", true);
 
 pref("signon.schemeUpgrades", true);
 
 // Enable the "Simplify Page" feature in Print Preview
 pref("print.use_simplify_page", true);
 
 // Space separated list of URLS that are allowed to send objects (instead of
 // only strings) through webchannels. This list is duplicated in mobile/android/app/mobile.js
-pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
+pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://hello.firefox.com https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -290,16 +290,28 @@
            side="right"
            type="arrow"
            hidden="true"
            flip="slide"
            rolluponmousewheel="true"
            noautofocus="true"
            position="topcenter topright"/>
 
+    <panel id="loop-notification-panel"
+           class="loop-panel social-panel"
+           type="arrow"
+           hidden="true"
+           noautofocus="true"/>
+
+    <panel id="loop-panel"
+           class="loop-panel social-panel"
+           type="arrow"
+           orient="horizontal"
+           hidden="true"/>
+
     <menupopup id="toolbar-context-menu"
                onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator'));">
       <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
                 accesskey="&customizeMenu.moveToPanel.accesskey;"
                 label="&customizeMenu.moveToPanel.label;"
                 contexttype="toolbaritem"
                 class="customize-context-moveToPanel"/>
       <menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode)"
--- a/browser/base/content/test/general/browser_parsable_css.js
+++ b/browser/base/content/test/general/browser_parsable_css.js
@@ -15,16 +15,26 @@ let whitelist = [
    isFromDevTools: true},
   // PDFjs is futureproofing its pseudoselectors, and those rules are dropped.
   {sourceName: /web\/viewer\.css$/i,
    errorMessage: /Unknown pseudo-class.*(fullscreen|selection)/i,
    isFromDevTools: false},
   // Tracked in bug 1004428.
   {sourceName: /aboutaccounts\/(main|normalize)\.css$/i,
     isFromDevTools: false},
+  // TokBox SDK assets, see bug 1032469.
+  {sourceName: /loop\/.*sdk-content\/.*\.css$/i,
+    isFromDevTools: false},
+  // Loop standalone client CSS uses placeholder cross browser pseudo-element
+  {sourceName: /loop\/.*\.css$/i,
+   errorMessage: /Unknown pseudo-class.*placeholder/i,
+   isFromDevTools: false},
+  {sourceName: /loop\/.*shared\/css\/common.css$/i,
+   errorMessage: /Unknown property .user-select./i,
+   isFromDevTools: false},
   // Highlighter CSS uses a UA-only pseudo-class, see bug 985597.
   {sourceName: /highlighters\.css$/i,
    errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
    isFromDevTools: true},
   // Responsive Design Mode CSS uses a UA-only pseudo-class, see Bug 1241714.
   {sourceName: /responsive-ua\.css$/i,
    errorMessage: /Unknown pseudo-class.*moz-dropdown-list/i,
    isFromDevTools: true},
--- a/browser/base/content/test/webrtc/browser.ini
+++ b/browser/base/content/test/webrtc/browser.ini
@@ -1,10 +1,12 @@
 [DEFAULT]
 support-files =
   get_user_media.html
   get_user_media_content_script.js
   head.js
 
 [browser_devices_get_user_media.js]
 skip-if = buildapp == 'mulet' || (os == "linux" && debug) # linux: bug 976544
+[browser_devices_get_user_media_about_urls.js]
+skip-if = e10s && debug
 [browser_devices_get_user_media_anim.js]
 [browser_devices_get_user_media_in_frame.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_about_urls.js
@@ -0,0 +1,219 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_LOOP_CSP = "loop.CSP";
+
+var gTab;
+
+// Taken from dom/media/tests/mochitest/head.js
+function isMacOSX10_6orOlder() {
+  var is106orOlder = false;
+
+  if (navigator.platform.indexOf("Mac") == 0) {
+    var version = Cc["@mozilla.org/system-info;1"]
+                    .getService(Ci.nsIPropertyBag2)
+                    .getProperty("version");
+    // the next line is correct: Mac OS 10.6 corresponds to Darwin version 10.x !
+    // Mac OS 10.7 is Darwin version 11.x. the |version| string we've got here
+    // is the Darwin version.
+    is106orOlder = (parseFloat(version) < 11.0);
+  }
+  return is106orOlder;
+}
+
+// Screensharing is disabled on older platforms (WinXP and Mac 10.6).
+function isOldPlatform() {
+  const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+  if (isMacOSX10_6orOlder() || isWinXP) {
+    info(true, "Screensharing disabled for OSX10.6 and WinXP");
+    return true;
+  }
+  return false;
+}
+
+// Linux prompts aren't working for screensharing.
+function isLinux() {
+  return navigator.platform.indexOf("Linux") != -1;
+}
+
+function loadPage(aUrl) {
+  let deferred = Promise.defer();
+
+  gTab.linkedBrowser.addEventListener("load", function onload() {
+    gTab.linkedBrowser.removeEventListener("load", onload, true);
+
+    is(PopupNotifications._currentNotifications.length, 0,
+       "should start the test without any prior popup notification");
+
+    deferred.resolve();
+  }, true);
+  content.location = aUrl;
+  return deferred.promise;
+}
+
+registerCleanupFunction(function() {
+  gBrowser.removeCurrentTab();
+});
+
+const permissionError = "error: NotAllowedError: The request is not allowed " +
+    "by the user agent or the platform in the current context.";
+
+var gTests = [
+
+{
+  desc: "getUserMedia about:loopconversation shouldn't prompt",
+  run: function checkAudioVideoLoop() {
+    yield SpecialPowers.pushPrefEnv({
+      "set": [[PREF_LOOP_CSP, "default-src 'unsafe-inline'"]],
+    });
+
+    yield loadPage("about:loopconversation");
+
+    info("requesting devices");
+    let promise = promiseObserverCalled("recording-device-events");
+    yield promiseRequestDevice(true, true);
+    yield promise;
+
+    // Wait for the devices to actually be captured and running before
+    // proceeding.
+    yield promisePopupNotification("webRTC-sharingDevices");
+
+    is((yield getMediaCaptureState()), "CameraAndMicrophone",
+       "expected camera and microphone to be shared");
+
+    yield closeStream();
+
+    yield SpecialPowers.popPrefEnv();
+  }
+},
+
+{
+  desc: "getUserMedia about:loopconversation should prompt for window sharing",
+  run: function checkShareScreenLoop() {
+    if (isOldPlatform() || isLinux()) {
+      return;
+    }
+
+    yield SpecialPowers.pushPrefEnv({
+      "set": [[PREF_LOOP_CSP, "default-src 'unsafe-inline'"]],
+    });
+
+    yield loadPage("about:loopconversation");
+
+    info("requesting screen");
+    let promise = promiseObserverCalled("getUserMedia:request");
+    yield promiseRequestDevice(false, true, null, "window");
+
+    // Wait for the devices to actually be captured and running before
+    // proceeding.
+    yield promisePopupNotification("webRTC-shareDevices");
+
+    is((yield getMediaCaptureState()), "none",
+       "expected camera and microphone not to be shared");
+
+    yield promiseMessage(permissionError, () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+
+    yield expectObserverCalled("getUserMedia:response:deny");
+    yield expectObserverCalled("recording-window-ended");
+
+    yield SpecialPowers.popPrefEnv();
+  }
+},
+
+{
+  desc: "getUserMedia about:evil should prompt",
+  run: function checkAudioVideoNonLoop() {
+    yield loadPage("about:evil");
+
+    let promise = promiseObserverCalled("getUserMedia:request");
+    yield promiseRequestDevice(true, true);
+    yield promise;
+
+    is((yield getMediaCaptureState()), "none",
+       "expected camera and microphone not to be shared");
+  }
+},
+
+];
+
+function test() {
+  waitForExplicitFinish();
+
+  gTab = gBrowser.addTab();
+  gBrowser.selectedTab = gTab;
+
+  gTab.linkedBrowser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true);
+
+  Task.spawn(function () {
+    yield ContentTask.spawn(gBrowser.selectedBrowser,
+                            getRootDirectory(gTestPath) + "get_user_media.html",
+                            function* (url) {
+      const Ci = Components.interfaces;
+      Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+      Components.utils.import("resource://gre/modules/Services.jsm");
+
+      /* A fake about module to map get_user_media.html to different about urls. */
+      function fakeLoopAboutModule() {
+      }
+
+      fakeLoopAboutModule.prototype = {
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+        newChannel: function (aURI, aLoadInfo) {
+          let uri = Services.io.newURI(url, null, null);
+          let chan = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+          chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
+          return chan;
+        },
+        getURIFlags: function (aURI) {
+          return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+                 Ci.nsIAboutModule.ALLOW_SCRIPT |
+                 Ci.nsIAboutModule.URI_CAN_LOAD_IN_CHILD |
+                 Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
+        }
+      };
+
+      var factory = XPCOMUtils._getFactory(fakeLoopAboutModule);
+      var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+      let UUIDGenerator = Components.classes["@mozilla.org/uuid-generator;1"]
+                                    .getService(Ci.nsIUUIDGenerator);
+      registrar.registerFactory(UUIDGenerator.generateUUID(), "",
+                                "@mozilla.org/network/protocol/about;1?what=loopconversation",
+                                factory);
+      registrar.registerFactory(UUIDGenerator.generateUUID(), "",
+                                "@mozilla.org/network/protocol/about;1?what=evil",
+                                factory);
+    });
+
+    yield SpecialPowers.pushPrefEnv({
+      "set": [[PREF_PERMISSION_FAKE, true],
+              ["media.getusermedia.screensharing.enabled", true]],
+    });
+
+    for (let test of gTests) {
+      info(test.desc);
+      yield test.run();
+
+      // Cleanup before the next test
+      expectNoObserverCalled();
+    }
+
+    yield ContentTask.spawn(gBrowser.selectedBrowser, null,
+                            function* () {
+      const Ci = Components.interfaces;
+      const Cc = Components.classes;
+      var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+      let cid = Cc["@mozilla.org/network/protocol/about;1?what=loopconversation"];
+      registrar.unregisterFactory(cid,
+                                  registrar.getClassObject(cid, Ci.nsIFactory));
+      cid = Cc["@mozilla.org/network/protocol/about;1?what=evil"];
+      registrar.unregisterFactory(cid,
+                                  registrar.getClassObject(cid, Ci.nsIFactory));
+    });
+  }).then(finish, ex => {
+    ok(false, "Unexpected Exception: " + ex);
+    finish();
+  });
+}
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -97,16 +97,32 @@ static RedirEntry kRedirMap[] = {
   { "downloads", "chrome://browser/content/downloads/contentAreaDownloadsView.xul",
     nsIAboutModule::ALLOW_SCRIPT },
 #ifdef MOZ_SERVICES_HEALTHREPORT
   { "healthreport", "chrome://browser/content/abouthealthreport/abouthealth.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
 #endif
   { "accounts", "chrome://browser/content/aboutaccounts/aboutaccounts.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
+  // Linkable because of indexeddb use (bug 1228118)
+  { "loopconversation", "chrome://loop/content/panels/conversation.html",
+    nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+    nsIAboutModule::ALLOW_SCRIPT |
+    nsIAboutModule::HIDE_FROM_ABOUTABOUT |
+    nsIAboutModule::MAKE_LINKABLE |
+    nsIAboutModule::ENABLE_INDEXED_DB },
+  // Linkable because of indexeddb use (bug 1228118)
+  { "looppanel", "chrome://loop/content/panels/panel.html",
+    nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
+    nsIAboutModule::ALLOW_SCRIPT |
+    nsIAboutModule::HIDE_FROM_ABOUTABOUT |
+    nsIAboutModule::MAKE_LINKABLE |
+    nsIAboutModule::ENABLE_INDEXED_DB,
+    // Shares an IndexedDB origin with about:loopconversation.
+    "loopconversation" },
   { "reader", "chrome://global/content/reader/aboutReader.html",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
 };
 static const int kRedirTotal = ArrayLength(kRedirMap);
 
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -103,16 +103,18 @@ static const mozilla::Module::ContractID
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "home", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "newtab", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "preferences", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "downloads", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "accounts", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #ifdef MOZ_SERVICES_HEALTHREPORT
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "healthreport", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #endif
+    { NS_ABOUT_MODULE_CONTRACTID_PREFIX "looppanel", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
+    { NS_ABOUT_MODULE_CONTRACTID_PREFIX "loopconversation", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "reader", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #if defined(XP_WIN)
     { NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
 #elif defined(XP_MACOSX)
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
 #endif
     { nullptr }
 };
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -59,16 +59,17 @@ const kSubviewEvents = [
 var kVersion = 6;
 
 /**
  * Buttons removed from built-ins by version they were removed. kVersion must be
  * bumped any time a new id is added to this. Use the button id as key, and
  * version the button is removed in as the value.  e.g. "pocket-button": 5
  */
 var ObsoleteBuiltinButtons = {
+  "loop-button": 5,
   "pocket-button": 6
 };
 
 /**
  * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
  * on their IDs.
  */
 var gPalette = new Map();
@@ -232,16 +233,17 @@ var CustomizableUIInternal = {
     PanelWideWidgetTracker.init();
 
     let navbarPlacements = [
       "urlbar-container",
       "search-container",
       "bookmarks-menu-button",
       "downloads-button",
       "home-button",
+      "loop-button",
     ];
 
     if (AppConstants.MOZ_DEV_EDITION) {
       navbarPlacements.splice(2, 0, "developer-button");
     }
 
     if (Services.prefs.getBoolPref(kPrefWebIDEInNavbar)) {
       navbarPlacements.push("webide-button");
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -237,16 +237,18 @@
                      label="&showAllBookmarks2.label;"
                      class="subviewbutton panel-subview-footer"
                      command="Browser:ShowAllBookmarks"
                      onclick="PanelUI.hide();"/>
     </panelview>
 
     <panelview id="PanelUI-socialapi" flex="1"/>
 
+    <panelview id="PanelUI-loopapi" flex="1"/>
+
     <panelview id="PanelUI-feeds" flex="1" oncommand="FeedHandler.subscribeToFeed(null, event);">
       <label value="&feedsMenu2.label;" class="panel-subview-header"/>
     </panelview>
 
     <panelview id="PanelUI-containers" flex="1">
       <label value="&containersMenu.label;" class="panel-subview-header"/>
       <vbox id="PanelUI-containersItems"/>
     </panelview>
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -148,16 +148,64 @@ this.UITour = {
     ["devtools",    {query: "#developer-button"}],
     ["help",        {query: "#PanelUI-help"}],
     ["home",        {query: "#home-button"}],
     ["forget", {
       allowAdd: true,
       query: "#panic-button",
       widgetName: "panic-button",
     }],
+    ["loop",        {
+      allowAdd: true,
+      query: "#loop-button",
+      widgetName: "loop-button",
+    }],
+    ["loop-newRoom", {
+      infoPanelPosition: "leftcenter topright",
+      query: (aDocument) => {
+        let loopUI = aDocument.defaultView.LoopUI;
+        // Use the parentElement full-width container of the button so our arrow
+        // doesn't overlap the panel contents much.
+        return loopUI.browser.contentDocument.querySelector(".new-room-button").parentElement;
+      },
+    }],
+    ["loop-roomList", {
+      infoPanelPosition: "leftcenter topright",
+      query: (aDocument) => {
+        let loopUI = aDocument.defaultView.LoopUI;
+        return loopUI.browser.contentDocument.querySelector(".room-list");
+      },
+    }],
+    ["loop-selectedRoomButtons", {
+      infoPanelOffsetY: -20,
+      infoPanelPosition: "start_after",
+      query: (aDocument) => {
+        let chatbox = aDocument.querySelector("chatbox[src^='about\:loopconversation'][selected]");
+
+        // Check that the real target actually exists
+        if (!chatbox || !chatbox.contentDocument ||
+            !chatbox.contentDocument.querySelector(".call-action-group")) {
+          return null;
+        }
+
+        // But anchor on the <browser> in the chatbox so the panel doesn't jump to undefined
+        // positions when the copy/email buttons disappear e.g. when the feedback form opens or
+        // somebody else joins the room.
+        return chatbox.content;
+      },
+    }],
+    ["loop-signInUpLink", {
+      query: (aDocument) => {
+        let loopBrowser = aDocument.defaultView.LoopUI.browser;
+        if (!loopBrowser) {
+          return null;
+        }
+        return loopBrowser.contentDocument.querySelector(".signin-link");
+      },
+    }],
     ["pocket", {
       allowAdd: true,
       query: "#pocket-button",
       widgetName: "pocket-button",
     }],
     ["privateWindow",  {query: "#privatebrowsing-button"}],
     ["quit",        {query: "#PanelUI-quit"}],
     ["readerMode-urlBar", {query: "#reader-mode-button"}],
@@ -842,22 +890,26 @@ this.UITour = {
     if (aTourPageClosing && openTourBrowsers) {
       openTourBrowsers.delete(aBrowser);
     }
 
     this.hideHighlight(aWindow);
     this.hideInfo(aWindow);
     // Ensure the menu panel is hidden before calling recreatePopup so popup events occur.
     this.hideMenu(aWindow, "appMenu");
+    this.hideMenu(aWindow, "loop");
     this.hideMenu(aWindow, "controlCenter");
 
     // Clean up panel listeners after calling hideMenu above.
     aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hideAppMenuAnnotations);
     aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hideAppMenuAnnotations);
     aWindow.PanelUI.panel.removeEventListener("popuphidden", this.onPanelHidden);
+    let loopPanel = aWindow.document.getElementById("loop-notification-panel");
+    loopPanel.removeEventListener("popuphidden", this.onPanelHidden);
+    loopPanel.removeEventListener("popuphiding", this.hideLoopPanelAnnotations);
     let controlCenterPanel = aWindow.gIdentityHandler._identityPopup;
     controlCenterPanel.removeEventListener("popuphidden", this.onPanelHidden);
     controlCenterPanel.removeEventListener("popuphiding", this.hideControlCenterAnnotations);
 
     this.resetTheme();
 
     // If there are no more tour tabs left in the window, teardown the tour for the whole window.
     if (!openTourBrowsers || openTourBrowsers.size == 0) {
@@ -1678,16 +1730,41 @@ this.UITour = {
 
       this.recreatePopup(popup);
 
       // Open the control center
       if (aOpenCallback) {
         popup.addEventListener("popupshown", onPopupShown);
       }
       aWindow.document.getElementById("identity-box").click();
+    } else if (aMenuName == "loop") {
+      let toolbarButton = aWindow.LoopUI.toolbarButton;
+      // It's possible to have a node that isn't placed anywhere
+      if (!toolbarButton || !toolbarButton.node ||
+          !CustomizableUI.getPlacementOfWidget(toolbarButton.node.id)) {
+        log.debug("Can't show the Loop menu since the toolbarButton isn't placed");
+        return;
+      }
+
+      let panel = aWindow.document.getElementById("loop-notification-panel");
+      panel.setAttribute("noautohide", true);
+      if (panel.state != "open") {
+        this.recreatePopup(panel);
+        this.clearAvailableTargetsCache();
+      }
+
+      // An event object is expected but we don't want to toggle the panel with a click if the panel
+      // is already open.
+      aWindow.LoopUI.openPanel({ target: toolbarButton.node, }, "rooms").then(() => {
+        if (aOpenCallback) {
+          aOpenCallback();
+        }
+      });
+      panel.addEventListener("popuphidden", this.onPanelHidden);
+      panel.addEventListener("popuphiding", this.hideLoopPanelAnnotations);
     } else if (aMenuName == "pocket") {
       this.getTarget(aWindow, "pocket").then(Task.async(function* onPocketTarget(target) {
         let widgetGroupWrapper = CustomizableUI.getWidget(target.widgetName);
         if (widgetGroupWrapper.type != "view" || !widgetGroupWrapper.viewId) {
           log.error("Can't open the pocket menu without a view");
           return;
         }
         let placement = CustomizableUI.getPlacementOfWidget(target.widgetName);
@@ -1736,16 +1813,19 @@ this.UITour = {
     if (aMenuName == "appMenu") {
       aWindow.PanelUI.hide();
     } else if (aMenuName == "bookmarks") {
       let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
       closeMenuButton(menuBtn);
     } else if (aMenuName == "controlCenter") {
       let panel = aWindow.gIdentityHandler._identityPopup;
       panel.hidePopup();
+    } else if (aMenuName == "loop") {
+      let panel = aWindow.document.getElementById("loop-notification-panel");
+      panel.hidePopup();
     }
   },
 
   hideAnnotationsForPanel: function(aEvent, aTargetPositionCallback) {
     let win = aEvent.target.ownerGlobal;
     let annotationElements = new Map([
       // [annotationElement (panel), method to hide the annotation]
       [win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)],
@@ -1768,16 +1848,22 @@ this.UITour = {
     });
     UITour.appMenuOpenForAnnotation.clear();
   },
 
   hideAppMenuAnnotations: function(aEvent) {
     UITour.hideAnnotationsForPanel(aEvent, UITour.targetIsInAppMenu);
   },
 
+  hideLoopPanelAnnotations: function(aEvent) {
+    UITour.hideAnnotationsForPanel(aEvent, (aTarget) => {
+      return aTarget.targetName.startsWith("loop-") && aTarget.targetName != "loop-selectedRoomButtons";
+    });
+  },
+
   hideControlCenterAnnotations(aEvent) {
     UITour.hideAnnotationsForPanel(aEvent, (aTarget) => {
       return aTarget.targetName.startsWith("controlCenter-");
     });
   },
 
   onPanelHidden: function(aEvent) {
     aEvent.target.removeAttribute("noautohide");
@@ -1840,16 +1926,22 @@ this.UITour = {
         appinfo["canSetDefaultBrowserInBackground"] =
           canSetDefaultBrowserInBackground;
 
         this.sendPageCallback(aMessageManager, aCallbackID, appinfo);
         break;
       case "availableTargets":
         this.getAvailableTargets(aMessageManager, aWindow, aCallbackID);
         break;
+      case "loop":
+        const FTU_VERSION = 1;
+        this.sendPageCallback(aMessageManager, aCallbackID, {
+          gettingStartedSeen: (Services.prefs.getIntPref("loop.gettingStarted.latestFTUVersion") >= FTU_VERSION),
+        });
+        break;
       case "search":
       case "selectedSearchEngine":
         Services.search.init(rv => {
           let data;
           if (Components.isSuccessCode(rv)) {
             let engines = Services.search.getVisibleEngines();
             data = {
               searchEngineIdentifier: Services.search.defaultEngine.identifier,
@@ -1883,16 +1975,20 @@ this.UITour = {
         // be set, not unset.
         try {
           let shell = aWindow.getShellService();
           if (shell) {
             shell.setDefaultBrowser(true, false);
           }
         } catch (e) {}
         break;
+      case "Loop:ResumeTourOnFirstJoin":
+        // Ignore aValue in this case to avoid accidentally setting it to false.
+        Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
+        break;
       default:
         log.error("setConfiguration: Unknown configuration requested: " + aConfiguration);
         break;
     }
   },
 
   getAvailableTargets: function(aMessageManager, aChromeWindow, aCallbackID) {
     Task.spawn(function*() {
--- a/browser/components/uitour/test/browser.ini
+++ b/browser/components/uitour/test/browser.ini
@@ -26,16 +26,19 @@ skip-if = os == "linux" # Intermittent f
 [browser_UITour3.js]
 skip-if = os == "linux" # Linux: Bug 986760, Bug 989101.
 [browser_UITour_availableTargets.js]
 [browser_UITour_annotation_size_attributes.js]
 [browser_UITour_defaultBrowser.js]
 [browser_UITour_detach_tab.js]
 [browser_UITour_forceReaderMode.js]
 [browser_UITour_heartbeat.js]
+[browser_UITour_loop.js]
+skip-if = true # Bug 1225832 - New Loop architecture is not compatible with test.
+[browser_UITour_loop_panel.js]
 [browser_UITour_modalDialog.js]
 skip-if = os != "mac" # modal dialog disabling only working on OS X.
 [browser_UITour_observe.js]
 [browser_UITour_panel_close_annotation.js]
 skip-if = true # Disabled due to frequent failures, bugs 1026310 and 1032137
 [browser_UITour_pocket.js]
 skip-if = true # Disabled pending removal of pocket UI Tour
 [browser_UITour_registerPageID.js]
--- a/browser/components/uitour/test/browser_UITour_availableTargets.js
+++ b/browser/components/uitour/test/browser_UITour_availableTargets.js
@@ -16,16 +16,17 @@ add_UITour_task(function* test_available
     "accountStatus",
     "addons",
     "appMenu",
     "backForward",
     "bookmarks",
     "customize",
     "help",
     "home",
+    "loop",
     "devtools",
       ...(hasPocket ? ["pocket"] : []),
     "privateWindow",
     "quit",
     "readerMode-urlBar",
     "search",
     "searchIcon",
     "trackingProtection",
@@ -44,16 +45,17 @@ add_UITour_task(function* test_available
   let data = yield getConfigurationPromise("availableTargets");
   ok_targets(data, [
     "accountStatus",
     "addons",
     "appMenu",
     "backForward",
     "customize",
     "help",
+    "loop",
     "devtools",
     "home",
       ...(hasPocket ? ["pocket"] : []),
     "privateWindow",
     "quit",
     "readerMode-urlBar",
     "search",
     "searchIcon",
@@ -79,16 +81,17 @@ add_UITour_task(function* test_available
     "accountStatus",
     "addons",
     "appMenu",
     "backForward",
     "bookmarks",
     "customize",
     "help",
     "home",
+    "loop",
     "devtools",
       ...(hasPocket ? ["pocket"] : []),
     "privateWindow",
     "quit",
     "readerMode-urlBar",
     "trackingProtection",
     "urlbar",
       ...(hasWebIDE ? ["webide"] : [])
new file mode 100644
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_loop.js
@@ -0,0 +1,414 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+var gContentWindow;
+var gMessageHandlers;
+var loopButton;
+var fakeRoom;
+var loopPanel = document.getElementById("loop-notification-panel");
+
+const { LoopAPI } = Cu.import("chrome://loop/content/modules/MozLoopAPI.jsm", {});
+const { LoopRooms } = Cu.import("chrome://loop/content/modules/LoopRooms.jsm", {});
+const { MozLoopServiceInternal } = Cu.import("chrome://loop/content/modules/MozLoopService.jsm", {});
+
+const FTU_VERSION = 1;
+
+function test() {
+  UITourTest();
+}
+
+function runOffline(fun) {
+  return (done) => {
+    Services.io.offline = true;
+    fun(function onComplete() {
+      Services.io.offline = false;
+      done();
+    });
+  }
+}
+
+var tests = [
+  taskify(function* test_gettingStartedClicked_linkOpenedWithExpectedParams() {
+    // Set latestFTUVersion to lower number to show FTU panel.
+    Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 0);
+    Services.prefs.setCharPref("loop.gettingStarted.url", "http://example.com");
+    is(loopButton.open, false, "Menu should initially be closed");
+    loopButton.click();
+
+    yield waitForConditionPromise(() => {
+      return loopButton.open;
+    }, "Menu should be visible after showMenu()");
+
+    gContentAPI.registerPageID("hello-tour_OpenPanel_testPage");
+    yield new Promise(resolve => {
+      gContentAPI.ping(() => resolve());
+    });
+
+    let loopDoc = document.getElementById("loop-notification-panel").children[0].contentDocument;
+    yield waitForConditionPromise(() => {
+      return loopDoc.readyState == 'complete';
+    }, "Loop notification panel document should be fully loaded.", 50);
+    let gettingStartedButton = loopDoc.getElementById("fte-button");
+    ok(gettingStartedButton, "Getting Started button should be found");
+
+    let newTabPromise = waitForConditionPromise(() => {
+      return gBrowser.currentURI.path.includes("utm_source=firefox-browser");
+    }, "New tab with utm_content=testPageNewID should have opened");
+
+    gettingStartedButton.click();
+    yield newTabPromise;
+    ok(gBrowser.currentURI.path.includes("utm_content=hello-tour_OpenPanel_testPage"),
+        "Expected URL opened (" + gBrowser.currentURI.path + ")");
+    yield gBrowser.removeCurrentTab();
+
+    checkLoopPanelIsHidden();
+  }),
+  taskify(function* test_gettingStartedClicked_linkOpenedWithExpectedParams2() {
+    // Set latestFTUVersion to lower number to show FTU panel.
+    Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 0);
+    // Force a refresh of the loop panel since going from seen -> unseen doesn't trigger
+    // automatic re-rendering.
+    let loopWin = document.getElementById("loop-notification-panel").children[0].contentWindow;
+    var event = new loopWin.CustomEvent("GettingStartedSeen", { detail: false });
+    loopWin.dispatchEvent(event);
+
+    UITour.pageIDsForSession.clear();
+    Services.prefs.setCharPref("loop.gettingStarted.url", "http://example.com");
+    is(loopButton.open, false, "Menu should initially be closed");
+    loopButton.click();
+
+    yield waitForConditionPromise(() => {
+      return loopButton.open;
+    }, "Menu should be visible after showMenu()");
+
+
+    gContentAPI.registerPageID("hello-tour_OpenPanel_testPageOldId");
+    yield new Promise(resolve => {
+      gContentAPI.ping(() => resolve());
+    });
+    // Set the time of the page ID to 10 hours earlier, so that it is considered "expired".
+    UITour.pageIDsForSession.set("hello-tour_OpenPanel_testPageOldId",
+                                   {lastSeen: Date.now() - (10 * 60 * 60 * 1000)});
+
+    let loopDoc = loopWin.document;
+    let gettingStartedButton = loopDoc.getElementById("fte-button");
+    ok(gettingStartedButton, "Getting Started button should be found");
+
+    let newTabPromise = waitForConditionPromise(() => {
+      Services.console.logStringMessage(gBrowser.currentURI.path);
+      return gBrowser.currentURI.path.includes("utm_source=firefox-browser");
+    }, "New tab with utm_content=testPageNewID should have opened");
+
+    gettingStartedButton.click();
+    yield newTabPromise;
+    ok(!gBrowser.currentURI.path.includes("utm_content=hello-tour_OpenPanel_testPageOldId"),
+       "Expected URL opened without the utm_content parameter (" +
+        gBrowser.currentURI.path + ")");
+    yield gBrowser.removeCurrentTab();
+
+    checkLoopPanelIsHidden();
+  }),
+  // Test the menu was cleaned up in teardown.
+  taskify(function* setup_menu_cleanup() {
+    gContentAPI.showMenu("loop");
+
+    yield waitForConditionPromise(() => {
+      return loopButton.open;
+    }, "Menu should be visible after showMenu()");
+
+    // Leave it open so it gets torn down and we can test below that teardown was succesful.
+  }),
+  taskify(function* test_menu_cleanup() {
+    // Test that the open menu from above was torn down fully.
+    checkLoopPanelIsHidden();
+  }),
+  function test_availableTargets(done) {
+    gContentAPI.showMenu("loop");
+    gContentAPI.getConfiguration("availableTargets", (data) => {
+      for (let targetName of ["loop-newRoom", "loop-roomList", "loop-signInUpLink"]) {
+        isnot(data.targets.indexOf(targetName), -1, targetName + " should exist");
+      }
+      done();
+    });
+  },
+  function test_getConfigurationLoop(done) {
+    let gettingStartedSeen = Services.prefs.getIntPref("loop.gettingStarted.latestFTUVersion") >= FTU_VERSION;
+    gContentAPI.getConfiguration("loop", (data) => {
+      is(data.gettingStartedSeen, gettingStartedSeen,
+         "The configuration property should equal that of the pref");
+      done();
+    });
+  },
+  function test_hideMenuHidesAnnotations(done) {
+    let infoPanel = document.getElementById("UITourTooltip");
+    let highlightPanel = document.getElementById("UITourHighlightContainer");
+
+    gContentAPI.showMenu("loop", function menuCallback() {
+      gContentAPI.showHighlight("loop-roomList");
+      gContentAPI.showInfo("loop-newRoom", "Make a new room", "AKA. conversation");
+      UITour.getTarget(window, "loop-newRoom").then((target) => {
+        waitForPopupAtAnchor(infoPanel, target.node, Task.async(function* checkPanelIsOpen() {
+          isnot(loopPanel.state, "closed", "Loop panel should still be open");
+          ok(loopPanel.hasAttribute("noautohide"), "@noautohide should still be on the loop panel");
+          is(highlightPanel.getAttribute("targetName"), "loop-roomList", "Check highlight @targetname");
+          is(infoPanel.getAttribute("targetName"), "loop-newRoom", "Check info panel @targetname");
+
+          info("Close the loop menu and make sure the annotations inside disappear");
+          let hiddenPromises = [promisePanelElementHidden(window, infoPanel),
+                                promisePanelElementHidden(window, highlightPanel)];
+          gContentAPI.hideMenu("loop");
+          yield Promise.all(hiddenPromises);
+          isnot(infoPanel.state, "open", "Info panel should have automatically hid");
+          isnot(highlightPanel.state, "open", "Highlight panel should have automatically hid");
+          done();
+        }), "Info panel should be anchored to the new room button");
+      });
+    });
+  },
+  runOffline(function test_notifyLoopChatWindowOpenedClosed(done) {
+    gContentAPI.observe((event, params) => {
+      is(event, "Loop:ChatWindowOpened", "Check Loop:ChatWindowOpened notification");
+      gContentAPI.observe((event, params) => {
+        is(event, "Loop:ChatWindowShown", "Check Loop:ChatWindowShown notification");
+        gContentAPI.observe((event, params) => {
+          is(event, "Loop:ChatWindowClosed", "Check Loop:ChatWindowClosed notification");
+          gContentAPI.observe((event, params) => {
+            ok(false, "No more notifications should have arrived");
+          });
+        });
+        done();
+      });
+      document.querySelector("#pinnedchats > chatbox").close();
+    });
+    LoopRooms.open("fakeTourRoom");
+  }),
+  runOffline(function test_notifyLoopRoomURLCopied(done) {
+    gContentAPI.observe((event, params) => {
+      is(event, "Loop:ChatWindowOpened", "Loop chat window should've opened");
+      gContentAPI.observe((event, params) => {
+        is(event, "Loop:ChatWindowShown", "Check Loop:ChatWindowShown notification");
+
+        let chat = document.querySelector("#pinnedchats > chatbox");
+        gContentAPI.observe((event, params) => {
+          is(event, "Loop:RoomURLCopied", "Check Loop:RoomURLCopied notification");
+          gContentAPI.observe((event, params) => {
+            is(event, "Loop:ChatWindowClosed", "Check Loop:ChatWindowClosed notification");
+          });
+          chat.close();
+          done();
+        });
+
+        let window = chat.content.contentWindow;
+        waitForConditionPromise(
+          () => chat.content.contentDocument.querySelector(".btn-copy"),
+          "Copy button should be there"
+        ).then(() => chat.content.contentDocument.querySelector(".btn-copy").click());
+      });
+    });
+    LoopRooms.open("fakeTourRoom");
+  }),
+  runOffline(function test_notifyLoopRoomURLEmailed(done) {
+    gContentAPI.observe((event, params) => {
+      is(event, "Loop:ChatWindowOpened", "Loop chat window should've opened");
+      gContentAPI.observe((event, params) => {
+        is(event, "Loop:ChatWindowShown", "Check Loop:ChatWindowShown notification");
+
+        let chat = document.querySelector("#pinnedchats > chatbox");
+        let composeEmailCalled = false;
+
+        gContentAPI.observe((event, params) => {
+          is(event, "Loop:RoomURLEmailed", "Check Loop:RoomURLEmailed notification");
+          ok(composeEmailCalled, "mozLoop.composeEmail should be called");
+          gContentAPI.observe((event, params) => {
+            is(event, "Loop:ChatWindowClosed", "Check Loop:ChatWindowClosed notification");
+          });
+          chat.close();
+          done();
+        });
+
+        gMessageHandlers.ComposeEmail = function(message, reply) {
+          let [subject, body, recipient] = message.data;
+          ok(subject, "composeEmail should be invoked with at least a subject value");
+          composeEmailCalled = true;
+          reply();
+        };
+
+        waitForConditionPromise(
+          () => chat.content.contentDocument.querySelector(".btn-email"),
+          "Email button should be there"
+        ).then(() => chat.content.contentDocument.querySelector(".btn-email").click());
+      });
+    });
+    LoopRooms.open("fakeTourRoom");
+  }),
+  taskify(function* test_arrow_panel_position() {
+    is(loopButton.open, false, "Menu should initially be closed");
+    let popup = document.getElementById("UITourTooltip");
+
+    yield showMenuPromise("loop");
+
+    let currentTarget = "loop-newRoom";
+    yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be on the side");
+    is(popup.popupBoxObject.alignmentPosition, "start_before", "Check " + currentTarget + " position");
+
+    currentTarget = "loop-roomList";
+    yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be on the side");
+    is(popup.popupBoxObject.alignmentPosition, "start_before", "Check " + currentTarget + " position");
+
+    currentTarget = "loop-signInUpLink";
+    yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be underneath");
+    is(popup.popupBoxObject.alignmentPosition, "after_end", "Check " + currentTarget + " position");
+  }),
+  taskify(function* test_setConfiguration() {
+    is(Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin"), false, "pref should be false but exist");
+    gContentAPI.setConfiguration("Loop:ResumeTourOnFirstJoin", true);
+
+    yield waitForConditionPromise(() => {
+      return Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin");
+    }, "Pref should change to true via setConfiguration");
+
+    Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
+  }),
+  taskify(function* test_resumeViaMenuPanel_roomClosedTabOpen() {
+    Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
+
+    // Create a fake room and then add a fake non-owner participant
+    let roomsMap = setupFakeRoom();
+    roomsMap.get("fakeTourRoom").participants = [{
+      owner: false,
+    }];
+
+    // Set the tour URL to be the current page with a different query param
+    let gettingStartedURL = gTestTab.linkedBrowser.currentURI.resolve("?gettingstarted=1");
+    Services.prefs.setCharPref("loop.gettingStarted.url", gettingStartedURL);
+
+    let observationPromise = new Promise((resolve) => {
+      gContentAPI.observe((event, params) => {
+        is(event, "Loop:IncomingConversation", "Page should have been notified about incoming conversation");
+        is(params.conversationOpen, false, "conversationOpen should be false");
+        is(gBrowser.selectedTab, gTestTab, "The same tab should be selected");
+        resolve();
+      });
+    });
+
+    // Now open the menu while that non-owner is in the fake room to trigger resuming the tour
+    yield showMenuPromise("loop");
+
+    yield observationPromise;
+    Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
+  }),
+  taskify(function* test_resumeViaMenuPanel_roomClosedTabClosed() {
+    Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
+
+    // Create a fake room and then add a fake non-owner participant
+    let roomsMap = setupFakeRoom();
+    roomsMap.get("fakeTourRoom").participants = [{
+      owner: false,
+    }];
+
+    // Set the tour URL to a page that's not open yet
+    Services.prefs.setCharPref("loop.gettingStarted.url", gBrowser.currentURI.prePath);
+
+    let newTabPromise = waitForConditionPromise(() => {
+      return gBrowser.currentURI.path.includes("incomingConversation=waiting");
+    }, "New tab with incomingConversation=waiting should have opened");
+
+    // Now open the menu while that non-owner is in the fake room to trigger resuming the tour
+    yield showMenuPromise("loop");
+
+    yield newTabPromise;
+
+    yield gBrowser.removeCurrentTab();
+    Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
+  }),
+];
+
+// End tests
+
+function checkLoopPanelIsHidden() {
+  ok(!loopPanel.hasAttribute("noautohide"), "@noautohide on the loop panel should have been cleaned up");
+  ok(!loopPanel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen");
+  isnot(loopPanel.state, "open", "The panel shouldn't be open");
+  is(loopButton.hasAttribute("open"), false, "Loop button should know that the panel is closed");
+}
+
+function setupFakeRoom() {
+  let room = Object.create(fakeRoom);
+  let roomsMap = new Map([
+    [room.roomToken, room]
+  ]);
+  LoopRooms.stubCache(roomsMap);
+  return roomsMap;
+}
+
+if (Services.prefs.getBoolPref("loop.enabled")) {
+  loopButton = window.LoopUI.toolbarButton.node;
+
+  fakeRoom = {
+    decryptedContext: { roomName: "fakeTourRoom" },
+    participants: [],
+    maxSize: 2,
+    ctime: Date.now()
+  };
+  for (let prop of ["roomToken", "roomOwner", "roomUrl"])
+    fakeRoom[prop] = "fakeTourRoom";
+
+  LoopAPI.stubMessageHandlers(gMessageHandlers = {
+    // Stub the rooms object API to fully control the test behavior.
+    "Rooms:*": function(action, message, reply) {
+      switch (action.split(":").pop()) {
+        case "GetAll":
+          reply([fakeRoom]);
+          break;
+        case "Get":
+          reply(fakeRoom);
+          break;
+        case "Join":
+          reply({
+            apiKey: "fakeTourRoom",
+            sessionToken: "fakeTourRoom",
+            sessionId: "fakeTourRoom",
+            expires: Date.now() + 240000
+          });
+          break;
+        case "RefreshMembership":
+          reply({ expires: Date.now() + 240000 });
+        default:
+          reply();
+      }
+    },
+    // Stub the metadata retrieval to suppress console warnings and return faster.
+    GetSelectedTabMetadata: function(message, reply) {
+      reply({ favicon: null });
+    }
+  });
+
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
+    Services.prefs.clearUserPref("loop.gettingStarted.latestFTUVersion");
+    Services.prefs.clearUserPref("loop.gettingStarted.url");
+    Services.io.offline = false;
+
+    // Copied from browser/components/loop/test/mochitest/head.js
+    // Remove the iframe after each test. This also avoids mochitest complaining
+    // about leaks on shutdown as we intentionally hold the iframe open for the
+    // life of the application.
+    let frameId = loopButton.getAttribute("notificationFrameId");
+    let frame = document.getElementById(frameId);
+    if (frame) {
+      frame.remove();
+    }
+
+    // Remove the stubbed rooms.
+    LoopRooms.stubCache(null);
+    // Restore the stubbed handlers.
+    LoopAPI.restore();
+  });
+} else {
+  ok(true, "Loop is disabled so skip the UITour Loop tests");
+  tests = [];
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/uitour/test/browser_UITour_loop_panel.js
@@ -0,0 +1,67 @@
+"use strict";
+
+var gTestTab;
+var gContentAPI;
+var gContentWindow;
+var gMessageHandlers;
+var loopButton;
+var fakeRoom;
+var loopPanel = document.getElementById("loop-notification-panel");
+
+const { LoopAPI } = Cu.import("chrome://loop/content/modules/MozLoopAPI.jsm", {});
+const { LoopRooms } = Cu.import("chrome://loop/content/modules/LoopRooms.jsm", {});
+
+if (!Services.prefs.getBoolPref("loop.enabled")) {
+  ok(true, "Loop is disabled so skip the UITour Loop tests");
+} else {
+  function checkLoopPanelIsHidden() {
+    ok(!loopPanel.hasAttribute("noautohide"), "@noautohide on the loop panel should have been cleaned up");
+    ok(!loopPanel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen");
+    isnot(loopPanel.state, "open", "The panel shouldn't be open");
+    is(loopButton.hasAttribute("open"), false, "Loop button should know that the panel is closed");
+  }
+
+  add_task(setup_UITourTest);
+
+  add_task(function() {
+    loopButton = window.LoopUI.toolbarButton.node;
+
+    registerCleanupFunction(() => {
+      Services.prefs.clearUserPref("loop.gettingStarted.latestFTUVersion");
+      Services.io.offline = false;
+
+      // Copied from browser/components/loop/test/mochitest/head.js
+      // Remove the iframe after each test. This also avoids mochitest complaining
+      // about leaks on shutdown as we intentionally hold the iframe open for the
+      // life of the application.
+      let frameId = loopButton.getAttribute("notificationFrameId");
+      let frame = document.getElementById(frameId);
+      if (frame) {
+        frame.remove();
+      }
+    });
+  });
+
+  add_UITour_task(function* test_menu_show_hide() {
+    // The targets to highlight only appear after getting started is launched.
+    // Set latestFTUVersion to lower number to show FTU panel.
+    Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 0);
+    is(loopButton.open, false, "Menu should initially be closed");
+    gContentAPI.showMenu("loop");
+
+    yield waitForConditionPromise(() => {
+      return loopPanel.state == "open";
+    }, "Menu should be visible after showMenu()");
+
+    ok(loopPanel.hasAttribute("noautohide"), "@noautohide should be on the loop panel");
+    ok(loopPanel.hasAttribute("panelopen"), "The panel should have @panelopen");
+    ok(loopButton.hasAttribute("open"), "Loop button should know that the menu is open");
+
+    gContentAPI.hideMenu("loop");
+    yield waitForConditionPromise(() => {
+        return !loopButton.open;
+    }, "Menu should be hidden after hideMenu()");
+
+    checkLoopPanelIsHidden();
+  });
+}
--- a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
+++ b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
@@ -94,16 +94,29 @@ email-link-button.tooltiptext3 = Email a
 
 # LOCALIZATION NOTE(quit-button.tooltiptext.linux2): %1$S is the brand name (e.g. Firefox),
 # %2$S is the keyboard shortcut
 quit-button.tooltiptext.linux2 = Quit %1$S (%2$S)
 # LOCALIZATION NOTE(quit-button.tooltiptext.mac): %1$S is the brand name (e.g. Firefox),
 # %2$S is the keyboard shortcut
 quit-button.tooltiptext.mac = Quit %1$S (%2$S)
 
+# LOCALIZATION NOTE(loop-call-button3.label): This is a brand name, request
+# approval before you change it.
+loop-call-button3.label = Hello
+loop-call-button3.tooltiptext2 = Browse this page with a friend
+loop-call-button3-error.tooltiptext = Error!
+loop-call-button3-donotdisturb.tooltiptext = Do not disturb
+loop-call-button3-screensharing.tooltiptext = You are sharing your screen
+loop-call-button3-active.tooltiptext2 = You are sharing your tabs
+loop-call-button3-participantswaiting.tooltiptext2 = Someone is waiting for you
+# LOCALIZATION NOTE(loop-call-button3-pb.tooltiptext): Shown when the button is
+# placed inside a Private Browsing window. %S is the value of loop-call-button3.label.
+loop-call-button3-pb.tooltiptext = %S is not available in Private Browsing
+
 social-share-button.label = Share This Page
 social-share-button.tooltiptext = Share this page
 
 panic-button.label = Forget
 panic-button.tooltiptext = Forget about some browsing history
 
 # LOCALIZATION NOTE(devtools-webide-button.label, devtools-webide-button.tooltiptext):
 # widget is only visible after WebIDE has been started once (Tools > Web Developers > WebIDE)
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -0,0 +1,9 @@
+# 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/.
+
+# Panel Strings
+
+## LOCALIZATION NOTE(clientShortname2): This should not be localized and
+## should remain "Firefox Hello" for all locales.
+clientShortname2=Firefox Hello
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -22,16 +22,17 @@
     locale/browser/aboutTabCrashed.dtd             (%chrome/browser/aboutTabCrashed.dtd)
     locale/browser/syncCustomize.dtd               (%chrome/browser/syncCustomize.dtd)
     locale/browser/aboutSyncTabs.dtd               (%chrome/browser/aboutSyncTabs.dtd)
     locale/browser/browser.dtd                     (%chrome/browser/browser.dtd)
     locale/browser/baseMenuOverlay.dtd             (%chrome/browser/baseMenuOverlay.dtd)
     locale/browser/browser.properties              (%chrome/browser/browser.properties)
     locale/browser/customizableui/customizableWidgets.properties (%chrome/browser/customizableui/customizableWidgets.properties)
     locale/browser/lightweightThemes.properties    (%chrome/browser/lightweightThemes.properties)
+    locale/browser/loop/loop.properties            (%chrome/browser/loop/loop.properties)
     locale/browser/newTab.dtd                      (%chrome/browser/newTab.dtd)
     locale/browser/newTab.properties               (%chrome/browser/newTab.properties)
     locale/browser/pageInfo.dtd                    (%chrome/browser/pageInfo.dtd)
     locale/browser/pageInfo.properties             (%chrome/browser/pageInfo.properties)
     locale/browser/quitDialog.properties           (%chrome/browser/quitDialog.properties)
     locale/browser/safeMode.dtd                    (%chrome/browser/safeMode.dtd)
     locale/browser/sanitize.dtd                    (%chrome/browser/sanitize.dtd)
     locale/browser/search.properties               (%chrome/browser/search.properties)
--- a/browser/modules/PanelFrame.jsm
+++ b/browser/modules/PanelFrame.jsm
@@ -76,16 +76,21 @@ var PanelFrameInternal = {
 
         "origin": aOrigin,
         "src": aSrc
       };
       if (aType == "social") {
         attrs["message"] = "true";
         attrs["messagemanagergroup"] = aType;
       }
+      if (aType == "loop") {
+        attrs.message = true;
+        attrs.messagemanagergroup = "social";
+        attrs.autocompletepopup = "PopupAutoComplete";
+      }
       for (let [k, v] of Iterator(attrs)) {
         frame.setAttribute(k, v);
       }
       aParent.appendChild(frame);
     } else {
       frame.setAttribute("origin", aOrigin);
       frame.setAttribute("src", aSrc);
     }
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -265,18 +265,25 @@ function getHost(uri, href) {
   try {
     if (!uri) {
       uri = Services.io.newURI(href, null, null);
     }
     host = uri.host;
   } catch (ex) {}
   if (!host) {
     if (uri && uri.scheme.toLowerCase() == "about") {
-      // For about URIs, just use the full spec, without any #hash parts.
-      host = uri.specIgnoringRef;
+      // Special case-ing Loop/ Hello gUM requests.
+      if (uri.specIgnoringRef == "about:loopconversation") {
+        const kBundleURI = "chrome://browser/locale/loop/loop.properties";
+        let bundle = Services.strings.createBundle(kBundleURI);
+        host = bundle.GetStringFromName("clientShortname2");
+      } else {
+        // For other about URIs, just use the full spec, without any #hash parts.
+        host = uri.specIgnoringRef;
+      }
     } else {
       // This is unfortunate, but we should display *something*...
       const kBundleURI = "chrome://browser/locale/browser.properties";
       let bundle = Services.strings.createBundle(kBundleURI);
       host = bundle.GetStringFromName("getUserMedia.sharingMenuUnknownHost");
     }
   }
   return host;
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -918,9 +918,9 @@ pref("dom.presentation.enabled", true);
 pref("dom.presentation.discovery.enabled", true);
 pref("dom.presentation.discovery.legacy.enabled", true); // for TV 2.5 backward capability
 
 pref("dom.audiochannel.audioCompeting", true);
 pref("dom.audiochannel.mediaControl", true);
 
 // Space separated list of URLS that are allowed to send objects (instead of
 // only strings) through webchannels. This list is duplicated in browser/app/profile/firefox.js
-pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
+pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://hello.firefox.com https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -282,16 +282,23 @@ user_pref("browser.aboutHomeSnippets.upd
 
 // Enable apps customizations
 user_pref("dom.apps.customization.enabled", true);
 
 // Don't fetch or send directory tiles data from real servers
 user_pref("browser.newtabpage.directory.source", 'data:application/json,{"testing":1}');
 user_pref("browser.newtabpage.directory.ping", "");
 
+// Enable Loop
+user_pref("loop.debug.loglevel", "All");
+user_pref("loop.enabled", true);
+user_pref("loop.throttled", false);
+user_pref("loop.server", "http://%(server)s/browser/browser/extensions/loop/chrome/test/mochitest/loop_fxa.sjs?");
+user_pref("loop.CSP","default-src 'self' about: file: chrome: data: wss://* http://* https://*");
+
 // Ensure UITour won't hit the network
 user_pref("browser.uitour.pinnedTabUrl", "http://%(server)s/uitour-dummy/pinnedTab");
 user_pref("browser.uitour.url", "http://%(server)s/uitour-dummy/tour");
 
 // Tell the search service we are running in the US.  This also has the desired
 // side-effect of preventing our geoip lookup.
 user_pref("browser.search.isUS", true);
 user_pref("browser.search.countryCode", "US");